SDE-2 DSA Handbook
From complexity analysis to dynamic programming in 17 modules, practice first: every lesson opens with a problem to try, every drawing steps through, every worked example asks before it tells, and 407 free Medium and Hard LeetCode problems come with spaced review.
Loading your progress.
How this handbook teaches
The goal is not to remember 400 solutions. The goal is to look at a new problem, see its structure, and derive the algorithm. So the handbook asks you to do something before it explains anything.
- Try before you read. Every lesson opens with one problem, or a two-minute question, to attempt before any explanation. Getting stuck first is what makes the explanation stick.
- Short lessons, depth on request. Each lesson shows what the technique solves, an analogy, and its signal and required condition. Proofs, variants, comparison tables, and Java for each idea sit in folded "Go deeper" sections.
- Step through every drawing. Figures start at step 1: predict the next state, then press Next. You should be able to redraw each one in under a minute; the drawing conventions show how.
- Guided solves instead of finished solutions. Each worked example asks five questions and hides every answer until you have written your own.
- One method, used everywhere. The decode method turns a statement into an algorithm in 8 steps. Its page starts with five drills, not an essay.
- Java that runs. Every code block in this handbook comes from Java files that compile with
javac --release 17(so they work on Java 17 and 21) and pass a test harness with 321 checks, including the edge cases named in the text. - Practice that builds transfer. 407 LeetCode problems in 43 pattern banks, Medium and Hard only (357 Medium, 50 Hard), ordered from foundation to challenge, with one line on what each problem teaches.
About the problem links. Every problem was checked against two independent copies of LeetCode's problem catalog on October 14, 2026 (a daily export of LeetCode's own API and the doocs index). Both list all 407 as free, with the exact titles and difficulty shown here. The individual pages were not opened one by one. LeetCode occasionally moves a problem behind Premium; if a link shows a lock, skip it and take the next problem in the same bank.
How to study one module
Use this loop for every lesson. It works whether you have 30 minutes or 3 hours, because each step is a natural stopping point.
- Clear the revision queue first at the start of every session. Re-solve those problems from a blank editor.
- Attempt the "Try it first" problem for 15 minutes before reading. A brute force counts. Set its status honestly.
- Read the short part and step through the figure. Then close the page and say out loud: what signal points to this pattern, and when does it fail?
- Work the guided solve one question at a time. Write your answer, then reveal and compare the reasoning, not just the code.
- Open a "Go deeper" section only when you need it: your attempt failed for a reason the short part does not explain, or a bank problem needs a variant.
- Work the practice bank in order. The first two problems build the mechanism, the middle ones apply it, the last ones stretch it.
- Tick the mastery checklist only when an item is true with your notes closed.
Problem statuses and the revision queue
Each problem has a status. When you set one, the handbook schedules the next review. Problems whose date has arrived appear in the revision queue above and in the practice tracker.
| Status | Meaning | Next review |
|---|---|---|
| Not started | Not attempted | Not scheduled |
| Not solved independently | You needed the solution | 2 days |
| Solved with hints | You needed a hint, an editorial idea, or many failed submissions | 4 days |
| Solved independently | Clean solve, could explain the invariant | 10 days |
| Needs revision | Solved before, but a re-attempt went badly | 1 day |
| Pattern mastered | Solved twice, weeks apart, without help, and can explain why it works | 30 days |
When a problem comes up for review, re-solve it without looking at your old code. Then set the new status, or press "Reviewed, same status" to restart its timer.
Where your progress is saved
Statuses and checkboxes save automatically to your account, privately: nobody else who opens this page can see them, and they follow you to other devices. The indicator in the top bar shows where the latest change went. If the page cannot reach your account (for example in a private window), it saves in this browser instead, and the practice tracker has a backup and restore box for moving progress by hand.
Scope
This handbook covers the topics that general SDE-2 coding rounds actually test, in the depth they test them. Competitive-programming material (segment trees, suffix structures, flows, advanced string matching, heavy number theory) is excluded on purpose: it rarely appears in these rounds, and time spent there is better spent on pattern recognition and clean implementation.
Syllabus and prerequisite map
Seventeen modules in prerequisite order, with the reason each one comes where it does, and how much interview weight it carries.
The map
The order is not alphabetical and not by difficulty alone. Each module reuses ideas from the ones before it:
- Complexity first, because every later decision ("is this fast enough?") is a complexity decision.
- Arrays and hashing next, because nearly every other topic stores data in arrays or maps.
- Two pointers, sliding window, sorting and binary search all exploit order in linear data. They are the highest-return patterns for Medium problems.
- Linked lists and stacks teach pointer discipline and the "most recent item" idea that recursion and tree traversal depend on.
- Recursion and backtracking before trees, because a tree problem is a recursion problem with a fixed shape.
- Trees before graphs, because a tree is a graph without cycles; BFS and DFS on trees become BFS and DFS on graphs with a visited set.
- Recursion and trees before DP, because a DP solution is a memoized recursion with a well-chosen state.
Priority overview
Priority reflects how often a topic decides general SDE-2 coding rounds at product companies, and how much other material depends on it.
| Module | Priority | Why it matters | Practice banks |
|---|---|---|---|
| 1. Complexity | Foundation | Judges every approach; asked after every solution | Exercises only |
| 2. Arrays, strings, hashing | Very high | The raw material of most problems; hashing is the most common optimization | 5 |
| 3. Two pointers, sliding window | Very high | The default tool for contiguous subarray and substring problems | 3 |
| 4. Sorting, binary search | Very high | Binary search on the answer is a top-frequency Medium pattern | 3 |
| 5. Intervals, sweep line | High | Scheduling and calendars appear often in product-company rounds | 1 |
| 6. Linked lists | Medium | Tests pointer discipline; common in phone screens | 2 |
| 7. Stacks, queues, monotonic | High | Monotonic stack is a frequent "aha" Medium | 3 |
| 8. Recursion, backtracking | High | Foundation for trees and DP; "generate all" questions | 3 |
| 9. Trees, BSTs | Very high | The single most common data structure in onsite rounds | 5 |
| 10. Heaps | High | Top-K, scheduling, and streams | 2 |
| 11. Greedy | Medium | Short solutions, but you must justify them | 1 |
| 12. Graphs | Very high | Grids, dependencies, shortest paths, connectivity | 5 |
| 13. Tries | Medium | Autocomplete and word-search style questions | 1 |
| 14. Dynamic programming | Very high | The most feared topic; a consistent derivation process removes the fear | 6 |
| 15. Bit manipulation | Supporting | A few classic tricks, quick to learn | 1 |
| 16. Essential math | Supporting | GCD, primes, modular arithmetic, overflow | 1 |
| 17. Data structure design | High | LRU cache and friends combine several modules | 1 |
Dependencies in detail
| Module | Needs first | Unlocks |
|---|---|---|
| 2. Arrays, hashing | 1 | Almost everything |
| 3. Two pointers, window | 2 | 6 (fast/slow), 7 (monotonic deque) |
| 4. Sorting, binary search | 2 | 5, 10, 11, binary search inside DP (LIS) |
| 5. Intervals | 4 | 11 (interval greedy), 10 (meeting rooms with heaps) |
| 6. Linked lists | 2 | 17 (LRU cache) |
| 7. Stacks, queues | 2 | 9 (iterative traversals), 12 (BFS queue), 17 |
| 8. Recursion, backtracking | 2 | 9, 14 |
| 9. Trees | 7, 8 | 10 (heap as a tree), 12, 13, 14 (tree DP) |
| 10. Heaps | 4, 9 | 11, 12 (Dijkstra), 17 |
| 11. Greedy | 4, 5, 10 | Contrasts with 14 |
| 12. Graphs | 7, 9 | Shortest-path view of DP |
| 13. Tries | 2, 9 | Word search with many words |
| 14. Dynamic programming | 8, 9 | |
| 15. Bits, 16. Math | 2 | Used inside 8, 13, 14 |
| 17. Design | 2, 6, 7, 10 |
How each module is organized
Every lesson follows the same practice-first order, so you always know what comes next:
- Try it first. One problem from the lesson's bank (or a two-minute question) to attempt for 15 minutes before any explanation.
- The short part. What the technique solves, an analogy, and the pattern card's two key fields: the signal, and the condition that must hold.
- A figure to step through. It starts at step 1; predict each next state, then press Next.
- A guided solve. Five questions (understand, brute force, key observation, build, check), each answer hidden until you reveal it.
- Go deeper sections, folded. Proofs, variants, comparison tables, and Java for each idea. Nothing was cut; open them when you need them.
- Common mistakes, the practice bank, and the module's mastery checklist.
Module 1: Complexity analysis
Complexity is how you decide, before writing code, whether an idea is fast enough. In an interview it is also the first follow-up question after every solution.
Before reading anything, write a complexity for each line. Then reveal and compare. Wrong answers are useful: they show which lesson to read closely.
- A loop over
nthat callsArrays.sorton a copy of the array in every iteration.Reveal
O(n^2 log n): n iterations, each copying (O(n)) and sorting (O(n log n)). - For each of
nwords of length up toL, sort its characters and insert the result into a map.Reveal
O(n * L log L): sorting each word dominates; hashing a key of length L is O(L). - BFS over a grid with
Rrows andCcolumns.Reveal
O(R * C) time and space: every cell enters the queue at most once. - Generate all subsets of
nitems and copy each one into the result.Reveal
O(2^n * n): there are 2^n subsets and each copy costs up to n. - A
while (lo < hi)binary search over the range 1 to 10^9 whose check loops over allnelements.Reveal
O(n log 10^9), about 30n: 30 halvings, each with an O(n) check.
1.1What complexity measures
Algorithm A does 100n steps and algorithm B does n^2 steps. Which is faster at n = 50? At n = 100,000?
Reveal the answer
At n = 50, B is faster: 2,500 steps against 5,000. At n = 100,000, A is faster by a factor of 1,000: 10^7 against 10^10. Constants decide small inputs; the growth rate decides large ones, and large inputs are what interviews test.
Two solutions can both be correct and still differ by a factor of a million in running time. Complexity analysis predicts how the cost grows as the input grows, so you can reject a slow idea in seconds instead of after 20 minutes of coding.
Think of estimating a commute. You do not count every traffic light; you say "about 2 minutes per kilometer". Complexity does the same with code: count the work per input element, ignore constants, and keep the part that dominates when the input is large.
Basic steps (comparisons, arithmetic, array reads, hash lookups) as a function of input size n. We drop constant factors and lower-order terms: 3n^2 + 10n + 7 is O(n^2), because for large n the n^2 term is all that matters.
Go deeperBig-O, Big-Theta, Big-Omega at a practical level
| Notation | Plain meaning | How interviewers use it |
|---|---|---|
O(f(n)) |
Grows at most like f(n) (upper bound) | "What is the complexity?" almost always means a tight O for the worst case |
Omega(f(n)) |
Grows at least like f(n) (lower bound) | "You cannot do better than reading the input": Omega(n) |
Theta(f(n)) |
Grows exactly like f(n) (both bounds) | Precise statements, e.g. merge sort is Theta(n log n) in every case |
In practice, say "O(n log n) time and O(n) extra space", and make sure the bound is tight. Calling a linear algorithm O(n^2) is technically true and practically wrong.
Go deeperBest, average, and worst case
The same algorithm can have different costs depending on the input arrangement.
| Algorithm | Best | Average | Worst | What causes the worst case |
|---|---|---|---|---|
| Linear search | O(1) | O(n) | O(n) | Target absent or last |
| Quicksort | O(n log n) | O(n log n) | O(n^2) | Bad pivots, e.g. sorted input with first-element pivot |
| HashMap get | O(1) | O(1) | O(n), or O(log n) once Java treeifies a crowded bucket | Many keys in one bucket |
| Insertion sort | O(n) | O(n^2) | O(n^2) | Reverse-sorted input |
Interviews default to the worst case, except for hashing, where O(1) average is the accepted convention. Say "O(1) average" when you rely on it.
1.2Counting the cost of code
What does this cost in total?
for (int i = 1; i < n; i *= 2) for (int j = 0; j < i; j++) work();
Reveal the answer
O(n), not O(n log n). The inner loop runs 1 + 2 + 4 + ... up to n times, a geometric series that stays below 2n. Count the total work over the whole run, not (outer iterations) x (longest inner loop).
Most code can be analyzed with five rules:
- Sequential blocks add:
O(a) + O(b), keep the larger. - Nested loops multiply when the inner loop's length depends on
n. - A loop that halves (or doubles) its range runs
log ntimes. - Independent inputs keep their own variables: a loop over
nitems then overmitems isO(n + m); nested isO(n * m). Do not merge them intoO(n). - Library calls have cost:
Arrays.sortis O(n log n),list.containsis O(n),String.substringcopies O(k) characters,s1 + s2creates a new string.
// O(n): one pass, constant work per element
static long sum(int[] nums) {
long total = 0;
for (int x : nums) total += x;
return total;
}
// O(n^2): the inner loop runs n-1, n-2, ..., 1 times -> about n^2 / 2
static int countPairsWithSum(int[] nums, int target) {
int count = 0;
for (int i = 0; i < nums.length; i++)
for (int j = i + 1; j < nums.length; j++)
if (nums[i] + nums[j] == target) count++;
return count;
}
// O(log n): the remaining range halves every iteration
static int halvingSteps(int n) {
int steps = 0;
while (n > 1) { n /= 2; steps++; }
return steps;
}
// O(n log n): n outer iterations, each doing O(log n) work
static int sumOfHalvingSteps(int n) {
int total = 0;
for (int i = 1; i <= n; i++) total += halvingSteps(i);
return total;
}
A useful mental table: log2(1000) ~ 10, log2(10^6) ~ 20, log2(10^9) ~ 30.
Go deeperThe trap of "two loops means O(n^2)"
Nested loops are not automatically quadratic. In a sliding window, the inner while loop moves left forward, and left never moves back. Across the whole run, left moves at most n times in total, so the algorithm is O(n). Count the total work over the entire run, not the worst case of one iteration multiplied by n.
1.3Recursion: time from the call tree, space from the deepest path
int f(int n) { return n == 0 ? 1 : f(n - 1) + f(n - 1); }
What are the time and the space of f(n)?
Reveal the answer
Time O(2^n): every call makes two more, so the call tree has about 2^(n+1) nodes. Space O(n): only one chain of calls is open at a time, and it is n + 1 frames deep.
For a recursive function, draw the tree of calls.
- Time = (number of calls) x (work per call, excluding the recursive calls).
- Space = (deepest chain of calls that are open at the same time) x (memory per frame), plus any data you allocate.
// Time O(2^n): two calls per level, n levels. Space O(n): deepest call chain.
static long fibNaive(int n) {
if (n <= 1) return n;
return fibNaive(n - 1) + fibNaive(n - 2);
}
// Time O(n), space O(n) for the memo plus O(n) stack depth.
static long fibMemo(int n, long[] memo) {
if (n <= 1) return n;
if (memo[n] != 0) return memo[n];
return memo[n] = fibMemo(n - 1, memo) + fibMemo(n - 2, memo);
}
Common recurrences you should recognize on sight:
| Recurrence | Example | Result |
|---|---|---|
| T(n) = T(n - 1) + O(1) | Recursive sum, linked list traversal | O(n) |
| T(n) = T(n / 2) + O(1) | Binary search | O(log n) |
| T(n) = 2T(n / 2) + O(n) | Merge sort | O(n log n) |
| T(n) = 2T(n / 2) + O(1) | Tree traversal of a balanced tree | O(n) |
| T(n) = 2T(n - 1) + O(1) | Naive Fibonacci, subsets | O(2^n) |
| T(n) = n * T(n - 1) | Permutations | O(n!) |
For merge sort, the call tree has log n levels and each level does O(n) merging work in total, which is where n log n comes from. You rarely need the full Master Theorem in interviews; "levels times work per level" handles almost everything.
1.4Space: auxiliary space and the recursion stack
A recursive DFS visits a tree of 10^5 nodes shaped like a linked list. It allocates no arrays. What is its space complexity, and what can go wrong in Java?
Reveal the answer
O(n), all of it call stack: the chain is 10^5 frames deep. That risks a StackOverflowError with Java's default stack, so switch to an explicit stack (or BFS) when the depth can reach about 10^4 or more.
- Input space is the memory for the input itself. It is usually not counted.
- Auxiliary space is what your algorithm adds: maps, copies, queues, the recursion stack.
- Output space is sometimes excluded by convention (for example, the result list of all subsets), but say so explicitly.
The recursion stack is the most common hidden cost. A recursive DFS on a skewed tree of n nodes uses O(n) stack, even though it allocates nothing else. In Java the default thread stack handles roughly 10^4 frames of a small method safely; deeper recursion (10^5 nodes in a line, large grids) risks StackOverflowError. Convert to an explicit stack when depth can be that large.
1.5Amortized analysis
An ArrayList starts with capacity 1 and doubles whenever it is full. About how many element copies happen in total while you add n elements?
Reveal the answer
Fewer than 2n. The resizes copy 1 + 2 + 4 + ... + n elements, a geometric series. Spread over n adds, that is O(1) per add on average over the sequence (amortized), even though one unlucky add costs O(n).
Amortized cost is the average cost per operation over a whole sequence of operations, in the worst case. It matters when most operations are cheap and a few are expensive, and the expensive ones are rare by construction.
// ArrayList.add is amortized O(1): most adds are cheap, and a resize
// (copy of all elements) happens only when the backing array is full.
// Capacity grows geometrically (about 1.5x in the JDK), so total copies
// for n adds form a geometric series bounded by c * n: O(n) overall.
static List<Integer> buildList(int n) {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(i);
return list;
}
Three amortized arguments that show up again and again in this handbook:
| Structure | Expensive step | Why the total stays linear |
|---|---|---|
ArrayList.add |
Resize copies everything | Capacity grows by a constant factor, so copies form a geometric series |
| Queue made of two stacks (Module 17) | Moving all of in to out |
Each element moves at most once in its lifetime |
| Monotonic stack, sliding window | A long run of pops or shrinks | Each element is pushed once and popped at most once |
Amortized O(1) is not the same as average-case O(1): hashing is O(1) on average over random inputs, while amortized bounds hold for every sequence.
1.6Reading constraints: will it pass?
n is at most 10^5. In Java, which of these finish in about a second: O(n^2), O(n sqrt n), O(n log n)?
Reveal the answer
O(n log n) is about 1.7 * 10^6 operations: easily. O(n sqrt n) is about 3 * 10^7: usually fine. O(n^2) is 10^10: far too slow. A limit of 10^5 is the problem telling you to look for O(n log n) or O(n).
LeetCode and most interview judges allow roughly 10^7 to 10^8 simple operations per test in Java. Use the input limits to rule approaches in or out before you design anything:
| Largest n | Target complexity | Typical techniques |
|---|---|---|
| n <= 10 to 12 | O(n!) | Permutations, brute-force search |
| n <= 20 to 25 | O(2^n) or O(2^n * n) | Subsets, bitmasks, backtracking |
| n <= 100 to 500 | O(n^3) | Interval DP, Floyd-Warshall |
| n <= 2,000 to 5,000 | O(n^2) | 2D DP, all pairs |
| n <= 10^5 to 10^6 | O(n log n) or O(n) | Sorting, heaps, binary search, two pointers, hashing, BFS |
| n up to 10^9 or more | O(log n) or O(1) | Binary search on the answer, math |
Also read the value constraints. Values up to 10^9 summed over 10^5 elements exceed int (max about 2.1 x 10^9), so use long. Small values (lowercase letters, values <= 10^4) allow counting arrays instead of maps.
1.7Operation costs of common Java structures
This drains a list of n items: while (!list.isEmpty()) { int x = list.remove(0); ... }. With an ArrayList, what does it cost, and what should it cost?
Reveal the answer
O(n^2): each remove(0) shifts every remaining element one place left. An ArrayDeque with pollFirst() removes from the front in O(1), so the loop becomes O(n).
| Structure | Access / search | Insert | Delete | Notes |
|---|---|---|---|---|
Array, ArrayList |
get(i) O(1), contains O(n) | add at end O(1) amortized, at index O(n) | O(n) (shifts) | ArrayList.remove(int) vs remove(Object) overloads |
HashMap, HashSet |
O(1) average | O(1) average | O(1) average | No order; keys must have correct equals/hashCode |
TreeMap, TreeSet |
O(log n) | O(log n) | O(log n) | floorKey, ceilingKey, firstKey, subMap |
ArrayDeque |
ends O(1) | ends O(1) | ends O(1) | Use as stack or queue; no nulls |
PriorityQueue |
peek O(1) | O(log n) | poll O(log n), remove(Object) O(n) | Iteration is not sorted |
LinkedList |
O(n) by index | ends O(1) | ends O(1) | Rarely the best choice |
String |
charAt O(1) | concatenation O(n + m) | substring O(k) copy | Immutable: build with StringBuilder |
| Sorting | Arrays.sort(int[]) dual-pivot quicksort, O(n log n) average; objects use TimSort, stable |
1.8Comparing alternative approaches
Two correct ways to detect a duplicate: sort and compare neighbors, or add values to a HashSet. Give the time and extra space of each, and one reason to pick the sort.
Reveal the answer
Sort: O(n log n) time, O(1) to O(log n) extra space, but it reorders the input. HashSet: O(n) average time, O(n) space. Pick the sort when memory is tight, when the input is already sorted, or when you need sorted order anyway.
Complexity is how you choose between correct ideas. Take "does any pair sum to target?":
| Approach | Time | Extra space | When to prefer it |
|---|---|---|---|
| Check every pair | O(n^2) | O(1) | n is tiny, or as the first correct baseline |
| Sort, then two pointers | O(n log n) | O(1) to O(log n) | Memory is tight, or the input is already sorted |
| Hash set of seen values | O(n) average | O(n) | Default choice; also keeps original indices with a map |
There is rarely one universally best answer. State the trade-off in one sentence: "Hashing is linear but uses O(n) memory; if memory matters, sorting gives O(1) extra space at O(n log n)."
1.9Common mistakes
"One loop over n items, so it is O(n)." The loop body calls list.contains(x) on an ArrayList that grows to n items. What is the real cost?
Reveal the answer
O(n^2). contains scans the list, so each iteration costs up to O(n). A HashSet makes the whole loop O(n). Library calls inside loops are the most common hidden cost.
- Ignoring hidden costs inside loops:
list.contains,str.substring,s += c,list.remove(0)are each O(n) and turn an O(n) loop into O(n^2). - Merging different inputs: a grid is
O(rows * cols), a graph isO(V + E), two strings areO(n * m)for a DP table. Do not call these O(n). - Forgetting the recursion stack in space complexity.
- Calling a sliding window O(n^2) because of the nested loop. Count total pointer movement.
- Quoting average case as worst case without saying so, outside of hashing.
- Forgetting the sort: "two pointers is O(n)" is wrong if you had to sort first; the total is O(n log n).
1.10Self-check exercises
Same drill, new code. If you miss one, the lesson it points to is worth a second look.
for (i = 0; i < n; i++) for (j = i + 1; j < n; j++)with O(1) work inside.Reveal
O(n^2): about n^2 / 2 pairs, and halving does not change the class.- Building a string of n characters with
s += cin a loop.Reveal
O(n^2): each+=copies the whole string built so far. AStringBuildermakes it O(n). - A memoized recursion over states
(i, j)with0 <= i < nand0 <= j < m, doing O(1) work per state.Reveal
O(n * m) time and space: each state is computed once, plus up to O(n + m) recursion stack. - Dijkstra with a binary heap on V vertices and E edges.
Reveal
O((V + E) log V): each edge can push one heap entry, and each heap operation is O(log V). - Sort n values once, then answer q queries with binary search.
Reveal
O(n log n + q log n). Sorting once is what makes each query cheap.
Mastery checklist
Tick an item only when it is true with your notes closed.
Decode an unfamiliar problem
You learn this method by using it: one screen of steps, then five problems to decode with a reveal for every step. The detail behind each step is folded further down for when a drill surprises you.
The method on one screen
| Step | Question you answer | What you write down |
|---|---|---|
| 1. Translate | What exactly is asked, under which constraints? | One plain sentence plus the task type |
| 2. Input structure | What properties does the input have? | A list: sorted? contiguous? graph? stream? |
| 3. Small example | What happens when I solve it by hand? | 2 to 4 tiny test cases and their answers |
| 4. Brute force | What is the simplest correct approach and its cost? | The approach, its complexity, the repeated work |
| 5. Key observation | Which work is repeated, and which candidates can be discarded? | One sentence: "because ..., we only need ..." |
| 6. Map to a pattern | Which technique exploits that observation? | Pattern name, and why its required condition holds |
| 7. Build it | What variables, invariant, update rule, and stopping rule? | A variable sheet |
| 8. Validate | Does it survive small, boundary, and adversarial cases? | Pass or a counterexample |
Spend most of the time on steps 3 to 5; steps 6 to 8 are quick once the observation is right. Keyword guessing ("it says substring, so sliding window") fails because a pattern is justified by a property of the problem, not a word in it.
Practice: decode five problems
Each drill has five steps. Write your answer to a step on paper, then reveal mine and compare. Give each problem about 15 minutes. You have not met every technique yet, and that is the point: the method should lead you to the right property of the problem before you know the technique's name. The module link on each drill is where that technique is taught.
For each spell strength, count the potions whose product with it is at least success. Both lists have up to 10^5 values.
- 1Translate the statement
Reveal my answer
For each value
sinspells, count the valuespinpotionswiths * p >= success, and return the counts in spell order. Counting task. n and m up to 10^5;successup to 10^10, so the product needslong. - 2Solve a tiny case by hand
Reveal my answer
spells = [5, 1, 3],potions = [1, 2, 3, 4, 5],success = 7. Spell 5 works with potions 2 to 5: 4. Spell 1 needs 7 or more: 0. Spell 3 needs 3 or more: 3. Answer[4, 0, 3]. By hand you looked for the smallest potion that works and counted everything above it. - 3Brute force and its repeated work
Reveal my answer
Check every pair: O(n * m), up to 10^10. The repeated work: every spell rescans all potions, although the potions never change.
- 4Key observation
Reveal my answer
The order of the potions does not matter, so sort them once. For a fixed spell,
s * p >= successis false for small potions and true from some point on: a monotonic yes/no over sorted data. - 5Pattern, build, validate
Reveal my answer
Sort plus binary search for the lower bound (Module 4). For each spell, find the first index with
(long) s * potions[i] >= success; the count ism - index. O((n + m) log m). Check: when no potion works the index is m and the count is 0.
A car with capacity seats only drives east. Each trip is (passengers, from, to). Can it serve every trip without ever exceeding capacity?
- 1Translate the statement
Reveal my answer
Each trip adds its passengers on the stretch
[from, to). Is the total load at every point at mostcapacity? Existence task (true or false). Up to 1,000 trips, locations 0 to 1,000. - 2Solve a tiny case by hand
Reveal my answer
trips = [[3, 2, 6], [2, 4, 8]],capacity = 4. Between 4 and 6 both groups ride: 5 > 4, so false. With capacity 5 it is true. By hand you only looked at the points where someone gets in or out. - 3Brute force and its repeated work
Reveal my answer
For every location, add up the trips that cover it: O(trips * range). Here that passes, but the repeated work is clear: each trip adds the same amount to every point of its stretch, one point at a time.
- 4Key observation
Reveal my answer
The load only changes where someone boards or leaves. Record
+patfromand-patto; a running sum from left to right then gives the load at every point. - 5Pattern, build, validate
Reveal my answer
Difference array, also called a sweep line (Modules 2 and 5).
diff[from] += p; diff[to] -= p, then a running sum over 0 to 1,000 that fails as soon as it exceeds capacity. O(trips + range). Check: a drop-off and a pick-up at the same point land in the same cell, so they cancel correctly.
For each day, how many days until a warmer temperature? Use 0 when there is none. Up to 10^5 days.
- 1Translate the statement
Reveal my answer
For each index
i, find the nearestj > iwithtemps[j] > temps[i]and outputj - i, or 0. n up to 10^5, so O(n^2) is too slow. - 2Solve a tiny case by hand
Reveal my answer
[70, 71, 69, 68, 72, 70]gives[1, 3, 2, 1, 0, 0]. By hand, when 72 arrives it answers 68, 69, and 71 at once. The days still waiting were 71, 69, 68: always colder the more recent they are. - 3Brute force and its repeated work
Reveal my answer
For each day, scan right until a warmer one: O(n^2) when temperatures keep falling. The repeated work: the same unanswered days are scanned past again and again.
- 4Key observation
Reveal my answer
Days still waiting form a decreasing sequence; a warmer day would already have answered the colder ones before it. A new day answers waiting days from the most recent backward and stops at the first one that is not colder.
- 5Pattern, build, validate
Reveal my answer
Monotonic stack of indices (Module 7). For each
i: while the stack is not empty andtemps[i] > temps[top], poptopand setans[top] = i - top; then pushi. Each index is pushed and popped at most once: O(n). Check: equal temperatures must not answer each other, which the strict>guarantees.
There are n courses and prerequisite pairs [a, b] meaning b must come before a. Can you finish every course?
- 1Translate the statement
Reveal my answer
Given directed edges
b -> a, is there an order of all n nodes that respects every edge? Existence task. n up to 2,000, up to 5,000 edges. - 2Solve a tiny case by hand
Reveal my answer
n = 3 with
[[1, 0], [2, 1]]: take 0, then 1, then 2: true. Add[0, 2]and 0 needs 2, 2 needs 1, 1 needs 0: a loop, nobody can start, false. By hand you began with courses that had no prerequisites and crossed them off. - 3Brute force and its repeated work
Reveal my answer
Try every order of the courses: O(n!). The repeated work: orders that already broke a rule early are still extended and checked.
- 4Key observation
Reveal my answer
The prerequisites form a directed graph, and an order exists exactly when it has no cycle. A course with no remaining prerequisites can always be taken now, and taking it can only free other courses.
- 5Pattern, build, validate
Reveal my answer
Topological sort with Kahn's algorithm (Module 12). Count incoming edges per node, queue every node with none, then pop, count, and decrement each neighbor, queueing it when it reaches 0. Answer: processed count equals n. O(V + E). Check: a self-loop
[a, a]never reaches 0, so the answer is false.
Coins of several values, unlimited supply of each. Return the fewest coins that make amount exactly, or -1.
- 1Translate the statement
Reveal my answer
Minimize the number of items whose values sum to exactly
amount, reusing any coin. Optimization task. Up to 12 coin values, amount up to 10^4. - 2Solve a tiny case by hand
Reveal my answer
coins = [1, 3, 4],amount = 6. Largest first gives 4 + 1 + 1 = 3 coins, but 3 + 3 uses 2. By hand you compared "use a 4 and solve 2", "use a 3 and solve 3", "use a 1 and solve 5". - 3Brute force and its repeated work
Reveal my answer
Try every combination recursively: exponential. The repeated work: "fewest coins for 2" is solved again on many different paths.
- 4Key observation
Reveal my answer
The best answer for an amount depends only on the amount, not on how you got there:
best(a) = 1 + min(best(a - c))over coinsc. Overlapping subproblems with optimal substructure, and the greedy rule already failed. - 5Pattern, build, validate
Reveal my answer
Dynamic programming, unbounded knapsack (Module 14).
dp[0] = 0, everything else starts atamount + 1; forafrom 1 to amount and each coinc <= a,dp[a] = min(dp[a], dp[a - c] + 1). Return -1 ifdp[amount]is stillamount + 1. O(amount * coins). Check: amount 0 returns 0.
The eight steps in detail
Open a step when a drill went wrong at that step. Each one lists the questions to ask and an example.
Go deeperWhy keyword guessing fails
"It says substring, so it is sliding window" works until it does not:
| Statement | Keyword guess | What actually works | Why the guess fails |
|---|---|---|---|
| Longest palindromic substring (LC 5) | Sliding window | Expand around each center | Being a palindrome is not monotonic in window size: "aba" is one, "abab" is not, "ababa" is again |
| Count subarrays with sum exactly k, values can be negative (LC 560) | Sliding window | Prefix sums + hash map | With negatives, growing a window can lower the sum, so you never know which side to move |
| Can this string be segmented into words (LC 139) | Backtracking ("try every split") | DP over prefixes | Many split paths reach the same prefix; the question is yes/no, so you only need each prefix once |
| Fewest coins for an amount (LC 322) | Greedy ("take the largest coin") | DP | Coins {1, 3, 4}, amount 6: greedy takes 4 + 1 + 1 = 3 coins, the optimum is 3 + 3 = 2 coins |
| Minimum capacity to ship within D days (LC 1011) | Greedy or DP | Binary search on the answer | Computing the answer directly is hard, but checking a candidate capacity is easy and monotonic |
A pattern is justified by a property of the problem: monotonicity, order, independence, overlapping subproblems. The method below finds those properties first and lets the pattern follow.
Go deeperStep 1: Translate the statement into a simple problem
Strip the story, keep every constraint. Answer six questions:
- What is given? Types and shapes: an array of ints, a grid, a list of pairs, a stream of calls.
- What must be returned? A number, a boolean, an index, a list, a constructed object. "Return any" vs "return all" changes everything.
- What operations are allowed? Delete, swap, reorder, reuse elements, pick each once.
- What are the constraints? Sizes (they set the complexity budget, see Module 1), value ranges (negatives? overflow?), uniqueness.
- What type of task is it? See the table below.
- What special conditions hold? Sorted, contiguous, distinct, bounded, a guaranteed answer.
| Task type | Typical phrasing | Families to consider first |
|---|---|---|
| Existence | "Is it possible", "can you", "return true if" | Search (BFS/DFS), DP over booleans, greedy with proof |
| Counting | "How many", "number of ways" (often modulo 10^9 + 7) | DP, prefix sums + hashing, combinatorics |
| Optimization | "Minimum", "maximum", "longest", "fewest" | DP, greedy, binary search on the answer, sliding window, Dijkstra |
| Search | "Find the index", "return the element" | Binary search, hashing |
| Ordering | "Order of", "schedule", "sequence that satisfies" | Sorting, topological sort, heaps |
| Connectivity | "Connected", "reachable", "groups", "islands" | BFS/DFS, Union-Find |
| Construction | "Return all", "generate", "design a class" | Backtracking, data structure design |
Example of stripping a statement. LC 1011 reads: "A conveyor belt has packages that must be shipped from one port to another within days days. The i-th package on the conveyor belt has a weight of weights[i]. Each day, we load the ship with packages on the conveyor belt (in the order given by weights)..." Translated:
Split the array into at most D contiguous, in-order groups. Minimize the largest group sum. n up to 5 * 10^4, weights up to 500.
That translation already contains the key facts: contiguous, in order, "minimize the maximum". The story about ports and ships contains none.
Go deeperStep 2: Identify the structure of the input
Each property removes approaches that cannot work, or enables ones that only work with it.
| Property | What it enables | What it rules out or warns about |
|---|---|---|
| Sorted (or can be sorted without losing information) | Binary search, two pointers, greedy by order | If original indices matter, sorting loses them (keep index pairs) |
| Contiguous subarray or substring required | Sliding window, prefix sums, Kadane, monotonic stack | Subsequence techniques (DP over choices) |
| Order does not matter (sets, counts) | Sorting, hashing, counting arrays | Anything that relies on positions |
| Repeated values matter | Frequency maps, duplicate skipping in backtracking | Plain sets |
| Relationships or dependencies between items | Graph modeling: BFS/DFS, topological sort, Union-Find | Treating items independently |
| Stream or repeated updates and queries | Heaps, two heaps, balanced trees, design with several structures | Re-sorting after each update |
| Answer depends on earlier choices | DP (if subproblems repeat), backtracking (if you need all answers) | Greedy, unless a choice is provably safe |
| Small n (<= 20) | Exponential search, bitmasks | Nothing; exponential is allowed |
| Small value range | Counting sort, bucket arrays, DP over values | |
| Non-negative values | Sliding window on sums, greedy accumulation | With negatives, these usually break |
Why this narrows the search: an interview problem is designed so that one property makes it solvable within the constraints. If n is 10^5, you need O(n log n) or better, and that means the problem must have order, monotonicity, or hashing-friendly structure somewhere. Your job is to find it.
Go deeperStep 3: Start with a small example
Before choosing an algorithm, solve 2 to 4 tiny cases by hand. Make your own; the given examples are often chosen to hide edge cases.
Building useful test cases:
- Minimal: empty input, one element, two elements.
- Typical: 5 to 8 elements where the answer is not at either end.
- Edge: all equal, all negative, already sorted, reverse sorted, answer is the whole input, no valid answer.
- Adversarial for your idea: once you have an idea, build the case most likely to break it.
While solving by hand, watch yourself. Your hand solution is usually an inefficient version of the right algorithm. Ask:
- What changes as I process each element?
- What information do I keep in my head? (That becomes your state.)
- What do I recompute? (That is what you will cache or maintain.)
- What can I safely forget? (That is what lets you discard or shrink.)
- What choice decides the next step? (That becomes a greedy rule or a DP transition.)
- When is a partial answer valid? (That becomes the invariant.)
Example: longest substring with at most 2 distinct characters, s = "eceba". By hand, you scan and keep a stretch of text:
Notice what you kept in your head: the current stretch and how many of each letter it has. Notice what you never did: go back and restart from an earlier position. Those two observations are the sliding window pattern, discovered rather than guessed.
Go deeperStep 4: Identify a brute-force approach
The brute force is the simplest approach you can prove correct. Write it in words (code it only if you are stuck or the interviewer asks). Then answer three questions:
- What does it try? All pairs, all subarrays, all subsets, all orders, every candidate answer.
- What work does it repeat? This is the most important question on the page.
- What does it cost, and how far is that from the budget? n = 10^5 with O(n^2) is 10^10 operations, far over budget.
Example: count subarrays with sum k. Brute force: for every start j, for every end i, sum the elements. O(n^3). The innermost work, "sum from j to i", is recomputed for every pair. Keeping a running sum per start makes it O(n^2). Now the repeated work is subtler: for each end i you check every start j, asking "does sum(j..i) equal k?" Since sum(j..i) = prefix(i) - prefix(j - 1), the question becomes "how many earlier prefixes equal prefix(i) - k?", which a hash map answers in O(1). The brute force pointed straight at the optimization.
The brute force is a tool for finding the optimization, not a box to tick. If you see the optimal idea immediately, say the brute force in one sentence and move on.
Go deeperStep 5: Discover the key observation
Look at the brute force's innermost loop and ask these questions in order. Each "yes" points to a technique.
| Question | What it looks like in the brute force | Technique | Example |
|---|---|---|---|
| Am I recalculating the same information? | A sum, count, or max over a range computed again for overlapping ranges | Prefix sums, running aggregates | LC 303, 238 |
| Can I reuse previous results? | The answer for i is built from answers for smaller inputs | DP, memoization | LC 70, 198 |
| Can hashing replace repeated searching? | An inner loop that searches for a value or complement | HashMap / HashSet | LC 1, 128 |
| Can sorted order eliminate possibilities? | Comparing every pair when order tells you which side to give up | Two pointers, binary search | LC 167, 15 |
| Can two pointers avoid checking every pair? | Pairs where moving one index monotonically changes the value | Two pointers | LC 11, 42 |
| Can a window be maintained instead of recalculated? | Re-scanning a contiguous range after adding one element | Sliding window | LC 3, 438 |
| Can a greedy decision build the answer? | Trying all choices when one choice is always at least as good | Greedy with an exchange argument | LC 435, 55 |
| Can I keep only the best or most relevant elements? | Scanning all previous elements to find the nearest greater, the max, the top k | Monotonic stack/deque, heap | LC 739, 239, 215 |
| Can the problem split into smaller states? | Same question on a prefix, suffix, subtree, or interval | Recursion, DP, divide and conquer | LC 1143, 124 |
| Can relationships be a graph? | Items that depend on, connect to, or transform into others | BFS/DFS, topological sort, Union-Find | LC 207, 127 |
| Does an invariant make some elements irrelevant forever? | Elements that, once beaten, can never be the answer again | Monotonic structures, pruning | LC 84, 402 |
| Is checking easier than computing? | You can verify a candidate answer quickly, and feasibility is monotonic | Binary search on the answer | LC 875, 410 |
How to discover the observation, not just recognize it. Three habits help:
- Name the inner loop's job in words. "This loop finds the previous index whose prefix equals X." Once named, you can ask "what data structure answers that in O(1) or O(log n)?"
- Look for monotonicity. When the right pointer moves forward, can the best left pointer only move forward too? When the capacity grows, does feasibility only go from false to true? Monotonic means you can discard instead of re-check.
- Look for dominance. If element A is worse than element B in every future comparison (older and smaller, for a window maximum), A can be deleted. Dominance is the idea behind monotonic stacks, deques, and many prunings.
Go deeperStep 6: Map the observation to a pattern
Each pattern in this handbook has a pattern card with seven fields. Use them as a checklist, not a lookup table:
- Signal: what in the problem suggests the pattern.
- Required condition: what must be true for it to be correct. Verify this; it is where wrong guesses fail.
- Mental model: how to think about it.
- Core operation: what the algorithm repeatedly does.
- Complexity benefit: what work it removes.
- Limitations: when it breaks.
- Alternative: what to use when the required condition fails.
Here is one card as an example. All cards are collected on the pattern cheat sheets page.
- Signal
- Longest or shortest contiguous subarray/substring satisfying a condition; 'at most k' distinct, zeros, or replacements.
- Required condition
- Monotonic validity: shrinking a valid window keeps it valid (or growing an invalid one keeps it invalid). Typically fails with negative numbers in sum conditions.
- Mental model
- An accordion: stretch right while it plays, squeeze from the left when it breaks.
- Core operation
- Expand right; while the window is invalid, shrink left; record the answer at the right moment.
- Complexity benefit
- Each index enters and leaves once: O(n) instead of O(n^2) over all windows.
- Limitations
- Breaks when adding an element can make the window either better or worse (sums with negatives).
- Alternative
- Prefix sums + hash map (exact sums with negatives); monotonic deque over prefix sums (shortest sum >= k with negatives); exactly(k) = atMost(k) - atMost(k-1).
The required condition is the field that saves you. Before committing, say it out loud and check it against your small examples: "Shrinking a valid window keeps it valid. Is that true here?" For "at most 2 distinct characters", yes. For "sum equals k with negatives", no, and the alternative row tells you where to go.
When two patterns seem to fit, use the decision tables at the bottom of this page.
Go deeperStep 7: Build the solution incrementally
Before writing code, fill in a variable sheet. It turns a vague idea into a precise algorithm and gives you something to explain to the interviewer.
| Item | Question | Example: minimum window substring (LC 76) |
|---|---|---|
| Variables | What do I store? | need[c] counts required, window[c] counts in window, left, formed |
| Meaning | What does each variable mean, exactly? | formed = number of distinct characters whose window count meets the requirement |
| State to maintain | What must be updated on each step? | Counts when a character enters or leaves, and formed when a count crosses its requirement |
| Invariant | What is always true at a checkpoint? | After the inner loop, the window [left, right] is the shortest valid window ending at right, or the window is invalid |
| Answer update | When do I record a candidate? | Inside the shrink loop, while the window is still valid |
| Discard rule | When can an element or state be dropped? | left advances when the window stays valid without s[left] |
| Termination | When and why does it stop? | right reaches the end; left never passes right + 1 |
If you cannot fill in the invariant, you do not understand the algorithm yet. Go back to the small example.
Go deeperStep 8: Validate the reasoning
An observation that passes the given examples is not proven. Test it before coding:
- Run the small cases from Step 3 through your variable sheet by hand.
- Hit the boundaries: empty, single element, all equal, maximum values (overflow), no valid answer.
- Recheck the constraints: does the complexity fit? Does the memory fit? Are negatives allowed?
- Try to break it. Ask "what input would make this wrong?" and build it. For a greedy rule, look for a case where the locally best choice blocks a better global answer.
- Prove it in one sentence. "Each element enters and leaves the window once, and the window is valid exactly when ..." If you cannot say why it works, an interviewer will ask.
Attractive but wrong: greedy coin change. "Always take the largest coin that fits" passes every example with coins {1, 5, 10, 25}. Trying to break it with {1, 3, 4} and amount 6 gives 4 + 1 + 1 (3 coins) against the optimum 3 + 3 (2 coins). One counterexample is enough to move to DP.
Stress testing. When an idea is subtle, code the brute force too, run both on hundreds of random tiny inputs, and compare. It is the fastest way to find a wrong observation, and it is standard practice in real engineering.
Full walkthrough: two problems decoded from scratch
Guided decodeLC 1004. Max Consecutive Ones IIIThe decode method arrives at a sliding window without guessing
Problem: 1004Max Consecutive Ones IIIMedium. Answer each question on paper first, then reveal.
- 1Translate and read the input
Strip the story: what is given, what is returned, and which properties does the input have?
Reveal step 1
Translate
Given a binary array and
k, return the length of the longest contiguous run of 1s if you may flip at mostkzeros. n up to 10^5. Task type: optimization (longest).Input structure
Contiguous subarray. Values are only 0 and 1. No sorting allowed (order matters).
- 2Small example and brute force
Solve a tiny case by hand. Then name the simplest correct approach, its cost, and the work it repeats.
Reveal step 2
Small example
[1,1,0,0,1,1,1,0,1],k = 1. By hand: I keep a stretch that contains at most one 0. When a second 0 arrives, I drop elements from the left until only one 0 remains. The answer is 5: indices 4 to 8 (1,1,1,0,1), flipping the zero at index 7.Brute force
Every subarray, count zeros, keep the longest with zeros <= k. O(n^2) with a running count. Repeated work: after extending to
right, I restart from everyleftagain. - 3Key observation
Which property of the problem lets you skip the repeated work?
Reveal step 3
Key observation
"Flipping at most k zeros" means "the subarray contains at most k zeros". If
[left, right]is valid, every shorter window inside it is valid too; if it is invalid, every window that contains it is invalid. Validity is monotonic, so whenrightmoves forward, the bestleftnever needs to move back. - 4Pattern and build
Name the technique, check its required condition, and write the variables and the update rule.
Reveal step 4
Pattern
Variable-size sliding window. Required condition (shrinking keeps validity) holds.
Build it
zeros= zeros in the window. Addnums[right]; whilezeros > k, removenums[left++]; thenbest = max(best, right - left + 1). Invariant: after the while loop, the window is the longest valid window ending atright. - 5Validate
Which boundary inputs could break it, and what does it cost?
Reveal step 5
Validate
All ones: window grows to n.
k = 0: longest run of ones.k >= number of zeros: answer n. Each index enters and leaves once: O(n) time, O(1) space.
Guided decodeLC 1011. Capacity To Ship Packages Within D DaysWhen computing the answer is hard but checking it is easy
Problem: 1011Capacity To Ship Packages Within D DaysMedium. Answer each question on paper first, then reveal.
- 1Translate and read the input
Strip the story: what is given, what is returned, and which properties does the input have?
Reveal step 1
Translate
Split
weightsinto at mostDcontiguous groups in order; minimize the largest group sum. Optimization (minimum of a maximum).Input structure
Order is fixed, groups are contiguous, weights are positive. n up to 5 * 10^4.
- 2Small example and brute force
Solve a tiny case by hand. Then name the simplest correct approach, its cost, and the work it repeats.
Reveal step 2
Small example
[1,2,3,4,5],D = 2. Try capacities: 7 needs[1,2,3] [4] [5]= 3 days, too many. 9 gives[1,2,3] [4,5]= 2 days, fine. 8 gives[1,2,3] [4] [5]= 3 days. So the answer is 9. Notice: checking one capacity was easy (greedy fill), and every capacity above 9 also works.Brute force
Try every capacity from
max(weights)up tosum(weights)and greedily check each. O(n * sum), up to 5 * 10^4 * 2.5 * 10^7: far too slow. Repeated work: checking capacities one by one when feasibility is monotonic. - 3Key observation
Which property of the problem lets you skip the repeated work?
Reveal step 3
Key observation
feasible(cap)is false for small capacities and true from the answer onward. A monotonic yes/no function over a range is exactly what binary search finds. - 4Pattern and build
Name the technique, check its required condition, and write the variables and the update rule.
Reveal step 4
Pattern
Binary search on the answer, with a greedy feasibility check. Required condition: monotonic feasibility (more capacity never needs more days). It holds.
Build it
lo = max(weights)(every package must fit),hi = sum(weights)(one day). Find the first capacity wheredaysNeeded(cap) <= D.daysNeededfills the current day until the next package would overflow. - 5Validate
Which boundary inputs could break it, and what does it cost?
Reveal step 5
Validate
D = ngivesmax(weights);D = 1gives the sum. Cost: O(n log(sum)). The same skeleton solves LC 875 and LC 410; see Module 4.
Decision tables for commonly confused techniques
These tables are not rules. Each one names the property that decides between two techniques, so you can check the property instead of guessing.
Go deeperHashing vs sorting
| Hashing | Sorting | |
|---|---|---|
| Use when | You need membership, counts, or a complement lookup, and original positions matter | You need order: neighbors, ranges, pairs via two pointers, grouping by adjacency |
| Cost | O(n) average time, O(n) space | O(n log n) time, O(1) to O(n) space |
| Breaks when | You need the k-th smallest, ranges, or "closest" values | You must return original indices (unless you sort index pairs) |
| Deciding question | Do I need order, or only identity? |
Go deeperTwo pointers vs sliding window
| Two pointers (opposite ends) | Sliding window (same direction) | |
|---|---|---|
| Use when | The answer is a pair of positions, and sorted order tells you which end to move | The answer is a contiguous range with a running condition |
| What the pointers mean | Two candidates being compared | The two boundaries of one range |
| Example | Two Sum II, 3Sum, Container With Most Water | Longest substring without repeats, minimum size subarray sum |
| Deciding question | Is the answer two separate elements, or everything between two indices? |
Go deeperSliding window vs prefix sums
| Sliding window | Prefix sums (+ hash map) | |
|---|---|---|
| Use when | Validity is monotonic: all values non-negative, or the condition is about counts | Exact sums, sums with negative numbers, many range queries |
| Finds | Longest or shortest valid window, or counts under "at most" | Number of subarrays with an exact sum, any range sum in O(1) |
| Breaks when | Negatives make window sums non-monotonic | You need the max or min of a range (not invertible) |
| Deciding question | If I extend the window, can the condition get both better and worse? If yes, use prefix sums. |
Go deeperPrefix sums vs difference arrays
| Prefix sums | Difference array | |
|---|---|---|
| Use when | Many range queries on fixed data | Many range updates, then read the final values once |
| Operation | prefix[r+1] - prefix[l] |
diff[l] += x; diff[r+1] -= x, then take a prefix sum |
| Example | Range Sum Query, Subarray Sum Equals K | Corporate Flight Bookings, Car Pooling |
| Deciding question | Am I asking about ranges, or adding to ranges? |
Go deeperBinary search on data vs binary search on an answer
| On data | On the answer | |
|---|---|---|
| Search space | Indices of a sorted or rotated array | A range of possible answer values (speed, capacity, time, distance) |
| Predicate | Compares nums[mid] with the target |
feasible(mid), computed by a greedy O(n) check |
| Signal | "Sorted", "find the position" | "Minimum X such that ...", "maximize the minimum", huge answer ranges |
| Deciding question | Am I locating something in the input, or guessing a value and checking it? |
Go deeperBFS vs DFS
| BFS | DFS | |
|---|---|---|
| Use when | Shortest path in unweighted graphs, levels, nearest anything, spreading over time | Reachability, components, cycle detection, topological order, backtracking, tree recursion |
| Memory | O(width) queue | O(depth) stack (recursion depth risk) |
| Breaks when | Graph is huge and only one deep path matters | You need shortest distances (DFS finds a path, not the shortest) |
| Deciding question | Do I need the closest or fewest-step answer? Use BFS. |
Go deeperBFS vs Dijkstra
| BFS | Dijkstra | |
|---|---|---|
| Edge weights | All equal (or 0/1 with a deque) | Non-negative, different |
| Cost | O(V + E) | O((V + E) log V) |
| First visit is final? | Yes, because levels are distances | Only when popped from the heap |
| Deciding question | Do moves have different costs? If yes, Dijkstra. |
Go deeperGreedy vs DP
| Greedy | DP | |
|---|---|---|
| Use when | A local choice is provably safe (exchange argument) | Choices interact; a locally good choice can hurt later |
| Cost | Usually O(n log n) or O(n) | States x transitions, often O(n^2) or O(n * W) |
| Proof | Exchange argument or "stays ahead" | Recurrence over subproblems |
| Deciding question | Can I find a small counterexample to the greedy rule? If yes, DP. |
Go deeperBacktracking vs DP
| Backtracking | DP | |
|---|---|---|
| Asked for | All solutions, or a construction | A count, a best value, or yes/no |
| Subproblems | Partial paths differ (the path itself matters) | Different paths reach the same state |
| Cost | Exponential, prunable | Polynomial in the number of states |
| Deciding question | Do I need to list the answers, or just measure them? |
Go deeperHeap vs sorting
| Heap | Sorting | |
|---|---|---|
| Use when | Data arrives over time, or you need only the top k | All data is known and you need full order |
| Cost | O(n log k) for top k | O(n log n) |
| Breaks when | You need arbitrary access to the middle | Data keeps changing (re-sort each time) |
| Deciding question | Do I need all of the order, or just the current extreme? |
Go deeperTrie vs HashMap
| Trie | HashMap / HashSet | |
|---|---|---|
| Use when | Prefix queries, autocomplete, wildcard search, sharing work across many words | Exact-match lookups |
| Cost | O(L) per word, high memory | O(L) to hash a string of length L, low overhead |
| Deciding question | Will I ask about prefixes, or only whole keys? |
Go deeperTree DP vs ordinary tree recursion
| Ordinary recursion | Tree DP | |
|---|---|---|
| Return value | One value (height, sum, found node) | A small tuple of states per node (take/skip, covered/not covered) |
| Use when | The parent needs one fact about each child | The parent's choice depends on the child's choice (adjacent nodes cannot both be picked) |
| Example | Max depth, diameter, LCA | House Robber III, Binary Tree Cameras |
| Deciding question | Does the parent need to know how the child decided, not just what it got? |
Go deeperRepeated computation vs memoization
| Plain recursion | Memoized recursion | |
|---|---|---|
| Use when | Every call has distinct arguments (tree traversal, backtracking) | The same arguments recur (Fibonacci, grid paths, word break) |
| Detect it | Draw the call tree for a small input: are there duplicate nodes? | |
| Cost | Number of calls in the tree | Number of distinct states x work per state |
| Deciding question | Does the function's result depend only on its arguments, and do arguments repeat? |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 2: Arrays, strings, and hashing
Arrays and strings are the input of most interview problems. This module covers the tools that make array work fast: in-place pointer techniques, hashing, prefix sums, and Kadane's running-best idea.
2.1Arrays and in-place modification
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Many problems ask you to rearrange, filter, or transform an array without allocating another one ("in place", "O(1) extra space"). The difficulty is not the operation; it is overwriting values you still need.
Picture tidying a bookshelf from left to right with two hands. One hand (the read pointer) looks at every book; the other (the write pointer) marks where the next book you keep should go. The write hand never gets ahead of the read hand, so you never overwrite a book you have not looked at yet.
"In place", "O(1) extra memory", "return the new length", "do not allocate another array", or matrix rotation and spiral orders.
Go deeperThe read/write pointer technique
The invariant is what makes this safe: nums[0..write) holds exactly the kept elements seen so far, in their original order. Because write <= read always, writing at write only overwrites a position that has already been read.
// LC 283. write = next position for a kept (non-zero) element.
static void moveZeroes(int[] nums) {
int write = 0;
for (int read = 0; read < nums.length; read++) {
if (nums[read] != 0) {
int tmp = nums[write]; nums[write] = nums[read]; nums[read] = tmp;
write++;
}
}
}
The same skeleton solves "remove element" (LC 27), "remove duplicates from sorted array" (LC 26, keep nums[read] when it differs from nums[write - 1]), and string compression (LC 443).
Go deeperMatrices and two-dimensional arrays
In Java, int[][] m is an array of row arrays: m.length is the number of rows, m[0].length the number of columns, and rows can even have different lengths. Iterate rows in the outer loop for cache-friendly access.
Three moves cover most matrix problems:
- Index mapping. Transpose maps
(r, c)to(c, r). A flat indexiin anR x Cmatrix maps to(i / C, i % C). That lets you binary search a sorted matrix as one array (LC 74). - Layer or boundary walking. Spiral order walks four boundaries (
top,bottom,left,right) and shrinks them after each side. - Composition of simple transforms. A clockwise rotation is a transpose followed by reversing each row; counterclockwise is transpose then reverse each column.
// LC 48. Clockwise rotation = transpose, then reverse each row.
static void rotate(int[][] m) {
int n = m.length;
for (int r = 0; r < n; r++)
for (int c = r + 1; c < n; c++) { // upper triangle only
int tmp = m[r][c]; m[r][c] = m[c][r]; m[c][r] = tmp;
}
for (int[] row : m)
for (int left = 0, right = n - 1; left < right; left++, right--) {
int tmp = row[left]; row[left] = row[right]; row[right] = tmp;
}
}
// LC 54. Four boundaries shrink inward; re-check after the turn.
static List<Integer> spiralOrder(int[][] m) {
List<Integer> out = new ArrayList<>();
int top = 0, bottom = m.length - 1, left = 0, right = m[0].length - 1;
while (top <= bottom && left <= right) {
for (int c = left; c <= right; c++) out.add(m[top][c]);
top++;
for (int r = top; r <= bottom; r++) out.add(m[r][right]);
right--;
if (top <= bottom) {
for (int c = right; c >= left; c--) out.add(m[bottom][c]);
bottom--;
}
if (left <= right) {
for (int r = bottom; r >= top; r--) out.add(m[r][left]);
left++;
}
}
return out;
}
The spiral's two if checks matter: after walking the top row and right column, the remaining region may be a single row or a single column, and without the checks you would walk it twice.
Using the matrix as its own scratch space. When O(1) space is required, store markers inside the input: the first row and column in Set Matrix Zeroes (LC 73), or an extra bit per cell in Game of Life (LC 289, encode "was alive, now dead" as a state number).
Common mistakes
- Advancing
writefor every element instead of only for kept ones. - Swapping
m[r][c]andm[c][r]for everycinstead of onlyc > r, which transposes twice and undoes the work. - Mixing up
m.lengthandm[0].lengthon non-square matrices (LC 867). - Off-by-one in boundary walks: decide whether boundaries are inclusive and keep that everywhere.
In-place array and matrix manipulation
8 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2149Rearrange Array Elements by Sign Two write pointers, one per class, into a fresh array | Medium | Medium | |
| 498Diagonal Traverse All cells on one diagonal share r + c; alternate direction | Medium | Medium | |
| 189Rotate Array Three reversals rotate in O(1) extra space | Medium | High | |
| 48Rotate Image Rotation = transpose + reverse each row | Medium | High | |
| 54Spiral Matrix Shrink four boundaries and re-check after each side | Medium | High | |
| 73Set Matrix Zeroes Reuse first row and column as marker storage | Medium | Medium | |
| 289Game of Life Encode old and new state in the same cell | Medium | Medium | |
| 31Next Permutation Find the rightmost ascent, swap, reverse the suffix | Medium | Medium |
2.2Hashing: HashMap and HashSet patterns
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
The most common source of wasted work is an inner loop that searches for something: a complement, a duplicate, a previous occurrence. A hash map answers "have I seen X, and where, and how often?" in O(1) average time, turning O(n^2) into O(n).
A coat check. Instead of searching every hook for your coat, you hand over a ticket number and get the coat back instantly. The key is the ticket; the value is what you stored.
- Signal
- You keep asking 'have I seen X?', 'how many X?', or 'where is X?' inside a loop. Pairs that must sum or match. Grouping by an equivalence (anagrams, same pattern).
- Required condition
- A key that identifies what you are looking for. Order of elements does not matter for the lookup itself.
Mental model, core operation, limits, and what to use instead
- Mental model
- A notebook keyed by value: before scanning again, check the notebook.
- Core operation
- For each element: query the map for its complement or key, then record the element.
- Complexity benefit
- Replaces an O(n) inner search with O(1) average lookup: O(n^2) to O(n).
- Limitations
- No ordering or range queries; O(n) extra memory; worst-case collisions; mutable keys break it.
- Alternative
- Sorting + two pointers (O(1) extra space), TreeMap for ordered or range queries, a counting array for small value ranges.
Go deeperThe four hashing moves
| Move | Question it answers | Java idiom |
|---|---|---|
| Seen set | Have I seen this value before? | if (!seen.add(x)) duplicate (add returns false if present) |
| Complement map | Is there an earlier element that pairs with this one? | map.get(target - x) then map.put(x, i) |
| Frequency map | How many times does each value appear? | freq.merge(x, 1, Integer::sum) |
| Grouping map | Which items share a canonical key? | groups.computeIfAbsent(key, k -> new ArrayList<>()).add(item) |
For lowercase letters or small value ranges, an int[26] or int[maxValue + 1] is a faster, simpler hash map. Two strings are anagrams exactly when their 26-count arrays are equal.
Go deeperBrute force to optimal: Two Sum
The brute force checks every pair: O(n^2). The inner loop's job, in words, is "find an earlier index whose value is target - nums[i]". That is a lookup, so store earlier values in a map.
// LC 1. For each x, ask "have I already seen target - x?"
static int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> indexOf = new HashMap<>(); // value -> index
for (int i = 0; i < nums.length; i++) {
int need = target - nums[i];
Integer j = indexOf.get(need); // null if absent
if (j != null) return new int[]{j, i};
indexOf.put(nums[i], i); // insert AFTER the check
}
return new int[0];
}
Inserting after the check matters: it prevents pairing an element with itself ([3] with target 6), while still finding [3, 3] because the first 3 is in the map when the second arrives.
Go deeperCanonical keys for grouping
Grouping problems need a key that is identical for items that belong together and different otherwise. For anagrams, the sorted letters ("eat" -> "aet") or the 26 counts joined into a string. For "same pattern" problems (LC 290, 205), map each item to the sequence of first-occurrence indices.
// LC 49. Canonical key: the sorted letters of each word.
static List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> groups = new HashMap<>();
for (String s : strs) {
char[] chars = s.toCharArray();
Arrays.sort(chars);
String key = new String(chars);
groups.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
}
return new ArrayList<>(groups.values());
}
Go deeperJava hashing pitfalls
- Arrays are not valid keys.
int[]uses identityhashCode, so two equal arrays are different keys. UseArrays.toString(arr), aString, orList<Integer>. - Never mutate a key after putting it in a map; it becomes unreachable.
Integercomparison with==works only for cached values in -128..127. Use.equalsor unbox:map.get(a).intValue() == map.get(b).map.getreturns null for missing keys; unboxing null intointthrowsNullPointerException. UsegetOrDefault.- Iteration order of
HashMapis unspecified. UseLinkedHashMapfor insertion order orTreeMapfor sorted order.
Guided solveLC 128. Longest Consecutive SequenceFive questions, each with a reveal
Problem: 128Longest Consecutive SequenceMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Given an unsorted array, return the length of the longest set of values that form consecutive integers (
x, x+1, x+2, ...), in any positions. Must run in O(n).Input analysis
Unsorted, duplicates possible, negatives allowed, n up to 10^5. Order in the array does not matter: this is about the set of values.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Order does not matter" plus "O(n) required" rules out sorting (O(n log n)) and points at hashing. The repeated question is "is
x + 1present?"Brute force
Sort and scan for runs: O(n log n), correct but over the stated budget. A naive hashing version, "for every x, count upward while x + 1 exists", is O(n^2) on
[1, 2, ..., n]because every element re-walks the rest of the run. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. only start counting at the beginning of a run
Key observation
Each run should be counted once, from its smallest element.
xis the start of a run exactly whenx - 1is not in the set. Skip every other element.Visualization
set = {100, 4, 200, 1, 3, 2} x = 100: 99 absent -> start; 101 absent -> length 1 x = 4: 3 present -> not a start, skip x = 200: 199 absent -> start; length 1 x = 1: 0 absent -> start; 2, 3, 4 present -> length 4 x = 3: 2 present -> skip x = 2: 1 present -> skip best = 4 - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Put all values in a set. For each value in the set: if
x - 1is present, skip. Otherwise count up whilex + lengthis present and update the best.Java implementation
Java// LC 128. O(n): only start counting at x when x - 1 is absent. static int longestConsecutive(int[] nums) { Set<Integer> set = new HashSet<>(); for (int x : nums) set.add(x); int best = 0; for (int x : set) { // iterate the set to skip duplicates if (set.contains(x - 1)) continue; // x is not a sequence start int length = 1; while (set.contains(x + length)) length++; best = Math.max(best, length); } return best; }Dry run
Shown in the visualization. Note that the inner
whileloop walked 1, 2, 3, 4 once in total, not once per element. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
Time O(n): each value is inserted once, and each value is visited by an inner
whileloop at most once (only from its run's start). Space O(n) for the set.Edge cases
Empty array (return 0, the loop never runs); duplicates (iterating the set, not the array, avoids re-counting); negatives (
x - 1works the same). Overflow: the constraints keep values within plus or minus 10^9, sox + lengthcannot overflow. If values could reachInteger.MAX_VALUE,x + lengthwould wrap toInteger.MIN_VALUEand could falsely continue a run, so you would compute withlong.Transfer lesson
"Start work only at a canonical starting point" turns nested-looking loops into linear ones. The same counting argument appears in sliding windows and monotonic stacks: bound the total inner-loop work across the run.
Common mistakes
- Using a list (
containsis O(n)) where a set was needed. - Inserting before checking in complement problems, which pairs an element with itself.
- Counting with
map.put(x, map.get(x) + 1)on a missing key:NullPointerException. - Using
chararithmetic on uppercase or non-letter input withc - 'a': check the character set in the constraints.
Hashing and frequency counting
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 1679Max Number of K-Sum Pairs Count complements in a map, pair them off and decrement | Medium | High | |
| 2352Equal Row and Column Pairs Use a row as a hash key, then look up each column | Medium | Medium | |
| 49Group Anagrams Design a canonical key so equivalent items collide on purpose | Medium | High | |
| 128Longest Consecutive Sequence Start counting only at sequence starts to stay O(n) | Medium | High | |
| 36Valid Sudoku Encode (row, col, box) membership as set keys | Medium | Medium | |
| 4544Sum II Meet in the middle: hash all pair sums of two arrays | Medium | Medium | |
| 554Brick Wall Count edge positions; answer is rows minus the most common edge | Medium | Medium | |
| 1657Determine if Two Strings Are Close Compare multisets of frequencies, not the strings | Medium | Medium | |
| 41First Missing Positive Use the array itself as the hash table (index = value - 1) | Hard | High |
2.3Prefix sums, suffix sums, and difference arrays
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Sums over ranges, asked many times, or counting subarrays whose sum meets a condition. Recomputing each range is O(n) per query; prefix sums make it O(1).
Mileposts on a highway. To know the distance from milepost 12 to milepost 47, you do not drive it again; you subtract the two readings.
- Signal
- Sum (or count, XOR) over a contiguous range, asked many times. 'Subarray sum equals / divisible by k'. Negative numbers allowed.
- Required condition
- An invertible combine operation (sum, XOR, count): range = prefix[r+1] - prefix[l]. Max and min are NOT invertible.
Mental model, core operation, limits, and what to use instead
- Mental model
- An odometer: distance between two mileposts is the difference of their readings.
- Core operation
- Build cumulative totals once; answer a range with one subtraction. With a hash map: count earlier prefixes equal to current - k.
- Complexity benefit
- Each range query drops from O(n) to O(1); counting subarrays drops from O(n^2) to O(n).
- Limitations
- Static data (updates force a rebuild); does not answer max/min ranges; watch int overflow.
- Alternative
- Sliding window when all values are non-negative; difference array for many range updates; Fenwick or segment tree when the array changes.
// prefix[i] = sum of nums[0..i-1]; prefix has length n + 1.
// Sum of nums[l..r] (inclusive) = prefix[r + 1] - prefix[l].
static long[] buildPrefix(int[] nums) {
long[] prefix = new long[nums.length + 1];
for (int i = 0; i < nums.length; i++) prefix[i + 1] = prefix[i] + nums[i];
return prefix;
}
static long rangeSum(long[] prefix, int l, int r) {
return prefix[r + 1] - prefix[l];
}
Why length n + 1? With prefix[0] = 0, the sum of nums[l..r] is always prefix[r + 1] - prefix[l], with no special case for l = 0. Use long when n * maxValue can exceed about 2.1 x 10^9.
Go deeperPrefix sums with a hash map: counting subarrays
A subarray nums[j+1..i] has sum prefix(i) - prefix(j). Asking "does it equal k?" for every pair is O(n^2). Rearranged, the question for each i becomes "how many earlier prefixes equal prefix(i) - k?", which a frequency map answers in O(1). Variations change only what you store and look up:
| Problem | Transform | Look up | Store |
|---|---|---|---|
| Sum equals k (LC 560) | none | count of prefix - k |
count of each prefix |
| Divisible by k (LC 974) | prefix mod k (fix negatives: ((p % k) + k) % k) |
count of the same remainder | count of each remainder |
| Equal 0s and 1s (LC 525) | 0 becomes -1, find sum 0 | first index of the same prefix | first index of each prefix (longest wants the earliest start) |
| Sum multiple of k, length >= 2 (LC 523) | prefix mod k |
first index of the same remainder | first index only |
Go deeperSuffix sums and prefix products
When an answer depends on everything to the left and everything to the right, compute both passes. Product of Array Except Self (LC 238) is the classic: the left products fill the output array, then a single running variable multiplies in the right products, giving O(1) extra space without division.
// LC 238. answer[i] = (product left of i) * (product right of i).
static int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] answer = new int[n];
answer[0] = 1;
for (int i = 1; i < n; i++) answer[i] = answer[i - 1] * nums[i - 1]; // left products
int right = 1;
for (int i = n - 1; i >= 0; i--) {
answer[i] *= right; // multiply in the product of everything to the right
right *= nums[i];
}
return answer;
}
Go deeperDifference arrays: many range updates
If you must add x to every element in [l, r] for many updates and read the result once, do not loop over the range. Record where the effect starts and stops, then take one prefix sum at the end.
n = 5, updates: +10 on [0,1], +20 on [1,2], +25 on [1,4] diff after updates : [10, 45, -10, -20, 0, -25] (index 5 is the extra slot) running prefix : [10, 55, 45, 25, 25]
// LC 1109. Add seats to ranges, then read final totals.
// diff[l] += x starts the effect; diff[r + 1] -= x stops it.
static int[] corpFlightBookings(int[][] bookings, int n) {
int[] diff = new int[n + 1]; // 1-based flights, extra slot for r + 1
for (int[] b : bookings) {
int first = b[0] - 1, last = b[1] - 1, seats = b[2];
diff[first] += seats;
diff[last + 1] -= seats;
}
int[] result = new int[n];
int running = 0;
for (int i = 0; i < n; i++) {
running += diff[i];
result[i] = running;
}
return result;
}
Guided solveLC 560. Subarray Sum Equals KFive questions, each with a reveal
Problem: 560Subarray Sum Equals KMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Count contiguous subarrays whose elements sum to exactly
k.Input analysis
n up to 2 * 10^4, values between -1000 and 1000 (negatives allowed), k between -10^7 and 10^7.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Contiguous" and "sum" suggest a sliding window, but negatives break the window's monotonicity: adding an element can lower the sum. "Exact sum" with negatives is the signal for prefix sums plus hashing.
Brute force
For each start, extend the end with a running sum and count hits: O(n^2) time, O(1) space. 4 * 10^8 operations at the limits: too slow in the worst case, and it hides the repeated question.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. count earlier prefixes equal to prefix - k
Key observation
For a fixed end
i, the brute force asks of every start "isprefix(i) - prefix(j) == k?" That is a single lookup: count how many earlier prefixes equalprefix(i) - k.Visualization
nums = [1, 2, 1, -1, 2],k = 3. The map starts with{0: 1}for the empty prefix.i nums[i] prefix need = prefix - k count[need] total map after insert 0 1 1 -2 0 0 {0:1, 1:1} 1 2 3 0 1 1 {0:1, 1:1, 3:1} 2 1 4 1 1 2 {..., 4:1} 3 -1 3 0 1 3 {..., 3:2} 4 2 5 2 0 3 {..., 5:1} The three subarrays:
[1,2](i=1),[2,1](i=2, from the prefix 1),[1,2,1,-1](i=3, from the empty prefix). - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Running prefix; for each element, add
count[prefix - k]to the answer, then incrementcount[prefix]. Seedcount[0] = 1.Java implementation
Java// LC 560. Count subarrays with sum exactly k. Works with negative numbers. // sum(j+1..i) = prefix(i) - prefix(j) = k <=> prefix(j) = prefix(i) - k static int subarraySum(int[] nums, int k) { Map<Integer, Integer> seen = new HashMap<>(); // prefix sum -> how many times seen.put(0, 1); // empty prefix int prefix = 0, count = 0; for (int x : nums) { prefix += x; count += seen.getOrDefault(prefix - k, 0); // look up BEFORE inserting seen.merge(prefix, 1, Integer::sum); } return count; }Dry run
The table above is the dry run. Check
[1, -1, 0]withk = 0: prefixes 1, 0, 0 give hits 0, 1 (empty prefix), 2 (empty prefix and index 1): total 3, which matches[1,-1],[1,-1,0],[0]. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(n) space for the map.
Edge cases
Forgetting
seen.put(0, 1)misses every subarray that starts at index 0. Inserting before looking up miscounts whenk = 0(a prefix would match itself, counting an empty subarray). Large sums: values here fit inint, but with bigger values uselongkeys.Transfer lesson
"Count pairs (j, i) with a relationship between f(j) and f(i)" is a hash map over f. It reappears in LC 974 (remainders), LC 525 (balance), LC 1442 (prefix XOR), and LC 437 (prefix sums along a tree path).
Common mistakes
- Forgetting the leading zero (empty prefix).
- Overflow in prefix arrays: use
long. - Java's
%returns negative results for negative numbers:-1 % 5 == -1. UseMath.floorModor((x % k) + k) % k. - Storing the latest index instead of the first when the question asks for the longest subarray.
Prefix sums and difference arrays
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2270Number of Ways to Split Array Left sum vs total - left at every split point | Medium | High | |
| 238Product of Array Except Self Prefix and suffix products without division | Medium | High | |
| 560Subarray Sum Equals K Count earlier prefixes equal to current - k (works with negatives) | Medium | High | |
| 974Subarray Sums Divisible by K Group prefixes by remainder; fix negative mod in Java | Medium | Medium | |
| 525Contiguous Array Map 0 to -1, then find equal prefix sums (first index wins) | Medium | Medium | |
| 523Continuous Subarray Sum Equal remainders two or more apart give a multiple of k | Medium | High | |
| 304Range Sum Query 2D - Immutable 2D prefix with inclusion-exclusion | Medium | Medium | |
| 1109Corporate Flight Bookings Difference array: +x at l, -x at r+1, then prefix | Medium | Medium | |
| 2381Shifting Letters II Difference array over string shifts with modulo | Medium | Medium | |
| 1074Number of Submatrices That Sum to Target Fix two rows, reduce to 1D subarray-sum-equals-k | Hard | Medium |
2.4Maximum subarray: Kadane's algorithm
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
The best (maximum or minimum) sum of a contiguous subarray in one pass, and the family of "running best" problems like Best Time to Buy and Sell Stock.
You are walking a path collecting coins, some negative. At each step you decide: keep the bag you have been filling, or empty it and start fresh here. You start fresh exactly when the bag's total has become a burden (negative).
- Signal
- Maximum (or minimum) sum or product of a contiguous subarray. 'Best time to buy and sell once'.
- Required condition
- The best answer ending at i can be built from the best answer ending at i - 1 plus nums[i].
Mental model, core operation, limits, and what to use instead
- Mental model
- Walking with a running score: when the carried total hurts more than it helps, drop it and restart here.
- Core operation
- endingHere = max(x, endingHere + x); best = max(best, endingHere).
- Complexity benefit
- O(n^2) or O(n^3) subarray enumeration becomes one O(n) pass with O(1) memory.
- Limitations
- Needs contiguity. Products need both max and min. Length constraints break the simple form.
- Alternative
- Prefix sums with a running minimum prefix (same idea), DP with extra states for deletions or circular arrays.
Go deeperFrom brute force to Kadane
- All subarrays with a fresh sum each: O(n^3).
- Running sum per start: O(n^2).
- Define
best(i)= the best sum of a subarray ending exactly at i. Any such subarray either is justnums[i], or extends the best subarray ending ati - 1. Sobest(i) = max(nums[i], best(i - 1) + nums[i]). The answer is the maximum ofbest(i)over alli. O(n), and sincebest(i)only needsbest(i - 1), O(1) space.
This is your first dynamic programming recurrence. Module 14 generalizes the same move: define the answer "ending here", then build it from the previous one.
// LC 53. best ending here = max(start fresh at x, extend previous run with x).
static int maxSubArray(int[] nums) {
int endingHere = nums[0], best = nums[0];
for (int i = 1; i < nums.length; i++) {
endingHere = Math.max(nums[i], endingHere + nums[i]);
best = Math.max(best, endingHere);
}
return best;
}
Go deeperVariations
| Problem | Change |
|---|---|
| Best Time to Buy and Sell Stock (LC 121) | Track the minimum price so far; profit = price - min. Same running-best shape. |
| Maximum Product Subarray (LC 152) | Keep both max and min ending here, because a negative flips them. |
| Circular array (LC 918) | Answer = max(normal Kadane, total - minimum subarray), unless all numbers are negative. |
| One deletion allowed (LC 1186) | Two states: best ending here with no deletion, and with one deletion used. |
// LC 152. A negative number turns the smallest product into the largest.
static int maxProduct(int[] nums) {
int maxHere = nums[0], minHere = nums[0], best = nums[0];
for (int i = 1; i < nums.length; i++) {
int x = nums[i];
int candMax = Math.max(x, Math.max(maxHere * x, minHere * x));
int candMin = Math.min(x, Math.min(maxHere * x, minHere * x));
maxHere = candMax;
minHere = candMin;
best = Math.max(best, maxHere);
}
return best;
}
Guided solveLC 53. Maximum SubarrayFive questions, each with a reveal
Problem: 53Maximum SubarrayMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Return the largest sum of any non-empty contiguous subarray.
Input analysis
n up to 10^5, values from -10^4 to 10^4. Negatives allowed, so the answer can be negative. The maximum sum fits in
int(10^5 * 10^4 = 10^9). - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Contiguous plus "maximum sum" plus negatives. A sliding window does not apply (no monotonic validity). The question "what is the best subarray ending here?" has a one-step recurrence.
Brute force
O(n^2) with running sums per start. The repeated work: for each end, we reconsider every start, although only the best start matters.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. best sum ending at i depends only on best sum ending at i - 1
Key observation
For a fixed end
i, the best start is eitheriitself or the best start fori - 1. So the whole history collapses into one number: the best sum ending ati - 1.Visualization
See the figure above: values on top, best-ending-here below each index.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
endingHere = nums[0],best = nums[0]. For each later element:endingHere = max(x, endingHere + x);best = max(best, endingHere).Java implementation
The code above.
Dry run
[-3, -1, -2]: endingHere = -3, then max(-1, -4) = -1, then max(-2, -3) = -2. best = -1. Startingbestat 0 instead ofnums[0]would wrongly return 0. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(1) space.
Edge cases
All negative (answer is the largest single element); single element; very long runs of zeros.
Transfer lesson
"Best answer ending exactly at i" is the most reusable DP state there is: LIS, longest valid parentheses, and many string problems use it. The follow-up "return the subarray itself" needs one extra variable: the start index, reset whenever you start fresh.
Common mistakes
- Initializing
best = 0, which fails for all-negative input. - Resetting the running sum to 0 when it goes negative before updating the best, which loses single negative answers.
- Forgetting the min track in product problems.
Maximum subarray (Kadane) and running best
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2606Find the Substring With Maximum Cost Map characters to values, then run Kadane with an empty option | Medium | Medium | |
| 53Maximum Subarray Extend the current run or restart at this element | Medium | High | |
| 1567Maximum Length of Subarray With Positive Product Track lengths of the positive and negative products ending here | Medium | Medium | |
| 152Maximum Product Subarray Keep both max and min because negatives flip signs | Medium | High | |
| 918Maximum Sum Circular Subarray Circular max = total - minimum subarray (all-negative edge case) | Medium | Medium | |
| 1749Maximum Absolute Sum of Any Subarray Run max-Kadane and min-Kadane together | Medium | Medium | |
| 1014Best Sightseeing Pair Split the score into a part for i and a part for j | Medium | Medium | |
| 1186Maximum Subarray Sum with One Deletion Two states: no deletion used vs one deletion used | Medium | Medium | |
| 1191K-Concatenation Maximum Sum Reason about how many copies can help; modulo at the end | Medium | Medium | |
| 2321Maximum Score Of Spliced Array Swap gain is a max-subarray of the difference array, both ways | Hard | Medium |
2.5Strings and character processing
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Strings add a few Java-specific costs and a few string-specific patterns: palindromes, parsing, and character counting.
Java essentials.
Stringis immutable.s += cinside a loop creates a new string each time: O(n^2) overall. UseStringBuilder(append,insert,reverse,setCharAt,deleteCharAt).s.charAt(i)is O(1).s.toCharArray()copies once and is convenient for in-place work.s.substring(a, b)copiesb - acharacters (since Java 7u6). Calling it inside a loop adds a factor of the substring length.- Character arithmetic:
c - 'a'maps lowercase letters to 0..25;(char) ('a' + i)maps back;c - '0'turns a digit character into its value.Character.isLetterOrDigit,Character.toLowerCasehandle the rest. - Compare strings with
.equals, never==.
// Frequently used string idioms
static String stringIdioms(String s) {
int[] freq = new int[26];
for (char c : s.toCharArray()) freq[c - 'a']++; // assumes lowercase a-z
StringBuilder sb = new StringBuilder(); // String is immutable: s + c in a loop is O(n^2)
for (int i = 0; i < 26; i++)
if (freq[i] > 0) sb.append((char) ('a' + i)).append(freq[i]);
String[] words = " the sky is ".trim().split("\\s+"); // regex: one or more spaces
return sb.toString() + ":" + String.join(",", words);
}
Go deeperPalindromes: expand around the center
A palindrome mirrors around its center. There are 2n - 1 possible centers: each character (odd length) and each gap between characters (even length). From each center, expand while the two ends match. This is O(n^2) time and O(1) space, simpler than the O(n^2)-space DP and fast enough for n up to a few thousand.
// LC 5. Expand around each of the 2n - 1 centers (characters and gaps).
static String longestPalindrome(String s) {
int bestStart = 0, bestLen = 0;
for (int center = 0; center < s.length(); center++) {
int odd = expand(s, center, center); // "aba"
int even = expand(s, center, center + 1); // "abba"
int len = Math.max(odd, even);
if (len > bestLen) {
bestLen = len;
bestStart = center - (len - 1) / 2;
}
}
return s.substring(bestStart, bestStart + bestLen);
}
// Returns the length of the longest palindrome centered between left and right.
private static int expand(String s, int left, int right) {
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1; // both pointers stopped one step past the palindrome
}
The returned length right - left - 1 is correct because both pointers stop one step outside the palindrome. Counting palindromic substrings (LC 647) uses the same loop and adds 1 for every successful expansion step.
Go deeperParsing step by step
Problems like String to Integer (LC 8) are about discipline, not algorithms. Process in phases (whitespace, sign, digits, stop), and check overflow before it happens: if (result > (Integer.MAX_VALUE - digit) / 10) then clamp.
Common mistakes
- Building strings with
+in loops. - Treating
split(" ")as safe when there are repeated spaces: it produces empty strings. Usetrim().split("\\s+"). - Assuming lowercase input when the constraints allow any ASCII characters: use
int[128]or a map.
String manipulation and palindromes
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 151Reverse Words in a String Tokenize carefully around repeated spaces | Medium | High | |
| 6Zigzag Conversion Simulate rows with a direction flip; know the period 2n - 2 | Medium | Medium | |
| 165Compare Version Numbers Compare revision by revision, treating missing parts as 0 | Medium | Medium | |
| 443String Compression Read pointer and write pointer on a char array | Medium | Medium | |
| 8String to Integer (atoi) Parse step by step and guard overflow before it happens | Medium | Medium | |
| 5Longest Palindromic Substring Expand around 2n - 1 centers | Medium | High | |
| 647Palindromic Substrings Count expansions instead of tracking the longest | Medium | Medium | |
| 43Multiply Strings Digit i * digit j lands at positions i + j and i + j + 1 | Medium | Medium | |
| 68Text Justification Greedy packing plus careful space distribution | Hard | Medium |
2.6Choosing between arrays, sorting, and hashing
Return true if any value in an array appears at least twice. Give three solutions with their time and extra space.
Reveal the answer
Check every pair: O(n^2) time, O(1) space. Sort, then compare neighbors: O(n log n) time, O(1) extra, and the input is reordered. A HashSet of seen values: O(n) average time, O(n) space. Which one wins depends on n, memory, and whether positions matter: that choice is this lesson.
When a problem can be solved several ways, compare them on the two resources and on what each preserves.
| Need | Counting array | Hash map / set | Sort first |
|---|---|---|---|
| Values in a small known range (letters, 0..10^4) | Best: fastest, simplest | Works | Unnecessary |
| Arbitrary values, membership or counts | Not possible | Best: O(n) average | O(n log n), O(1) extra |
| Original indices must be returned | Works with index storage | Best | Must sort (value, index) pairs |
| Neighbors in value order, ranges, closest pairs | No | No | Best |
| Memory is tight | O(range) | O(n) | O(1) to O(log n) |
| Deterministic worst-case time | Yes | Average only | Yes |
A good interview answer names the trade-off: "A hash set gives O(n) time and O(n) space. If we cannot afford the memory, sorting gives O(n log n) time with O(1) extra space, but we lose the original order."
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 3: Two pointers and sliding window
Both techniques replace "check every pair" or "check every subarray" with two indices that only move forward (or toward each other). Each index moves at most n times, so O(n^2) becomes O(n).
3.1Two pointers
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Finding pairs or triplets that meet a target, partitioning an array in place, comparing a sequence with its reverse. The brute force checks all O(n^2) pairs; two pointers checks O(n) pairs and proves the rest cannot matter.
Two people search a sorted bookshelf for two books whose page counts add up to exactly 500. One starts at the thinnest book, the other at the thickest. If their sum is too small, only making the left book thicker can help; if too big, only making the right book thinner can help. Each step permanently rules out one book.
- Signal
- Sorted array or string; pairs or triplets meeting a target; palindromes; in-place removal or partitioning.
- Required condition
- Moving one pointer must safely discard candidates. Usually needs sorted order or a symmetric structure.
Mental model, core operation, limits, and what to use instead
- Mental model
- Two fingers on a ruler: the comparison tells you which finger can move without missing the answer.
- Core operation
- Compare the elements at the pointers, move the pointer that cannot be part of a better answer.
- Complexity benefit
- Checks all useful pairs in O(n) instead of O(n^2), with O(1) extra space.
- Limitations
- Unsorted input needs an O(n log n) sort first (and loses original indices). No safe discard rule, no pattern.
- Alternative
- Hash map for unsorted pair search; sliding window when the pair bounds a contiguous range with a running condition.
Go deeperOpposite-direction pointers: why moving is safe
At positions (L, R) on sorted data with sum < target: for every k <= R, nums[L] + nums[k] <= nums[L] + nums[R] < target. So nums[L] cannot pair with anything still in range, and L can move. The symmetric argument handles sum > target. This "eliminate one candidate entirely" argument is the correctness proof; say it in the interview.
The same reasoning drives Container With Most Water (LC 11): the area is limited by the shorter wall, so keeping the shorter wall and moving the taller one inward can only shrink the width without raising the limit. Move the shorter one.
// LC 11. Area is limited by the shorter wall, so moving the taller wall
// inward can only shrink width without raising the limit.
static int maxArea(int[] height) {
int left = 0, right = height.length - 1, best = 0;
while (left < right) {
int h = Math.min(height[left], height[right]);
best = Math.max(best, h * (right - left));
if (height[left] < height[right]) left++;
else right--;
}
return best;
}
Go deeperThree-way partitioning
When elements fall into three classes (less than, equal to, greater than a pivot; or colors 0, 1, 2), keep three regions and one unknown region in the middle. Each step shrinks the unknown region by one.
[0, low) all 0s [low, mid) all 1s [mid, high] unknown (high, n-1] all 2s
// LC 75. Invariant: [0, low) = 0s, [low, mid) = 1s, (high, n-1] = 2s, [mid, high] unknown.
static void sortColors(int[] nums) {
int low = 0, mid = 0, high = nums.length - 1;
while (mid <= high) {
if (nums[mid] == 0) swap(nums, low++, mid++);
else if (nums[mid] == 1) mid++;
else swap(nums, mid, high--); // do NOT advance mid: the swapped-in value is unknown
}
}
private static void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }
When nums[mid] == 2 is swapped with nums[high], the value that arrives at mid has not been examined yet, so mid stays. When nums[mid] == 0 is swapped with nums[low], the arriving value is a known 1 (or low == mid), so both advance.
Go deeperSame-direction pointers
When both pointers move left to right at different speeds, the slow one usually marks the end of a "kept" or "processed" region and the fast one scans. You met this as read/write pointers in Module 2. Two other forms:
- Merging two sorted sequences: one pointer per sequence, always advance the one with the smaller current value.
- Fast and slow pointers on linked lists and implicit sequences (cycle detection, middle element): covered in Module 6.
Guided solveLC 15. 3SumFive questions, each with a reveal
Problem: 153SumMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Return all unique triplets
[a, b, c]from the array witha + b + c = 0. Triplets are sets of values; the same values in a different order count once.Input analysis
n up to 3000, values from -10^5 to 10^5, duplicates allowed. O(n^2) = 9 * 10^6 is fine; O(n^3) = 2.7 * 10^10 is not.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Pairs/triplets summing to a target" plus "order of output does not matter" means we may sort. Once sorted, for a fixed first element the rest is Two Sum II, which two pointers solve in O(n).
Brute force
Three nested loops plus a set of sorted triplets to remove duplicates: O(n^3).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. fix one element, two-pointer the rest, skip duplicates at every level
Key observation
Fix the smallest element
nums[i]. The remaining two must sum to-nums[i]withinnums[i+1..], which is sorted: two pointers. Duplicates come from equal values at the same "role", so skip equal values fori, and after a hit skip equal values forleft.Visualization
sorted: [-4, -1, -1, 0, 1, 2] i=0 (-4): need 4. L=-1,R=2 -> 1 <4 L++ ... no pair i=1 (-1): need 1. L=-1(idx2),R=2 -> sum 0: hit [-1,-1,2]; L->0,R->1: sum 0: hit [-1,0,1] i=2 (-1): same value as i=1 -> skip i=3 (0): need 0. L=1,R=2 -> 3 > 0, R-- -> L==R stop
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Sort. For each
i(skip if equal tonums[i-1], stop ifnums[i] > 0): two pointers on[i+1, n-1]; on a hit, record, move both pointers, and skip duplicateleftvalues.Java implementation
Java// LC 15. Sort, fix nums[i], then find pairs in nums[i+1..] that sum to -nums[i]. static List<List<Integer>> threeSum(int[] nums) { Arrays.sort(nums); List<List<Integer>> result = new ArrayList<>(); for (int i = 0; i < nums.length - 2; i++) { if (i > 0 && nums[i] == nums[i - 1]) continue; // same first value -> same triplets if (nums[i] > 0) break; // smallest is positive: no zero sum int left = i + 1, right = nums.length - 1; while (left < right) { int sum = nums[i] + nums[left] + nums[right]; if (sum < 0) left++; // need a bigger sum else if (sum > 0) right--; // need a smaller sum else { result.add(List.of(nums[i], nums[left], nums[right])); left++; right--; while (left < right && nums[left] == nums[left - 1]) left++; // skip duplicate } } } return result; }Dry run
The visualization is the dry run. Check
[0, 0, 0, 0]:i = 0, hit[0,0,0], thenleftskips the remaining zeros;i = 1is skipped as a duplicate. One triplet. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
Sorting O(n log n) plus O(n) per
i: O(n^2) time. Extra space O(1) beyond the output (sorting uses O(log n) stack).Edge cases
Fewer than 3 elements (loop does not run). All zeros. No solution. Many duplicates (the skips keep it O(n^2) and the output unique).
Transfer lesson
"k-sum = fix one element + (k-1)-sum" generalizes to 4Sum. The duplicate-skipping rule "skip equal values in the same role" is exactly the rule you will use in backtracking (Subsets II, Combination Sum II).
Common mistakes
- Two pointers on unsorted data for a sum problem: the elimination argument needs order.
- Sorting when the problem asks for original indices (LC 1): sort index pairs or use a map.
- Skipping duplicates with
while (nums[left] == nums[left + 1])before recording, which can skip valid answers and overrun the array. - Using
left <= rightwhen the two pointers must be different elements.
Two pointers (opposite and same direction)
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 167Two Sum II - Input Array Is Sorted Sorted order tells you which pointer to move | Medium | High | |
| 80Remove Duplicates from Sorted Array II Compare with the element two positions back in the kept prefix | Medium | Medium | |
| 153Sum Fix one element, two-pointer the rest, skip duplicates | Medium | High | |
| 11Container With Most Water Moving the taller wall can never help, so move the shorter | Medium | High | |
| 75Sort Colors Three regions with three pointers (Dutch national flag) | Medium | High | |
| 163Sum Closest Same skeleton, track the best distance instead of equality | Medium | Medium | |
| 184Sum Extend 3Sum by one fixed level; use long for the sum | Medium | High | |
| 611Valid Triangle Number Sort, fix the largest side, two-pointer count the rest | Medium | Medium | |
| 881Boats to Save People Pair heaviest with lightest when possible | Medium | Medium | |
| 42Trapping Rain Water Water at i depends on min(maxLeft, maxRight); move the smaller side | Hard | High |
3.2Fixed-size sliding window
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Questions about every contiguous block of exactly k elements: maximum sum, average, number of vowels, whether a block is a permutation of a pattern. Recomputing each block costs O(k), for O(n * k) total; sliding costs O(1) per step.
A camera frame of fixed width panning across a long photograph. Each step, one column enters on the right and one leaves on the left; everything in between is unchanged.
- Signal
- 'Every subarray/substring of length k', anagram or permutation of a pattern, averages over k items.
- Required condition
- Window state can be updated in O(1) when one element enters and one leaves.
Mental model, core operation, limits, and what to use instead
- Mental model
- A frame of width k sliding over a film strip: one frame enters on the right, one leaves on the left.
- Core operation
- Add nums[right]; once right >= k, remove nums[right - k]; evaluate when the window is full.
- Complexity benefit
- O(n * k) recomputation becomes O(n).
- Limitations
- The state must support removal. Max/min of a window needs a monotonic deque, not a single variable.
- Alternative
- Monotonic deque for window max/min; prefix sums for window sums with random access.
Go deeperThe template
for right in 0..n-1:
add nums[right] to the state
if right >= k: remove nums[right - k] from the state
if right >= k - 1: the window [right - k + 1, right] is full -> evaluate
The state can be a sum, a count, or a frequency array. Comparing two int[26] arrays is O(26) = O(1), so anagram-style windows stay linear.
// LC 438. Fixed window of length p.length(); compare counts as it slides.
static List<Integer> findAnagrams(String s, String p) {
List<Integer> result = new ArrayList<>();
int k = p.length();
if (s.length() < k) return result;
int[] need = new int[26], window = new int[26];
for (char c : p.toCharArray()) need[c - 'a']++;
for (int right = 0; right < s.length(); right++) {
window[s.charAt(right) - 'a']++; // element enters
if (right >= k) window[s.charAt(right - k) - 'a']--; // element leaves
if (right >= k - 1 && Arrays.equals(window, need)) result.add(right - k + 1);
}
return result;
}
A useful reframing. "Take k cards from either end" (LC 1423) looks like a choice problem, but taking k from the ends means leaving a contiguous block of n - k in the middle. Minimize that block's sum with a fixed window. When a choice problem fixes how many items you take from the ends, look for the complementary window.
Common mistakes
- Evaluating before the first window is full.
- Removing
nums[right - k + 1]instead ofnums[right - k](the element that just left). - Window maximum with a single variable: when the maximum leaves, you cannot recover the next one. Use a monotonic deque (Module 7).
Fixed-size sliding window
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 1343Number of Sub-arrays of Size K and Average Greater than or Equal to Threshold Fixed window sum compared against threshold * k | Medium | High | |
| 1456Maximum Number of Vowels in a Substring of Given Length Window count with a membership test | Medium | Medium | |
| 438Find All Anagrams in a String Window frequency array compared against the target | Medium | High | |
| 567Permutation in String Same as 438, return on the first match | Medium | High | |
| 1423Maximum Points You Can Obtain from Cards Taking from both ends = leaving a middle window of n - k | Medium | Medium | |
| 2461Maximum Sum of Distinct Subarrays With Length K Window sum plus a frequency map to track distinctness | Medium | Medium | |
| 1052Grumpy Bookstore Owner Base gain plus the best extra gain from one window | Medium | Medium | |
| 2134Minimum Swaps to Group All 1's Together II Circular window whose length is the total count of ones | Medium | Medium | |
| 30Substring with Concatenation of All Words Run word-length-stepped windows from each offset | Hard | Medium |
3.3Variable-size sliding window
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
The longest or shortest contiguous subarray or substring that satisfies a condition, and the number of subarrays that satisfy an "at most" condition.
An accordion: stretch it to the right while the condition holds, squeeze from the left when it breaks. You never move the left hand back, because a window that was invalid stays invalid when you only add to it.
- Signal
- Longest or shortest contiguous subarray/substring satisfying a condition; 'at most k' distinct, zeros, or replacements.
- Required condition
- Monotonic validity: shrinking a valid window keeps it valid (or growing an invalid one keeps it invalid). Typically fails with negative numbers in sum conditions.
Mental model, core operation, limits, and what to use instead
- Mental model
- An accordion: stretch right while it plays, squeeze from the left when it breaks.
- Core operation
- Expand right; while the window is invalid, shrink left; record the answer at the right moment.
- Complexity benefit
- Each index enters and leaves once: O(n) instead of O(n^2) over all windows.
- Limitations
- Breaks when adding an element can make the window either better or worse (sums with negatives).
- Alternative
- Prefix sums + hash map (exact sums with negatives); monotonic deque over prefix sums (shortest sum >= k with negatives); exactly(k) = atMost(k) - atMost(k-1).
Go deeperThree flavors and where the answer is updated
| Goal | Shrink while | Update the answer | Example |
|---|---|---|---|
| Longest valid window | the window is invalid | after the while loop (window is valid again) | LC 3, 424, 1004, 904 |
| Shortest valid window | the window is valid | inside the while loop, before removing nums[left] |
LC 209, 76 |
| Count subarrays (at most) | the window is invalid | add right - left + 1 after the loop: every subarray ending at right with start in [left, right] is valid |
LC 713 |
"Exactly k" is rarely monotonic, but "at most k" is. Use exactly(k) = atMost(k) - atMost(k - 1) (LC 992, 930, 1248).
// LC 3. Longest window with all-distinct characters.
static int lengthOfLongestSubstring(String s) {
Map<Character, Integer> count = new HashMap<>();
int left = 0, best = 0;
for (int right = 0; right < s.length(); right++) {
char in = s.charAt(right);
count.merge(in, 1, Integer::sum);
while (count.get(in) > 1) { // window became invalid
char out = s.charAt(left++);
count.merge(out, -1, Integer::sum);
}
best = Math.max(best, right - left + 1); // window is valid here
}
return best;
}
// LC 209. Shortest window with sum >= target (all numbers positive).
static int minSubArrayLen(int target, int[] nums) {
int left = 0, sum = 0, best = Integer.MAX_VALUE;
for (int right = 0; right < nums.length; right++) {
sum += nums[right];
while (sum >= target) { // valid: record, then try smaller
best = Math.min(best, right - left + 1);
sum -= nums[left++];
}
}
return best == Integer.MAX_VALUE ? 0 : best;
}
// LC 992. Exactly K distinct = atMost(K) - atMost(K - 1).
static int subarraysWithKDistinct(int[] nums, int k) {
return atMostKDistinct(nums, k) - atMostKDistinct(nums, k - 1);
}
private static int atMostKDistinct(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
int left = 0, total = 0;
for (int right = 0; right < nums.length; right++) {
count.merge(nums[right], 1, Integer::sum);
while (count.size() > k) {
int out = nums[left++];
if (count.merge(out, -1, Integer::sum) == 0) count.remove(out);
}
total += right - left + 1; // every subarray ending at right, starting in [left, right]
}
return total;
}
Go deeperWhen sliding window does not work
The window's correctness rests on one property: if a window is valid, every window inside it is valid (for longest), or if a window is valid, every window containing it is valid (for shortest). Negative numbers in sum conditions break this.
When values can be negative:
Go deeperTwo pointers vs sliding window vs prefix sums
| Clue in the problem | Technique |
|---|---|
| A pair of elements; sorted data; "two values that sum to" | Two pointers (opposite ends) |
| A contiguous range; a condition that stays true when you shrink a valid window (non-negative sums, counts, distinct characters) | Sliding window |
| A contiguous range; exact sums; negative values; many range queries | Prefix sums (with a hash map for counting) |
| A contiguous range; max/min inside each window | Monotonic deque |
Guided solveLC 3. Longest Substring Without Repeating CharactersFive questions, each with a reveal
Problem: 3Longest Substring Without Repeating CharactersMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Length of the longest contiguous substring with all distinct characters.
Input analysis
Length up to 5 * 10^4, characters are English letters, digits, symbols, and spaces (so not just a to z).
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Longest contiguous" with a condition ("all distinct"). Check the required condition: if a substring has no repeats, every substring inside it has none. Monotonic, so a variable window works.
Brute force
All substrings with a set check: O(n^3), or O(n^2) by extending each start until a repeat. Repeated work: after a repeat, the brute force restarts from
start + 1and re-scans characters it already knows are distinct. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. a duplicate inside the window can only be fixed by moving left past its earlier copy
Key observation
When
s[right]duplicates a character in the window, every window that still contains the earlier copy is invalid. Soleftmust move past that copy, and it never needs to come back.Visualization
See the figure above.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Count characters in the window. Add
s[right]; while its count exceeds 1, removes[left++]; update the best with the window length.Java implementation
The
lengthOfLongestSubstringcode above. A faster variant stores each character's last index and jumpsleft = max(left, last[c] + 1)directly.Dry run
"pwwkew": p (len 1), w (2), w duplicates: drop p, drop w, window "w" (1); k (2); e (3: "wke"); w duplicates: drop w, window "kew" (3). Answer 3. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time: each index enters once and leaves at most once. O(min(n, alphabet)) space.
Edge cases
Empty string (0), all identical characters (1), all distinct (n), spaces and symbols (use a map or
int[128], notint[26]).Transfer lesson
"Longest window where every element appears at most once" generalizes to "at most k distinct" (LC 904), "at most k replacements" (LC 424), "at most k zeros" (LC 1004). Only the validity check changes.
Guided solveLC 76. Minimum Window SubstringFive questions, each with a reveal
Problem: 76Minimum Window SubstringHard. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Shortest substring of
sthat contains every character oftwith multiplicity.Input analysis
Up to 10^5 characters, upper and lower case letters.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Shortest contiguous window satisfying a coverage condition. If a window covers
t, every larger window covers it too: the shortest-window flavor.Brute force
Every substring with a count comparison: O(n^2 * alphabet).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. track how many required characters are fully satisfied
Key observation
Comparing full count arrays at every step costs O(alphabet). Instead keep
formed= number of distinct characters whose requirement is met. The window is valid exactly whenformed == required. Updateformedonly when a count crosses its requirement.Visualization
s = "ADOBECODEBANC", t = "ABC" (need A:1, B:1, C:1, required = 3) R=5 'C': ADOBEC valid (formed 3) -> record length 6; drop A -> formed 2 R=10 'A': DOBECODEBA valid -> shrink while valid down to CODEBA (6, not shorter); drop C -> formed 2 R=12 'C': ODEBANC valid -> shrink: EBANC (record 5), BANC (record 4); drop B -> formed 2 answer "BANC"
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Expand
right; when the count ofs[right]reaches its requirement,formed++. Whileformed == required: record the window if shorter, removes[left]; if its count drops below the requirement,formed--;left++.Java implementation
Java// LC 76. formed = how many distinct required characters currently meet their count. static String minWindow(String s, String t) { int[] need = new int[128], window = new int[128]; int required = 0; for (char c : t.toCharArray()) if (need[c]++ == 0) required++; int formed = 0, left = 0, bestLen = Integer.MAX_VALUE, bestStart = 0; for (int right = 0; right < s.length(); right++) { char c = s.charAt(right); if (++window[c] == need[c]) formed++; // c just became satisfied while (formed == required) { // valid: record, then shrink if (right - left + 1 < bestLen) { bestLen = right - left + 1; bestStart = left; } char out = s.charAt(left++); if (window[out]-- == need[out]) formed--; // out drops below its need } } return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestStart, bestStart + bestLen); }The check
++window[c] == need[c]fires only for required characters: for a character withneed[c] == 0, the window count becomes at least 1 and never equals 0.Dry run
Shown above. Note that
formedonly changes at the moments a count crosses its requirement, which keeps each step O(1). - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(|s| + |t|) time, O(alphabet) space.
Edge cases
tlonger thans(empty result),thas repeated characters (multiplicity counts), no valid window.Transfer lesson
"Count how many constraints are satisfied" turns an O(alphabet) validity check into O(1). The same trick appears in LC 438 and LC 567 if you want them faster than comparing arrays.
Common mistakes
- Using
ifinstead ofwhileto shrink: one removal may not restore validity. - Updating the answer at the wrong place for the flavor (see the table).
- Applying a sum window to arrays with negative numbers.
- Recomputing the window's validity from scratch each step, which hides an O(alphabet) or O(k) factor.
Variable-size sliding window (longest, shortest, counting)
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 3Longest Substring Without Repeating Characters Grow right, shrink left until the window is valid again | Medium | High | |
| 209Minimum Size Subarray Sum Shortest window: shrink while valid, record before each shrink | Medium | High | |
| 424Longest Repeating Character Replacement Valid iff windowLen - maxFreq <= k | Medium | High | |
| 1004Max Consecutive Ones III Window may contain at most k zeros | Medium | High | |
| 904Fruit Into Baskets Longest window with at most 2 distinct values | Medium | Medium | |
| 713Subarray Product Less Than K Counting: each right end adds (right - left + 1) subarrays | Medium | Medium | |
| 1358Number of Substrings Containing All Three Characters Count windows that are valid at or beyond a boundary | Medium | Medium | |
| 2962Count Subarrays Where Max Element Appears at Least K Times Counting with 'at least': add left index after shrinking | Medium | Medium | |
| 992Subarrays with K Different Integers exactly(k) = atMost(k) - atMost(k - 1) | Hard | High | |
| 76Minimum Window Substring Track how many required characters are satisfied | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 4: Sorting and binary search
Sorting creates order; binary search exploits it. The most valuable idea in this module is that binary search works on any monotonic yes/no question, including questions about the answer itself.
4.1Sorting in practice
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Sorting turns a problem about arbitrary positions into a problem about neighbors. Duplicates become adjacent, pairs can be found with two pointers, intervals can be swept in order, and greedy choices become "take the next one". It costs O(n log n), which is within budget for n up to about 10^6.
Sorting a pile of exam papers by student ID makes finding duplicates trivial: they end up next to each other.
Go deeperWhat you need to know about sorting algorithms
| Algorithm | Time | Extra space | Stable | Where you meet it |
|---|---|---|---|---|
| Merge sort | O(n log n) always | O(n) | Yes | Counting inversions (LC 315, 493), linked list sort (LC 148) |
| Quicksort | O(n log n) average, O(n^2) worst | O(log n) stack | No | Quickselect for the kth element (LC 215) |
| Heap sort | O(n log n) | O(1) | No | Rarely written; the idea behind priority queues |
| Counting / bucket sort | O(n + range) | O(range) | Yes (counting) | Small value ranges, frequencies (LC 451, 347) |
| TimSort | O(n log n), O(n) on nearly sorted | O(n) | Yes | Java's Arrays.sort(Object[]) and List.sort |
| Dual-pivot quicksort | O(n log n) | O(log n) | No (primitives, so it does not matter) | Java's Arrays.sort(int[]) |
Interviews rarely ask you to implement a sort from scratch, but merge sort comes up because its merge step can count things (inversions, smaller elements to the right) while it sorts. Know it cold:
// LC 912. Merge sort: O(n log n) always, stable, O(n) extra space.
static int[] sortArray(int[] nums) {
mergeSort(nums, new int[nums.length], 0, nums.length - 1);
return nums;
}
private static void mergeSort(int[] a, int[] buf, int lo, int hi) {
if (lo >= hi) return;
int mid = lo + (hi - lo) / 2;
mergeSort(a, buf, lo, mid);
mergeSort(a, buf, mid + 1, hi);
int i = lo, j = mid + 1, k = lo;
while (i <= mid && j <= hi) buf[k++] = (a[i] <= a[j]) ? a[i++] : a[j++]; // <= keeps it stable
while (i <= mid) buf[k++] = a[i++];
while (j <= hi) buf[k++] = a[j++];
System.arraycopy(buf, lo, a, lo, hi - lo + 1);
}
Stability means equal elements keep their original relative order. It matters when you sort by one key after another: sort by name, then stably by department, and each department stays sorted by name.
Go deeperCustom sorting and comparators in Java
static void comparatorExamples() {
int[][] intervals = {{5, 6}, {1, 3}, {1, 2}};
// Sort by start, then by end. Integer.compare avoids overflow from a[0] - b[0].
Arrays.sort(intervals, (a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
: Integer.compare(a[1], b[1]));
String[] words = {"banana", "kiwi", "apple", "fig"};
// Length ascending, then alphabetical. Comparator chaining reads clearly.
Arrays.sort(words, Comparator.comparingInt(String::length)
.thenComparing(Comparator.naturalOrder()));
// Primitive int[] cannot take a comparator. Box it (Integer[]) or sort indices.
Integer[] boxed = {3, 1, 2};
Arrays.sort(boxed, Collections.reverseOrder());
List<int[]> people = new ArrayList<>(List.of(new int[]{170, 0}, new int[]{160, 1}));
people.sort((a, b) -> Integer.compare(b[0], a[0])); // descending by height
}
A comparator returns a negative number when a should come first, positive when b should, zero when they tie. Three rules prevent most bugs:
- Never subtract for comparison:
a[0] - b[0]overflows for values nearInteger.MIN_VALUEorMAX_VALUE. UseInteger.compare,Long.compare,Comparator.comparingInt. - Primitive arrays cannot take a comparator.
Arrays.sort(int[], comparator)does not exist. UseInteger[],int[][](each row is an object), or sort an array of indices. - Be consistent. A comparator that says
a < bandb < a(for example, a random tie-break) can throw "Comparison method violates its general contract".
Sorting by a derived key is a common trick: Largest Number (LC 179) sorts strings by comparing a + b with b + a; Car Fleet (LC 853) sorts by position and then compares arrival times.
Common mistakes
- Forgetting that sorting destroys original indices (store
{value, index}pairs). - Claiming O(n) for "sort then two pointers".
- Sorting inside a loop.
Sorting and custom comparators
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 937Reorder Data in Log Files Comparator with a stable tie-break and two classes of items | Medium | Medium | |
| 912Sort an Array Implement merge sort; know why quicksort can degrade | Medium | High | |
| 179Largest Number Comparator on concatenation a+b vs b+a | Medium | High | |
| 451Sort Characters By Frequency Bucket sort by frequency beats comparison sort | Medium | Medium | |
| 791Custom Sort String Counting sort driven by a custom order | Medium | Medium | |
| 406Queue Reconstruction by Height Sort tall-first, then insert by index | Medium | Medium | |
| 853Car Fleet Sort by position, compare arrival times from the front | Medium | Medium | |
| 1366Rank Teams by Votes Multi-key comparator over vote counts, then a letter tie-break | Medium | Medium | |
| 315Count of Smaller Numbers After Self Count during the merge step of merge sort | Hard | High |
4.2Binary search fundamentals
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Finding a position in sorted data in O(log n): an exact value, the first element at least x, the insertion point, the boundary between two kinds of elements.
Looking up a word in a paper dictionary. Open in the middle; the word you see tells you which half to keep. Every look discards half of what remains, so a million pages need about 20 looks.
- Signal
- Sorted or rotated-sorted input, 'first/last position', insertion point, peak, or any O(log n) requirement.
- Required condition
- A predicate over indices that is false...false true...true (monotonic). Sorting gives one for free.
Mental model, core operation, limits, and what to use instead
- Mental model
- Guessing a number between 1 and 100 with 'higher/lower' hints: every guess discards half.
- Core operation
- Test mid; keep the half that still contains the answer; stop when the range has one candidate.
- Complexity benefit
- O(n) scan becomes O(log n).
- Limitations
- Needs a monotonic predicate. Duplicates can break rotated-array reasoning (worst case O(n)).
- Alternative
- Linear scan when unsorted and queried once; hash set for membership only; TreeMap floor/ceiling for dynamic data.
Go deeperOne template for every boundary search
The most reliable way to write binary search is to stop thinking about "find the target" and start thinking about a predicate that is false for a prefix of the range and true for the rest:
index: 0 1 2 3 4 5 6
nums: 1 3 3 3 5 8 9
nums >= 3: false true true true true true true
^ first true = lower bound
Find the first true. Everything else is a choice of predicate.
// First index i with nums[i] >= target (nums.length if none).
// Half-open search space [lo, hi). Invariant: answer is always inside [lo, hi].
static int lowerBound(int[] nums, int target) {
int lo = 0, hi = nums.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2; // avoids (lo + hi) overflow
if (nums[mid] >= target) hi = mid; // mid could be the answer: keep it
else lo = mid + 1; // mid is too small: discard it
}
return lo; // lo == hi
}
// First index i with nums[i] > target. Same loop, strict comparison.
static int upperBound(int[] nums, int target) {
int lo = 0, hi = nums.length;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] > target) hi = mid;
else lo = mid + 1;
}
return lo;
}
// LC 34. First and last position = [lowerBound, upperBound - 1].
static int[] searchRange(int[] nums, int target) {
int first = lowerBound(nums, target);
if (first == nums.length || nums[first] != target) return new int[]{-1, -1};
return new int[]{first, upperBound(nums, target) - 1};
}
Go deeperDeriving the boundaries and update rules
Write these four decisions down before coding:
| Decision | Choice used here | Why |
|---|---|---|
| Search space | [lo, hi) with lo = 0, hi = n |
hi = n represents "no element qualifies" naturally |
| Loop condition | while (lo < hi) |
Stop when one candidate remains |
| Midpoint | lo + (hi - lo) / 2 (rounds down) |
Never equals hi, so nums[mid] is always valid; no overflow |
| Updates | true: hi = mid; false: lo = mid + 1 |
Each branch strictly shrinks the range, so the loop terminates |
Why this never loops forever: when hi - lo >= 1, mid is in [lo, hi - 1]. hi = mid makes hi smaller; lo = mid + 1 makes lo larger. The classic infinite loop comes from writing lo = mid with a rounded-down mid: when hi = lo + 1, mid = lo and nothing changes. If you ever need lo = mid, round the midpoint up: mid = lo + (hi - lo + 1) / 2.
Go deeperThe variants, all from one template
| Question | Predicate | Result |
|---|---|---|
| First index with value >= x (lower bound, insertion position) | nums[i] >= x |
first true |
| First index with value > x (upper bound) | nums[i] > x |
first true |
| First occurrence of x | lower bound, then check nums[i] == x |
first true if it matches |
| Last occurrence of x | upper bound - 1, then check | |
| Largest value <= x (floor) | upper bound - 1 | |
| Count of x | upper bound - lower bound |
The classic "closed interval" version (while (lo <= hi), hi = mid - 1, return mid on equality) is fine for finding an exact value. For boundaries, the predicate version above is far less error prone. Pick one style and use it everywhere.
Common mistakes
(lo + hi) / 2overflowing when indices or values are near 2^31 (uselo + (hi - lo) / 2, or(lo + hi) >>> 1).- Mixing interval conventions:
hi = nwithwhile (lo <= hi)readsnums[n]. - Returning
lowithout checking that it is in range and actually matches when the target may be absent. - Writing
lo = midwith a rounded-down midpoint (infinite loop).
4.3Rotated and modified sorted arrays
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Searching arrays that are not fully sorted but keep enough order: rotated sorted arrays, arrays with a single peak, sorted arrays with one odd element out.
Binary search does not need the array to be sorted. It needs a test at mid that tells you which half cannot contain the answer.
// LC 33. At least one half of [lo, hi] is sorted. Check whether target lies in it.
static int searchRotated(int[] nums, int target) {
int lo = 0, hi = nums.length - 1; // closed interval [lo, hi]
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] == target) return mid;
if (nums[lo] <= nums[mid]) { // left half [lo, mid] is sorted
if (nums[lo] <= target && target < nums[mid]) hi = mid - 1;
else lo = mid + 1;
} else { // right half [mid, hi] is sorted
if (nums[mid] < target && target <= nums[hi]) lo = mid + 1;
else hi = mid - 1;
}
}
return -1;
}
// LC 153. Compare with the right end: if nums[mid] > nums[hi], the drop is right of mid.
static int findMin(int[] nums) {
int lo = 0, hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2;
if (nums[mid] > nums[hi]) lo = mid + 1;
else hi = mid; // mid may be the minimum
}
return nums[lo];
}
Why compare with nums[hi] for the minimum? If nums[mid] > nums[hi], the drop (the minimum) is strictly to the right of mid. Otherwise the segment [mid, hi] is sorted, so the minimum is at mid or to its left. Comparing with nums[lo] instead fails on an unrotated array.
Peaks without sorting. In LC 162, if nums[mid] < nums[mid + 1] the values rise to the right, and since the array "falls off" to negative infinity past the end, a peak must exist on the right. Otherwise one exists at mid or to its left.
// LC 162. If nums[mid] < nums[mid + 1], a peak exists to the right (the slope must come down).
static int findPeakElement(int[] nums) {
int lo = 0, hi = nums.length - 1;
while (lo < hi) {
int mid = lo + (hi - lo) / 2; // mid < hi, so mid + 1 is valid
if (nums[mid] < nums[mid + 1]) lo = mid + 1;
else hi = mid;
}
return lo;
}
Duplicates (LC 81, 154) break the "which half is sorted" test when nums[lo] == nums[mid] == nums[hi]. The fix is to shrink by one (lo++ or hi--), which makes the worst case O(n). Mention this if asked about duplicates.
Guided solveLC 33. Search in Rotated Sorted ArrayFive questions, each with a reveal
Problem: 33Search in Rotated Sorted ArrayMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
A sorted array of distinct values was rotated at an unknown pivot. Return the index of
targetor -1, in O(log n).Input analysis
Distinct values (important: it makes the sorted-half test unambiguous), n up to 5000, O(log n) required.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"O(log n)" plus "sorted, but modified" means binary search with a modified decision rule.
Brute force
Linear scan: O(n). Or find the pivot first, then binary search the correct side: also O(log n), but two searches and more edge cases.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. one half is always sorted; test the target against that half only
Key observation
For any
mid, either[lo, mid]or[mid, hi]is sorted (the rotation point can only be in one of them). In a sorted range, "is the target inside?" is a simple range check.Visualization
See the figure above.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Closed interval
[lo, hi]. Ifnums[mid] == target, return. Ifnums[lo] <= nums[mid], the left half is sorted: go left iffnums[lo] <= target < nums[mid]. Otherwise the right half is sorted: go right iffnums[mid] < target <= nums[hi].Java implementation
searchRotatedabove.Dry run
[3, 1], target 1:lo = 0, hi = 1, mid = 0.nums[0] = 3 != 1.nums[lo] <= nums[mid](3 <= 3): left half[3]is "sorted"; is3 <= 1 < 3? No, solo = 1. Nextmid = 1, found. The<=innums[lo] <= nums[mid]is what handles the caselo == mid. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(log n) time, O(1) space.
Edge cases
Not rotated at all; rotated by n - 1; single element; target smaller than everything; two elements (the
<=case above).Transfer lesson
Ask "what does
midtell me about which side cannot contain the answer?" rather than "is the array sorted?". The same question solves peaks (LC 162), the single non-duplicate (LC 540: pairs before the single element start at even indices), and the minimum of a rotated array (LC 153).
Binary search on sorted or monotonic data
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2300Successful Pairs of Spells and Potions Sort once, then a lower bound per query | Medium | High | |
| 34Find First and Last Position of Element in Sorted Array Lower bound and upper bound as two searches | Medium | High | |
| 74Search a 2D Matrix Treat the matrix as one sorted array via index math | Medium | Medium | |
| 658Find K Closest Elements Binary search the left edge of the k-window | Medium | Medium | |
| 33Search in Rotated Sorted Array One half is always sorted; decide which side holds the target | Medium | High | |
| 153Find Minimum in Rotated Sorted Array Compare mid with the right end to locate the drop | Medium | High | |
| 162Find Peak Element Move toward the rising neighbor; a peak must exist there | Medium | High | |
| 540Single Element in a Sorted Array Search on the parity pattern of pair positions | Medium | Medium | |
| 981Time Based Key-Value Store Upper-bound style search per key on timestamps | Medium | Medium | |
| 4Median of Two Sorted Arrays Binary search a partition of the smaller array | Hard | High |
4.4Binary search on the answer
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Optimization problems of the form "find the minimum X such that the task is possible" or "the maximum X such that ...", where X ranges over a large space (speeds, capacities, days, distances) and checking a single X is easy.
Adjusting a shower's temperature. You cannot compute the perfect setting, but for any setting you can tell "too cold" or "warm enough", and anything warmer than warm enough is also warm enough. Halve the range each time.
- Signal
- 'Minimum largest', 'maximum smallest', 'least capacity/speed/time so that...'. You can check a candidate answer easily but cannot compute it directly.
- Required condition
- feasible(x) is monotonic: if x works, every larger (or smaller) x also works.
Mental model, core operation, limits, and what to use instead
- Mental model
- Adjusting a thermostat: too cold, go up; warm enough, try a little lower.
- Core operation
- Pick bounds that surely contain the answer, binary search for the first feasible value, check feasibility greedily in O(n).
- Complexity benefit
- Searching a huge answer range costs only log(range) feasibility checks: O(n log range).
- Limitations
- Feasibility must be monotonic and cheap. Off-by-one bounds and overflow in the check are the usual bugs.
- Alternative
- Direct formula, greedy, or DP when the answer can be constructed; heap-based simulation for some scheduling problems.
Go deeperThe recipe
- Name the answer variable and what "feasible" means for a candidate value.
- Prove monotonicity in one sentence. "If Koko can finish at speed k, she can finish at any speed above k."
- Pick bounds that surely contain the answer. Low: the smallest value that could possibly work (often the max single item). High: a value that always works (often the total).
- Write the feasibility check, usually a greedy O(n) pass.
- Binary search for the first feasible value (or the last feasible, for "maximize the minimum": search for the first infeasible and subtract one).
| Phrase | Shape | Search for |
|---|---|---|
| "Minimum speed / capacity / time such that ..." | false ... false true ... true | First true |
| "Minimize the maximum ..." (split array, distribute) | Same | First true of "max part <= cap" |
| "Maximize the minimum ..." (magnetic force, aggressive cows) | true ... true false ... false | Last true |
Guided solveLC 875. Koko Eating BananasFive questions, each with a reveal
Problem: 875Koko Eating BananasMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Piles of bananas; per hour Koko eats up to
kbananas from one pile. Find the minimum integerkthat lets her finish all piles withinhhours.Input analysis
Up to 10^4 piles, each up to 10^9 bananas,
hup to 10^9. The answer lies in[1, max(piles)], a range of up to 10^9 values. - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Minimum k such that ..." with a huge range for k, and a quick way to compute hours for a given k: binary search on the answer.
Brute force
Try
k = 1, 2, 3, ...until feasible: up to 10^9 checks of O(n) each. Far too slow. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. the answer is hard to compute but easy to check
Key observation
hours(k) = sum of ceil(pile / k)never increases askgrows, sohours(k) <= his false then true. Binary search finds the boundary with about 30 checks.Visualization
See the figure: speeds 1 to 11 for piles
[3, 6, 7, 11],h = 8. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
lo = 1,hi = max(piles). Whilelo < hi: ifcanFinish(mid),hi = mid; elselo = mid + 1. Returnlo.Java implementation
Java// LC 875. canFinish(speed) is false...false true...true as speed grows. // Find the first true. static int minEatingSpeed(int[] piles, int h) { int lo = 1, hi = 0; for (int p : piles) hi = Math.max(hi, p); // speed = max pile always works while (lo < hi) { int mid = lo + (hi - lo) / 2; if (canFinish(piles, h, mid)) hi = mid; // feasible: try slower else lo = mid + 1; // infeasible: must go faster } return lo; } private static boolean canFinish(int[] piles, int h, int speed) { long hours = 0; // long: sum can exceed int for (int p : piles) hours += (p + (long) speed - 1) / speed; // ceil(p / speed); long avoids overflow return hours <= h; }Dry run
[3, 6, 7, 11],h = 8:lo = 1, hi = 11. mid 6: 6 hours, feasible,hi = 6. mid 3: 10 hours,lo = 4. mid 5: 8 hours,hi = 5. mid 4: 8 hours,hi = 4. Return 4. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n log(max pile)) time, O(1) space.
Edge cases
Overflow, twice. The hour total can exceed
int(10^4 piles x 10^9 hours), so it is along. Andp + speed - 1can exceedintwhen both are near 10^9, so the ceiling is computed inlongtoo. Also:h == piles.lengthforcesk = max(piles).Transfer lesson
Ship within D days (LC 1011), split array largest sum (LC 410), and m bouquets (LC 1482) all use this exact skeleton. Only the feasibility function and the bounds change.
Second example, minimize the maximum. In Split Array Largest Sum (LC 410), the candidate is a cap on part sums; the greedy check counts how many parts you need if you never exceed the cap. More cap means fewer parts, so feasibility is monotonic.
// LC 410. Minimize the largest part sum when splitting into k parts.
static int splitArray(int[] nums, int k) {
long lo = 0, hi = 0;
for (int x : nums) { lo = Math.max(lo, x); hi += x; }
while (lo < hi) {
long mid = lo + (hi - lo) / 2;
if (partsNeeded(nums, mid) <= k) hi = mid; // cap 'mid' is achievable
else lo = mid + 1;
}
return (int) lo;
}
// Greedy: extend the current part until adding the next number would exceed cap.
private static int partsNeeded(int[] nums, long cap) {
int parts = 1;
long current = 0;
for (int x : nums) {
if (current + x > cap) { parts++; current = 0; }
current += x;
}
return parts;
}
Go deeperWhen binary search applies, and when it does not
| Applies | Does not apply |
|---|---|
| Sorted (or rotated sorted) data, searching a position | Unsorted data searched once: a linear scan is O(n) and sorting costs more |
| A monotonic predicate over indices or answers | Feasibility that is not monotonic (more capacity sometimes worse) |
| A cheap check for a candidate answer | When the check is as hard as the original problem |
| Integer answers, or real answers to a fixed precision | When the answer is a structure, not a number on a line |
Common mistakes
- Bounds that exclude the answer:
lo = 0when 0 is not valid and the check divides by it;hitoo small. - A feasibility check with
intoverflow. - Searching for the first true when the problem needs the last true ("maximize the minimum").
- Forgetting to prove monotonicity. If you cannot say why more of X never hurts, the method may be wrong.
Binary search on the answer
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 875Koko Eating Bananas Feasibility(speed) is monotonic; find the minimum feasible | Medium | High | |
| 1283Find the Smallest Divisor Given a Threshold Same template, different feasibility check | Medium | Medium | |
| 1011Capacity To Ship Packages Within D Days Bounds: low = max item, high = total | Medium | High | |
| 2226Maximum Candies Allocated to K Children Maximize the pile size with a counting feasibility check | Medium | Medium | |
| 1482Minimum Number of Days to Make m Bouquets Feasibility counts consecutive bloomed flowers | Medium | Medium | |
| 2187Minimum Time to Complete Trips Search over time; watch long overflow in the check | Medium | Medium | |
| 1552Magnetic Force Between Two Balls Maximize the minimum: largest feasible gap | Medium | Medium | |
| 2064Minimized Maximum of Products Distributed to Any Store Minimize the maximum load per store | Medium | Medium | |
| 410Split Array Largest Sum Classic minimize-the-maximum with greedy feasibility | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 5: Intervals and sweep line
Interval problems look varied (meetings, bookings, balloons, ranges) but almost all reduce to three moves: sort by start and merge, sort by end and select, or turn intervals into events and sweep.
5.1Interval fundamentals
Do the closed intervals [1, 3] and [3, 5] overlap? Write a one-line test for whether closed intervals a and b overlap.
Reveal the answer
Yes, they share the point 3. Closed intervals overlap exactly when a.start <= b.end && b.start <= a.end. It is easier to negate "one ends before the other starts" than to list all the ways two intervals can overlap.
Questions about pairs [start, end]: do they overlap, what is their union, how many overlap at once, how many must be removed so that none overlap.
A shared calendar. Laid out on a timeline, overlaps become visible: you just walk left to right.
- Signal
- Pairs [start, end]: overlaps, merging, free time, rooms needed, max concurrent events.
- Required condition
- Sort order exposes the structure: by start for merging, by end for selecting, by event time for counting.
Mental model, core operation, limits, and what to use instead
- Mental model
- A timeline: walk left to right and react at each start and end event.
- Core operation
- Sort, then compare each interval with the last kept one; or turn intervals into +1/-1 events and keep a running count.
- Complexity benefit
- Pairwise overlap checks O(n^2) become O(n log n) for the sort plus O(n) scan.
- Limitations
- Endpoint semantics (closed vs half-open) change tie-breaking. Online insert and query needs a TreeMap.
- Alternative
- Min-heap of end times for rooms; difference array when coordinates are small integers; TreeMap for dynamic calendars.
Go deeperThe overlap test
Two closed intervals [a1, a2] and [b1, b2] overlap exactly when a1 <= b2 && b1 <= a2. Read it as "each one starts before the other ends". For half-open intervals [start, end) (meetings where one ends at 10 and the next starts at 10 do not clash), use strict <. Always check which one the problem means; LeetCode problems differ ("touching" counts as overlapping in LC 56 but not in LC 435).
Go deeperWhich sort order?
| Goal | Sort by | Why |
|---|---|---|
| Merge, insert, find gaps | start | An interval can only overlap the most recent merged block |
| Keep the maximum number of non-overlapping intervals | end | The interval that ends first leaves the most room (greedy, proven below) |
| Count overlaps at a point in time | event time (starts and ends separately) | A running count at each event is the number of active intervals |
5.2Merging and inserting
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Why only the last block? Blocks are built in start order and do not overlap each other. If the current interval does not overlap the last block (it starts after the last block's end), it starts after every earlier block's end too.
Guided solveLC 56. Merge IntervalsFive questions, each with a reveal
Problem: 56Merge IntervalsMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Merge all overlapping intervals and return the union as non-overlapping intervals.
Input analysis
Up to 10^4 intervals, unsorted. Touching intervals (
[1,4]and[4,5]) count as overlapping. - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Overlap questions on unsorted intervals: sort by start first.
Brute force
Repeatedly find any overlapping pair and merge it until none remain: O(n^2) or worse.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. sort by start, then each interval either extends the last block or opens a new one
Key observation
In start order, the current interval overlaps the merged result if and only if it overlaps the last merged block.
Visualization
The figure above.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Sort by start. For each interval: if it starts at or before the last block's end, set the block's end to the max of both ends. Otherwise append it as a new block.
Java implementation
Java// LC 56. After sorting by start, an interval can only overlap the LAST merged one. static int[][] merge(int[][] intervals) { Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); List<int[]> merged = new ArrayList<>(); for (int[] cur : intervals) { int[] last = merged.isEmpty() ? null : merged.get(merged.size() - 1); if (last != null && cur[0] <= last[1]) { last[1] = Math.max(last[1], cur[1]); // overlap: extend (cur may be inside last) } else { merged.add(new int[]{cur[0], cur[1]}); // gap: start a new block } } return merged.toArray(new int[0][]); }Dry run
[[1,4],[2,3]]: block[1,4];[2,3]starts at 2 <= 4, end becomesmax(4, 3) = 4. Usingcur[1]instead of the max would wrongly shrink the block to[1,3]. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n log n) for the sort, O(n) for the scan. Output space O(n).
Edge cases
One interval, all intervals overlapping into one, nested intervals, touching endpoints, unsorted input.
Transfer lesson
"Sort, then compare with the last kept item" is also how you solve Insert Interval in O(n) (input already sorted), Remove Covered Intervals (sort by start ascending, end descending), and finding free time between meetings (the gaps between merged blocks).
Go deeperInserting into a sorted list
When the list is already sorted and non-overlapping, sorting again wastes time. Walk it in three phases: intervals entirely before the new one, intervals that overlap it (absorb them), intervals entirely after.
// LC 57. Input is sorted and non-overlapping: three phases in one pass.
static int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> out = new ArrayList<>();
int i = 0, n = intervals.length;
int start = newInterval[0], end = newInterval[1];
while (i < n && intervals[i][1] < start) out.add(intervals[i++]); // 1. entirely before
while (i < n && intervals[i][0] <= end) { // 2. overlapping
start = Math.min(start, intervals[i][0]);
end = Math.max(end, intervals[i][1]);
i++;
}
out.add(new int[]{start, end});
while (i < n) out.add(intervals[i++]); // 3. entirely after
return out.toArray(new int[0][]);
}
Go deeperIntersections of two sorted lists
With two pointers, one per list: the intersection of two intervals is [max(starts), min(ends)] when that is non-empty. Then advance whichever interval ends first, because it cannot intersect anything later in the other list.
// LC 986. Intersection of two intervals = [max(starts), min(ends)] if non-empty.
static int[][] intervalIntersection(int[][] a, int[][] b) {
List<int[]> out = new ArrayList<>();
int i = 0, j = 0;
while (i < a.length && j < b.length) {
int lo = Math.max(a[i][0], b[j][0]);
int hi = Math.min(a[i][1], b[j][1]);
if (lo <= hi) out.add(new int[]{lo, hi});
if (a[i][1] < b[j][1]) i++; // the one ending first cannot meet anything later
else j++;
}
return out.toArray(new int[0][]);
}
5.3Selecting non-overlapping intervals (greedy by end)
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Keep as many intervals as possible with no overlaps (equivalently, remove as few as possible). Also: minimum arrows to burst balloons (LC 452), activity selection.
// LC 435. Keep the interval that ends earliest; it leaves the most room.
static int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
int kept = 0;
long lastEnd = Long.MIN_VALUE;
for (int[] iv : intervals) {
if (iv[0] >= lastEnd) { // touching endpoints do not overlap here
kept++;
lastEnd = iv[1];
}
}
return intervals.length - kept;
}
Why the greedy is correct (exchange argument). Let g be the interval with the earliest end. Take any optimal selection and let o be its earliest-ending interval. Since g ends no later than o, replacing o with g cannot create an overlap with the rest of the optimal selection (they all start after o ends, hence after g ends). So some optimal selection starts with g. Remove everything overlapping g and repeat. This argument is worth saying out loud in an interview; Module 11 covers exchange arguments in general.
Why not sort by start? Counterexample: [1,10], [2,3], [4,5]. By start you would keep [1,10] and lose both others. By end you keep [2,3] and [4,5].
5.4Meeting rooms and the sweep line
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
The minimum number of rooms (groups, platforms, cars) needed so that overlapping intervals never share one. The answer is the maximum number of intervals active at the same moment.
Go deeperTwo ways to compute the maximum overlap
Events. Turn each interval into a +1 event at its start and a -1 event at its end (or end + 1 for closed integer intervals). Sort events by time; at equal times, process ends before starts when touching intervals do not overlap. The running sum's maximum is the answer.
Min-heap of end times. Sort intervals by start. Keep a heap of end times of rooms in use. For each interval, if the earliest-ending room has ended, reuse it (poll); then add this interval's end. The heap size at the end is the number of rooms.
// LC 2406 (same idea as "meeting rooms II"): max number of intervals alive at once.
// Closed intervals [l, r]: +1 at l, -1 at r + 1. Sort events; at equal time, ends first.
static int minGroups(int[][] intervals) {
int[][] events = new int[intervals.length * 2][];
int k = 0;
for (int[] iv : intervals) {
events[k++] = new int[]{iv[0], +1};
events[k++] = new int[]{iv[1] + 1, -1};
}
Arrays.sort(events, (x, y) -> x[0] != y[0] ? Integer.compare(x[0], y[0])
: Integer.compare(x[1], y[1])); // -1 before +1
int active = 0, best = 0;
for (int[] e : events) {
active += e[1];
best = Math.max(best, active);
}
return best;
}
// Same answer with a min-heap of end times: reuse a room whose meeting already ended.
static int minGroupsHeap(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
PriorityQueue<Integer> ends = new PriorityQueue<>();
for (int[] iv : intervals) {
if (!ends.isEmpty() && ends.peek() < iv[0]) ends.poll(); // closed: must end strictly before
ends.offer(iv[1]);
}
return ends.size();
}
| Events | Heap | |
|---|---|---|
| Time | O(n log n) | O(n log n) |
| Extra space | O(n) events | O(rooms) heap |
| Also gives | The busiest time | Which room each meeting can use (with room ids, LC 2402) |
When coordinates are small integers, a difference array replaces the sort: diff[start] += 1, diff[end + 1] -= 1, then a prefix sum (Car Pooling, LC 1094, Maximum Population Year, LC 1854).
Common mistakes
- Wrong tie-breaking at equal times (an end and a start at the same moment): decide whether touching intervals overlap, then order events accordingly.
- Comparators with subtraction overflow on large endpoints: use
Integer.compare. - Sorting by start for the selection problem (the counterexample above).
- Modifying input intervals in place when the caller still needs them.
Intervals and sweep line
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 56Merge Intervals Sort by start; merge when next.start <= current.end | Medium | High | |
| 57Insert Interval Three phases: before, overlapping, after | Medium | High | |
| 1288Remove Covered Intervals Sort start asc, end desc; covered when end <= max end so far | Medium | High | |
| 986Interval List Intersections Two pointers; advance the interval that ends first | Medium | High | |
| 435Non-overlapping Intervals Sort by end; keep the earliest-finishing interval | Medium | High | |
| 452Minimum Number of Arrows to Burst Balloons Same greedy as 435 with closed endpoints | Medium | Medium | |
| 729My Calendar I Ordered map floor/ceiling to test overlap on insert | Medium | Medium | |
| 2406Divide Intervals Into Minimum Number of Groups Max overlap at any point = number of rooms needed | Medium | High | |
| 1094Car Pooling Sweep line with capacity check at each event | Medium | Medium | |
| 2402Meeting Rooms III Two heaps: free rooms by id, busy rooms by end time | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 6: Linked lists
Linked list problems are rarely about algorithms. They test whether you can rewire references without losing part of the list, and whether you handle the head, the tail, and null every time.
6.1Linked list fundamentals
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
A linked list stores elements as nodes that point to the next node. Inserting or removing at a known position is O(1) (no shifting), but reaching position i is O(n). Interviews use it to test pointer discipline; production code mostly uses it inside other structures (LRU caches, hash buckets, queues).
A treasure hunt: each clue tells you where the next clue is. You cannot jump to clue 7; you follow the chain.
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
| Singly linked | Doubly linked | |
|---|---|---|
| Links per node | next |
prev and next |
| Remove a node you hold a reference to | Need its predecessor: O(n) to find, O(1) if you track it | O(1) |
| Typical use | Most interview problems | LRU cache, deques, editors |
Go deeperFour habits that prevent most bugs
- Save before you overwrite. Before changing
curr.next, store it:ListNode next = curr.next;. Once overwritten, the rest of the list is unreachable. - Use a dummy (sentinel) node whenever the head can change: removing the head, merging, partitioning. Return
dummy.next. - Check what you dereference.
fast.next.nextneeds bothfast != nullandfast.next != null. Write the loop condition from the expression you are about to evaluate. - Draw three nodes and the arrows for every rewiring step. Interviewers expect it, and it catches order-of-assignment bugs immediately.
Go deeperPointer invariants
Name what each pointer means and keep it true after every iteration:
previs the last node of the already-processed part,curris the first unprocessed node.tailis the last node of the list being built (start it at the dummy).- After
ksteps,slowisknodes from the head andfastis2k.
When a loop goes wrong, check these after one iteration on paper. It is faster than debugging output.
6.2Reversal and in-place rewiring
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Reversing a list or part of it, and problems that need to walk a list backward (palindrome check, adding numbers from the end, reordering).
// LC 206. Invariant: prev is the reversed part, curr is the rest.
static ListNode reverseList(ListNode head) {
ListNode prev = null, curr = head;
while (curr != null) {
ListNode next = curr.next; // 1. save the rest before breaking the link
curr.next = prev; // 2. point backward
prev = curr; // 3. grow the reversed part
curr = next; // 4. move on
}
return prev;
}
// Recursive version: reverse the rest, then hook this node to the end.
static ListNode reverseRecursive(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverseRecursive(head.next);
head.next.next = head; // the old next is now the tail of the reversed part
head.next = null;
return newHead;
}
Iterative vs recursive. Both are O(n) time. The iterative version uses O(1) space; the recursive one uses O(n) stack and can overflow for long lists (10^5 nodes). Prefer iterative in interviews, but understand the recursive version: it is the cleanest example of "trust the recursive call, then fix up this node" (Module 8).
Go deeperReversing a sublist and reconnecting
Reversing positions left..right (LC 92) adds one difficulty: reconnecting both ends. Keep before (the node before position left; a dummy makes it exist even when left = 1) and the original first node of the segment (which becomes its tail). After reversing, before.next gets the new segment head and the old segment head points to the node after right. Reverse Nodes in k-Group (LC 25) repeats this per block, after first checking that k nodes exist.
6.3Merging, removing, and rearranging
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
// LC 21. A dummy node removes "is this the first node?" checks.
static ListNode mergeTwoLists(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), tail = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { tail.next = a; a = a.next; }
else { tail.next = b; b = b.next; }
tail = tail.next;
}
tail.next = (a != null) ? a : b; // attach the leftover list
return dummy.next;
}
// LC 19. Move fast n + 1 steps ahead of slow; when fast hits null, slow is before the target.
static ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head); // handles removing the head itself
ListNode fast = dummy, slow = dummy;
for (int i = 0; i <= n; i++) fast = fast.next;
while (fast != null) { fast = fast.next; slow = slow.next; }
slow.next = slow.next.next;
return dummy.next;
}
The gap technique in Remove Nth From End: advance fast n + 1 steps from the dummy, then move both until fast is null. The gap stays n + 1, so slow stops just before the node to remove, which is exactly where you need to be to unlink it.
Guided solveLC 143. Reorder ListFive questions, each with a reveal
Problem: 143Reorder ListMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Rearrange
L0 -> L1 -> ... -> LnintoL0 -> Ln -> L1 -> Ln-1 -> ...in place, by rewiring nodes (not values).Input analysis
Up to 5 * 10^4 nodes. Singly linked, so we cannot walk backward from the end.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
We need the nodes from the end in reverse order. Walking backward on a singly linked list means reversing it. Taking "from both ends" means splitting at the middle.
Brute force
Copy nodes into an
ArrayList, then relink using two indices from both ends. O(n) time, O(n) space. Perfectly fine as a first answer; the follow-up is O(1) space. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. middle, reverse the second half, interleave
Key observation
The result interleaves the first half with the reversed second half. All three steps (find middle, reverse, merge alternately) are standard O(1)-space operations.
Visualization
1 -> 2 -> 3 -> 4 -> 5 middle (slow) = 3 first half: 1 -> 2 -> 3 second half: 4 -> 5 reverse second half: 5 -> 4 interleave: 1 -> 5 -> 2 -> 4 -> 3
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
- Find the middle with slow/fast where
faststops whenfast.nextorfast.next.nextis null (slow ends at the end of the first half). - Reverse
slow.next, then cut withslow.next = null. - Alternate: take one from the first half, one from the second, until the second half is used up.
Java implementation
Java// LC 143. L0 L1 ... Ln -> L0 Ln L1 Ln-1 ... static void reorderList(ListNode head) { if (head == null || head.next == null) return; ListNode slow = head, fast = head; // 1. find the middle while (fast.next != null && fast.next.next != null) { slow = slow.next; fast = fast.next.next; } ListNode second = reverseList(slow.next); // 2. reverse second half slow.next = null; // cut: first half ends at slow ListNode first = head; // 3. interleave while (second != null) { ListNode firstNext = first.next, secondNext = second.next; first.next = second; second.next = firstNext; first = firstNext; second = secondNext; } }Dry run
Even length
1 2 3 4: slow stops at 2 (sincefast.next.nextis null at 3). Second half3 4reversed to4 3. Interleave:1 4 2 3. Odd length is shown above; the first half keeps the extra node. - Find the middle with slow/fast where
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(1) extra space.
Edge cases
Empty list, one node, two nodes (the early return handles 0 and 1; two nodes produce the same order). Forgetting the cut leaves a cycle:
3 -> 4still exists while4points back into the first half.Transfer lesson
"Middle + reverse second half" also solves Palindrome Linked List (LC 234) and Maximum Twin Sum (LC 2130). Breaking a hard list problem into the three standard operations is the usual route.
Common mistakes
- Losing the rest of the list by overwriting
nextbefore saving it. - Returning
headafter the head changed (use the dummy). - Forgetting to set the new tail's
nextto null, creating a cycle (common after reversing or reordering). - Comparing nodes with
.valwhen the problem needs identity (==on references).
Linked list manipulation
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 86Partition List Build two lists with two dummies, then join them | Medium | High | |
| 61Rotate List Close the list into a ring, then cut at length - k % length | Medium | High | |
| 19Remove Nth Node From End of List Gap of n between two pointers; dummy for head removal | Medium | High | |
| 92Reverse Linked List II Reverse a sublist and reconnect both ends | Medium | High | |
| 2Add Two Numbers Carry propagation while either list or carry remains | Medium | High | |
| 24Swap Nodes in Pairs Rewire three pointers per step; draw it first | Medium | Medium | |
| 328Odd Even Linked List Build two chains and join them | Medium | Medium | |
| 143Reorder List Find middle, reverse second half, interleave | Medium | High | |
| 138Copy List with Random Pointer Map old node -> new node, or interleave copies | Medium | High | |
| 25Reverse Nodes in k-Group Reverse in blocks; check k nodes exist first | Hard | High |
6.4Fast and slow pointers
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Finding the middle, detecting a cycle, finding where a cycle starts, and the same questions on any sequence defined by "next" (Happy Number, Find the Duplicate Number), all with O(1) extra memory.
Two runners on a track. If the track is a straight road, the fast runner reaches the end. If it loops, the fast runner eventually laps the slow one from behind.
- Signal
- Cycle detection, middle element, kth from end, a sequence that might repeat (happy number), duplicates in [1..n].
- Required condition
- A function next(x) that defines a path; a cycle exists if the path revisits a state.
Mental model, core operation, limits, and what to use instead
- Mental model
- Two runners on a track: if it loops, the faster one laps the slower.
- Core operation
- slow moves 1, fast moves 2; on meeting, reset one to the start and move both by 1 to find the entry.
- Complexity benefit
- O(1) memory instead of an O(n) visited set.
- Limitations
- Only for 'next' functions with one successor; does not give the cycle path directly.
- Alternative
- HashSet of visited states (simpler, O(n) memory).
Go deeperFinding the middle
// LC 876. For even length this returns the SECOND middle.
static ListNode middleNode(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
With while (fast != null && fast.next != null), odd lengths give the exact middle and even lengths give the second of the two middles. With while (fast.next != null && fast.next.next != null) (as in Reorder List), even lengths give the first middle. Pick the one your problem needs and test both parities.
Go deeperCycle detection and the entry point (Floyd's algorithm)
Why they meet. Inside the cycle, the distance from fast to slow shrinks by exactly one each step, so it reaches zero; fast cannot jump over slow.
Why resetting finds the entry. Let a = distance from head to entry, b = distance from entry to the meeting point, c = cycle length. When they meet, slow walked a + b and fast walked 2(a + b). Fast's extra a + b is a whole number of laps: a + b = k * c. So a = k * c - b: walking a steps from the meeting point lands exactly on the entry. A pointer starting at the head reaches the entry after the same a steps.
// LC 142. Phase 1: detect a meeting. Phase 2: reset one pointer to head;
// moving both one step at a time, they meet at the cycle entry.
static ListNode detectCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) { // reference equality, not val
ListNode p = head;
while (p != slow) { p = p.next; slow = slow.next; }
return p;
}
}
return null; // fast reached the end: no cycle
}
Guided solveLC 287. Find the Duplicate NumberFive questions, each with a reveal
Problem: 287Find the Duplicate NumberMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
n + 1integers, each in[1, n], exactly one value repeated (possibly many times). Find it without modifying the array and with O(1) extra space.Input analysis
The value range
[1, n]matches the valid indices1..n. That match is the hint. - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"O(1) space, no modification" rules out sorting and hash sets. Reading each value as a pointer
i -> nums[i]gives a sequence that must revisit some index, because there are more positions than distinct values.Brute force
Hash set: O(n) space. Sort: modifies the array. Count values <= mid and binary search on the value range: O(n log n), valid but slower.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. values are pointers, so the array is a linked list with a cycle
Key observation
Start at index 0 (nothing points to it, since values are >= 1). Follow
i -> nums[i]. Two different indices hold the duplicate value, so two arrows point at the same index: that index is the entry of a cycle, and the entry's index is the duplicate value.Visualization
nums = [1, 3, 4, 2, 2] index: 0 1 2 3 4 path: 0 -> 1 -> 3 -> 2 -> 4 -> 2 -> 4 -> ... indices 3 and 4 both point to 2: cycle entry = 2 = duplicate
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Phase 1:
slow = nums[slow],fast = nums[nums[fast]]until equal. Phase 2: reset one pointer to 0 and move both one step at a time; they meet at the entry.Java implementation
Java// LC 287. Read nums[i] as "next pointer". Index 0 is the head (no value points to 0). static int findDuplicate(int[] nums) { int slow = 0, fast = 0; do { slow = nums[slow]; fast = nums[nums[fast]]; } while (slow != fast); int p = 0; while (p != slow) { p = nums[p]; slow = nums[slow]; } return p; // the cycle entry is the repeated value }The structure is identical to
detectCycleabove withnextreplaced by array lookup.Dry run
[1,3,4,2,2], phase 1: (slow, fast) = (1, 3), (3, 4), (2, 4), (4, 4): they meet at index 4. Phase 2 from p = 0 and slow = 4: (1, 2), (3, 4), (2, 2): they meet at 2, the duplicate. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(1) space.
Edge cases
The duplicate appears more than twice (still a single entry). The duplicate is 1. n = 1 (
[1, 1]).Transfer lesson
Any function
next(x)over a finite set defines a path that must eventually cycle. Happy Number (LC 202) and Circular Array Loop (LC 457) use the same tool on implicit sequences.
Common mistakes
- Starting
fastathead.nextand then using the reset trick: the distance argument assumes both start at the head. - Comparing values instead of references for cycle detection (two different nodes can hold the same value).
- Mixing up the two middle conventions on even lengths.
Fast and slow pointers
8 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2095Delete the Middle Node of a Linked List Stop slow one node early to delete | Medium | Medium | |
| 1721Swapping Nodes in a Linked List Gap technique finds the kth from the end in one pass | Medium | High | |
| 142Linked List Cycle II After meeting, reset one pointer to head; they meet at the entry | Medium | High | |
| 2130Maximum Twin Sum of a Linked List Reverse second half and walk both halves | Medium | Medium | |
| 109Convert Sorted List to Binary Search Tree Middle node becomes the root; recurse on both halves | Medium | Medium | |
| 148Sort List Split at the middle with fast/slow, then merge sort | Medium | Medium | |
| 287Find the Duplicate Number Treat value as next pointer, apply cycle entry logic | Medium | High | |
| 457Circular Array Loop Fast/slow on an implicit graph with direction rules | Medium | Medium |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 7: Stacks, queues, and monotonic structures
A stack answers "what is the most recent unresolved thing?", a queue answers "what is the oldest?", and a monotonic stack or deque keeps only the elements that can still matter. Together they turn many O(n^2) "look back" scans into O(n).
7.1Stack, queue, and deque fundamentals
You process tasks in arrival order, but sometimes put a task back at the front to retry it first. Stack, queue, or deque? Which Java class?
Reveal the answer
A deque: you remove from the front and also insert at the front. Use ArrayDeque (offerLast, pollFirst, offerFirst). Avoid java.util.Stack (legacy and synchronized) and LinkedList (slower, allows nulls).
| Structure | Order | Core operations | Typical use |
|---|---|---|---|
| Stack | Last in, first out | push, pop, peek | Matching, undo, nesting, DFS |
| Queue | First in, first out | offer, poll, peek | BFS, scheduling, streams, sliding time windows |
| Deque | Both ends | offer/poll/peek First and Last | Monotonic deque, palindromes, both-ended simulation |
In Java, use ArrayDeque for all three. java.util.Stack is a legacy, synchronized subclass of Vector, and LinkedList allocates a node per element. ArrayDeque is a resizable circular array: all end operations are amortized O(1).
static void dequeApi() {
Deque<Integer> stack = new ArrayDeque<>(); // prefer over java.util.Stack (synchronized, legacy)
stack.push(1); stack.push(2); // push/pop/peek work on the FRONT
int top = stack.peek(); // 2
stack.pop();
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(1); queue.offer(2); // offer adds at the BACK
int front = queue.poll(); // 1, poll removes from the FRONT
Deque<Integer> deque = new ArrayDeque<>();
deque.offerFirst(1); deque.offerLast(2);
deque.peekFirst(); deque.peekLast(); // null if empty (peek/poll), exception for get/remove
deque.pollFirst(); deque.pollLast();
// ArrayDeque does NOT allow null elements.
if (top + front != 3) throw new AssertionError();
}
Watch the method pairs: push/pop/peek act on the front; offer/poll on the back and front respectively. Mixing push with poll on the same deque silently turns your stack into a queue. Pick one vocabulary per deque.
Queue from stacks and stack from queues (LC 232, 225) are design questions that combine these structures; they are covered with their amortized analysis in Module 17.
7.2Stacks for matching, parsing, and nesting
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Problems where an element must be matched with the most recent unmatched one: brackets, tags, function calls, nested encodings, collisions that resolve against the latest survivor.
A pile of plates: you only ever deal with the top one.
- Signal
- Matching pairs (brackets, tags), undo, nested structure (decode string, calculators), collisions that resolve against the latest item.
- Required condition
- The element you need next is always the most recently added unresolved one (LIFO).
Mental model, core operation, limits, and what to use instead
- Mental model
- A pile of plates: you only touch the top.
- Core operation
- Push unresolved items; on a closing or resolving event, pop and combine.
- Complexity benefit
- Avoids rescanning back through earlier input: O(n) total.
- Limitations
- Only gives access to the top; not for 'oldest first' (use a queue).
- Alternative
- Recursion (an implicit stack), a counter when only one bracket type exists.
// LC 20. The most recent unmatched opener must match the next closer.
static boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')'); // push the EXPECTED closer
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
else if (stack.isEmpty() || stack.pop() != c) return false;
}
return stack.isEmpty(); // leftover openers are invalid
}
Go deeperExpression evaluation
Postfix (Reverse Polish) notation is evaluated with a single operand stack: numbers are pushed, an operator pops two operands and pushes the result. Mind the order: for a - b, the first pop is b.
// LC 150. Operands wait on the stack; an operator consumes the top two.
static int evalRPN(String[] tokens) {
Deque<Integer> stack = new ArrayDeque<>();
for (String t : tokens) {
switch (t) {
case "+" -> stack.push(stack.pop() + stack.pop());
case "*" -> stack.push(stack.pop() * stack.pop());
case "-" -> { int b = stack.pop(), a = stack.pop(); stack.push(a - b); } // order matters
case "/" -> { int b = stack.pop(), a = stack.pop(); stack.push(a / b); } // truncates toward zero
default -> stack.push(Integer.parseInt(t));
}
}
return stack.pop();
}
For infix with precedence and no parentheses (LC 227), apply * and / immediately to the stack top and push +x or -x for addition and subtraction; the answer is the sum of the stack. With parentheses (LC 224), push the running result and sign when you see (, and combine when you see ).
Guided solveLC 394. Decode StringFive questions, each with a reveal
Problem: 394Decode StringMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Expand encodings of the form
k[text], which can nest:3[a2[c]]becomesaccaccacc.Input analysis
Length up to 30, counts up to 300 and possibly multi-digit, valid input guaranteed.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Nesting with matching open and close markers, and the inner part must be finished before the outer part can use it: a stack.
Brute force
Repeatedly find an innermost
k[...](no brackets inside), replace it with its expansion, and repeat. Correct but O(n^2) string rebuilding per level. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. '[' saves the current context, ']' restores it
Key observation
When
[appears, whatever we were building is paused. When the matching]arrives, the paused text resumes with the repeated inner text appended. "Paused contexts resumed in reverse order" is exactly a stack.Visualization
s = 3[a2[c]] read 3 k = 3 read [ push (3, "") current = "" read a current = "a" read 2 k = 2 read [ push (2, "a") current = "" read c current = "c" read ] pop (2, "a") current = "a" + "cc" = "acc" read ] pop (3, "") current = "" + "acc" x 3 = "accaccacc"
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Keep
currenttext and the pending countk. Digits extendk.[pusheskandcurrent, then resets both. A letter appends tocurrent.]pops the count and the outer text, appendscurrentthat many times to the outer text, and makes it current.Java implementation
Java// LC 394. "3[a2[c]]" -> "accaccacc". On '[' save the context; on ']' restore and repeat. static String decodeString(String s) { Deque<Integer> counts = new ArrayDeque<>(); Deque<StringBuilder> outer = new ArrayDeque<>(); StringBuilder current = new StringBuilder(); int k = 0; for (char c : s.toCharArray()) { if (Character.isDigit(c)) { k = k * 10 + (c - '0'); // multi-digit counts like "12[" } else if (c == '[') { counts.push(k); // remember how many times to repeat outer.push(current); // remember the text before '[' current = new StringBuilder(); k = 0; } else if (c == ']') { int times = counts.pop(); StringBuilder before = outer.pop(); for (int i = 0; i < times; i++) before.append(current); current = before; // continue building the outer level } else { current.append(c); } } return current.toString(); }Dry run
The trace above. Check
2[abc]3[cd]ef: after the first], current isabcabc; the second group pushes it and returnsabcabccdcdcd; thenefis appended. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(output length) time and space: every output character is appended a bounded number of times per nesting level.
Edge cases
Multi-digit counts (
10[a]), letters outside any brackets, adjacent groups, deep nesting.Transfer lesson
"Save context on open, restore on close" is the pattern behind calculators with parentheses (LC 224), file path simplification (LC 71), and iterative tree traversal. The recursion version of this problem is equivalent; the stack is the explicit form of the call stack.
Common mistakes
- Popping from an empty stack: check
isEmpty()first, or return early. - Forgetting leftover elements at the end (
"(("is invalid). - Reversing operand order in subtraction and division.
- Assuming single-digit numbers when parsing.
Stacks: matching, parsing, simulation
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 921Minimum Add to Make Parentheses Valid Count unmatched openers and closers without a stack | Medium | High | |
| 1209Remove All Adjacent Duplicates in String II Stack of (char, count) pairs collapses runs of length k | Medium | High | |
| 150Evaluate Reverse Polish Notation Operands on a stack; mind operand order for - and / | Medium | High | |
| 71Simplify Path Stack of directory names with '..' pops | Medium | Medium | |
| 394Decode String Two stacks: counts and partial strings | Medium | High | |
| 735Asteroid Collision Resolve collisions against the stack top in a loop | Medium | High | |
| 227Basic Calculator II Apply * and / immediately, defer + and - | Medium | High | |
| 1249Minimum Remove to Make Valid Parentheses Stack of unmatched indices to delete | Medium | High | |
| 946Validate Stack Sequences Simulate push and greedy pop | Medium | Medium | |
| 224Basic Calculator Save (result, sign) on '(' and restore on ')' | Hard | High |
7.3Monotonic stack
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
For every element, find the nearest element to its left or right that is greater (or smaller). Also: spans ("how many previous days were cooler"), and every problem where an element is bounded by its nearest larger or smaller neighbors (histogram rectangles, subarray minimums, removing digits).
People standing in a line, all looking to the right. A tall person hides everyone shorter standing behind them, so when you ask "who is the first taller person I can see?", the hidden people never matter again.
- Signal
- Next/previous greater or smaller element, span/width bounded by a taller or shorter element, 'how far until warmer'.
- Required condition
- Once a bigger (or smaller) element arrives, the elements it beats can never be the answer for anything later.
Mental model, core operation, limits, and what to use instead
- Mental model
- People in a queue looking right: a tall person hides everyone shorter behind them.
- Core operation
- Keep indices with monotonic values; pop while the new element beats the top, resolving each popped index; then push.
- Complexity benefit
- Every index is pushed and popped once: O(n) instead of O(n^2) nearest-greater searches.
- Limitations
- Answers nearest-boundary questions only; strict vs non-strict comparisons matter with duplicates.
- Alternative
- Monotonic deque when the window also expires from the left; sorting + DSU for some offline variants.
Go deeperHow it works
Keep a stack of indices whose values are monotonic (for "next greater", decreasing from bottom to top). When a new element arrives:
- While the top's value is smaller than the new value, the new element is the top's next greater: pop and record.
- Push the new index.
Each index is pushed once and popped at most once, so the whole scan is O(n), even though there is a loop inside a loop.
temps: 73 74 75 71 69 72 76 73 i=0 73 stack [] push 0 stack [0] i=1 74 74 > 73: pop 0 -> answer[0] = 1 stack [1] i=2 75 pop 1 -> answer[1] = 1 stack [2] i=3 71 71 < 75 stack [2, 3] i=4 69 stack [2, 3, 4] i=5 72 pop 4 -> 1, pop 3 -> 2; 72 < 75 stack [2, 5] i=6 76 pop 5 -> 1, pop 2 -> 4 stack [6] i=7 73 stack [6, 7] end: indices 6, 7 never found a warmer day -> 0
// LC 739. Stack holds indices of days still waiting for a warmer day.
// Temperatures at those indices are decreasing from bottom to top.
static int[] dailyTemperatures(int[] temps) {
int[] answer = new int[temps.length]; // default 0 = never warmer
Deque<Integer> waiting = new ArrayDeque<>();
for (int i = 0; i < temps.length; i++) {
while (!waiting.isEmpty() && temps[waiting.peek()] < temps[i]) {
int j = waiting.pop(); // day i is j's next warmer day
answer[j] = i - j;
}
waiting.push(i);
}
return answer;
}
Go deeperChoosing the variant
| Want | Stack holds values that are | Pop while | Answer comes from |
|---|---|---|---|
| Next greater (to the right) | decreasing | nums[top] < nums[i] |
the element that causes the pop |
| Next smaller | increasing | nums[top] > nums[i] |
the element that causes the pop |
| Previous greater (to the left) | decreasing | nums[top] <= nums[i] |
the stack top after popping |
| Previous smaller | increasing | nums[top] >= nums[i] |
the stack top after popping |
Duplicates. When counting subarrays (LC 907), make one side strict and the other non-strict, so each subarray's minimum is attributed to exactly one index.
Circular arrays (LC 503): iterate i from 0 to 2n - 1 and use i % n, pushing only during the first pass.
Guided solveLC 84. Largest Rectangle in HistogramFive questions, each with a reveal
Problem: 84Largest Rectangle in HistogramHard. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Bars of width 1 with given heights. Find the largest rectangle area that fits under the bars.
Input analysis
Up to 10^5 bars, heights up to 10^4. O(n^2) (10^10) is too slow.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Any rectangle's height equals its shortest bar. So for each bar, ask "how far can a rectangle of exactly this height extend?" It extends until it hits a shorter bar on each side: nearest smaller to the left and right, the monotonic stack question.
Brute force
For every pair
(l, r), area =min(heights[l..r]) * width: O(n^2) with a running minimum. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. each bar's best rectangle is bounded by its previous and next smaller bars
Key observation
The largest rectangle has some bar as its shortest. For that bar
i, the rectangle spans(prevSmaller(i), nextSmaller(i))exclusive. Computing all previous and next smaller bars takes one monotonic pass.Visualization
Figure 7.2The highlighted rectangle is the best one for the bar of height 5: area 5 x 2 = 10. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Keep an increasing stack of indices. When the current bar is lower than the top, the top bar's next smaller is
iand its previous smaller is the new stack top after popping. Width =i - newTop - 1. A sentinel height 0 at the end flushes the stack.Java implementation
Java// LC 84. For each bar, the widest rectangle of its height spans between // the previous smaller bar and the next smaller bar. static int largestRectangleArea(int[] heights) { int n = heights.length, best = 0; Deque<Integer> stack = new ArrayDeque<>(); // indices with increasing heights for (int i = 0; i <= n; i++) { int h = (i == n) ? 0 : heights[i]; // sentinel 0 flushes the stack at the end while (!stack.isEmpty() && heights[stack.peek()] >= h) { int height = heights[stack.pop()]; int leftBoundary = stack.isEmpty() ? -1 : stack.peek(); // previous smaller int width = i - leftBoundary - 1; // i is the next smaller best = Math.max(best, height * width); } stack.push(i); } return best; }Dry run
[2, 1, 5, 6, 2, 3]. Ati = 1(height 1): pop 0 (height 2), width1 - (-1) - 1 = 1, area 2. Ati = 4(height 2): pop 3 (height 6), left boundary 2, width 1, area 6; pop 2 (height 5), left boundary 1, width 2, area 10. At the sentinel: pop 5 (3), width 1, area 3; pop 4 (2), left boundary 1, width 4, area 8; pop 1 (1), empty stack, width 6, area 6. Best 10. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(n) stack.
Edge cases
Strictly increasing heights (everything is resolved by the sentinel), all equal heights (popping on
>=handles equal bars: the last one popped gets the full width), single bar.Transfer lesson
"For each element, the range where it is the minimum (or maximum)" solves Maximal Rectangle (LC 85, one histogram per row), Sum of Subarray Minimums (LC 907, contribution
value * leftCount * rightCount), and Maximum Width Ramp variants.
Common mistakes
- Storing values instead of indices, then being unable to compute distances or widths.
- Getting strictness wrong with duplicates.
- Forgetting to flush the stack at the end (a sentinel or a final loop).
Monotonic stack
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 739Daily Temperatures Store indices so you can compute distances | Medium | High | |
| 1019Next Greater Node In Linked List Next greater element on a list: copy to an array first | Medium | High | |
| 503Next Greater Element II Circular array: iterate 2n with index mod n | Medium | Medium | |
| 901Online Stock Span Streaming monotonic stack that stores collapsed spans | Medium | Medium | |
| 456132 Pattern Scan from the right keeping the best 'middle' candidate | Medium | Medium | |
| 402Remove K Digits Greedy removal of a larger previous digit | Medium | High | |
| 907Sum of Subarray Minimums Contribution: count subarrays where each element is the minimum | Medium | High | |
| 316Remove Duplicate Letters Monotonic stack with 'can it appear later' check | Medium | Medium | |
| 84Largest Rectangle in Histogram Previous and next smaller bars bound each rectangle | Hard | High | |
| 85Maximal Rectangle Row-by-row histograms reuse problem 84 | Hard | Medium |
7.4Queues and the monotonic deque
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Plain queues handle BFS, scheduling, and "events in the last T seconds" windows (LC 933). A monotonic deque answers "what is the maximum (or minimum) of the current window?" in O(1) amortized per step, which a single variable or a stack cannot.
A leaderboard for the last k games. When a new player scores higher than older players still on the board, those older players can never top the board again (the newcomer is both better and will stay longer), so they are removed. The oldest players also drop off when their games leave the window.
- Signal
- Max or min of every sliding window; DP where dp[i] needs the best of the last k states.
- Required condition
- Old elements expire from the front; new elements make worse older ones useless from the back.
Mental model, core operation, limits, and what to use instead
- Mental model
- A leaderboard for the last k games: newer, better players push out older, weaker ones.
- Core operation
- Pop expired indices from the front, pop dominated indices from the back, push i, read the front.
- Complexity benefit
- Window max for all windows in O(n) instead of O(n * k) or O(n log k) with a heap.
- Limitations
- Only max or min, not arbitrary order statistics (median needs two heaps).
- Alternative
- Heap with lazy deletion (O(n log n)); TreeMap of counts for any order statistic.
Go deeperTwo rules, two ends
- Front: expiry. If the index at the front has left the window (
<= i - k), poll it. - Back: dominance. While the value at the back is
<=the new value, poll it from the back. It is older and not larger, so it can never be the maximum again.
After both rules, push i at the back. The front is the window maximum.
nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3 (deque shows values; it stores indices) i=0 1 deque [1] i=1 3 3 >= 1: drop 1 [3] i=2 -1 [3, -1] window max 3 i=3 -3 [3, -1, -3] max 3 i=4 5 index of 3 expired; 5 drops -3, -1 [5] max 5 i=5 3 [5, 3] max 5 i=6 6 drops 3, 5 [6] max 6 i=7 7 drops 6 [7] max 7
Guided solveLC 239. Sliding Window MaximumFive questions, each with a reveal
Problem: 239Sliding Window MaximumHard. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Return the maximum of every contiguous window of size
k.Input analysis
n up to 10^5, k up to n. O(n * k) can be 10^10.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
A fixed window whose state is a maximum. Removal from a max is not O(1) with a plain variable, so we need a structure that supports expiry from one end and dominance from the other.
Brute force
Scan each window: O(n * k). A max-heap with lazy deletion: O(n log n).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. an element older and smaller than a newer one can never be a window maximum
Key observation
If
j < iandnums[j] <= nums[i], thenjcan never be the maximum of any window that containsi, and every future window that containsjalso containsi. Sojcan be deleted. What remains is decreasing, and the front is the max.Visualization
The trace above.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
For each
i: drop expired front, drop dominated back, pushi, and oncei >= k - 1recordnums[front].Java implementation
Java// LC 239. Deque of indices; values decrease from front to back. // Front = max of the current window. static int[] maxSlidingWindow(int[] nums, int k) { int[] result = new int[nums.length - k + 1]; Deque<Integer> dq = new ArrayDeque<>(); for (int i = 0; i < nums.length; i++) { if (!dq.isEmpty() && dq.peekFirst() <= i - k) dq.pollFirst(); // left the window while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) dq.pollLast(); // can never be max again dq.offerLast(i); if (i >= k - 1) result[i - k + 1] = nums[dq.peekFirst()]; } return result; }Dry run
The trace above produces
[3, 3, 5, 5, 6, 7]. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time: each index enters and leaves the deque once. O(k) space.
Edge cases
k = 1(every element),k = n(one window), all equal values (the<=drops older equals, which is fine), strictly decreasing input (the deque fills to k).Transfer lesson
A monotonic deque is how you get "best of the last k states" in DP (Jump Game VI, LC 1696; Constrained Subsequence Sum, LC 1425) and "smallest prefix sum to the left" with negatives (Shortest Subarray with Sum at Least K, LC 862). Two deques (one max, one min) handle "max - min <= limit" windows (LC 1438).
Common mistakes
- Storing values instead of indices, which makes expiry impossible.
- Checking expiry with
<instead of<=, which keeps a window of size k + 1. - Using a heap and forgetting stale entries.
Queues, deques, and the monotonic deque
5 Medium 3 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 950Reveal Cards In Increasing Order Simulate the reveal process on indices with a queue | Medium | High | |
| 641Design Circular Deque Array with head index and modulo arithmetic | Medium | Medium | |
| 649Dota2 Senate Two queues of indices; winner re-enters at index + n | Medium | Medium | |
| 1438Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit Two monotonic deques give window max and min | Medium | High | |
| 1696Jump Game VI DP whose max over a window comes from a deque | Medium | Medium | |
| 1425Constrained Subsequence Sum DP plus a monotonic deque over the last k states | Hard | Medium | |
| 239Sliding Window Maximum Decreasing deque of indices; front is the max | Hard | High | |
| 862Shortest Subarray with Sum at Least K Monotonic deque over prefix sums handles negatives | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 8: Recursion and backtracking
Recursion solves a problem by solving a smaller copy of it. Backtracking is recursion that builds candidates one choice at a time and undoes each choice after exploring it. Both are the foundation for trees (Module 9) and dynamic programming (Module 14).
8.1Recursion fundamentals
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Problems whose answer on an input can be expressed through answers on smaller inputs: a suffix of an array, half of a range, a subtree, a shorter string.
Delegation. A manager asked "how many people work under you?" asks each direct report the same question, adds the answers, and adds one for each report. The manager never needs to know how the reports computed their numbers.
- Signal
- The problem on input n can be stated using the same problem on a smaller input (n - 1, halves, subtrees, splits).
- Required condition
- A base case that stops the recursion and a smaller subproblem each call.
Mental model, core operation, limits, and what to use instead
- Mental model
- Delegation: trust the recursive call to solve the smaller version, then combine.
- Core operation
- Define what the function returns, handle the base case, reduce, combine.
- Complexity benefit
- Expresses tree-shaped and nested work directly; divide and conquer can turn O(n^2) into O(n log n).
- Limitations
- Stack depth (Java default handles about 10^4 frames safely); repeated subproblems explode without memoization.
- Alternative
- Iteration with an explicit stack for deep inputs; memoization or DP for overlapping subproblems.
Go deeperThree questions for every recursive function
- What does the function return, in words? "
sum(a, i)returns the sum ofa[i..]." Write this down; it is the contract the recursive call fulfills. - What is the smallest input, and its answer? The base case. Here
i == a.length, an empty suffix, sum 0. - How does the answer for this input use the answer for a smaller one?
a[i] + sum(a, i + 1).
If those three are right, the function is right. Do not trace the whole recursion in your head; trust the contract (the "leap of faith") and check only one level.
// Sum of a[i..]: the call for i trusts the call for i + 1.
static int sum(int[] a, int i) {
if (i == a.length) return 0; // base case: empty suffix
return a[i] + sum(a, i + 1); // reduce, then combine
}
Go deeperTracing the call stack
Each call gets its own frame with its own local variables. Frames pile up until a base case returns, then unwind in reverse order, each combining its child's result.
To trace on a whiteboard, draw a column of frames: write each call's arguments when it starts and its return value when it finishes. For branching recursion, draw the call tree instead (see the Fibonacci tree in Module 1).
Go deeperReducing to a smaller instance
Some recursions shrink in a less obvious way. The Josephus problem (LC 1823): n friends in a circle, every k-th is removed. After the first removal, the remaining n - 1 people form the same problem, just relabeled to start k positions later. So the winner's position shifts by k:
// LC 1823. Winner (0-based) among n friends, removing every k-th.
// After the first removal, the circle restarts k positions later with n - 1 people.
static int winner(int n, int k) {
if (n == 1) return 0;
return (winner(n - 1, k) + k) % n; // shift the smaller answer back into this circle
}
static int findTheWinner(int n, int k) { return winner(n, k) + 1; } // 1-based
Finding "the same problem, relabeled" is the skill. K-th Symbol in Grammar (LC 779) works the same way: row n's symbol at position k is determined by its parent in row n - 1 at position (k + 1) / 2.
Go deeperDivide and conquer
Split the input into parts, solve each recursively, and combine. Merge sort (Module 4) is the classic. Different Ways to Add Parentheses (LC 241) splits at every operator:
// LC 241. Split at every operator; combine every left result with every right result.
static List<Integer> diffWaysToCompute(String expr) {
List<Integer> results = new ArrayList<>();
for (int i = 0; i < expr.length(); i++) {
char op = expr.charAt(i);
if (op != '+' && op != '-' && op != '*') continue;
for (int a : diffWaysToCompute(expr.substring(0, i)))
for (int b : diffWaysToCompute(expr.substring(i + 1)))
results.add(op == '+' ? a + b : op == '-' ? a - b : a * b);
}
if (results.isEmpty()) results.add(Integer.parseInt(expr)); // base case: a plain number
return results;
}
This version recomputes the same substrings many times; caching results per substring (memoization) fixes that, which is the step from recursion to DP.
Go deeperRecursion vs iteration
| Recursion | Iteration | |
|---|---|---|
| Clarity | Natural for trees, nested structures, choices | Natural for linear scans |
| Space | O(depth) stack frames | Usually O(1) beyond explicit data structures |
| Risk | StackOverflowError beyond roughly 10^4 frames |
None from depth |
| Conversion | Any recursion can be made iterative with an explicit stack |
Tail recursion (the recursive call is the last action) does not help in Java: the JVM does not eliminate tail calls.
Common mistakes
- No base case, or a base case that is never reached (the input does not shrink).
- Mutating shared state (a list, a counter) without undoing it when the call returns.
- Exponential repetition: two recursive calls on overlapping inputs without memoization.
Recursion fundamentals and divide and conquer
8 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 779K-th Symbol in Grammar Relate the answer at row n to row n - 1 | Medium | Medium | |
| 1545Find Kth Bit in Nth Binary String Mirror structure: map position k back to the previous string | Medium | High | |
| 1823Find the Winner of the Circular Game Josephus recurrence from a smaller instance | Medium | Medium | |
| 390Elimination Game Track only the head, step, and remaining count | Medium | Medium | |
| 241Different Ways to Add Parentheses Split at every operator and combine sub-results | Medium | Medium | |
| 486Predict the Winner Recursion returning a score difference | Medium | Medium | |
| 894All Possible Full Binary Trees Build results from all left/right size splits | Medium | Medium | |
| 395Longest Substring with At Least K Repeating Characters Divide at characters that can never be in the answer | Medium | Medium |
8.2Backtracking: subsets, permutations, combinations
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Generating every valid configuration: all subsets, all permutations, all combinations that sum to a target, all ways to partition a string. The number of answers is exponential, so the algorithm is too; the goal is to enumerate each answer exactly once and skip dead ends early.
Exploring a maze with chalk. Walk down a corridor, marking the wall. At a dead end, walk back to the last junction, erasing the mark, and try the next corridor.
- Signal
- 'All', 'every', 'generate', 'list all valid' combinations, permutations, partitions, placements. Small n (about 10 to 20).
- Required condition
- The answer is built by a sequence of choices; partial choices can be undone; output size itself may be exponential.
Mental model, core operation, limits, and what to use instead
- Mental model
- Exploring a maze with chalk: walk a path, mark it, and erase the mark when you back out.
- Core operation
- choose -> recurse -> un-choose, with pruning on invalid or hopeless partial states.
- Complexity benefit
- Systematic search with pruning skips large dead subtrees; nothing better exists when you must list all answers.
- Limitations
- Exponential. If the question asks only for a count or best value and subproblems repeat, it is a DP problem.
- Alternative
- DP (count or optimize), bitmask enumeration for n <= 20, greedy when a choice is provably safe.
Go deeperThe choose, explore, un-choose loop
Every backtracking function has the same shape:
backtrack(state):
if state is a complete answer: record a COPY; return
for each choice available from state:
if the choice is invalid or cannot lead to an answer: skip (prune)
make the choice (modify state)
backtrack(state) (explore)
undo the choice (restore state exactly)
The undo step is what makes it backtracking: one shared path list is reused for the whole search, so it must be restored before the next choice.
Go deeperTwo ways to draw the decision tree
Choose / not-choose (binary). At depth i, decide whether element i is in the subset. Leaves are complete subsets.
// LC 78. Choose / not-choose for each index. Leaves of the tree are the subsets.
static List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
chooseOrSkip(nums, 0, new ArrayList<>(), result);
return result;
}
private static void chooseOrSkip(int[] nums, int i, List<Integer> path, List<List<Integer>> result) {
if (i == nums.length) {
result.add(new ArrayList<>(path)); // COPY: path keeps changing after this
return;
}
path.add(nums[i]); // choose nums[i]
chooseOrSkip(nums, i + 1, path, result);
path.remove(path.size() - 1); // undo the choice
chooseOrSkip(nums, i + 1, path, result); // skip nums[i]
}
Pick-the-next (for loop). Each node is a partial answer; its children add one more element chosen from a start index onward. Every node, not only leaves, is a subset. This shape makes duplicate handling and pruning easier.
// LC 90. "For loop" style: each call picks the NEXT element to add.
// Sorting + skipping equal siblings removes duplicate subsets.
static List<List<Integer>> subsetsWithDup(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> result = new ArrayList<>();
pickNext(nums, 0, new ArrayList<>(), result);
return result;
}
private static void pickNext(int[] nums, int start, List<Integer> path, List<List<Integer>> result) {
result.add(new ArrayList<>(path)); // every node is a valid subset
for (int i = start; i < nums.length; i++) {
if (i > start && nums[i] == nums[i - 1]) continue; // same value at the same depth
path.add(nums[i]);
pickNext(nums, i + 1, path, result);
path.remove(path.size() - 1);
}
}
Go deeperPermutations: order matters
For permutations every unused element can be placed next, so the loop runs over all indices and a used[] array marks what is taken. Answers are at the leaves (when the path has n elements).
// LC 46. Order matters: any unused element can go in the next position.
static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
place(nums, new boolean[nums.length], new ArrayList<>(), result);
return result;
}
private static void place(int[] nums, boolean[] used, List<Integer> path, List<List<Integer>> result) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.add(nums[i]);
place(nums, used, path, result);
path.remove(path.size() - 1);
used[i] = false;
}
}
Go deeperCombinations: start index and reuse
| Problem | Recurse with | Why |
|---|---|---|
| Each element at most once (Combinations, Combination Sum II) | i + 1 |
The next choice must come after this one, so {1, 2} and {2, 1} are not both produced |
| Elements reusable (Combination Sum) | i |
The same element may be chosen again, but never an earlier one |
| Order matters (Permutations) | 0 with used[] |
Every position may take any unused element |
// LC 39. Reuse allowed -> recurse with i (not i + 1). Sorted -> break early.
static List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
List<List<Integer>> result = new ArrayList<>();
combine(candidates, 0, target, new ArrayList<>(), result);
return result;
}
private static void combine(int[] c, int start, int remaining, List<Integer> path, List<List<Integer>> result) {
if (remaining == 0) {
result.add(new ArrayList<>(path));
return;
}
for (int i = start; i < c.length; i++) {
if (c[i] > remaining) break; // prune: every later candidate is larger
path.add(c[i]);
combine(c, i, remaining - c[i], path, result);
path.remove(path.size() - 1);
}
}
Go deeperDuplicate handling
When the input contains equal values, the same answer can be built along different branches. The rule: sort, then at a given depth, skip a value equal to the previous sibling's value (i > start && nums[i] == nums[i - 1]). Taking equal values at different depths is still allowed ({2, 2} is valid).
For permutations with duplicates (LC 47), skip nums[i] if it equals nums[i - 1] and nums[i - 1] is not currently used: this forces equal values to be placed in their original order.
Go deeperPruning
Pruning cuts subtrees that cannot produce an answer. Good prunes are cheap checks done before recursing:
- Sorted candidates + sum target: once
c[i] > remaining, every later candidate is also too big:break, notcontinue. - Counts: in Combinations (LC 77), stop when fewer elements remain than slots to fill.
- Validity of partial state: Generate Parentheses (LC 22) never places more
)than(so far, so invalid strings are never built. - Bounds: in partition problems, a bucket that would exceed the target is skipped.
Guided solveLC 90. Subsets IIFive questions, each with a reveal
Problem: 90Subsets IIMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Return all subsets of an array that may contain duplicates, without duplicate subsets.
Input analysis
n up to 10, values from -10 to 10. 2^10 = 1024 subsets at most: exponential output is expected.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"All subsets" is enumeration: backtracking. "Without duplicates" with repeated values: sort and skip.
Brute force
Generate all 2^n subsets, sort each, insert into a
Set<List<Integer>>. Works, but wastes time building duplicates and costs O(n) per subset to deduplicate. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. sort, then skip equal siblings at the same depth
Key observation
Duplicates arise when two equal values are both tried as the next element at the same depth. After sorting, equal values are adjacent, so the second one can be skipped whenever it is not the first choice at that depth.
Visualization
See the duplicate tree above.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Sort.
pickNext(start): record the current path; forifromstart: skip ifi > start && nums[i] == nums[i-1]; addnums[i], recurse withi + 1, remove.Java implementation
subsetsWithDupabove.Dry run
[1, 2, 2]: record[]. Pick 1: record[1]; pick 2 (index 1): record[1,2]; pick 2 (index 2):[1,2,2]. Back at[1], index 2 is skipped (equal sibling). Back at root: pick 2 (index 1):[2], then[2,2]; index 2 skipped at the root. Result:[], [1], [1,2], [1,2,2], [2], [2,2]. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(2^n * n) time (up to 2^n subsets, each copied in O(n)), O(n) recursion depth plus output.
Edge cases
All elements equal (n + 1 subsets: sizes 0 to n), no duplicates (same as Subsets), negative values (sorting still groups them).
Transfer lesson
"Skip an equal value at the same depth" is the universal dedupe rule: Combination Sum II (LC 40), 3Sum (Module 3), Permutations II (with the used-twin condition).
Common mistakes
- Adding
pathitself instead ofnew ArrayList<>(path): every answer ends up as the same (empty) list. - Forgetting to undo (
path.remove(path.size() - 1)or resettingused[i]). path.remove(x)on aList<Integer>with anint xremoves by index, not value. Remove the last index explicitly.continuewherebreakis valid (missed pruning), orbreakwhere onlycontinueis valid (missed answers).
Backtracking: subsets, permutations, combinations
10 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 78Subsets Every node of the decision tree is an answer | Medium | High | |
| 90Subsets II Sort, then skip equal siblings at the same depth | Medium | High | |
| 46Permutations used[] array; answers are at the leaves | Medium | High | |
| 47Permutations II Skip a duplicate unless its twin is already used | Medium | High | |
| 77Combinations Start index prevents reordered duplicates; prune by remaining count | Medium | High | |
| 39Combination Sum Reuse allowed: recurse with the same start index | Medium | High | |
| 40Combination Sum II No reuse plus duplicate skipping | Medium | High | |
| 216Combination Sum III Combinations with both a size and a sum constraint | Medium | Medium | |
| 17Letter Combinations of a Phone Number One level per digit, branching per letter | Medium | High | |
| 22Generate Parentheses Prune with open/close counts instead of validating at the end | Medium | High |
8.3Constraint search on grids, boards, and partitions
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Finding one or all configurations that satisfy constraints: a word path in a grid, queens that do not attack, a Sudoku fill, a partition into equal-sum groups.
Go deeperGrid backtracking
A grid search is backtracking where the choices are the four neighbors and the state includes "visited on the current path". Marking the cell in place (and restoring it after) avoids a separate visited array.
// LC 79. DFS from every cell; mark the cell during the path and restore after.
static boolean exist(char[][] board, String word) {
for (int r = 0; r < board.length; r++)
for (int c = 0; c < board[0].length; c++)
if (dfs(board, word, 0, r, c)) return true;
return false;
}
private static boolean dfs(char[][] b, String word, int i, int r, int c) {
if (i == word.length()) return true;
if (r < 0 || c < 0 || r >= b.length || c >= b[0].length || b[r][c] != word.charAt(i)) return false;
char saved = b[r][c];
b[r][c] = '#'; // visited on the current path
boolean found = dfs(b, word, i + 1, r + 1, c) || dfs(b, word, i + 1, r - 1, c)
|| dfs(b, word, i + 1, r, c + 1) || dfs(b, word, i + 1, r, c - 1);
b[r][c] = saved; // restore for other paths
return found;
}
The restore line matters: a cell used by one failed path must be available to other paths. This is the difference from a graph BFS/DFS that marks cells visited permanently (Module 12).
Go deeperConstraint checks in O(1)
N-Queens places one queen per row, so the choices per row are columns. A square (r, c) is attacked if its column, its diagonal (r - c constant), or its anti-diagonal (r + c constant) already has a queen. Three boolean arrays answer that in O(1).
// LC 51. One queen per row; sets make "is this square attacked?" O(1).
static List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
int[] colOfRow = new int[n];
placeRow(0, n, colOfRow, new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1], result);
return result;
}
private static void placeRow(int row, int n, int[] colOfRow, boolean[] cols,
boolean[] diag, boolean[] anti, List<List<String>> result) {
if (row == n) {
List<String> board = new ArrayList<>();
for (int r = 0; r < n; r++) {
char[] line = new char[n];
Arrays.fill(line, '.');
line[colOfRow[r]] = 'Q';
board.add(new String(line));
}
result.add(board);
return;
}
for (int c = 0; c < n; c++) {
int d = row - c + n - 1, a = row + c; // each diagonal has a unique index
if (cols[c] || diag[d] || anti[a]) continue;
cols[c] = diag[d] = anti[a] = true;
colOfRow[row] = c;
placeRow(row + 1, n, colOfRow, cols, diag, anti, result);
cols[c] = diag[d] = anti[a] = false; // undo
}
}
Guided solveLC 79. Word SearchFive questions, each with a reveal
Problem: 79Word SearchMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Does the word exist as a path of horizontally or vertically adjacent cells, using each cell at most once?
Input analysis
Grid up to 6 x 6, word length up to 15. Small: exponential search with pruning is intended.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"A path where each cell is used once" means the state includes the path itself, so plain BFS (which forgets paths) does not apply. Backtracking over neighbors does.
Brute force
Enumerate all simple paths of length
Lfrom every cell and compare: that is what backtracking does, but checking the character at each step prunes almost every path immediately. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. DFS from each cell, mark the path, restore on the way back
Key observation
A partial path is only worth extending if its characters match the word's prefix. Checking one character per step prunes most branches at depth 1 or 2.
Visualization
A B C E word = ABCCED S F C S (0,0)A -> (0,1)B -> (0,2)C -> (1,2)C -> (2,2)E -> (2,1)D found A D E E path cells are marked '#' while on the path, restored after
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
For each cell,
dfs(i, r, c): ifi == length, success. If out of bounds or the cell does not matchword[i], fail. Mark the cell, try four neighbors withi + 1, restore the cell, return whether any succeeded.Java implementation
existabove.Dry run
For
ABCB: A, B, C match, then the next B would have to be the already-used (0,1): it is marked#, so the path fails, and no other path exists. Result false. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(R * C * 3^L): from each start, each step has at most 3 new directions (not going back). Space O(L) recursion depth.
Edge cases
Single cell; word longer than the number of cells (could return false immediately); repeated letters that tempt reusing a cell.
Transfer lesson
For many words at once, a trie prunes all of them together (Word Search II, LC 212, in Module 13). For "longest path" or "count paths" without the each-cell-once constraint, memoization turns this into DP (Module 14).
Go deeperEstimating backtracking cost
State it as (number of nodes in the decision tree) x (work per node):
| Search | Nodes (roughly) | With copying answers |
|---|---|---|
| Subsets of n | 2^n | O(2^n * n) |
| Permutations of n | n! (times e, counting internal nodes) | O(n! * n) |
| Combinations C(n, k) | C(n, k) leaves | O(C(n, k) * k) |
| Grid path of length L | R * C * 3^L |
8.4When backtracking is the wrong tool
Count the ways to climb 45 stairs taking 1 or 2 steps at a time. Would you backtrack over every path? Estimate how many paths there are.
Reveal the answer
No. There are 1,836,311,903 paths (a Fibonacci number), and backtracking visits each one. You only need the count, and the count for step i depends only on steps i - 1 and i - 2, so DP answers it in O(n). Backtrack when you must list answers; count or optimize with DP.
| Situation | Better tool | Why |
|---|---|---|
| Count the answers, or find the best one, and different paths reach the same state | DP | Each state is solved once instead of once per path |
| A choice is provably safe (exchange argument) | Greedy | No search needed |
| Shortest sequence of moves between states | BFS | First arrival is the shortest; backtracking explores all paths |
| n is large (over about 25 for subsets, 12 for permutations) | Rethink: DP over bitmasks, math, greedy | The tree is too big even with pruning |
Word Break (LC 139) is the standard example: backtracking over split points is exponential on inputs like "aaaa...ab", but the question "can s[0..i) be segmented?" depends only on i, so DP solves it in O(n^2).
Backtracking: constraint search, grids, partitions
7 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 79Word Search Mark visited in place and restore on the way back | Medium | High | |
| 131Palindrome Partitioning Branch on the end of the next piece | Medium | High | |
| 93Restore IP Addresses Strong pruning by remaining length | Medium | Medium | |
| 1219Path with Maximum Gold Try every start; DFS with undo on the grid | Medium | Medium | |
| 2305Fair Distribution of Cookies Assign items to buckets; prune by current best | Medium | Medium | |
| 473Matchsticks to Square Sort descending to fail fast | Medium | Medium | |
| 698Partition to K Equal Sum Subsets Fill buckets one at a time; skip equivalent empty buckets | Medium | Medium | |
| 51N-Queens Column and diagonal sets give O(1) safety checks | Hard | High | |
| 37Sudoku Solver Most-constrained choices and full undo | Hard | Medium |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 9: Trees and binary search trees
Trees are the most common data structure in onsite rounds. Nearly every tree problem is answered by one question: what should each node return to its parent, or receive from it?
9.1Tree fundamentals
A binary tree has n nodes. What are its smallest and largest possible heights, and why does that matter for recursive code?
Reveal the answer
Smallest: about log2(n) levels, when every level is full. Largest: n levels, when every node has one child (a chain). Recursion depth equals the height, so balanced trees cost O(log n) stack and a chain costs O(n), which can overflow Java's stack near 10^5.
Trees represent hierarchy (file systems, org charts, the DOM), sorted data with fast updates (BSTs), and the call structure of recursion itself.
class TreeNode {
int val;
TreeNode left, right;
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val; this.left = left; this.right = right;
}
}
| Term | Meaning |
|---|---|
| Depth of a node | Edges from the root down to it (root has depth 0) |
| Height of a node | Edges on the longest path from it down to a leaf (some problems count nodes; read carefully) |
| Balanced | For every node, left and right heights differ by at most 1 (one common definition) |
| Complete | Every level full except possibly the last, which fills left to right (heaps) |
| Full | Every node has 0 or 2 children |
| BST | Left subtree values < node < right subtree values, for every node |
Complexity vocabulary. Visiting every node is O(n). Recursion depth is O(h), where h is the height: O(log n) for balanced trees, O(n) for skewed ones. Say "O(h) space, which is O(n) in the worst case".
9.2Traversals: DFS orders and BFS levels
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Every tree algorithm visits nodes in some order. Choosing the order is choosing when a node's information is available: before its children (preorder), between them (inorder), after them (postorder), or level by level (BFS).
// Recursive DFS: the only difference between orders is WHERE you visit the node.
static void preorder(TreeNode node, List<Integer> out) {
if (node == null) return;
out.add(node.val); // visit BEFORE children
preorder(node.left, out);
preorder(node.right, out);
}
static void inorder(TreeNode node, List<Integer> out) {
if (node == null) return;
inorder(node.left, out);
out.add(node.val); // visit BETWEEN children (sorted order for a BST)
inorder(node.right, out);
}
static void postorder(TreeNode node, List<Integer> out) {
if (node == null) return;
postorder(node.left, out);
postorder(node.right, out);
out.add(node.val); // visit AFTER children (children's answers are ready)
}
| Order | Node processed | Use it when |
|---|---|---|
| Preorder | Before children | Copying or serializing a tree, passing info down (path from root) |
| Inorder | Between children | BSTs: produces sorted order |
| Postorder | After children | The node needs its children's answers (height, diameter, deleting a tree) |
| Level order (BFS) | By depth | Anything per level, minimum depth, nearest node |
Go deeperIterative DFS
Recursion uses the call stack; an explicit stack does the same job without the depth limit. Iterative inorder pushes the whole left spine, pops the leftmost node, visits it, then moves to its right subtree.
// LC 94. Simulate the call stack: go left as far as possible, visit, then go right.
static List<Integer> inorderIterative(TreeNode root) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode curr = root;
while (curr != null || !stack.isEmpty()) {
while (curr != null) { // push the whole left spine
stack.push(curr);
curr = curr.left;
}
curr = stack.pop(); // leftmost unvisited node
out.add(curr.val);
curr = curr.right; // then its right subtree
}
return out;
}
Iterative preorder is simpler: pop a node, visit it, push right then left (so left is processed first). Postorder can be produced as reversed "node, right, left" preorder.
Go deeperBFS with level separation
// LC 102. Queue size at the start of a round = number of nodes on this level.
static List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> levels = new ArrayList<>();
if (root == null) return levels;
Deque<TreeNode> queue = new ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
int size = queue.size(); // snapshot BEFORE adding children
List<Integer> level = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
levels.add(level);
}
return levels;
}
Snapshotting queue.size() before the inner loop is what separates levels: exactly that many nodes belong to the current level, and everything added during the loop belongs to the next.
DFS can also answer level questions. Pass the depth down and use it as an index. The right side view (LC 199) visits the right child first, so the first node seen at each depth is the visible one:
// LC 199. DFS right child first; the first node seen at each depth is the visible one.
static List<Integer> rightSideView(TreeNode root) {
List<Integer> view = new ArrayList<>();
see(root, 0, view);
return view;
}
private static void see(TreeNode node, int depth, List<Integer> view) {
if (node == null) return;
if (depth == view.size()) view.add(node.val); // first visit to this depth
see(node.right, depth + 1, view);
see(node.left, depth + 1, view);
}
Common mistakes
- Reading
queue.size()inside the loop condition, which changes as children are added. - Null checks: either check before recursing or start each call with
if (node == null) return ...; mixing both leads to missed cases. - Width problems with heap indices (LC 662): indices double per level and overflow; subtract the level's first index.
Tree traversals: DFS orders and BFS levels
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 102Binary Tree Level Order Traversal Snapshot queue size to separate levels | Medium | High | |
| 199Binary Tree Right Side View Last node of each level (or DFS right-first by depth) | Medium | High | |
| 103Binary Tree Zigzag Level Order Traversal Level BFS with alternating insertion direction | Medium | Medium | |
| 1161Maximum Level Sum of a Binary Tree Level BFS with a running sum per level | Medium | High | |
| 513Find Bottom Left Tree Value BFS order choice changes which node you see last | Medium | Medium | |
| 2415Reverse Odd Levels of Binary Tree BFS by level, reverse values on odd levels | Medium | Medium | |
| 116Populating Next Right Pointers in Each Node Use already-built next pointers as a level list | Medium | Medium | |
| 662Maximum Width of Binary Tree Heap-style indices per level; normalize to avoid overflow | Medium | Medium | |
| 987Vertical Order Traversal of a Binary Tree Collect (col, row, val) then sort with a comparator | Hard | High |
9.3Tree recursion with return values
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Height, diameter, balance, maximum path sum, lowest common ancestor, tree DP: anything where a node's answer is built from its children's answers.
A company report going up the org chart. Each manager receives a summary from each direct report, combines them, and passes a single summary up. The CEO's answer emerges without anyone seeing the whole company.
- Signal
- Height, diameter, balance, path sums through a node, LCA, subtree properties, 'information from children'.
- Required condition
- A node's answer can be computed from its children's answers plus its own value.
Mental model, core operation, limits, and what to use instead
- Mental model
- A company report: each manager summarizes their team's numbers for their own boss.
- Core operation
- Recurse left and right, combine their return values, optionally update a global best, return what the parent needs.
- Complexity benefit
- Every node visited once: O(n). Avoids recomputing subtree facts (naive balance check is O(n^2)).
- Limitations
- The return value must be what the PARENT needs, which is often different from the final answer.
- Alternative
- Top-down parameters for ancestor information; BFS for level-based questions; parent pointers + BFS for distance-k.
Go deeperThe framework: return what the parent needs
Before writing code, answer two questions:
- What does this function return to its parent? Often not the final answer. For diameter, the parent needs the child's height, not its diameter.
- What global answer is updated along the way? The best path that bends at a node uses both children, so it cannot be passed up; it is recorded in a variable outside the recursion.
The smallest example of the framework is the diameter of a tree (the longest path between any two nodes). Each node returns its height; the best path that bends at a node is left + right, recorded on the side:
// LC 543. Each call RETURNS height; the answer is collected on the side.
static int diameterOfBinaryTree(TreeNode root) {
int[] best = {0}; // array as a mutable holder for the recursion
height(root, best);
return best[0];
}
// Returns the number of NODES on the longest downward path (null -> 0, leaf -> 1).
// So left + right = number of EDGES on the longest path that bends at this node.
private static int height(TreeNode node, int[] best) {
if (node == null) return 0;
int left = height(node.left, best);
int right = height(node.right, best);
best[0] = Math.max(best[0], left + right); // best path that bends here
return 1 + Math.max(left, right); // what the parent needs
}
Guided solveLC 124. Binary Tree Maximum Path SumFive questions, each with a reveal
Problem: 124Binary Tree Maximum Path SumHard. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
A path is any sequence of connected nodes, each used at most once, and it need not pass through the root. Return the largest sum of node values on any non-empty path.
Input analysis
Up to 3 * 10^4 nodes, values from -1000 to 1000. Negative values mean a path should sometimes stop early, and the answer itself can be negative.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Every path has one highest node where it bends. At that node the path is (best downward chain on the left) + node + (best downward chain on the right). That needs information from children: postorder, the same shape as the diameter above.
Brute force
For every node, compute the best downward chain on each side from scratch: O(n) per node, O(n^2) on skewed trees.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. return a one-sided gain, record the two-sided path
Key observation
A parent can extend only one side of a child's path (a path cannot fork), so each call returns the best one-sided gain
node.val + max(leftGain, rightGain). A negative gain helps nobody, so clamp it to 0, which means "do not extend into that subtree".Visualization
tree: [-10, 9, 20, null, null, 15, 7] node left gain right gain path through node best returns (one side) 9 0 0 9 9 9 15 0 0 15 15 15 7 0 0 7 15 7 20 15 7 15 + 20 + 7 = 42 42 20 + 15 = 35 -10 9 35 9 - 10 + 35 = 34 42 -10 + 35 = 25 answer 42 (path 15 -> 20 -> 7)
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
gain(node): null returns 0.left = max(0, gain(node.left)),right = max(0, gain(node.right)). Updatebest = max(best, node.val + left + right). Returnnode.val + max(left, right).Java implementation
Java// LC 124. Each call returns the best DOWNWARD path starting at node (one side only). // The best path THROUGH node may use both sides; that goes into the global answer. static int maxPathSum(TreeNode root) { int[] best = {Integer.MIN_VALUE}; gain(root, best); return best[0]; } private static int gain(TreeNode node, int[] best) { if (node == null) return 0; int left = Math.max(0, gain(node.left, best)); // a negative branch is better skipped int right = Math.max(0, gain(node.right, best)); best[0] = Math.max(best[0], node.val + left + right); return node.val + Math.max(left, right); // a parent can extend only one side }Dry run
The table above. For the single node
[-3]: both gains are 0,best = -3, which is the answer. Initializingbestto 0 instead ofInteger.MIN_VALUEwould wrongly return 0 here. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(h) space for the recursion.
Edge cases
All values negative (the answer is the largest single value), a single node, a chain (recursion depth n), the best path entirely inside one subtree (the global variable catches it).
Transfer lesson
"Return the one-sided value, record the two-sided value" also solves Longest Univalue Path (LC 687), Longest ZigZag Path (LC 1372, return one value per direction), and Diameter of N-ary Tree problems. When the parent needs more than one fact, return a pair or a small array, as in tree DP below.
Go deeperUsing a sentinel return value
To check balance in O(n), the recursion must return a height and a "failed" signal. Returning -1 for "unbalanced" lets one integer carry both:
// LC 110. Return the height, or -1 as a "not balanced" signal so each node is visited once.
static boolean isBalanced(TreeNode root) {
return checkHeight(root) != -1;
}
private static int checkHeight(TreeNode node) {
if (node == null) return 0;
int left = checkHeight(node.left);
if (left == -1) return -1; // stop early: already unbalanced
int right = checkHeight(node.right);
if (right == -1 || Math.abs(left - right) > 1) return -1;
return 1 + Math.max(left, right);
}
Go deeperLowest common ancestor
// LC 236. Return p or q if found in this subtree, else null.
// The first node where both sides return non-null is the LCA.
static TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q) return root;
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left != null && right != null) return root; // p and q split here
return (left != null) ? left : right; // pass up whatever was found
}
This works because the problem guarantees both nodes exist. If one might be missing, count how many were found instead of returning early.
Go deeperTree DP: return several states
When the parent's choice depends on the child's choice (not just the child's best value), return one value per possible state. In House Robber III (LC 337), robbing a node forbids robbing its children, so each node returns the best total if robbed and if skipped.
// LC 337. Tree DP: each node returns {best if robbed, best if skipped}.
static int rob(TreeNode root) {
int[] r = robPair(root);
return Math.max(r[0], r[1]);
}
private static int[] robPair(TreeNode node) {
if (node == null) return new int[]{0, 0};
int[] l = robPair(node.left), r = robPair(node.right);
int robThis = node.val + l[1] + r[1]; // children must be skipped
int skipThis = Math.max(l[0], l[1]) + Math.max(r[0], r[1]); // children are free to choose
return new int[]{robThis, skipThis};
}
| Ordinary tree recursion | Tree DP |
|---|---|
| One return value per node | A small tuple of states per node |
| "Tell me your best result" | "Tell me your best result for each way you might decide" |
| Height, diameter, LCA | House Robber III, Binary Tree Cameras (3 states), Distribute Coins (excess) |
Common mistakes
- Returning the final answer instead of what the parent needs (diameter instead of height).
- Recomputing subtree facts in a loop over nodes (O(n^2) balance checks).
- Forgetting that negative values exist in path sums: clamp gains at 0, but initialize the global answer to
Integer.MIN_VALUE, not 0.
Tree recursion with return values (info from children)
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2265Count Nodes Equal to Average of Subtree Return (sum, count) pairs from each subtree | Medium | High | |
| 687Longest Univalue Path Return one-sided run, update the best with both sides | Medium | High | |
| 236Lowest Common Ancestor of a Binary Tree Return what you found; the split point is the LCA | Medium | High | |
| 1448Count Good Nodes in Binary Tree Pass the max-so-far down, count up | Medium | Medium | |
| 1372Longest ZigZag Path in a Binary Tree Return two values: best zigzag going left and going right | Medium | Medium | |
| 1339Maximum Product of Splitted Binary Tree Compute the total first, then every subtree sum in one pass | Medium | Medium | |
| 337House Robber III Return a pair (rob this, skip this) per node | Medium | High | |
| 979Distribute Coins in Binary Tree Return excess coins; moves = sum of |excess| | Medium | Medium | |
| 124Binary Tree Maximum Path Sum Return one-sided gain, update global with both sides | Hard | High | |
| 968Binary Tree Cameras Three states per node, greedy from the leaves | Hard | High |
9.4Information from ancestors (top-down) and root-to-leaf paths
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Problems where a node's answer depends on its ancestors: the path sum so far, the maximum on the path, the current number formed by digits, which values appear along the path.
Pass the ancestor information down as a parameter. Return results up (or collect them in a list). When the parameter is a mutable structure (a path list, a count map), undo your change before returning, exactly as in backtracking.
| Problem | Passed down | Collected |
|---|---|---|
| Path Sum (LC 112) | remaining target | true if any leaf hits 0 |
| Sum Root to Leaf Numbers (LC 129) | value * 10 + digit |
sum at leaves |
| Count Good Nodes (LC 1448) | max so far on the path | count of nodes >= max |
| Max Ancestor Difference (LC 1026) | min and max on the path | best difference |
| Path Sum II (LC 113) | path list (mutable, undo after) | copies of paths at leaves |
Guided solveLC 437. Path Sum IIIFive questions, each with a reveal
Problem: 437Path Sum IIIMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Count downward paths (from any node to any descendant) whose values sum to
target.Input analysis
Up to 1000 nodes, values up to 10^9 in magnitude (sums need
long). - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
A downward path is a contiguous segment of a root-to-node path. "Count segments with a given sum" on a sequence is LC 560: prefix sums plus a frequency map. Here the sequence is the current root-to-node path.
Brute force
From every node, DFS downward and count sums: O(n * h), up to O(n^2).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. prefix sums along the current root-to-node path
Key observation
Keep the counts of prefix sums for the nodes on the current path only. Entering a node adds its prefix; leaving removes it (backtracking), so sibling subtrees never see each other's prefixes.
Visualization
10 prefix at 10 = 10 / \ 5 -3 prefix at 5 = 15 / \ \ 3 2 11 prefix at 3 = 18: need 18 - 8 = 10 -> one path (5, 3) / \ \ prefix at 2 = 17: need 9 -> none 3 -2 1 prefix at 1 = 18: need 10 -> one path (5, 2, 1) prefix at 11 = 18 (via -3): need 10 -> one path (-3, 11) target 8: total 3 - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
DFS with the running sum. At each node:
count += prefixCount[sum - target], then addsumto the map, recurse on both children, then removesum.Java implementation
Java// LC 437. Count downward paths summing to target. // Same idea as LC 560, applied to the root-to-current path. static int pathSum(TreeNode root, int targetSum) { Map<Long, Integer> prefixCount = new HashMap<>(); prefixCount.put(0L, 1); return countPaths(root, 0L, targetSum, prefixCount); } private static int countPaths(TreeNode node, long sum, int target, Map<Long, Integer> prefixCount) { if (node == null) return 0; sum += node.val; // long: values can overflow int int count = prefixCount.getOrDefault(sum - target, 0); prefixCount.merge(sum, 1, Integer::sum); // enter: this prefix is on the path count += countPaths(node.left, sum, target, prefixCount); count += countPaths(node.right, sum, target, prefixCount); prefixCount.merge(sum, -1, Integer::sum); // leave: backtrack return count; }Dry run
As above. The removal step matters: after finishing the subtree under 5, the prefix 15 is removed, so the right subtree (under -3) does not count paths that would jump from the left subtree.
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(h) for the map entries on the path plus recursion.
Edge cases
Overflow (use
long), negative values, target 0 with zero-valued nodes, the empty-prefix seed0 -> 1for paths that start at the root.Transfer lesson
Top-down state plus backtracking turns any "segment of a root-to-node path" question into the corresponding array technique. When ancestor information must reach nodes outside the downward path (All Nodes Distance K, LC 863), add parent pointers and treat the tree as a graph (Module 12).
Root-to-leaf paths and information from ancestors
9 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 113Path Sum II Add on the way down, remove on the way up | Medium | High | |
| 129Sum Root to Leaf Numbers Carry value * 10 + digit down the path | Medium | Medium | |
| 988Smallest String Starting From Leaf Build strings leaf to root; compare at leaves | Medium | Medium | |
| 437Path Sum III Prefix sums along the current root-to-node path | Medium | High | |
| 1026Maximum Difference Between Node and Ancestor Pass min and max of ancestors down | Medium | Medium | |
| 1457Pseudo-Palindromic Paths in a Binary Tree Parity bitmask carried along the path | Medium | Medium | |
| 2096Step-By-Step Directions From a Binary Tree Node to Another LCA, then two root paths; up moves become 'U' | Medium | Medium | |
| 863All Nodes Distance K in Binary Tree Add parent links, then BFS like a graph | Medium | High | |
| 2385Amount of Time for Binary Tree to Be Infected Tree to undirected graph, then BFS levels | Medium | Medium |
9.5Constructing trees and serialization
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Rebuilding a tree from traversal sequences, converting between trees and strings, and building balanced trees from sorted data.
Go deeperWhich traversals determine a tree?
| Given | Unique binary tree? | How |
|---|---|---|
| Preorder + inorder (distinct values) | Yes | Preorder gives the root; inorder splits left and right |
| Postorder + inorder | Yes | Same, reading postorder from the end |
| Preorder + postorder | Only for full binary trees | Left subtree root is pre[1]; find it in postorder to size the left subtree |
| Preorder with null markers | Yes | Read tokens in the same order they were written |
| Preorder of a BST | Yes | Values bound the subtrees (LC 1008) |
Guided solveLC 105. Construct Binary Tree from Preorder and Inorder TraversalFive questions, each with a reveal
Problem: 105Construct Binary Tree from Preorder and Inorder TraversalMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Rebuild the tree from its preorder and inorder sequences (values are unique).
Input analysis
Up to 3000 nodes. Unique values make "position in inorder" well defined.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Recursive structure: the tree is a root plus two subtrees, and both sequences decompose the same way.
Brute force
For each root, scan inorder to find it (O(n)) and copy subarrays: O(n^2) time and space.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. preorder supplies roots in order; inorder index ranges define subtrees
Key observation
A hash map from value to inorder index finds the split in O(1). And preorder is consumed strictly left to right if we build the left subtree before the right one, so a single shared index replaces array copies.
Visualization
The figure above: root 3, left
[9], right[15, 20, 7]in inorder; then root 20 splits[15]and[7]. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
build(inLo, inHi): if empty, null. Takepre[next++]as the root, find its inorder indexmid, build left from(inLo, mid - 1), then right from(mid + 1, inHi).Java implementation
Java// LC 105. preorder[0] is the root; its position in inorder splits left and right. static TreeNode buildTree(int[] preorder, int[] inorder) { Map<Integer, Integer> inIndex = new HashMap<>(); // values are unique for (int i = 0; i < inorder.length; i++) inIndex.put(inorder[i], i); int[] next = {0}; // next root in preorder return build(preorder, next, 0, inorder.length - 1, inIndex); } private static TreeNode build(int[] pre, int[] next, int inLo, int inHi, Map<Integer, Integer> inIndex) { if (inLo > inHi) return null; int rootVal = pre[next[0]++]; TreeNode root = new TreeNode(rootVal); int mid = inIndex.get(rootVal); root.left = build(pre, next, inLo, mid - 1, inIndex); // left FIRST: preorder order root.right = build(pre, next, mid + 1, inHi, inIndex); return root; }Dry run
next = 0: root 3, mid = 1. Left
(0, 0): root 9, children empty. Right(2, 4): root 20 (next = 2), mid = 3; left(2, 2): 15; right(4, 4): 7. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time and O(n) space (map plus recursion).
Edge cases
Single node; skewed trees (recursion depth n); building right before left (breaks the preorder index).
Transfer lesson
"One sequence supplies the order of construction, the other supplies boundaries" also builds from postorder (consume from the end, right subtree first) and builds BSTs from preorder with value bounds instead of a map.
Go deeperSerialization and deserialization
// LC 297. Preorder with "#" for null. The null markers make the shape unambiguous.
static String serialize(TreeNode root) {
StringBuilder sb = new StringBuilder();
writeNode(root, sb);
return sb.toString();
}
private static void writeNode(TreeNode node, StringBuilder sb) {
if (node == null) { sb.append("#,"); return; }
sb.append(node.val).append(',');
writeNode(node.left, sb);
writeNode(node.right, sb);
}
static TreeNode deserialize(String data) {
Deque<String> tokens = new ArrayDeque<>(Arrays.asList(data.split(",")));
return readNode(tokens);
}
private static TreeNode readNode(Deque<String> tokens) {
String t = tokens.poll();
if (t.equals("#")) return null;
TreeNode node = new TreeNode(Integer.parseInt(t));
node.left = readNode(tokens); // consume tokens in the same order they were written
node.right = readNode(tokens);
return node;
}
Preorder with explicit null markers is self-describing: the reader consumes tokens in exactly the order the writer produced them, so no sizes or indices are needed. A level-order format (LeetCode's own) also works. Serializing subtrees as strings is also a hashing tool: Find Duplicate Subtrees (LC 652) maps each subtree's serialization to a count.
Tree construction and serialization
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 606Construct String from Binary Tree Preorder with rules for empty children | Medium | Medium | |
| 105Construct Binary Tree from Preorder and Inorder Traversal Preorder gives root; inorder index splits left/right | Medium | High | |
| 106Construct Binary Tree from Inorder and Postorder Traversal Same idea from the end of postorder | Medium | Medium | |
| 889Construct Binary Tree from Preorder and Postorder Traversal Left subtree root determines the split | Medium | Medium | |
| 654Maximum Binary Tree Recursive split at max (monotonic stack for O(n)) | Medium | Medium | |
| 1008Construct Binary Search Tree from Preorder Traversal Upper-bound recursion builds a BST in O(n) | Medium | Medium | |
| 1382Balance a Binary Search Tree Inorder to a sorted list, then rebuild from the middle | Medium | Medium | |
| 449Serialize and Deserialize BST BST property removes the need for null markers | Medium | Medium | |
| 652Find Duplicate Subtrees Serialize subtrees as keys in a hash map | Medium | Medium | |
| 297Serialize and Deserialize Binary Tree Preorder with null markers and a shared index | Hard | High |
9.6Binary search trees
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Ordered data with fast search, insert, and delete in O(h). In interviews BSTs appear as a structure to exploit (sorted inorder, one-sided search) more than as one to implement.
- Signal
- 'BST' in the statement, sorted order, kth smallest, floor/ceiling, validate, range sum.
- Required condition
- Every left-subtree value < node < every right-subtree value (strict, unless stated).
Mental model, core operation, limits, and what to use instead
- Mental model
- A decision tree of 'smaller go left, larger go right' questions.
- Core operation
- Choose one side by comparison (O(h)); inorder traversal visits values in sorted order.
- Complexity benefit
- Search, insert, delete in O(h): O(log n) when balanced, O(n) when skewed.
- Limitations
- Unbalanced trees degrade to lists; interview BSTs are usually not self-balancing.
- Alternative
- TreeMap / TreeSet in Java (red-black tree, guaranteed O(log n)); sorted array + binary search for static data.
// LC 700 and 701. One comparison per level chooses the side: O(h).
static TreeNode searchBST(TreeNode node, int val) {
while (node != null && node.val != val) node = (val < node.val) ? node.left : node.right;
return node;
}
static TreeNode insertIntoBST(TreeNode root, int val) {
if (root == null) return new TreeNode(val);
if (val < root.val) root.left = insertIntoBST(root.left, val);
else root.right = insertIntoBST(root.right, val);
return root;
}
// LC 230. Inorder visits a BST in sorted order; stop at the k-th node.
static int kthSmallest(TreeNode root, int k) {
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode curr = root;
while (true) {
while (curr != null) { stack.push(curr); curr = curr.left; }
curr = stack.pop();
if (--k == 0) return curr.val;
curr = curr.right;
}
}
Go deeperDeletion
Three cases: a leaf is removed; a node with one child is replaced by that child; a node with two children takes the value of its inorder successor (smallest value in the right subtree), and the successor is deleted from the right subtree.
// LC 450. Cases: leaf, one child, two children (replace with inorder successor).
static TreeNode deleteNode(TreeNode root, int key) {
if (root == null) return null;
if (key < root.val) root.left = deleteNode(root.left, key);
else if (key > root.val) root.right = deleteNode(root.right, key);
else {
if (root.left == null) return root.right; // covers the leaf case too
if (root.right == null) return root.left;
TreeNode succ = root.right; // smallest value in the right subtree
while (succ.left != null) succ = succ.left;
root.val = succ.val;
root.right = deleteNode(root.right, succ.val); // remove the duplicate successor
}
return root;
}
Guided solveLC 98. Validate Binary Search TreeFive questions, each with a reveal
Problem: 98Validate Binary Search TreeMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Is the tree a valid BST (strictly smaller on the left, strictly larger on the right, for every subtree)?
Input analysis
Up to 10^4 nodes, values span the full
intrange, includingInteger.MIN_VALUEandMAX_VALUE. - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
The BST rule is about whole subtrees, which means information from ancestors: pass allowed ranges down.
Brute force
For each node, check that the max of its left subtree is smaller and the min of its right subtree is larger: O(n^2) if recomputed, O(n) if returned bottom-up. The common wrong answer checks only
left.val < node.val < right.val. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. every node must respect bounds from all its ancestors
Key observation
Going left tightens the upper bound to the current value; going right tightens the lower bound. A node is valid if it lies in its range and both children are valid in their tightened ranges.
Visualization
Figure 9.7The node 3 is a valid left child of 6, but it sits in 5's right subtree, where every value must exceed 5. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
inRange(node, low, high): null is valid; the node must satisfylow < val < high; recurse with(low, val)on the left and(val, high)on the right.Java implementation
Java// LC 98. Every node must lie in (low, high) set by ALL ancestors, not just its parent. static boolean isValidBST(TreeNode root) { return inRange(root, Long.MIN_VALUE, Long.MAX_VALUE); // long: values can equal int limits } private static boolean inRange(TreeNode node, long low, long high) { if (node == null) return true; if (node.val <= low || node.val >= high) return false; return inRange(node.left, low, node.val) && inRange(node.right, node.val, high); }Dry run
On the invalid tree: root 5 in
(-inf, inf); right child 6 in(5, inf); its left child 3 in(5, 6): 3 <= 5, invalid. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(h) space.
Edge cases
A single node with value
Integer.MAX_VALUE(int bounds would wrongly reject it; long bounds or nullable bounds fix it), duplicates (invalid under the strict definition).Transfer lesson
The alternative proof is "inorder traversal is strictly increasing", checked with a
prevvariable. Both views (bounds from ancestors, sorted inorder) solve most BST problems: kth smallest (inorder count), recover BST (find the two inversions in inorder), convert to greater tree (reverse inorder with a running sum).
Go deeperBSTs in Java practice
Interview BSTs are not balanced; their height can be n. When you need a sorted structure in your own solution, use TreeMap / TreeSet (red-black trees): floorKey, ceilingKey, firstKey, lastKey, headMap, tailMap, all O(log n).
Binary search trees
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 701Insert into a Binary Search Tree Walk down to a null slot | Medium | High | |
| 450Delete Node in a BST Zero, one, or two children; inorder successor swap | Medium | High | |
| 98Validate Binary Search Tree Pass (low, high) bounds; checking only children is wrong | Medium | High | |
| 230Kth Smallest Element in a BST Iterative inorder and stop at k | Medium | High | |
| 235Lowest Common Ancestor of a Binary Search Tree Split point by value comparison | Medium | High | |
| 669Trim a Binary Search Tree Use BST order to drop whole subtrees at once | Medium | High | |
| 173Binary Search Tree Iterator Controlled inorder with a stack; O(h) memory | Medium | High | |
| 538Convert BST to Greater Tree Reverse inorder with a running sum | Medium | Medium | |
| 99Recover Binary Search Tree Find the two inversions in the inorder sequence | Medium | Medium | |
| 1373Maximum Sum BST in Binary Tree Return (isBST, min, max, sum) bottom-up | Hard | Medium |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 10: Heaps and priority queues
A heap gives you the smallest (or largest) element in O(1) and lets you insert or remove in O(log n). Use it whenever you repeatedly need the current extreme while data keeps arriving.
10.1Heap fundamentals
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
"Repeatedly take the most urgent item": the next meeting to end, the closest unvisited node, the k largest so far, the smallest head among k sorted lists. Sorting answers this once; a heap keeps answering it as items are added and removed.
An emergency room. Patients arrive in any order, but the doctor always sees the most urgent one next. The nurse does not re-sort the whole waiting room on each arrival; she only checks where the new patient fits relative to a few others.
- Signal
- Top k, kth largest/smallest, 'repeatedly take the smallest/largest', merging sorted sources, scheduling by time or priority.
- Required condition
- You need fast access to the current extreme while elements keep arriving or changing.
Mental model, core operation, limits, and what to use instead
- Mental model
- An emergency room queue: the most urgent patient is always seen next, whatever the arrival order.
- Core operation
- offer and poll in O(log n), peek in O(1). Keep the heap at size k for top-k.
- Complexity benefit
- Top k in O(n log k) instead of O(n log n) sorting; streaming answers without re-sorting.
- Limitations
- No fast search or removal of arbitrary elements (O(n)); iteration order is not sorted.
- Alternative
- Sorting when all data is known up front; quickselect for one kth element (O(n) average); TreeMap when you need removal by key.
Go deeperStructure: a complete binary tree stored in an array
| Operation | How it works | Cost |
|---|---|---|
| peek | Read index 0 | O(1) |
| offer (insert) | Append at the end, sift up: swap with the parent while smaller | O(log n) |
| poll (remove top) | Move the last element to index 0, sift down: swap with the smaller child while larger | O(log n) |
| heapify n elements | Sift down from the last parent to the root | O(n), not O(n log n) |
| remove arbitrary / contains | Linear search, then fix | O(n) |
You rarely implement a heap in an interview, but you should be able to explain sift up and sift down, and why the height is log n (the tree is complete).
Go deeperPriorityQueue in Java
static void priorityQueueApi() {
PriorityQueue<Integer> minHeap = new PriorityQueue<>(); // smallest on top
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder()); // largest on top
PriorityQueue<int[]> byDist = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
minHeap.offer(5); minHeap.offer(1); maxHeap.offer(5); maxHeap.offer(1);
byDist.offer(new int[]{3, 0});
// offer/poll: O(log n). peek: O(1). size: O(1).
// remove(Object) and contains(Object): O(n) - avoid them in hot loops.
// Iterating a PriorityQueue does NOT give sorted order; only repeated poll() does.
if (minHeap.peek() != 1 || maxHeap.peek() != 5 || byDist.peek()[0] != 3) throw new AssertionError();
}
Pitfalls specific to PriorityQueue:
- Iteration order is not sorted.
for (int x : pq)ornew ArrayList<>(pq)gives heap order. Only repeatedpoll()produces sorted output. remove(Object)is O(n). For "delete this element" use lazy deletion (mark it invalid and skip it when it reaches the top) or aTreeMapwith counts.- Mutating an element inside the heap breaks it. Remove, change, and re-insert.
- Comparator overflow.
(a, b) -> a - bfails for large or negative values; useInteger.compare.
10.2Top-K and the kth element
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
The size-k heap trick. To keep the k largest elements of a stream, use a min-heap of size k: its top is the smallest of the k largest, which is exactly the kth largest. Every new element either beats the top (replace it) or is ignored. It sounds backward until you see why: the top is the element most likely to be evicted.
| Want | Heap type | Size | Top is |
|---|---|---|---|
| k largest / kth largest | min-heap | k | kth largest |
| k smallest / kth smallest | max-heap | k | kth smallest |
| k closest points | max-heap by distance | k | the farthest of the k closest |
// LC 215. Keep the k largest seen so far in a MIN-heap; its top is the kth largest.
static int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int x : nums) {
heap.offer(x);
if (heap.size() > k) heap.poll(); // evict the smallest of the k + 1
}
return heap.peek();
}
Guided solveLC 215. Kth Largest Element in an ArrayFive questions, each with a reveal
Problem: 215Kth Largest Element in an ArrayMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Return the kth largest value (by sorted order, duplicates count separately).
Input analysis
n up to 10^5, values from -10^4 to 10^4. The follow-up asks for better than sorting.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"kth largest" with k possibly much smaller than n: a size-k heap. Values in a small range also allow counting sort.
Brute force
Sort descending and take index
k - 1: O(n log n). - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. a min-heap of size k holds the k largest; its top is the answer
Key observation
We never need the order among elements outside the top k, nor among elements inside it except for the smallest. A min-heap of size k tracks exactly that.
Visualization
nums = [3, 2, 1, 5, 6, 4], k = 2 heap (min at the left) 3 -> [3] 2 -> [2, 3] 1 -> [1, 2, 3] -> poll 1 -> [2, 3] 5 -> [2, 3, 5] -> poll 2 -> [3, 5] 6 -> [3, 5, 6] -> poll 3 -> [5, 6] 4 -> [4, 5, 6] -> poll 4 -> [5, 6] top = 5
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Offer each element; if the size exceeds k, poll. Return the top.
Java implementation
findKthLargestabove.Dry run
The trace above returns 5.
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n log k) time, O(k) space. With k close to n, it approaches O(n log n).
Edge cases
k = 1 (the max), k = n (the min), duplicates (they occupy separate slots, which is what "kth largest by sorted order" means).
Transfer lesson
Alternatives worth naming: quickselect partitions around a pivot and recurses into one side, O(n) average and O(n^2) worst (randomize the pivot); counting works when the value range is small. For top-k by frequency (LC 347), count first, then apply the same heap to (value, count) pairs, or bucket by frequency in O(n).
// LC 347. Count with a map, then keep the k most frequent in a size-k min-heap.
static int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
for (int x : nums) freq.merge(x, 1, Integer::sum);
PriorityQueue<Map.Entry<Integer, Integer>> heap =
new PriorityQueue<>((a, b) -> Integer.compare(a.getValue(), b.getValue()));
for (Map.Entry<Integer, Integer> e : freq.entrySet()) {
heap.offer(e);
if (heap.size() > k) heap.poll();
}
int[] result = new int[k];
for (int i = k - 1; i >= 0; i--) result[i] = heap.poll().getKey(); // most frequent first
return result;
}
Go deeperHeaps for scheduling and simulation
When the next event to process is "the one with the smallest time" or "the most valuable available option", a heap drives the simulation:
- Single-Threaded CPU (LC 1834): sort tasks by arrival; whenever the CPU is free, add every arrived task to a heap ordered by (duration, index) and run the top.
- IPO (LC 502): sort projects by required capital; unlock all affordable ones into a max-heap of profits; take the best k times.
- Furthest Building (LC 1642): use ladders on the largest climbs. Keep a min-heap of the climbs assigned to ladders; when it exceeds the number of ladders, the smallest climb switches to bricks.
- Meeting rooms with room ids (LC 2402): one heap of free room ids, one heap of (end time, room) for busy rooms.
The pattern is "sort the inputs by when they become available, and keep the available ones in a heap ordered by priority".
Heaps: top-K, kth element, scheduling
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 1962Remove Stones to Minimize the Total Max-heap simulation: always halve the largest pile | Medium | High | |
| 215Kth Largest Element in an Array Size-k heap vs quickselect trade-offs | Medium | High | |
| 347Top K Frequent Elements Count, then heap of size k (or bucket sort) | Medium | High | |
| 973K Closest Points to Origin Max-heap of size k keyed by squared distance | Medium | High | |
| 692Top K Frequent Words Tie-breaking comparator: frequency then lexicographic | Medium | Medium | |
| 621Task Scheduler Greedy on the most frequent task; formula or heap simulation | Medium | High | |
| 767Reorganize String Always place the most frequent char that is not the last one | Medium | Medium | |
| 1834Single-Threaded CPU Sort by arrival, heap by (duration, index) | Medium | Medium | |
| 2542Maximum Subsequence Score Sort by one array, keep the k best of the other in a heap | Medium | Medium | |
| 502IPO Unlock projects by capital, pick best profit from a heap | Hard | High |
10.3K-way merge and frontier expansion
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Merging k sorted sequences, or exploring candidates in increasing order when each candidate generates a few "next" candidates (pairs from two sorted arrays, cells of a sorted matrix, ugly numbers).
The next smallest element overall must be the smallest among the current fronts of the sources. Keep one front per source in a heap; when you take a front, push its successor.
// LC 23. The heap holds one candidate (the current head) from each list.
static ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> heap = new PriorityQueue<>((a, b) -> Integer.compare(a.val, b.val));
for (ListNode head : lists) if (head != null) heap.offer(head);
ListNode dummy = new ListNode(0), tail = dummy;
while (!heap.isEmpty()) {
ListNode smallest = heap.poll();
tail.next = smallest;
tail = smallest;
if (smallest.next != null) heap.offer(smallest.next); // its successor becomes a candidate
}
return dummy.next;
}
Merging N total elements from k lists costs O(N log k): each element enters and leaves a heap of size at most k.
Frontier expansion. For K Pairs with Smallest Sums (LC 373), the "sources" are rows i paired with columns j. Start with (i, 0) for each i (or just (0, 0) and expand both directions with a visited set), and when (i, j) is taken, push (i, j + 1). The heap never holds more than k + rows entries, and you never generate pairs you will not need.
10.4Two heaps: the running median
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Split the numbers into a lower half and an upper half. The lower half is a max-heap (its top is the largest small number); the upper half is a min-heap (its top is the smallest large number). The median lives at the tops.
- Signal
- Running median, balancing two halves, merging k sorted lists, smallest pairs from sorted arrays.
- Required condition
- The answer is defined by a boundary between two halves, or by the next smallest among k sorted frontiers.
Mental model, core operation, limits, and what to use instead
- Mental model
- Median: two piles, small values on the left pile (top = largest), large on the right (top = smallest).
- Core operation
- Median: push, move tops across to keep order and sizes. K-way: heap holds one frontier element per source.
- Complexity benefit
- Median in O(log n) per insert; merging N elements from k lists in O(N log k).
- Limitations
- Deletion of arbitrary elements needs lazy deletion bookkeeping.
- Alternative
- TreeMap multiset; divide-and-conquer pairwise merging for k-way merge.
Guided solveLC 295. Find Median from Data StreamFive questions, each with a reveal
Problem: 295Find Median from Data StreamHard. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Support
addNum(x)andfindMedian()over a growing stream.Input analysis
Up to 5 * 10^4 calls, values within
int. The median of an even count is the mean of the middle two. - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
We need the middle of a changing multiset. Sorting after each insert is O(n log n) per call. A heap gives one extreme; two heaps give the two middle values.
Brute force
Keep a sorted
ArrayListand insert with binary search: O(log n) to find the spot, O(n) to shift. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. two heaps meeting in the middle, kept balanced
Key observation
Only the boundary between the halves matters. Maintain two invariants: every element of
lowis <= every element ofhigh, andlowhas the same size ashighor one more.Visualization
add 5: low [5] high [] median 5 add 2: low [2] high [5] median (2 + 5) / 2 = 3.5 add 8: low [5, 2] high [8] median 5 add 1: low [2, 1] high [5, 8] median (2 + 5) / 2 = 3.5 (low is a max-heap: its top is listed first)
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Push the new number into
low, then movelow's top tohigh(this restores the ordering invariant whatever the number was). Ifhighis now larger thanlow, movehigh's top back. Median:low.peek()if sizes differ, else the mean of both tops.Java implementation
Java// LC 295. low = max-heap of the smaller half, high = min-heap of the larger half. // Invariants: every value in low <= every value in high; low.size() is high.size() or +1. static class MedianFinder { private final PriorityQueue<Integer> low = new PriorityQueue<>(Collections.reverseOrder()); private final PriorityQueue<Integer> high = new PriorityQueue<>(); void addNum(int num) { low.offer(num); high.offer(low.poll()); // move low's max across: keeps the ordering invariant if (high.size() > low.size()) low.offer(high.poll()); // rebalance sizes } double findMedian() { if (low.size() > high.size()) return low.peek(); return ((long) low.peek() + high.peek()) / 2.0; // long avoids overflow } }Dry run
The trace above. Note that "push into low, move top to high" means a large number travels to
highimmediately and a small number pusheslow's largest up. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
addNumO(log n),findMedianO(1). Space O(n).Edge cases
Two
Integer.MAX_VALUEvalues: their sum overflowsint, hence the cast tolong. Negative numbers and duplicates need no special handling.Transfer lesson
Two heaps solve any "balance two halves" problem. For a sliding window median (LC 480), add lazy deletion: record outgoing values in a map and discard them when they surface at a heap top, adjusting the size accounting.
10.5Heap vs sorting vs other structures
You need the 10 largest values from a stream of 10^9 numbers and cannot store them all. Sort, a heap of size 10, or quickselect?
Reveal the answer
A min-heap of size 10: push each value, pop the smallest whenever the size exceeds 10. O(n log 10) time, O(10) memory, and it works on a stream. Sorting and quickselect both need every value in memory at once.
| Situation | Best choice |
|---|---|
| All data known, need full order | Sort, O(n log n) |
| All data known, need the kth element once | Quickselect O(n) average, or size-k heap O(n log k) |
| Data arrives over time, need the current extreme repeatedly | Heap |
| Need the extreme and also delete arbitrary elements | TreeMap<value, count> or heap + lazy deletion |
| Need the median or another fixed rank repeatedly | Two heaps |
| Values in a small range | Counting / bucket arrays |
Common mistakes
- Using a max-heap of all n elements for top-k (O(n log n) instead of O(n log k)).
- Expecting a heap to be sorted when iterated or printed.
- Forgetting ties in the comparator when the problem specifies a tie-break (Top K Frequent Words: higher frequency first, then lexicographic).
- Pushing duplicates of a frontier state without a visited set.
Heaps: k-way merge, two heaps, streams
6 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 373Find K Pairs with Smallest Sums Lazy frontier expansion from the heap | Medium | Medium | |
| 378Kth Smallest Element in a Sorted Matrix K-way merge of rows (or binary search on value) | Medium | Medium | |
| 264Ugly Number II Generate in order from a heap or three pointers | Medium | Medium | |
| 1642Furthest Building You Can Reach Use ladders on the largest climbs via a min-heap | Medium | Medium | |
| 2462Total Cost to Hire K Workers Two heaps over the two ends of the array | Medium | Medium | |
| 1801Number of Orders in the Backlog Order book with a buy max-heap and sell min-heap | Medium | Medium | |
| 23Merge k Sorted Lists Heap holds the current head of each list | Hard | High | |
| 295Find Median from Data Stream Max-heap low half, min-heap high half, rebalance | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 11: Greedy algorithms
A greedy algorithm makes the locally best choice at each step and never reconsiders it. It is often the shortest code in the interview and the hardest to justify. The skill is not finding a greedy rule; it is proving it or breaking it.
11.1What greedy means and when it works
Coins are {1, 3, 4} and the amount is 6. Does "always take the largest coin that fits" give the fewest coins?
Reveal the answer
No. Greedy takes 4 + 1 + 1 = 3 coins; the optimum is 3 + 3 = 2 coins. One small counterexample is enough to reject a greedy rule. This lesson is about telling the rules that are safe from the ones that only look safe.
Optimization problems where one natural choice is always safe: the interval that ends first, the furthest reachable index, the cheapest option per unit, the largest coin in some (not all) coin systems.
Packing a car trunk by always placing the largest remaining bag that fits. Sometimes this is optimal; sometimes a cleverer arrangement fits more. Greedy is correct only when you can show that no cleverer arrangement exists.
- Signal
- Optimize one quantity with a natural order (earliest end, largest value per unit, furthest reach); local decision seems obviously safe.
- Required condition
- Greedy-choice property: some optimal solution starts with the greedy choice. Prove it (exchange argument) or find a counterexample.
Mental model, core operation, limits, and what to use instead
- Mental model
- Packing a suitcase by a rule you can defend: if a better packing existed, you could swap it into yours without loss.
- Core operation
- Sort or scan by the key, take or reject each item once, never revisit.
- Complexity benefit
- Usually O(n log n) or O(n), far below DP or search.
- Limitations
- Fails silently when the choice is not safe (coin change with arbitrary coins). Always test small counterexamples.
- Alternative
- DP when choices interact; heap-based greedy when the best choice changes as you go.
Go deeperLocal vs global optimum
A greedy rule optimizes the next step. It is correct when the problem has the greedy-choice property (some optimal solution begins with the greedy choice) and optimal substructure (after making it, the rest is the same kind of problem). DP needs only the second property; that is why DP is more general and slower.
Go deeperThree ways to test a greedy idea
- Hunt for a counterexample first. Try 3 to 5 tiny inputs, especially ones where the "obvious" choice blocks a better later option. One counterexample ends the idea. Coin change with coins
{1, 3, 4}and amount 6: largest-first gives4 + 1 + 1(3 coins), but3 + 3uses 2. - Exchange argument. Take any optimal solution that differs from the greedy one. Show you can swap one of its choices for the greedy choice without making it worse. Repeating the swap turns the optimal solution into the greedy one, so greedy is optimal.
- Stays ahead. Show that after every step, the greedy solution is at least as good as any other solution measured by some quantity (reach, count, finish time). Then it is also at least as good at the end.
Go deeperAn exchange argument, written out
Two City Scheduling (LC 1029): send exactly n of 2n people to city A, the rest to B, minimizing cost. For each person, sending them to A instead of B changes the total by a - b. Claim: send to A the n people with the smallest a - b.
Proof. Suppose an optimal assignment sends person x to A and person y to B, with a_x - b_x > a_y - b_y. Swapping them (y to A, x to B) changes the cost by (a_y - b_y) - (a_x - b_x) < 0, which is an improvement: contradiction. So in an optimal assignment, everyone sent to A has a smaller a - b than everyone sent to B. That is the sorted split.
// LC 1029. Exchange argument: sending person p to A instead of B changes cost by a - b.
// Sort by (a - b) and send the first half to A.
static int twoCitySchedCost(int[][] costs) {
Arrays.sort(costs, (x, y) -> Integer.compare(x[0] - x[1], y[0] - y[1]));
int n = costs.length / 2, total = 0;
for (int i = 0; i < costs.length; i++) total += (i < n) ? costs[i][0] : costs[i][1];
return total;
}
11.2Greedy patterns you will meet
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Go deeperReachability: track the furthest point
// LC 55. Track the furthest index reachable from everything seen so far.
static boolean canJump(int[] nums) {
int furthest = 0;
for (int i = 0; i < nums.length; i++) {
if (i > furthest) return false; // i itself is unreachable
furthest = Math.max(furthest, i + nums[i]);
}
return true;
}
Stays-ahead argument: furthest is the maximum index reachable using only indices <= i; if i exceeds it, no path can reach i, and therefore nothing beyond it.
For the minimum number of jumps, think of BFS layers without a queue: all indices reachable with j jumps form a contiguous range, and the next range ends at the furthest point reachable from the current one.
// LC 45. Layers like BFS: [start, currentEnd] is reachable in 'jumps' jumps.
static int jump(int[] nums) {
int jumps = 0, currentEnd = 0, furthest = 0;
for (int i = 0; i < nums.length - 1; i++) { // no jump needed from the last index
furthest = Math.max(furthest, i + nums[i]);
if (i == currentEnd) { // layer exhausted: must jump
jumps++;
currentEnd = furthest;
}
}
return jumps;
}
Reset when a segment fails
Guided solveLC 134. Gas StationFive questions, each with a reveal
Problem: 134Gas StationMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Stations in a circle;
gas[i]is added at station i,cost[i]is needed to reach the next. Find the unique start index from which you can complete the circle, or -1.Input analysis
n up to 10^5. O(n^2) (try every start) is too slow.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
A single pass with a running total, where a failure tells you something about many starts at once: greedy with a reset.
Brute force
Simulate from each start: O(n^2).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. a failing segment rules out every start inside it
Key observation
Two facts. (a) If the total gain
sum(gas - cost)is negative, no start works. (b) If you start at s and first run dry going from i to i + 1, then no start between s and i works either. The run from s reached each such k with a non-negative tank; starting fresh at k means arriving there with an empty tank, so from k onward you never have more gas than the run from s had, and you also run dry before i + 1. So the next candidate is i + 1.Visualization
Figure 11.2gas = [1,2,3,4,5], cost = [3,4,5,1,2]. Starts 0, 1, and 2 fail immediately; start 3 survives to the end, and the total gain is 0, so it completes the circle. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
One pass: accumulate
totalandtank; whentank < 0, setstart = i + 1andtank = 0. At the end, returnstartiftotal >= 0, else -1.Java implementation
Java// LC 134. If the tank goes negative at i, no start in [start, i] can work, // because each of them arrives at i with no more gas than 'start' did. static int canCompleteCircuit(int[] gas, int[] cost) { int total = 0, tank = 0, start = 0; for (int i = 0; i < gas.length; i++) { int gain = gas[i] - cost[i]; total += gain; tank += gain; if (tank < 0) { // cannot reach i + 1 from 'start' start = i + 1; tank = 0; } } return total >= 0 ? start : -1; // total >= 0 guarantees the remaining start works }Dry run
Gains
-2, -2, -2, 3, 3: tank fails at 0, 1, 2 (start becomes 3), then 3, 6. Total 0, so start 3. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time, O(1) space.
Edge cases
One station; total exactly 0 (still feasible); the answer at index 0 (no resets).
Transfer lesson
"When a prefix fails, the whole prefix is ruled out as a starting point" is Kadane's restart rule in disguise (Module 2). Proving that the skipped starts are all hopeless is what makes it correct.
Go deeperExtend a segment to cover its constraints
Partition Labels (LC 763): a part must reach the last occurrence of every character inside it. Scan, extending the current part's end to last[c]; when the scan reaches the end, the part is closed. Each cut is as early as possible, so the number of parts is maximal.
// LC 763. A part must extend at least to the last occurrence of every char inside it.
static List<Integer> partitionLabels(String s) {
int[] last = new int[26];
for (int i = 0; i < s.length(); i++) last[s.charAt(i) - 'a'] = i;
List<Integer> sizes = new ArrayList<>();
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
end = Math.max(end, last[s.charAt(i) - 'a']);
if (i == end) { // nothing inside reaches further
sizes.add(end - start + 1);
start = i + 1;
}
}
return sizes;
}
Go deeperSort first, then take greedily
Many greedy solutions are "sort by the right key, then scan once":
| Problem | Sort by | Then |
|---|---|---|
| Assign Cookies (LC 455) | children's greed and cookie sizes | give the smallest sufficient cookie to the least greedy child |
| Maximum Units on a Truck (LC 1710) | units per box, descending | take boxes until the truck is full |
| Non-overlapping Intervals (LC 435) | end time | keep each interval that starts after the last kept end (Module 5) |
| Two City Scheduling (LC 1029) | cost difference | first half to A |
| Hand of Straights (LC 846) | card value (TreeMap) | always start a group at the smallest remaining card |
Choosing the key is the whole problem. Test the key against small cases where a different key would choose differently.
Go deeperSatisfy constraints in two passes
Candy (LC 135): each child with a higher rating than a neighbor needs more candy than that neighbor. A left-to-right pass satisfies the left-neighbor constraints; a right-to-left pass satisfies the right-neighbor constraints while keeping the larger of the two values. Each pass is greedy for one direction; the max combines them.
Go deeperTrack a range of possibilities
Valid Parenthesis String (LC 678): with * able to be (, ), or empty, keep the range [low, high] of possible open counts. ( raises both, ) lowers both, * lowers low and raises high. If high < 0 the string is invalid; clamp low at 0. Valid if low == 0 at the end. Tracking a range instead of one value avoids branching.
11.3Greedy vs dynamic programming
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
| Greedy | DP | |
|---|---|---|
| Decision | Commit to the locally best choice | Evaluate every choice, reuse subresults |
| Needs | Greedy-choice property + optimal substructure | Optimal substructure + overlapping subproblems |
| Typical cost | O(n log n) or O(n) | O(states x transitions) |
| Proof | Exchange or stays-ahead argument | The recurrence is the proof |
| Failure mode | Silently wrong answers | Too slow or too much memory |
Same story, different answer. Activity selection (maximize the number of intervals) is greedy; weighted interval scheduling (maximize total weight) is DP, because a short low-value interval can block a long high-value one. Fractional knapsack is greedy by value density; 0/1 knapsack is DP. Coin change is greedy for canonical coin systems like {1, 5, 10, 25} and DP in general. When the interviewer adds weights or values to a greedy problem, re-check the property.
Common mistakes
- Trusting a greedy rule because it passes the examples. Spend two minutes hunting for a counterexample.
- Sorting by the wrong key (start instead of end for interval selection).
- Not explaining why it works. "It seems right" is not an answer; give the exchange argument in two sentences.
Greedy algorithms
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 945Minimum Increment to Make Array Unique Sort, then push each value just above the previous one | Medium | High | |
| 1899Merge Triplets to Form Target Triplet Discard triplets that overshoot, then check coverage | Medium | Medium | |
| 55Jump Game Track the furthest reachable index | Medium | High | |
| 45Jump Game II BFS-like layers: jump when you reach the current layer end | Medium | High | |
| 134Gas Station If the tank goes negative, no start in that segment works | Medium | High | |
| 763Partition Labels Extend the cut to the last occurrence of every seen char | Medium | High | |
| 846Hand of Straights Always start groups from the smallest remaining card | Medium | Medium | |
| 1029Two City Scheduling Exchange argument on the cost difference | Medium | Medium | |
| 678Valid Parenthesis String Track the range [low, high] of possible open counts | Medium | Medium | |
| 135Candy Two passes satisfy left and right constraints separately | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 13: Tries
A trie stores strings by their characters, one level per position, so that all words with a common prefix share a path. It turns "which words start with this?" from a scan of every word into a walk of the prefix length.
13.1Trie structure and operations
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Many words, many prefix questions: autocomplete, "does any word start with ...", dictionary lookups with wildcards, replacing words by their shortest root, searching a grid for many words at once.
A phone menu. Pressing 1 narrows the options to one branch, pressing 3 next narrows further. Two callers who both start with "1, 3" share the first two steps.
- Signal
- Many prefix queries, autocomplete, 'words starting with', dictionary with wildcards, word search with many words, max XOR.
- Required condition
- Keys are sequences (strings, bits) and queries share prefixes.
Mental model, core operation, limits, and what to use instead
- Mental model
- A phone tree menu: each key press narrows the choices.
- Core operation
- Walk one character at a time, creating or following child links; mark word ends.
- Complexity benefit
- Prefix lookup in O(L) independent of the number of words; prunes searches that share prefixes.
- Limitations
- Memory heavy (26 references per node); overkill for exact-match lookups.
- Alternative
- HashSet for exact matches; sorted list + binary search for prefix ranges; HashSet of all prefixes for small inputs.
// LC 208. Each node has up to 26 children; a path from the root spells a prefix.
static class Trie {
private static class Node {
Node[] children = new Node[26];
boolean isEnd; // a word ends exactly here
}
private final Node root = new Node();
void insert(String word) {
Node node = root;
for (char ch : word.toCharArray()) {
int i = ch - 'a';
if (node.children[i] == null) node.children[i] = new Node();
node = node.children[i];
}
node.isEnd = true;
}
boolean search(String word) {
Node node = walk(word);
return node != null && node.isEnd; // the full word, not just a prefix
}
boolean startsWith(String prefix) {
return walk(prefix) != null;
}
private Node walk(String s) { // follow s; null if the path breaks
Node node = root;
for (char ch : s.toCharArray()) {
node = node.children[ch - 'a'];
if (node == null) return null;
}
return node;
}
}
| Operation | Time | Notes |
|---|---|---|
| insert(word) | O(L) | Creates at most L nodes |
| search(word) | O(L) | Must also check isEnd |
| startsWith(prefix) | O(L) | Only checks the path exists |
| Space | O(total characters x alphabet) | 26 references per node with arrays; a HashMap per node uses less for sparse alphabets |
Array or map children? TrieNode[26] is fastest and simplest for lowercase letters. For large or unknown alphabets (Unicode, mixed case, digits), use Map<Character, TrieNode>.
Go deeperPutting data on nodes
The isEnd flag is the minimum. Many problems store more:
| Store on each node | Solves |
|---|---|
| The word itself at its end node | Word Search II: no need to rebuild the word from the path |
| Count of words passing through | Sum of Prefix Scores (LC 2416), counting words with a prefix |
| Sum of values below | Map Sum Pairs (LC 677) |
| Up to 3 best words below | Search Suggestions System (LC 1268) |
Go deeperWildcards: branch where the pattern is unknown
With . matching any letter (LC 211), the search follows the single matching child for a normal character and every existing child for .. The trie prunes all branches that do not exist, so the search is fast unless the pattern is mostly dots.
// LC 211. '.' matches any letter: branch into every existing child.
static class WordDictionary {
private final WordDictionary[] children = new WordDictionary[26];
private boolean isEnd;
void addWord(String word) {
WordDictionary node = this;
for (char ch : word.toCharArray()) {
if (node.children[ch - 'a'] == null) node.children[ch - 'a'] = new WordDictionary();
node = node.children[ch - 'a'];
}
node.isEnd = true;
}
boolean search(String word) { return match(word, 0, this); }
private static boolean match(String w, int i, WordDictionary node) {
if (i == w.length()) return node.isEnd;
char ch = w.charAt(i);
if (ch != '.') {
WordDictionary next = node.children[ch - 'a'];
return next != null && match(w, i + 1, next);
}
for (WordDictionary child : node.children)
if (child != null && match(w, i + 1, child)) return true;
return false;
}
}
13.2Tries inside other searches
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Guided solveLC 212. Word Search IIFive questions, each with a reveal
Problem: 212Word Search IIHard. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Return every word from a list that can be traced in the grid (adjacent cells, each cell used once per word).
Input analysis
Grid up to 12 x 12, up to 3 * 10^4 words of length up to 10.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Word Search I (Module 8) per word would repeat the grid search 3 * 10^4 times. The words share prefixes, so a structure that searches all of them at once is needed: a trie.
Brute force
Run Word Search for each word: O(W * R * C * 3^L). Too slow.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. walk the grid and the trie together; stop when the path leaves the trie
Key observation
A single DFS from each cell can follow the trie: moving to a neighbor cell is allowed only if the trie has a child for that letter. Any grid path that is not a prefix of some word is cut immediately, and every word is found during the same walk.
Visualization
board words: oath, pea, eat, rain o a a n from (0,0) 'o': trie has o -> a -> t -> h e t a e path o(0,0) a(0,1) t(1,1) h(2,1): "oath" found i h k r from (1,0) 'e': the trie needs 'a' next; neighbors are o, i, t: stop i f l v from (1,3) 'e': e(1,3) a(1,2) t(1,1): "eat" found
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Build a trie storing each word at its end node. For each cell, DFS with the current trie node: stop if out of bounds, already used, or no child for this letter. If the child holds a word, record it and clear it (so it is reported once). Mark the cell, recurse into four neighbors, restore.
Java implementation
Java// LC 212. Insert all words into a trie, then DFS the grid once, following trie edges. // A path that is not a prefix of any word is abandoned immediately. static List<String> findWords(char[][] board, String[] words) { TNode root = new TNode(); for (String w : words) { TNode n = root; for (char ch : w.toCharArray()) { if (n.next[ch - 'a'] == null) n.next[ch - 'a'] = new TNode(); n = n.next[ch - 'a']; } n.word = w; // store the word at its end node } List<String> found = new ArrayList<>(); for (int r = 0; r < board.length; r++) for (int c = 0; c < board[0].length; c++) explore(board, r, c, root, found); return found; } private static class TNode { TNode[] next = new TNode[26]; String word; // non-null if a word ends here } private static void explore(char[][] b, int r, int c, TNode parent, List<String> found) { if (r < 0 || c < 0 || r >= b.length || c >= b[0].length) return; char ch = b[r][c]; if (ch == '#' || parent.next[ch - 'a'] == null) return; // visited, or not a prefix TNode node = parent.next[ch - 'a']; if (node.word != null) { found.add(node.word); node.word = null; // report each word once } b[r][c] = '#'; explore(b, r + 1, c, node, found); explore(b, r - 1, c, node, found); explore(b, r, c + 1, node, found); explore(b, r, c - 1, node, found); b[r][c] = ch; }Dry run
The trace above finds "oath" and "eat"; "pea" fails because no 'p' exists, "rain" because no path continues r -> a.
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(R * C * 4 * 3^(L-1)) in the worst case, but the trie prunes most paths early. Building the trie is O(total characters).
Edge cases
Duplicate words in the list, a word that is a prefix of another (both are found, because finding one does not stop the walk), words longer than the number of cells.
Transfer lesson
An optional optimization removes trie leaves after their word is found, so later searches skip exhausted branches. More generally, when you would run the same search for many keys, ask whether a trie (or a hash of all prefixes) lets one search serve them all.
Go deeperBinary tries for XOR problems
Numbers are strings of bits. A trie over the 31 bits of each number (most significant first) lets you find, for any x, the stored number that maximizes x ^ y: at each level, prefer the child with the opposite bit. Maximum XOR of Two Numbers (LC 421) is O(31 n).
13.3Trie vs HashMap vs sorting
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
| Need | Best tool | Why |
|---|---|---|
| Exact membership only | HashSet<String> |
Simplest; O(L) per lookup to hash |
| Many prefix queries on a fixed list | Trie, or sorted list + binary search for the prefix range | Trie is O(L) per query; sorting is O(L log n) |
| Autocomplete with the top few results | Trie with results cached on nodes | Answer read directly at the prefix node |
| Wildcards or grid search with many words | Trie | Branching and pruning by prefix |
| Shortest root that prefixes a word | Trie | Stop at the first end node while walking |
| Memory is tight | HashSet of words (or of all prefixes, if few) |
Tries cost up to 26 references per node |
Common mistakes
searchreturning true for a prefix that is not a word (forgotisEnd).- Building a new trie per query instead of once.
- Assuming lowercase letters when the input can contain other characters.
- Recursing into all 26 children for every character instead of only for wildcards.
Tries
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 208Implement Trie (Prefix Tree) Node = children array + end-of-word flag | Medium | High | |
| 211Design Add and Search Words Data Structure DFS over children when you see a wildcard | Medium | High | |
| 648Replace Words Stop at the shortest root found while walking | Medium | Medium | |
| 1268Search Suggestions System Walk the prefix, then collect up to 3 words | Medium | High | |
| 677Map Sum Pairs Store aggregated values on nodes | Medium | Medium | |
| 720Longest Word in Dictionary Only extend through nodes that end a word | Medium | Medium | |
| 1233Remove Sub-Folders from the Filesystem Sort paths or use a trie of folder names | Medium | Medium | |
| 421Maximum XOR of Two Numbers in an Array Binary trie: greedily pick the opposite bit | Medium | Medium | |
| 2416Sum of Prefix Scores of Strings Count words passing through each node | Hard | High | |
| 212Word Search II Trie-guided grid backtracking with pruning | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 12: Graphs
Graphs model anything with connections: maps, networks, dependencies, states of a puzzle, cells of a grid. The hard part is rarely the algorithm; it is noticing that the problem is a graph and choosing among four tools: traversal, shortest path, ordering, and connectivity.
12.1Graph fundamentals and representation
Undirected edges [[0,1], [1,2], [2,0], [3,4]] with n = 5. Write the adjacency list. How many connected components are there?
Reveal the answer
0: [1, 2], 1: [0, 2], 2: [1, 0], 3: [4], 4: [3]. Two components: {0, 1, 2} and {3, 4}. Each undirected edge is stored twice, once in each direction.
Relationships between items. A tree is a graph with no cycles and one path between any two nodes; general graphs can have cycles, multiple paths, directions, and weights, which is why they need a visited set.
| Term | Meaning |
|---|---|
| Vertex (node), edge | Items and the connections between them; V and E denote their counts |
| Directed / undirected | One-way (u -> v) or two-way connections |
| Weighted / unweighted | Edges carry a cost, or all count as 1 |
| Degree, in-degree, out-degree | Number of edges at a node; in directed graphs, incoming vs outgoing |
| Path, cycle | A sequence of edges; a path that returns to its start |
| Connected component | A maximal set of nodes that can all reach each other (undirected) |
| DAG | Directed acyclic graph: a directed graph with no cycles (it has a topological order) |
| Dense / sparse | E close to V^2, or E close to V |
Go deeperRepresentations
| Representation | Space | Check edge (u, v) | List neighbors of u | Use when |
|---|---|---|---|---|
| Adjacency list | O(V + E) | O(degree) | O(degree) | Default for interviews; sparse graphs |
| Adjacency matrix | O(V^2) | O(1) | O(V) | Dense graphs, small V (<= ~1000), Floyd-Warshall |
| Edge list | O(E) | O(E) | O(E) | Kruskal, Bellman-Ford |
| Implicit (grid, state space) | O(1) to describe | computed | computed (4 or 8 neighbors) | Grids, puzzles, word ladders |
// Adjacency list from an edge list. Undirected -> add both directions.
static List<List<Integer>> buildGraph(int n, int[][] edges, boolean directed) {
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
for (int[] e : edges) {
adj.get(e[0]).add(e[1]);
if (!directed) adj.get(e[1]).add(e[0]);
}
return adj;
}
For weighted graphs, store pairs: List<List<int[]>> where each entry is {neighbor, weight}. For grids, do not build a graph at all; compute neighbors with a direction array.
12.2BFS, DFS, and connected components
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Visiting everything reachable from a start: reachability, counting components (islands, provinces), flood fill, cloning, marking regions.
- Signal
- Connections, reachability, islands, regions, 'can you get from A to B', components. Grids are graphs.
- Required condition
- You can list each node's neighbors. Mark visited to avoid infinite loops.
Mental model, core operation, limits, and what to use instead
- Mental model
- Spreading a rumor: everyone who hears it tells their friends, and nobody is told twice.
- Core operation
- Start from a source, push it, mark visited, expand neighbors until the frontier is empty.
- Complexity benefit
- O(V + E) to explore everything reachable.
- Limitations
- DFS recursion depth on large grids (10^5 to 10^6 cells) can overflow the stack.
- Alternative
- Union-Find for connectivity with incremental edges; iterative DFS with an explicit stack.
Go deeperThe visited rule
In a graph, unlike a tree, a node can be reached along several paths. Without a visited mark, traversal loops forever on a cycle or does exponential work. Two rules:
- Mark when you add to the queue or stack, not when you remove. Otherwise the same node is queued many times (correct for reachability, but wasteful, and wrong for BFS distances in some implementations).
- The visited structure can be the input (overwrite grid cells), a boolean array, a
Set, or a map you are already building (Clone Graph'soriginal -> clonemap).
Go deeperBFS vs DFS for traversal
Both visit every reachable node in O(V + E). Use BFS when distances or levels matter; use DFS when you need to follow a path to its end (cycle detection, topological order, backtracking) or when recursion is simply shorter. On large grids (10^6 cells), recursive DFS can overflow the stack; switch to BFS or an explicit stack.
// BFS from a source: dist[v] = fewest edges from source (-1 = unreachable).
// Mark visited when ENQUEUING, so a node is never added twice.
static int[] bfsDistances(List<List<Integer>> adj, int source) {
int[] dist = new int[adj.size()];
Arrays.fill(dist, -1);
Deque<Integer> queue = new ArrayDeque<>();
dist[source] = 0;
queue.offer(source);
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : adj.get(u)) {
if (dist[v] == -1) {
dist[v] = dist[u] + 1;
queue.offer(v);
}
}
}
return dist;
}
Guided solveLC 200. Number of IslandsFive questions, each with a reveal
Problem: 200Number of IslandsMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
In a grid of
'1'(land) and'0'(water), count groups of land connected horizontally or vertically.Input analysis
Up to 300 x 300 = 9 * 10^4 cells. The grid is an implicit graph: each cell connects to its four neighbors.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Groups of connected things" is connected components. Each traversal from an unvisited land cell discovers one entire island.
Brute force
There is nothing simpler that is also correct: any solution must look at each cell. The question is only how to avoid double counting.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. count how many times a DFS has to be started
Key observation
After a traversal "sinks" an island (marks all its cells), the outer scan can never start a second traversal inside it. So the number of traversals started equals the number of islands.
Visualization
1 1 0 0 0 scan finds (0,0): DFS sinks the 2x2 block count 1 1 1 0 0 0 scan finds (2,2): DFS sinks one cell count 2 0 0 1 0 0 scan finds (3,3): DFS sinks (3,3), (3,4) count 3 0 0 0 1 1
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Scan every cell. On
'1', increment the count and DFS from it, setting each visited land cell to'0'.Java implementation
Java// LC 200. Each DFS launch from unvisited land discovers one whole island. static int numIslands(char[][] grid) { int count = 0; for (int r = 0; r < grid.length; r++) for (int c = 0; c < grid[0].length; c++) if (grid[r][c] == '1') { count++; sink(grid, r, c); } return count; } private static void sink(char[][] grid, int r, int c) { if (r < 0 || c < 0 || r >= grid.length || c >= grid[0].length || grid[r][c] != '1') return; grid[r][c] = '0'; // mark visited by modifying the input sink(grid, r + 1, c); sink(grid, r - 1, c); sink(grid, r, c + 1); sink(grid, r, c - 1); }Dry run
The trace above gives 3.
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(R * C) time: each cell is sunk at most once, and each sink checks 4 neighbors. Space O(R * C) recursion depth in the worst case (a grid that is all land in a snake shape).
Edge cases
All water (0), all land (1), a single row or column. If modifying the input is not allowed, use a
boolean[][]visited array.Transfer lesson
Count components (LC 547), measure them (Max Area of Island, LC 695: DFS returns the size), or classify them (Surrounded Regions, LC 130, and Number of Enclaves, LC 1020: start from the border first and mark what escapes). Union-Find solves the same problem when cells or edges arrive over time.
Reverse thinking for regions. "Which cells can reach the border?" is expensive per cell but cheap in reverse: start a traversal from every border cell and mark what it reaches (LC 130, 417, 1020). When a question asks "can X reach Y" for many X, search backward from Y.
Graph traversal and connected components
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 841Keys and Rooms Reachability from a single source | Medium | Medium | |
| 200Number of Islands Count DFS launches over unvisited land | Medium | High | |
| 695Max Area of Island DFS returns component size | Medium | Medium | |
| 1905Count Sub Islands A sub-island is a component that never touches water in grid1 | Medium | Medium | |
| 547Number of Provinces Adjacency matrix traversal (or Union-Find) | Medium | Medium | |
| 133Clone Graph Map original -> clone doubles as the visited set | Medium | High | |
| 130Surrounded Regions Reverse thinking: mark what touches the border first | Medium | High | |
| 417Pacific Atlantic Water Flow Two reverse searches from each ocean, intersect | Medium | High | |
| 399Evaluate Division Model ratios as a weighted graph and search | Medium | High | |
| 827Making A Large Island Label components with sizes, then try flipping each zero | Hard | Medium |
12.3Shortest paths in unweighted graphs and multi-source BFS
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Fewest moves, fewest edges, minimum minutes, nearest X: whenever every move costs the same.
- Signal
- Fewest steps, minimum moves, nearest X from every cell, spreading per minute. All moves cost the same.
- Required condition
- Every edge has equal weight. (0/1 weights: 0-1 BFS with a deque.)
Mental model, core operation, limits, and what to use instead
- Mental model
- Ripples: a node's first visit happens at its shortest distance.
- Core operation
- Queue all sources at distance 0, expand level by level, mark visited when enqueuing.
- Complexity benefit
- Shortest paths in O(V + E) instead of Dijkstra's O(E log V).
- Limitations
- Weighted edges break it: the first visit is no longer the cheapest.
- Alternative
- Dijkstra for non-negative weights; Bellman-Ford rounds for edge-count limits or negative weights.
Why the first visit is the shortest. The queue always holds nodes of distance d followed by nodes of distance d + 1 (never more than two consecutive distances, in that order). So nodes are discovered in non-decreasing order of distance, and a node discovered later could only have a larger or equal distance.
Go deeperMulti-source BFS
When the question is "distance to the nearest source" from every cell, put all sources into the queue at distance 0 before starting. The BFS then behaves as if a single super-source were connected to all of them.
Guided solveLC 994. Rotting OrangesFive questions, each with a reveal
Problem: 994Rotting OrangesMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Every minute, fresh oranges adjacent to a rotten one become rotten. Return the minutes until none are fresh, or -1 if some never rot.
Input analysis
Grid up to 10 x 10. Values 0 (empty), 1 (fresh), 2 (rotten). Several rotten oranges at the start.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Spreading one step per minute in all directions is BFS levels. Several starting points mean multi-source.
Brute force
Simulate minute by minute, scanning the whole grid each minute: O((R * C)^2).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. all rotten oranges start the BFS together; levels are minutes
Key observation
An orange rots at minute = its BFS distance to the nearest initially rotten orange. A multi-source BFS computes exactly that in one pass.
Visualization
Figure 12.3Right: the minute each orange rots. The last one rots at minute 4. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Queue all rotten cells and count fresh ones. Process level by level; each fresh neighbor becomes rotten (mark on enqueue), decrementing the fresh count. Count levels until no fresh remain. If fresh oranges are left when the queue empties, return -1.
Java implementation
Java// LC 994. Multi-source BFS: every rotten orange starts in the queue at minute 0. static int orangesRotting(int[][] grid) { int rows = grid.length, cols = grid[0].length, fresh = 0; Deque<int[]> queue = new ArrayDeque<>(); for (int r = 0; r < rows; r++) for (int c = 0; c < cols; c++) { if (grid[r][c] == 2) queue.offer(new int[]{r, c}); else if (grid[r][c] == 1) fresh++; } int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; int minutes = 0; while (!queue.isEmpty() && fresh > 0) { int size = queue.size(); // one level = one minute for (int i = 0; i < size; i++) { int[] cell = queue.poll(); for (int[] d : dirs) { int nr = cell[0] + d[0], nc = cell[1] + d[1]; if (nr < 0 || nc < 0 || nr >= rows || nc >= cols || grid[nr][nc] != 1) continue; grid[nr][nc] = 2; // rot when enqueued fresh--; queue.offer(new int[]{nr, nc}); } } minutes++; } return fresh == 0 ? minutes : -1; }Dry run
Example above: levels 1 to 4, fresh count reaches 0, return 4. The loop condition
fresh > 0avoids counting an extra minute after the last orange rots. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(R * C) time and space.
Edge cases
No fresh oranges at all (0, even with no rotten ones), a fresh orange walled off by empty cells (-1), no rotten oranges but some fresh (-1).
Transfer lesson
01 Matrix (LC 542), As Far from Land as Possible (LC 1162), and "walls and gates" style problems are the same multi-source BFS. Shortest Bridge (LC 934) combines a DFS to collect one island as the source set with a BFS outward.
Go deeperBFS over states
The nodes do not have to be cells. In Open the Lock (LC 752) a node is a 4-digit combination; in Word Ladder (LC 127) a node is a word; in Shortest Path with Obstacles Elimination (LC 1293) a node is (row, col, removals left). The state must contain everything that affects future moves. If two arrivals at the same cell can behave differently later, they are different states.
// LC 127. BFS over words; neighbors differ by one letter. Generating neighbors by
// changing each position to a..z costs O(L * 26) per word instead of O(N * L) per word.
static int ladderLength(String begin, String end, List<String> wordList) {
Set<String> words = new HashSet<>(wordList);
if (!words.contains(end)) return 0;
Deque<String> queue = new ArrayDeque<>();
queue.offer(begin);
words.remove(begin);
for (int steps = 1; !queue.isEmpty(); steps++) {
for (int size = queue.size(); size > 0; size--) {
char[] w = queue.poll().toCharArray();
for (int i = 0; i < w.length; i++) {
char original = w[i];
for (char c = 'a'; c <= 'z'; c++) {
if (c == original) continue;
w[i] = c;
String next = new String(w);
if (!words.contains(next)) continue;
if (next.equals(end)) return steps + 1;
words.remove(next); // removing = marking visited
queue.offer(next);
}
w[i] = original;
}
}
}
return 0;
}
Shortest paths in unweighted graphs and multi-source BFS
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 994Rotting Oranges Multi-source BFS; levels are minutes | Medium | High | |
| 54201 Matrix Start BFS from all zeros at once | Medium | High | |
| 1091Shortest Path in Binary Matrix 8-direction BFS with distance in the queue | Medium | High | |
| 1926Nearest Exit from Entrance in Maze Stop at the first exit popped | Medium | Medium | |
| 1162As Far from Land as Possible Multi-source BFS from land cells | Medium | Medium | |
| 934Shortest Bridge DFS to find island one, BFS to expand toward island two | Medium | Medium | |
| 752Open the Lock Implicit graph of states; deadends as visited | Medium | High | |
| 909Snakes and Ladders Board as an implicit graph with index conversion | Medium | Medium | |
| 127Word Ladder Wildcard patterns to find neighbors fast | Hard | High | |
| 1293Shortest Path in a Grid with Obstacles Elimination State = (row, col, removalsLeft) | Hard | High |
12.4Cycles, bipartite graphs, and topological sort
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Go deeperCycle detection
| Graph | Method | A cycle exists when |
|---|---|---|
| Undirected | DFS/BFS with parent tracking | A visited neighbor is not the node you came from |
| Undirected | Union-Find | An edge joins two nodes already in the same set |
| Directed | DFS with three colors | An edge reaches a GRAY node (one on the current DFS path) |
| Directed | Kahn's algorithm | Fewer than V nodes get processed |
// Directed cycle detection with three colors.
// WHITE = unvisited, GRAY = on the current DFS path, BLACK = fully explored.
// An edge to a GRAY node closes a cycle.
static boolean hasDirectedCycle(List<List<Integer>> adj) {
int[] color = new int[adj.size()]; // 0 white, 1 gray, 2 black
for (int u = 0; u < adj.size(); u++)
if (color[u] == 0 && dfsCycle(adj, u, color)) return true;
return false;
}
private static boolean dfsCycle(List<List<Integer>> adj, int u, int[] color) {
color[u] = 1;
for (int v : adj.get(u)) {
if (color[v] == 1) return true; // back edge
if (color[v] == 0 && dfsCycle(adj, v, color)) return true;
}
color[u] = 2;
return false;
}
// Undirected cycle detection: a visited neighbor that is not the parent closes a cycle.
static boolean hasUndirectedCycle(List<List<Integer>> adj) {
boolean[] seen = new boolean[adj.size()];
for (int s = 0; s < adj.size(); s++) {
if (seen[s]) continue;
Deque<int[]> stack = new ArrayDeque<>(); // {node, parent}
stack.push(new int[]{s, -1});
seen[s] = true;
while (!stack.isEmpty()) {
int[] top = stack.pop();
for (int v : adj.get(top[0])) {
if (v == top[1]) continue; // the edge we came in on
if (seen[v]) return true;
seen[v] = true;
stack.push(new int[]{v, top[0]});
}
}
}
return false;
}
Why three colors in directed graphs? A visited node that is fully explored (BLACK) is not necessarily on the current path: in 0 -> 1, 0 -> 2, 1 -> 2, node 2 is reached twice without any cycle. Only an edge back to a node still on the current path (GRAY) closes a cycle.
Go deeperBipartite graphs
A graph is bipartite if its nodes split into two groups with every edge going across. Equivalently, it has no odd-length cycle. Test it by two-coloring with BFS or DFS: neighbors get opposite colors, and a conflict means not bipartite. Start from every uncolored node, because the graph may be disconnected.
// LC 785. Two-color with BFS. Graph may be disconnected: start from every uncolored node.
static boolean isBipartite(int[][] graph) {
int[] color = new int[graph.length]; // 0 = uncolored, 1 / -1 = the two sides
for (int s = 0; s < graph.length; s++) {
if (color[s] != 0) continue;
Deque<Integer> queue = new ArrayDeque<>();
queue.offer(s);
color[s] = 1;
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v : graph[u]) {
if (color[v] == color[u]) return false; // edge inside one side
if (color[v] == 0) {
color[v] = -color[u];
queue.offer(v);
}
}
}
}
return true;
}
Possible Bipartition (LC 886) is the same test after building a graph from "dislikes" pairs: recognizing that "split into two groups so that no pair is in the same group" means bipartite is the modeling step.
Go deeperTopological sort
Ordering tasks so every task comes after its prerequisites, deciding whether such an order exists, and dynamic programming over a DAG.
- Signal
- Prerequisites, dependencies, build order, 'is it possible to finish', ordering with constraints.
- Required condition
- Directed graph. A valid order exists if and only if there is no directed cycle.
Mental model, core operation, limits, and what to use instead
- Mental model
- Getting dressed: socks before shoes. Always do something whose prerequisites are all done.
- Core operation
- Kahn: queue in-degree-0 nodes, remove them, decrement neighbors. DFS: postorder reversed, three colors for cycles.
- Complexity benefit
- O(V + E) to order or detect a cycle.
- Limitations
- Undirected graphs need a different cycle test (parent check or DSU).
- Alternative
- DFS three-color for cycle detection; DP over the topological order for longest paths in a DAG.
Guided solveLC 207 and 210. Course Schedule I and IIFive questions, each with a reveal
Problem: 207Course ScheduleMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
numCoursescourses and pairs[a, b]meaning "b before a". Can all courses be finished (207)? If so, return an order (210).Input analysis
Up to 2000 courses and 5000 pairs. Dependencies between items: a directed graph with edges
b -> a. - 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Prerequisites" and "order" are the signals for topological sort. "Possible to finish" is "the graph has no directed cycle".
Brute force
Repeatedly scan all courses for one whose prerequisites are done: O(V * (V + E)).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. repeatedly take a course whose prerequisites are all done
Key observation
Track how many unmet prerequisites each course has (in-degree). Finishing a course decrements its dependents' counts, and any course that reaches zero becomes available. A queue of available courses makes each step O(1) amortized.
Visualization
The figure above: order 0, 1, 2, 3 (or 0, 2, 1, 3; both are valid).
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Build adjacency lists and in-degrees. Queue all in-degree-0 nodes. Pop a node, append it to the order, decrement its neighbors, and queue any that reach 0. If the order has all nodes, return it; otherwise a cycle blocked the rest.
Java implementation
Java// LC 210. Kahn's algorithm: repeatedly take a node with no remaining prerequisites. // Edge [course, pre] means pre -> course. static int[] findOrder(int numCourses, int[][] prerequisites) { List<List<Integer>> adj = new ArrayList<>(); for (int i = 0; i < numCourses; i++) adj.add(new ArrayList<>()); int[] indegree = new int[numCourses]; for (int[] p : prerequisites) { adj.get(p[1]).add(p[0]); indegree[p[0]]++; } Deque<Integer> ready = new ArrayDeque<>(); for (int i = 0; i < numCourses; i++) if (indegree[i] == 0) ready.offer(i); int[] order = new int[numCourses]; int done = 0; while (!ready.isEmpty()) { int u = ready.poll(); order[done++] = u; for (int v : adj.get(u)) if (--indegree[v] == 0) ready.offer(v); // last prerequisite just finished } return done == numCourses ? order : new int[0]; // fewer than n -> a cycle blocked some nodes }Dry run
[[1,0],[0,1]]: both nodes have in-degree 1, the queue starts empty, 0 nodes are processed: cycle, return empty. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(V + E) time and space.
Edge cases
No prerequisites (any order), self-dependency
[a, a](a cycle), duplicate pairs (in-degree counted twice and decremented twice; consistent), disconnected components (all start in the queue).Transfer lesson
Kahn's order lets you do DP in dependency order: longest path in a DAG, propagate reachability (Course Schedule IV, LC 1462), resolve recipes from supplies (LC 2115). Peeling leaves layer by layer (Minimum Height Trees, LC 310) is the same idea on an undirected tree with degree 1 instead of in-degree 0.
DFS-based topological order. Reverse postorder: a node is finished only after everything it points to, so listing nodes in reverse finish order puts each before its dependents. Combine with three colors to detect cycles in the same pass.
// Topological order by DFS: a node is added AFTER all its descendants (postorder);
// reversing the postorder puts every node before the nodes it points to.
static List<Integer> topoOrderDfs(List<List<Integer>> adj) {
int n = adj.size();
boolean[] seen = new boolean[n];
Deque<Integer> order = new ArrayDeque<>();
for (int u = 0; u < n; u++) if (!seen[u]) post(adj, u, seen, order);
return new ArrayList<>(order);
}
private static void post(List<List<Integer>> adj, int u, boolean[] seen, Deque<Integer> order) {
seen[u] = true;
for (int v : adj.get(u)) if (!seen[v]) post(adj, v, seen, order);
order.push(u); // push to the front = reversed postorder
}
Cycles, bipartite graphs, and topological sort
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 207Course Schedule Kahn's algorithm: processed count < n means a cycle | Medium | High | |
| 210Course Schedule II Return the Kahn processing order | Medium | High | |
| 785Is Graph Bipartite? Two-color with BFS; handle disconnected parts | Medium | High | |
| 886Possible Bipartition Model dislikes as edges, then two-color | Medium | Medium | |
| 802Find Eventual Safe States Three-color DFS or reverse-graph Kahn | Medium | Medium | |
| 1462Course Schedule IV Propagate reachability in topological order | Medium | Medium | |
| 2115Find All Possible Recipes from Given Supplies Dependency resolution on named nodes | Medium | Medium | |
| 2192All Ancestors of a Node in a Directed Acyclic Graph Propagate ancestor sets in topological order | Medium | Medium | |
| 310Minimum Height Trees Peel leaves layer by layer toward the center | Medium | Medium | |
| 329Longest Increasing Path in a Matrix DAG longest path with memoized DFS | Hard | High |
12.5Weighted shortest paths
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Minimum total cost when edges have different non-negative weights: network delay, cheapest route, least effort.
- Signal
- Minimum total cost, time, or effort over weighted edges; 'cheapest', 'fastest', 'least effort path'.
- Required condition
- All edge weights are non-negative, and the path cost never decreases as you extend a path.
Mental model, core operation, limits, and what to use instead
- Mental model
- Water flooding a pipe network: the closest junction fills first.
- Core operation
- Min-heap of (dist, node); pop the closest, skip if stale, relax its edges.
- Complexity benefit
- O((V + E) log V) single-source shortest paths.
- Limitations
- Negative edges break the 'finalized when popped' guarantee. Extra constraints (stop limits) need state expansion or Bellman-Ford.
- Alternative
- BFS when weights are equal; 0-1 BFS for weights 0/1; Bellman-Ford for negatives or k-edge limits; Floyd-Warshall for all pairs with small V.
Go deeperWhy Dijkstra works, and why it needs non-negative weights
The heap always pops the unfinalized node with the smallest tentative distance d. Any other path to that node would have to leave the finalized region through some node with tentative distance >= d, and with non-negative edges it can only get longer from there. So d is final. A negative edge breaks this: a longer-looking path could later become shorter.
Guided solveLC 743. Network Delay TimeFive questions, each with a reveal
Problem: 743Network Delay TimeMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
A signal starts at node k and travels along directed weighted edges. How long until all nodes receive it? -1 if some never do.
Input analysis
Up to 100 nodes and 6000 edges, weights 0 to 100 (non-negative).
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
"Time for a signal to reach each node" is the shortest path from k to each node, with weights: Dijkstra. All nodes receive it when the farthest one does: take the max.
Brute force
Bellman-Ford: relax all edges V - 1 times, O(V * E). Fine here, but Dijkstra is the expected answer.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. single-source shortest paths, answer is the largest distance
Key observation
Non-negative weights mean the closest unfinalized node can be finalized, so a min-heap processes nodes in order of distance.
Visualization
Pop Distance Relaxations Heap after A 0 B = 4, C = 1 (1, C), (4, B) C 1 B = min(4, 3) = 3, D = 6 (3, B), (4, B), (6, D) B 3 D = min(6, 4) = 4 (4, B), (4, D), (6, D) B 4 stale (4 > 3), skip (4, D), (6, D) D 4 none (6, D) D 6 stale, skip empty - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
dist[k] = 0, heap of(dist, node). Pop; skip if stale; relax each outgoing edge, pushing improved distances. Answer: max overdist, or -1 if any is infinite.Java implementation
Java// LC 743. Dijkstra with a min-heap of (distance, node) and lazy deletion. // Requires non-negative weights. static int networkDelayTime(int[][] times, int n, int k) { List<List<int[]>> adj = new ArrayList<>(); for (int i = 0; i <= n; i++) adj.add(new ArrayList<>()); for (int[] t : times) adj.get(t[0]).add(new int[]{t[1], t[2]}); // u -> (v, w) int[] dist = new int[n + 1]; Arrays.fill(dist, Integer.MAX_VALUE); dist[k] = 0; PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0])); heap.offer(new int[]{0, k}); while (!heap.isEmpty()) { int[] top = heap.poll(); int d = top[0], u = top[1]; if (d > dist[u]) continue; // stale entry: a shorter path was already found for (int[] edge : adj.get(u)) { int v = edge[0], nd = d + edge[1]; if (nd < dist[v]) { // relax dist[v] = nd; heap.offer(new int[]{nd, v}); } } } int answer = 0; for (int v = 1; v <= n; v++) { if (dist[v] == Integer.MAX_VALUE) return -1; answer = Math.max(answer, dist[v]); } return answer; }Dry run
The table above, using the figure's graph.
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O((V + E) log V) with lazy deletion (the heap can hold up to E entries, and log E = O(log V)). Space O(V + E).
Edge cases
Unreachable nodes (-1), zero-weight edges (still fine), self-loops and parallel edges (relaxation handles them).
Transfer lesson
Change what "distance" means and keep the algorithm: max of edge weights on the path (Path With Minimum Effort, LC 1631; Swim in Rising Water, LC 778), product of probabilities with a max-heap (LC 1514), number of shortest paths (LC 1976, count ways when
nd == dist[v]). This works whenever extending a path never makes its cost better.
Go deeperVariants and the other shortest-path tools
0-1 BFS. When weights are only 0 or 1, a deque replaces the heap: 0-cost moves go to the front, 1-cost moves to the back. O(V + E).
// LC 2290. Edge cost 0 (empty cell) or 1 (obstacle). Deque: cost-0 moves go to the front,
// cost-1 moves to the back, so the deque stays sorted by distance like Dijkstra's heap.
static int minimumObstacles(int[][] grid) {
int rows = grid.length, cols = grid[0].length;
int[][] dist = new int[rows][cols];
for (int[] row : dist) Arrays.fill(row, Integer.MAX_VALUE);
Deque<int[]> dq = new ArrayDeque<>();
dist[0][0] = 0;
dq.offerFirst(new int[]{0, 0});
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
while (!dq.isEmpty()) {
int[] cell = dq.pollFirst();
int r = cell[0], c = cell[1];
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr < 0 || nc < 0 || nr >= rows || nc >= cols) continue;
int nd = dist[r][c] + grid[nr][nc];
if (nd < dist[nr][nc]) {
dist[nr][nc] = nd;
if (grid[nr][nc] == 0) dq.offerFirst(new int[]{nr, nc});
else dq.offerLast(new int[]{nr, nc});
}
}
}
return dist[rows - 1][cols - 1];
}
Bellman-Ford with an edge limit. "At most k stops" (LC 787) breaks Dijkstra, because the cheapest path to a node may use too many edges, and a pricier path with fewer edges might be the one that reaches the destination in time. Run k + 1 rounds of relaxing all edges, each round reading only the previous round's distances, so a round can extend any path by at most one edge.
// LC 787. At most k stops = at most k + 1 edges. Run k + 1 relaxation rounds,
// each reading only the PREVIOUS round's distances.
static int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
for (int round = 0; round <= k; round++) {
int[] next = dist.clone(); // snapshot prevents chaining edges in one round
for (int[] f : flights) {
int u = f[0], v = f[1], price = f[2];
if (dist[u] != Integer.MAX_VALUE && dist[u] + price < next[v]) next[v] = dist[u] + price;
}
dist = next;
}
return dist[dst] == Integer.MAX_VALUE ? -1 : dist[dst];
}
Floyd-Warshall gives all-pairs shortest paths in O(V^3) with three nested loops. Use it when V is at most a few hundred and you need distances between many pairs.
// LC 1334. All-pairs shortest paths for small n (here n <= 100): O(n^3).
// After processing k, dist[i][j] is the shortest path using only intermediates 0..k.
static int findTheCity(int n, int[][] edges, int threshold) {
int INF = 1_000_000_000;
int[][] dist = new int[n][n];
for (int[] row : dist) Arrays.fill(row, INF);
for (int i = 0; i < n; i++) dist[i][i] = 0;
for (int[] e : edges) { dist[e[0]][e[1]] = e[2]; dist[e[1]][e[0]] = e[2]; }
for (int k = 0; k < n; k++) // k must be the OUTER loop
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++)
if (dist[i][k] + dist[k][j] < dist[i][j]) dist[i][j] = dist[i][k] + dist[k][j];
int best = -1, fewest = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
int reach = 0;
for (int j = 0; j < n; j++) if (i != j && dist[i][j] <= threshold) reach++;
if (reach <= fewest) { fewest = reach; best = i; } // ties go to the larger index
}
return best;
}
| Situation | Tool | Cost |
|---|---|---|
| Unweighted | BFS | O(V + E) |
| Weights 0 or 1 | 0-1 BFS | O(V + E) |
| Non-negative weights, one source | Dijkstra | O((V + E) log V) |
| Edge-count limit, or negative weights (no negative cycles) | Bellman-Ford | O(V * E) or O(k * E) |
| All pairs, small V | Floyd-Warshall | O(V^3) |
| DAG, any weights | DP in topological order | O(V + E) |
Weighted shortest paths (Dijkstra and variants)
7 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 743Network Delay Time Plain Dijkstra; answer is the max distance | Medium | High | |
| 1631Path With Minimum Effort Dijkstra where path cost is a max, not a sum | Medium | High | |
| 1514Path with Maximum Probability Max-heap Dijkstra on multiplied weights | Medium | Medium | |
| 787Cheapest Flights Within K Stops Stop limit breaks plain Dijkstra; use Bellman-Ford rounds | Medium | High | |
| 1976Number of Ways to Arrive at Destination Count shortest paths during relaxation | Medium | Medium | |
| 1334Find the City With the Smallest Number of Neighbors at a Threshold Distance All-pairs with small n: Floyd-Warshall or n Dijkstras | Medium | Medium | |
| 2812Find the Safest Path in a Grid Multi-source BFS then maximize the minimum along a path | Medium | Medium | |
| 2290Minimum Obstacle Removal to Reach Corner 0-1 BFS with a deque | Hard | High | |
| 778Swim in Rising Water Minimize the maximum height on the path | Hard | High |
12.6Union-Find and minimum spanning trees
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Grouping items as connections arrive, answering "are these two in the same group?", counting groups, detecting the edge that creates a cycle, and building minimum spanning trees.
- Signal
- Grouping by connections that arrive over time, 'are A and B connected', counting components, redundant edge, Kruskal.
- Required condition
- Only unions happen (no edge deletions). Undirected relationships.
Mental model, core operation, limits, and what to use instead
- Mental model
- Clubs merging: each club has a president; to compare two people, compare their presidents.
- Core operation
- find with path compression, union by size; union returns false if already connected.
- Complexity benefit
- Near O(1) amortized per operation versus re-running BFS after each edge.
- Limitations
- No deletions; no paths; directed reachability is not supported.
- Alternative
- BFS/DFS when the graph is static and queried once.
// Disjoint Set Union with path compression and union by size.
// Both together make find/union effectively O(1) amortized (inverse Ackermann).
static class UnionFind {
private final int[] parent, size;
private int components;
UnionFind(int n) {
parent = new int[n];
size = new int[n];
components = n;
for (int i = 0; i < n; i++) { parent[i] = i; size[i] = 1; }
}
int find(int x) {
while (parent[x] != x) {
parent[x] = parent[parent[x]]; // path halving: point to grandparent
x = parent[x];
}
return x;
}
boolean union(int a, int b) { // false if already connected
int ra = find(a), rb = find(b);
if (ra == rb) return false;
if (size[ra] < size[rb]) { int t = ra; ra = rb; rb = t; }
parent[rb] = ra; // attach the smaller tree under the larger
size[ra] += size[rb];
components--;
return true;
}
int components() { return components; }
}
// LC 684. The first edge whose endpoints are already connected creates the cycle.
static int[] findRedundantConnection(int[][] edges) {
UnionFind uf = new UnionFind(edges.length + 1); // nodes are 1..n
for (int[] e : edges)
if (!uf.union(e[0], e[1])) return e;
return new int[0];
}
Why both optimizations. Union by size keeps trees shallow (height O(log n) on its own); path compression flattens them further as they are used. Together, any sequence of m operations takes O(m * alpha(n)), where alpha is the inverse Ackermann function, at most 4 for any realistic n. Say "effectively constant amortized time".
Union-Find vs BFS/DFS. For a fixed graph queried once, BFS/DFS is just as good. Union-Find wins when edges arrive over time with connectivity questions in between, when you need to merge groups by a shared key (Accounts Merge, LC 721), or inside Kruskal.
Go deeperMinimum spanning tree
A spanning tree connects all V nodes with V - 1 edges; the minimum one has the smallest total weight. The cut property makes greedy correct: for any split of the nodes into two sides, the cheapest edge crossing the split belongs to some MST.
| Kruskal | Prim | |
|---|---|---|
| Idea | Sort edges; add the cheapest that joins two components (Union-Find) | Grow one tree; repeatedly add the cheapest edge leaving it |
| Cost | O(E log E) | O(E log V) with a heap; O(V^2) with arrays (best for dense graphs) |
| Best for | Sparse graphs, edges given as a list | Dense or complete graphs (all pairs of points) |
// LC 1584 with Kruskal: sort all edges, add the cheapest that joins two components.
static int minCostConnectPointsKruskal(int[][] points) {
int n = points.length;
List<int[]> edges = new ArrayList<>(); // {cost, i, j}
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
edges.add(new int[]{manhattan(points, i, j), i, j});
edges.sort((a, b) -> Integer.compare(a[0], b[0]));
UnionFind uf = new UnionFind(n);
int total = 0, used = 0;
for (int[] e : edges) {
if (uf.union(e[1], e[2])) {
total += e[0];
if (++used == n - 1) break; // a tree has n - 1 edges
}
}
return total;
}
// LC 1584 with Prim (O(n^2), best for dense graphs): grow one tree,
// always attaching the closest outside point.
static int minCostConnectPointsPrim(int[][] points) {
int n = points.length, total = 0;
int[] best = new int[n]; // cheapest edge from the tree to each point
Arrays.fill(best, Integer.MAX_VALUE);
boolean[] inTree = new boolean[n];
best[0] = 0;
for (int added = 0; added < n; added++) {
int u = -1;
for (int v = 0; v < n; v++)
if (!inTree[v] && (u == -1 || best[v] < best[u])) u = v;
inTree[u] = true;
total += best[u];
for (int v = 0; v < n; v++)
if (!inTree[v]) best[v] = Math.min(best[v], manhattan(points, u, v));
}
return total;
}
private static int manhattan(int[][] p, int i, int j) {
return Math.abs(p[i][0] - p[j][0]) + Math.abs(p[i][1] - p[j][1]);
}
For Min Cost to Connect All Points (LC 1584) the graph is complete (about n^2 / 2 edges), so the O(V^2) Prim avoids building and sorting all edges.
Union-Find and minimum spanning trees
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 684Redundant Connection The first edge joining an already-connected pair | Medium | High | |
| 1319Number of Operations to Make Network Connected components - 1, if enough cables exist | Medium | Medium | |
| 990Satisfiability of Equality Equations Union all '==' first, then check '!=' | Medium | Medium | |
| 721Accounts Merge Union by shared email, group by root | Medium | High | |
| 947Most Stones Removed with Same Row or Column Answer = stones - components | Medium | Medium | |
| 1202Smallest String With Swaps Sort characters within each component | Medium | Medium | |
| 2316Count Unreachable Pairs of Nodes in an Undirected Graph Component sizes and pair counting with long | Medium | Medium | |
| 1584Min Cost to Connect All Points Prim's on a dense complete graph | Medium | High | |
| 1579Remove Max Number of Edges to Keep Graph Fully Traversable Two DSUs; shared edges first | Hard | High | |
| 1489Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree Kruskal with an edge forced in or excluded | Hard | High |
12.7Recognizing graph problems in disguise
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
| The statement talks about | The graph is | Typical tool |
|---|---|---|
| A grid with walls, islands, or spreading | Cells with 4 or 8 neighbors | BFS / DFS |
| Words that differ by one letter, locks with digits, puzzle boards | States, with an edge per legal move | BFS over states |
| Prerequisites, build steps, recipes, ordering rules | A directed graph of dependencies | Topological sort |
Ratios or equations between variables (a / b = 2) |
Weighted directed edges | DFS multiplying weights, or weighted DSU |
| Accounts sharing emails, people sharing friends, stones sharing rows | Items linked by a shared key | Union-Find |
| Flights, roads, network latency | Weighted edges | Dijkstra / Bellman-Ford |
| "Two groups such that no conflicting pair is together" | Conflict edges | Bipartite check |
| A tree where you need distances in every direction | The tree as an undirected graph | Parent pointers + BFS |
12.8Choosing the algorithm
Name the algorithm for each: (a) fewest moves on a grid where every move costs 1; (b) cheapest route when moves cost 1 to 100; (c) an order for tasks with prerequisites; (d) "are these two accounts linked?" while links keep arriving.
Reveal the answer
(a) BFS: levels are distances. (b) Dijkstra: non-negative, unequal weights. (c) Topological sort (Kahn's algorithm). (d) Union-Find: it merges groups as edges arrive and answers connectivity in nearly O(1).
| Question | Algorithm |
|---|---|
| Can I reach Y from X? How many groups? | BFS or DFS (or Union-Find with incremental edges) |
| Fewest steps, equal costs? | BFS (multi-source if several starts) |
| Cheapest path, non-negative costs? | Dijkstra |
| Cheapest path with at most k edges? | Bellman-Ford rounds |
| An order that respects dependencies? Any cycle? | Topological sort (Kahn) |
| Two-colorable? | BFS/DFS coloring |
| Connect everything at minimum total cost? | Kruskal or Prim |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 14: Dynamic programming
Dynamic programming is recursion that remembers. If a brute-force recursion solves the same subproblem many times, store each answer once and reuse it. Every DP in this module is derived with the same six steps; no transition is memorized.
14.1What DP is and when it applies
Plain recursive fib(40) (two calls per call) makes about how many calls? How many distinct values does it actually compute?
Reveal the answer
About 3.3 * 10^8 calls (331,160,281). There are only 41 distinct values, fib(0) to fib(40). Caching each one the first time turns exponential work into O(n): that gap is what dynamic programming removes.
Counting ways, minimizing or maximizing cost, and yes/no feasibility, where the answer is built from a sequence of decisions and later decisions depend on earlier ones.
Climbing a staircase where you can take 1 or 2 steps. The number of ways to reach step 10 is (ways to reach step 9) + (ways to reach step 8), because your last move came from one of them. You do not need to know how anyone got to step 9; you only need how many ways there are. Compute each step's count once, from the bottom up.
- Signal
- Count the ways, min/max cost, 'is it possible', with choices at each step where later choices depend on earlier ones. A brute-force recursion re-solves the same subproblems.
- Required condition
- Optimal substructure (the best answer is built from best sub-answers) and overlapping subproblems.
Mental model, core operation, limits, and what to use instead
- Mental model
- Filling a table where every cell is a smaller version of the question, answered once and reused.
- Core operation
- Define state, write the transition from the last decision, set base cases, fill in dependency order.
- Complexity benefit
- Exponential recursion becomes polynomial: number of states times work per state.
- Limitations
- Needs a small state; if you must list every answer, output size dominates (backtracking).
- Alternative
- Greedy when a choice is provably safe; backtracking when you need all solutions; graph shortest path when states form a weighted graph.
Go deeperThe two properties
- Optimal substructure: the best answer for a problem can be built from the best answers of smaller subproblems. (Shortest path: the best route to B through A uses the best route to A.)
- Overlapping subproblems: the recursion reaches the same subproblem along many different paths. Without overlap, plain recursion or divide and conquer is enough.
How to detect overlap quickly: write the brute-force recursion's signature, e.g. best(i) or ways(i, j). If the result depends only on those arguments, and the number of distinct argument values is small (n, n^2, n * W) while the number of calls is exponential, it is DP.
Go deeperRecognizing a DP problem
| Signal | Example |
|---|---|
| "Number of ways", often "modulo 10^9 + 7" | Climbing Stairs, Decode Ways, Unique Paths |
| "Minimum / maximum cost, length, profit" with choices at each step | House Robber, Coin Change, Edit Distance |
| "Is it possible" where choices interact | Word Break, Partition Equal Subset Sum |
| Two strings or sequences compared position by position | LCS, Edit Distance, Interleaving String |
| Greedy has a counterexample | Coin change with |
| Backtracking would explore many paths that end in the same state | Word Break, Target Sum |
14.2The six-step derivation process
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
Use these steps for every DP problem in this module. Write the answers down before coding.
| Step | Question | House Robber (LC 198) |
|---|---|---|
| 1. State | What subproblem does one value answer? Say it in words. | best(i) = most money from houses i..n-1 |
| 2. Transition | What is the last (or first) decision, and what is left after it? | Rob house i (then skip i + 1) or skip it: best(i) = max(best(i+1), nums[i] + best(i+2)) |
| 3. Base cases | Where does the recursion stop? | No houses left: best(n) = best(n+1) = 0 |
| 4. Answer | Which state is the original question? | best(0) |
| 5. Order | Which states must be computed before which? | best(i) needs i+1 and i+2: fill from n-1 down to 0 |
| 6. Space | Which previous values are still needed? | Only the next two: two variables |
Finding the transition is the step that feels like magic, and it is not. Ask: "Consider an optimal solution. What happened at the last (or first) position? What are all the possibilities, and what subproblem remains in each case?" Each possibility becomes one term in the max, min, sum, or OR.
Choosing the state. The state must contain everything the future depends on. If two partial solutions with the same state could have different best futures, the state is missing a variable (see the multi-state section below).
Go deeperMemoization vs tabulation
| Memoization (top-down) | Tabulation (bottom-up) | |
|---|---|---|
| How | Recursive function + cache | Loops filling a table in dependency order |
| Computes | Only the states actually reached | Every state |
| Order | Automatic (recursion handles it) | You must choose it (step 5) |
| Risk | Stack depth for large n | Getting the loop order wrong |
| Space optimization | Hard | Natural: keep only the rows or cells still read |
| Best for | Deriving and explaining; sparse state spaces | Final versions; tight memory |
In an interview, deriving with memoization and then converting is a strong, communicative path. Either form is a complete answer.
Guided solveLC 198. House Robber, all four formsFive questions, each with a reveal
Problem: 198House RobberMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Houses in a row with money; you cannot rob two adjacent houses. Maximize the total.
Input analysis
Up to 100 houses, values up to 400. Even O(n^2) would pass, but the natural DP is O(n).
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
A sequence of take/skip decisions where taking constrains the next decision. Greedy fails: on
[2, 1, 1, 2], taking every other house gives 3 either way, but houses 0 and 3 give 4. The subproblem "best from house i onward" repeats along many decision paths.Brute force
Recursion over take/skip: O(2^n), because
best(i + 2)is reached both by "take i" and by "skip i, skip i + 1". - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. rob this house and skip the next, or skip this house
Key observation
best(i)depends only oni. There are n distinct states and two choices per state: O(n).Visualization
Figure 14.2Filled from right to left; each cell reads the two cells to its right. - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
State, transition, base cases, and order from the table above.
Java implementation
Memoized recursion, then the table, then two variables:
Java// LC 198, step 1: recursion that follows the decision directly. // best(i) = max money from houses i..n-1 // = max(skip i -> best(i + 1), rob i -> nums[i] + best(i + 2)) static int robMemo(int[] nums) { int[] memo = new int[nums.length]; Arrays.fill(memo, -1); // -1 = not computed (answers are >= 0) return best(nums, 0, memo); } private static int best(int[] nums, int i, int[] memo) { if (i >= nums.length) return 0; // base case: no houses left if (memo[i] != -1) return memo[i]; int skip = best(nums, i + 1, memo); int rob = nums[i] + best(nums, i + 2, memo); return memo[i] = Math.max(skip, rob); }Java// Step 2: the same recurrence as a table. dp[i] depends on dp[i+1] and dp[i+2], // so fill i from right to left. dp has 2 extra slots for the base cases. static int robTab(int[] nums) { int n = nums.length; int[] dp = new int[n + 2]; // dp[n] = dp[n+1] = 0 for (int i = n - 1; i >= 0; i--) dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]); return dp[0]; } // Step 3: only two later values are ever read, so keep two variables. static int robO1(int[] nums) { int next1 = 0, next2 = 0; // dp[i+1], dp[i+2] for (int i = nums.length - 1; i >= 0; i--) { int cur = Math.max(next1, nums[i] + next2); next2 = next1; next1 = cur; } return next1; }Dry run
The figure:
dp[4] = 1,dp[3] = max(1, 3) = 3,dp[2] = max(3, 9 + 1) = 10,dp[1] = max(10, 7 + 3) = 10,dp[0] = max(10, 2 + 10) = 12. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n) time. Memo and table: O(n) space; the final version O(1).
Edge cases
Empty array (0), one house, two houses, all zeros.
Transfer lesson
"Take this and jump, or skip this" is the shape of Delete and Earn (LC 740, after bucketing values), House Robber II (LC 213, run twice excluding the first or the last house), and Solving Questions With Brainpower (LC 2140, the jump length varies). The circular variant shows a general trick: break a cycle by fixing one decision and solving the linear problem for each case.
14.3One-dimensional and linear DP
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
dp[i] = the answer for a prefix (or suffix) of length i, or "ending exactly at i". The transition looks back a fixed number of steps or over all earlier positions.
| Problem | State | Transition |
|---|---|---|
| Climbing Stairs (LC 70) | ways to reach step i | dp[i-1] + dp[i-2] |
| Min Cost Climbing Stairs (LC 746) | cheapest cost to stand on step i | min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]) |
| Decode Ways (LC 91) | ways to decode s[0..i) |
dp[i-1] if the last digit is valid, plus dp[i-2] if the last two are 10..26 |
| Word Break (LC 139) | can s[0..i) be segmented |
OR over j of dp[j] && word(s[j..i)) |
| Minimum Cost For Tickets (LC 983) | cheapest cover of days up to i | min over 1-, 7-, 30-day passes ending today |
// LC 91. dp[i] = ways to decode s[0..i). Last step used one digit or two digits.
static int numDecodings(String s) {
int n = s.length();
int[] dp = new int[n + 1];
dp[0] = 1; // empty prefix: one way (decode nothing)
for (int i = 1; i <= n; i++) {
char one = s.charAt(i - 1);
if (one != '0') dp[i] += dp[i - 1]; // s[i-1] alone is 1..9
if (i >= 2) {
int two = (s.charAt(i - 2) - '0') * 10 + (one - '0');
if (two >= 10 && two <= 26) dp[i] += dp[i - 2]; // "10".."26"; "05" is not valid
}
}
return dp[n];
}
Decode Ways shows how base cases encode meaning: dp[0] = 1 says "there is one way to decode an empty prefix", which makes the first real step add correctly. Zeros are the trap: "06" has no decodings, and "10" only one.
// LC 139. dp[i] = s[0..i) can be segmented. Check every word that could end at i.
static boolean wordBreak(String s, List<String> wordDict) {
Set<String> words = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true; // empty prefix
for (int i = 1; i <= s.length(); i++)
for (int j = 0; j < i && !dp[i]; j++)
if (dp[j] && words.contains(s.substring(j, i))) dp[i] = true;
return dp[s.length()];
}
Common mistakes
- Base case off by one (
dp[0]meaning "empty prefix" vs "first element"). - Forgetting a case in the transition (Decode Ways: a zero can only be the second digit of 10 or 20).
- Using greedy reasoning in a DP problem ("just pick the bigger neighbor").
1D and linear DP
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 198House Robber Take or skip at each house | Medium | High | |
| 213House Robber II Circle = two linear runs excluding one end each | Medium | High | |
| 2466Count Ways To Build Good Strings Climbing stairs with steps of length zero and one | Medium | High | |
| 740Delete and Earn Reduce to House Robber on value buckets | Medium | Medium | |
| 91Decode Ways One-digit and two-digit transitions with zero rules | Medium | High | |
| 139Word Break dp[i] = some dictionary word ends at i with dp[start] true | Medium | High | |
| 790Domino and Tromino Tiling Add a state for a partially filled column | Medium | Medium | |
| 983Minimum Cost For Tickets Decisions look back 1, 7, or 30 days | Medium | Medium | |
| 2140Solving Questions With Brainpower Iterate right to left so jumps point to solved states | Medium | Medium | |
| 132Palindrome Partitioning II Minimum cuts with a palindrome table built first | Hard | Medium |
14.4Grid and path DP
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
dp[r][c] = the answer for reaching (or leaving) cell (r, c). Moves are usually right/down, so each cell depends on its top and left neighbors.
// LC 62. dp[c] in the current row = paths from top (old dp[c]) + from left (dp[c - 1]).
static int uniquePaths(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1); // first row: one way to each cell
for (int r = 1; r < m; r++)
for (int c = 1; c < n; c++)
dp[c] += dp[c - 1]; // dp[c] (top) + dp[c - 1] (left)
return dp[n - 1];
}
Rolling one row. Because row r only reads row r - 1 and the current row, a single 1D array works: before updating, dp[c] still holds the value from the row above, and dp[c - 1] already holds the current row's value to the left.
| Problem | Twist |
|---|---|
| Unique Paths II (LC 63) | Obstacle cells are 0; the first row and column stop at the first obstacle |
| Minimum Path Sum (LC 64) | grid[r][c] + min(top, left) |
| Triangle (LC 120) | Bottom-up removes boundary checks: dp[c] = tri[r][c] + min(dp[c], dp[c+1]) |
| Maximal Square (LC 221) | 1 + min(top, left, diagonal) = side of the largest square ending here |
| Dungeon Game (LC 174) | Fill from the destination backward: the needed health depends on the future, not the past |
| Cherry Pickup II (LC 1463) | Two robots moving down together: state (row, col1, col2) |
Direction matters. Dungeon Game cannot be solved forward because the minimum health needed at a cell depends on what comes after it. When the constraint is about the future, define the state as "from here to the end" and fill backward.
Grid and path DP
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 62Unique Paths Paths into a cell = from top + from left | Medium | High | |
| 63Unique Paths II Obstacles zero out a cell; first row/col care | Medium | High | |
| 64Minimum Path Sum Min instead of sum; roll to one row | Medium | High | |
| 120Triangle Bottom-up removes boundary checks | Medium | Medium | |
| 931Minimum Falling Path Sum Three parents per cell | Medium | Medium | |
| 221Maximal Square Side = 1 + min(top, left, diagonal) | Medium | High | |
| 1277Count Square Submatrices with All Ones Same dp value counts squares ending at a cell | Medium | Medium | |
| 1594Maximum Non Negative Product in a Matrix Keep both max and min per cell because negatives flip | Medium | Medium | |
| 174Dungeon Game Direction matters: compute from the destination backward | Hard | High | |
| 1463Cherry Pickup II Two walkers moving together: state (row, c1, c2) | Hard | High |
14.5The knapsack family: subset sum, partition, coin change
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
dp[c] = the best value (or feasibility, or count) using capacity c, after considering some prefix of the items. Each item is a decision: take it or not.
Go deeper0/1 vs unbounded
| 0/1 knapsack (each item once) | Unbounded (items reusable) | |
|---|---|---|
| Transition | dp[c] = best(dp[c], dp[c - w] + v) using the dp from before this item |
same, using the dp including this item |
| 1D loop over capacity | Downward, so dp[c - w] is still the old value |
Upward, so dp[c - w] may already include this item |
| Problems | Partition Equal Subset Sum, Target Sum, Ones and Zeroes, Last Stone Weight II | Coin Change, Coin Change II, Perfect Squares, Combination Sum IV |
Guided solveLC 416. Partition Equal Subset SumFive questions, each with a reveal
Problem: 416Partition Equal Subset SumMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Can the array be split into two subsets with equal sums?
Input analysis
Up to 200 numbers, each up to 100: the total is at most 20,000, so a table indexed by sums is small.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Two equal halves means one subset sums to
total / 2. "Choose a subset hitting an exact sum" is 0/1 knapsack feasibility.Brute force
All 2^n subsets: hopeless for n = 200.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. find a subset that sums to half the total
Key observation
Whether sum
sis reachable using the first i numbers depends only on(i, s). Sums are bounded by 10,000, so the state space is 200 x 10,000.Visualization
See the downward-iteration figure above.
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
If the total is odd, return false.
reachable[0] = true. For each number x, forsfromtargetdown tox:reachable[s] |= reachable[s - x]. Answerreachable[target].Java implementation
Java// LC 416. 0/1 knapsack: can a subset reach total / 2? // Iterate capacity DOWNWARD so each number is used at most once. static boolean canPartition(int[] nums) { int total = 0; for (int x : nums) total += x; if (total % 2 != 0) return false; int target = total / 2; boolean[] reachable = new boolean[target + 1]; reachable[0] = true; for (int x : nums) for (int s = target; s >= x; s--) // downward: reachable[s - x] is from before x reachable[s] = reachable[s] || reachable[s - x]; return reachable[target]; }Dry run
[1, 5, 11, 5], target 11: after 1: {0, 1}; after 5: {0, 1, 5, 6}; after 11: {0, 1, 5, 6, 11}: done (true). For[1, 2, 5](target 4), an upward loop would reuse the 2 and wrongly mark 4 as reachable; the correct answer is false, and the test harness checks exactly this case. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(n * target) time, O(target) space.
Edge cases
Odd total, a single element larger than the target, all equal elements.
Transfer lesson
Target Sum (LC 494) becomes "count subsets with sum
(total + target) / 2" after algebra on the signs. Last Stone Weight II (LC 1049) becomes "the reachable sum closest tototal / 2". Recognizing a disguised subset sum is the main skill in this family.
Guided solveLC 322. Coin ChangeFive questions, each with a reveal
Problem: 322Coin ChangeMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Fewest coins (unlimited supply of each denomination) that sum to
amount, or -1.Input analysis
Up to 12 coin types, amount up to 10^4.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Minimize a count over choices that repeat: greedy fails for
{1, 3, 4}, so DP over amounts.Brute force
Try every coin as the first choice, recurse on the remainder: exponential, with the same remainders reached many times.
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. the last coin used leaves a smaller amount
Key observation
If the last coin in an optimal solution is
c, the rest is an optimal solution foramount - c. Sodp[a] = 1 + min over coins of dp[a - c].Visualization
Figure 14.5dp[11] = 1 + min(dp[10], dp[9], dp[6]) = 1 + 2 = 3 (5 + 5 + 1). - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
dp[0] = 0, others "infinity" (amount + 1is enough). Forafrom 1 up: try each coin that fits. Answerdp[amount], or -1 if still infinity.Java implementation
Java// LC 322. dp[a] = fewest coins to make amount a. Unbounded: each coin reusable. static int coinChange(int[] coins, int amount) { int INF = amount + 1; // more coins than could ever be needed int[] dp = new int[amount + 1]; Arrays.fill(dp, INF); dp[0] = 0; for (int a = 1; a <= amount; a++) for (int coin : coins) if (coin <= a) dp[a] = Math.min(dp[a], dp[a - coin] + 1); // last coin used = coin return dp[amount] >= INF ? -1 : dp[amount]; } // LC 518. Number of COMBINATIONS: coins outer loop, so each combination is built // in one canonical coin order and counted once. static int change(int amount, int[] coins) { int[] ways = new int[amount + 1]; ways[0] = 1; for (int coin : coins) for (int a = coin; a <= amount; a++) // ascending: the same coin may be reused ways[a] += ways[a - coin]; return ways[amount]; }Dry run
The figure. Note how
dp[3] = 2(2 + 1) anddp[6] = 2(5 + 1) feed later amounts. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(amount * coins) time, O(amount) space.
Edge cases
Amount 0 (0 coins), unreachable amounts (stay at infinity), using
Integer.MAX_VALUEas infinity and then adding 1 (overflow: henceamount + 1).Transfer lesson
Loop order decides what is counted. For the number of combinations (LC 518), put coins in the outer loop: each combination is built in one fixed coin order and counted once. For the number of ordered sequences (LC 377), put the amount in the outer loop: every order is counted. This is the most common silent bug in counting DPs.
Knapsack family: subset sum, partition, coin change
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 322Coin Change Unbounded knapsack for a minimum count | Medium | High | |
| 518Coin Change II Coins in the outer loop count combinations, not orderings | Medium | High | |
| 377Combination Sum IV Target in the outer loop counts orderings | Medium | Medium | |
| 279Perfect Squares Unbounded knapsack with square items | Medium | Medium | |
| 416Partition Equal Subset Sum 0/1 subset sum to total / 2; iterate capacity downward | Medium | High | |
| 494Target Sum Transform signs into a subset-sum count | Medium | High | |
| 1049Last Stone Weight II Disguised partition: minimize the difference | Medium | Medium | |
| 474Ones and Zeroes 0/1 knapsack with two capacities | Medium | Medium | |
| 879Profitable Schemes Capped profit dimension in the state | Hard | High |
14.6Longest increasing subsequence family
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
dp[i] = the best answer for a subsequence ending at index i. The transition looks at every earlier j that can precede i.
// LC 300, O(n^2): dp[i] = length of the longest increasing subsequence ENDING at i.
static int lengthOfLISQuadratic(int[] nums) {
int[] dp = new int[nums.length];
int best = 0;
for (int i = 0; i < nums.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++)
if (nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
best = Math.max(best, dp[i]);
}
return best;
}
// O(n log n): tails[len] = smallest possible tail of an increasing subsequence of length len + 1.
// tails is sorted, so each number finds its slot by binary search.
static int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int size = 0;
for (int x : nums) {
int lo = 0, hi = size; // lower bound of x in tails[0..size)
while (lo < hi) {
int mid = (lo + hi) >>> 1;
if (tails[mid] >= x) hi = mid;
else lo = mid + 1;
}
tails[lo] = x; // replace, or extend when lo == size
if (lo == size) size++;
}
return size; // tails itself is NOT a valid subsequence
}
Go deeperFrom O(n^2) to O(n log n)
Keep tails[k] = the smallest possible last value of an increasing subsequence of length k + 1. tails is always sorted, so each new number finds its position by binary search: it either extends the longest run or lowers some tail, which can only help future extensions.
Go deeperReconstructing the sequence
To return the subsequence itself, record parent[i] = the j that gave dp[i] its value, then walk back from the best ending index. The same idea reconstructs any DP: store the choice that produced each state, or recompute it by comparing neighbors.
// Reconstruct one LIS: remember, for each i, which j it extended (parent[i]).
static List<Integer> lisSequence(int[] nums) {
int n = nums.length;
int[] len = new int[n], parent = new int[n];
int bestEnd = -1;
for (int i = 0; i < n; i++) {
len[i] = 1;
parent[i] = -1;
for (int j = 0; j < i; j++)
if (nums[j] < nums[i] && len[j] + 1 > len[i]) { len[i] = len[j] + 1; parent[i] = j; }
if (bestEnd == -1 || len[i] > len[bestEnd]) bestEnd = i;
}
LinkedList<Integer> seq = new LinkedList<>();
for (int i = bestEnd; i != -1; i = parent[i]) seq.addFirst(nums[i]); // walk back
return seq;
}
| Problem | Twist |
|---|---|
| Number of LIS (LC 673) | Track a count per index alongside the length |
| Largest Divisible Subset (LC 368) | Sort, then "divides" replaces "less than"; reconstruct with parents |
| Longest String Chain (LC 1048) | Sort by length; predecessors found by deleting one character, looked up in a map |
| Russian Doll Envelopes (LC 354) | Sort width ascending and height descending, then LIS on heights in O(n log n) |
| Longest Arithmetic Subsequence (LC 1027) | State (i, difference): one map per index |
Longest increasing subsequence family
8 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 300Longest Increasing Subsequence O(n^2) dp, then patience sorting O(n log n) | Medium | High | |
| 673Number of Longest Increasing Subsequence Track length and count per index | Medium | Medium | |
| 646Maximum Length of Pair Chain LIS on pairs, or greedy by end | Medium | Medium | |
| 368Largest Divisible Subset LIS with divisibility plus reconstruction via parent[] | Medium | Medium | |
| 1048Longest String Chain Sort by length, dp over predecessors in a map | Medium | Medium | |
| 1218Longest Arithmetic Subsequence of Given Difference dp keyed by value in a hash map | Medium | Medium | |
| 1027Longest Arithmetic Subsequence State (index, difference) | Medium | Medium | |
| 376Wiggle Subsequence Two states: last move up or down | Medium | Medium | |
| 354Russian Doll Envelopes Sort width asc, height desc, then LIS on heights | Hard | High |
14.7Two-sequence and string DP
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
dp[i][j] = the answer for the prefixes a[0..i) and b[0..j). Row 0 and column 0 are the empty prefixes. The transition considers the last characters a[i-1] and b[j-1]: match them, or drop one of them.
Guided solveLC 1143. Longest Common SubsequenceFive questions, each with a reveal
Problem: 1143Longest Common SubsequenceMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Length of the longest sequence that appears (not necessarily contiguously) in both strings.
Input analysis
Lengths up to 1000 each: a 1001 x 1001 table (10^6 cells) is fine.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Two sequences compared position by position, with "skip a character" choices: two-sequence DP.
Brute force
All subsequences of one string checked against the other: O(2^m * n).
- 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. compare the last characters of the two prefixes
Key observation
Look at the last characters of the prefixes. If they match, some LCS uses them both, leaving
dp[i-1][j-1]. If not, at least one of them is unused: the answer is the better of droppinga[i-1]or droppingb[j-1].Visualization
Figure 14.7Each cell reads its upper-left (match), upper, and left neighbors. The walk back from the corner reconstructs "ace". - 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
Table of size
(m + 1) x (n + 1), zeros on the borders. Fill row by row with the transition above. Answerdp[m][n].Java implementation
Java// LC 1143. dp[i][j] = LCS of text1[0..i) and text2[0..j). static int longestCommonSubsequence(String a, String b) { int m = a.length(), n = b.length(); int[][] dp = new int[m + 1][n + 1]; // row 0 and col 0 = empty prefix = 0 for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) dp[i][j] = (a.charAt(i - 1) == b.charAt(j - 1)) ? dp[i - 1][j - 1] + 1 // use the matching pair : Math.max(dp[i - 1][j], dp[i][j - 1]); // drop one character return dp[m][n]; } // Reconstruction: walk back from dp[m][n] following the choice that produced each cell. static String lcsString(String a, String b) { int m = a.length(), n = b.length(); int[][] dp = new int[m + 1][n + 1]; for (int i = 1; i <= m; i++) for (int j = 1; j <= n; j++) dp[i][j] = (a.charAt(i - 1) == b.charAt(j - 1)) ? dp[i - 1][j - 1] + 1 : Math.max(dp[i - 1][j], dp[i][j - 1]); StringBuilder sb = new StringBuilder(); int i = m, j = n; while (i > 0 && j > 0) { if (a.charAt(i - 1) == b.charAt(j - 1)) { sb.append(a.charAt(i - 1)); i--; j--; } else if (dp[i - 1][j] >= dp[i][j - 1]) i--; else j--; } return sb.reverse().toString(); }Dry run
In the figure, row
cand columncmeet with a match:dp = dp[upper-left] + 1 = 2. The walk back moves diagonally on matches (green) and toward the larger neighbor otherwise. - 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(m * n) time and space. Space can drop to O(min(m, n)) with two rows (or one row plus a saved diagonal), but then reconstruction needs the full table.
Edge cases
An empty string (0), identical strings (their length), no common characters (0).
Transfer lesson
Delete Operation for Two Strings (LC 583) is
m + n - 2 * LCS. Shortest Common Supersequence (LC 1092) is the LCS walk-back that also emits the unmatched characters. Longest Palindromic Subsequence (LC 516) is LCS of a string with its reverse, or an interval DP on(i, j).
Go deeperEdit distance: three moves, three neighbors
// LC 72. dp[i][j] = edits to turn word1[0..i) into word2[0..j).
static int minDistance(String w1, String w2) {
int m = w1.length(), n = w2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) dp[i][0] = i; // delete all i characters
for (int j = 0; j <= n; j++) dp[0][j] = j; // insert all j characters
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++) {
if (w1.charAt(i - 1) == w2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1];
else dp[i][j] = 1 + Math.min(dp[i - 1][j - 1], // replace
Math.min(dp[i - 1][j], // delete from w1
dp[i][j - 1])); // insert into w1
}
return dp[m][n];
}
With dp[i][j] = edits to turn w1[0..i) into w2[0..j): if the last characters match, no edit is needed (dp[i-1][j-1]). Otherwise the last operation was a replace (dp[i-1][j-1] + 1), a delete from w1 (dp[i-1][j] + 1), or an insert into w1 (dp[i][j-1] + 1). The base cases are the cost of deleting or inserting everything.
Two-sequence and string DP
6 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 1143Longest Common Subsequence dp[i][j] over prefixes; match vs skip one side | Medium | High | |
| 583Delete Operation for Two Strings Answer = m + n - 2 * LCS | Medium | Medium | |
| 712Minimum ASCII Delete Sum for Two Strings LCS skeleton with weighted costs | Medium | Medium | |
| 72Edit Distance Insert, delete, replace map to three neighbor cells | Medium | High | |
| 97Interleaving String dp[i][j]: can prefixes build the first i + j chars | Medium | Medium | |
| 516Longest Palindromic Subsequence Interval dp on (i, j) or LCS with the reverse | Medium | High | |
| 115Distinct Subsequences Counting DP: use or skip a matching character | Hard | High | |
| 1092Shortest Common Supersequence Reconstruct the answer by walking the LCS table back | Hard | High |
14.8Multiple state variables: state machines and intervals
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
When one index is not enough. If two partial solutions with the same index can have different best futures, the state is missing information. Add the missing fact as another dimension:
| Missing fact | Extra state | Example |
|---|---|---|
| Am I holding a stock? In cooldown? | a small enum of modes | Stock problems (LC 122, 309, 714, 123) |
| How many transactions or operations are left? | a count k |
Stock III and IV, Ones and Zeroes |
| Where are both walkers? | a second position | Cherry Pickup II |
| Which items are used (small n) | a bitmask | Assignment problems with n <= 15 |
| Whose turn is it, or what is the current limit? | turn or parameter | Stone Game II (index, M) |
Go deeperState machine DP
Draw the states as nodes and the allowed daily actions as arrows. Each arrow becomes one term in the transition.
// LC 309. Three states at the end of each day:
// hold = own a share, sold = sold today (cooldown tomorrow), rest = free to buy.
static int maxProfitCooldown(int[] prices) {
int hold = Integer.MIN_VALUE / 2, sold = 0, rest = 0; // MIN/2: safe to add to
for (int p : prices) {
int prevHold = hold, prevSold = sold, prevRest = rest;
hold = Math.max(prevHold, prevRest - p); // keep holding, or buy (only from rest)
sold = prevHold + p; // sell what we held
rest = Math.max(prevRest, prevSold); // stay free, or finish the cooldown
}
return Math.max(sold, rest);
}
Each day reads the previous day's three values (saved in prev* variables first, because all three updates must use yesterday's values). With a transaction fee (LC 714), subtract it on one transition; with at most k transactions, index the states by transactions used.
Go deeperInterval DP
dp[l][r] = the answer for the subarray or substring between l and r. Fill by increasing interval length, and choose a split point k inside the interval.
// LC 312. Interval DP. dp[l][r] = best coins from bursting every balloon strictly between l and r.
// Choose k = the LAST balloon burst in (l, r): its neighbors at that moment are l and r.
static int maxCoins(int[] nums) {
int n = nums.length;
int[] a = new int[n + 2];
a[0] = a[n + 1] = 1; // virtual balloons with value 1
for (int i = 0; i < n; i++) a[i + 1] = nums[i];
int[][] dp = new int[n + 2][n + 2];
for (int len = 2; len <= n + 1; len++) // shorter intervals first
for (int l = 0; l + len <= n + 1; l++) {
int r = l + len;
for (int k = l + 1; k < r; k++)
dp[l][r] = Math.max(dp[l][r], dp[l][k] + a[l] * a[k] * a[r] + dp[k][r]);
}
return dp[0][n + 1];
}
The key decision in Burst Balloons (LC 312) is choosing k as the last balloon burst in (l, r), not the first. If k is last, its neighbors at that moment are exactly l and r, and the two sides are independent subproblems. Choosing the first balloon would leave sides whose neighbors keep changing. When an interval problem seems to have dependent sides, try "last action" instead of "first action".
Multi-state, state-machine, and interval DP
8 Medium 2 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 122Best Time to Buy and Sell Stock II Two states: holding or not holding | Medium | High | |
| 309Best Time to Buy and Sell Stock with Cooldown Add a cooldown state and draw the state machine | Medium | High | |
| 714Best Time to Buy and Sell Stock with Transaction Fee Fee on one transition only | Medium | Medium | |
| 1911Maximum Alternating Subsequence Sum Even/odd position states | Medium | Medium | |
| 935Knight Dialer State = (digit, steps left); transitions are knight moves | Medium | Medium | |
| 1155Number of Dice Rolls With Target Sum State (dice used, sum) with modulo counting | Medium | Medium | |
| 1140Stone Game II Game DP with state (index, M) | Medium | Medium | |
| 1039Minimum Score Triangulation of Polygon Interval DP: choose the splitting vertex | Medium | Medium | |
| 123Best Time to Buy and Sell Stock III Transaction count becomes part of the state | Hard | High | |
| 312Burst Balloons Interval DP: pick the last balloon to burst | Hard | High |
14.9DP vs greedy vs backtracking vs plain recursion
Pick DP, greedy, or backtracking: (a) list every subset that sums to 10; (b) count the subsets that sum to 10; (c) the largest number of non-overlapping meetings.
Reveal the answer
(a) Backtracking: you must output each subset, so the cost is at least the size of the output. (b) DP: the count for (items used, remaining sum) repeats across paths. (c) Greedy: sort by end time and take each meeting that fits.
| Situation | Use | Why |
|---|---|---|
| Subproblems repeat, and you need a count, best value, or yes/no | DP | Each state is solved once |
| Subproblems do not repeat (tree traversal, divide and conquer on disjoint parts) | Plain recursion | Nothing to cache |
| You must list every solution | Backtracking | Output size is exponential anyway |
| A local choice is provably safe | Greedy | No need to compare alternatives |
| States form a graph with costs, and transitions can go in cycles | Shortest path (BFS / Dijkstra) | DP needs an order without cycles |
Common mistakes across all DP
- A state that does not capture everything the future depends on.
- Wrong loop order: reading a cell before it is computed, or reading an updated cell that should still be old (0/1 knapsack).
- Initializing minimization tables with 0 instead of "infinity", or using
Integer.MAX_VALUEand then adding to it. - Forgetting the modulo on every addition in counting problems (and using
longfor intermediate products). - Optimizing space too early, which makes reconstruction impossible and bugs harder to see.
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 15: Bit manipulation
A handful of bit operations, used precisely, replace extra memory or loops: canceling pairs with XOR, counting set bits, testing powers of two, and representing small sets as integers.
15.1Binary representation and the operators
Without running it: what are 6 & 3, 6 | 3, 6 ^ 3, and 6 << 2?
Reveal the answer
6 is 110 and 3 is 011. AND = 010 = 2, OR = 111 = 7, XOR = 101 = 5, and 6 << 2 = 24 (multiply by 2^2).
Problems about parity, pairs that cancel, subsets of a small set, flags, and arithmetic without extra space.
An integer is a row of 32 light switches. AND keeps a switch on only if both rows have it on, OR turns it on if either does, XOR turns it on if exactly one does, NOT flips every switch, and shifts slide the whole row.
- Signal
- 'Without extra space', pairs canceling out, powers of two, subsets of a small set, flags, XOR in the statement.
- Required condition
- Values fit in a fixed width (32 or 64 bits); operations act on each bit independently.
Mental model, core operation, limits, and what to use instead
- Mental model
- A row of light switches: AND keeps, OR sets, XOR flips.
- Core operation
- Masks with 1 << i; x & (x - 1) clears the lowest set bit; XOR cancels pairs.
- Complexity benefit
- O(1) extra space and constant-time set operations on up to 64 items.
- Limitations
- Readability; Java's signed ints and shift rules (>> vs >>>, shift mod 32).
- Alternative
- HashSet or boolean arrays when clarity matters more than space.
| Operator | Java | Bit rule | Typical use |
|---|---|---|---|
| AND | a & b |
1 if both are 1 | Test or clear bits (masking) |
| OR | a | b |
1 if either is 1 | Set bits |
| XOR | a ^ b |
1 if they differ | Toggle bits, cancel pairs |
| NOT | ~a |
Flip every bit | ~a == -a - 1 in two's complement |
| Left shift | a << k |
Multiply by 2^k (until overflow) | Build masks 1 << i |
| Signed right shift | a >> k |
Divide by 2^k, rounding toward negative infinity; copies the sign bit | Halving signed values |
| Unsigned right shift | a >>> k |
Shift in zeros | Treating int as unsigned; safe midpoints (lo + hi) >>> 1 |
Two's complement. Java int is 32 bits; negative numbers are stored as ~x + 1. So -1 is all ones, Integer.MIN_VALUE is 1 followed by 31 zeros, and -x equals ~x + 1. This is why x & -x isolates the lowest set bit.
15.2Checking, setting, clearing, toggling
x = 11 (1011 in binary). Write the expression and the result to: check bit 2, set bit 2, clear bit 0, toggle bit 3.
Reveal the answer
Check: (x >> 2) & 1 = 0. Set: x | (1 << 2) = 1111 = 15. Clear: x & ~1 = 1010 = 10. Toggle: x ^ (1 << 3) = 0011 = 3.
static boolean isSet(int x, int i) { return (x & (1 << i)) != 0; } // check bit i
static int setBit(int x, int i) { return x | (1 << i); } // force bit i to 1
static int clearBit(int x, int i) { return x & ~(1 << i); } // force bit i to 0
static int toggleBit(int x, int i) { return x ^ (1 << i); } // flip bit i
static int lowestSetBit(int x) { return x & -x; } // e.g. 12 (1100) -> 4 (0100)
static int clearLowestSetBit(int x) { return x & (x - 1); } // 12 (1100) -> 8 (1000)
static boolean isPowerOfTwo(int n) { return n > 0 && (n & (n - 1)) == 0; }
static void javaPitfalls() {
int neg = -8; // 11111111 11111111 11111111 11111000
int arith = neg >> 1; // -4: sign bit is copied in
int logical = neg >>> 1; // 2147483644: zeros shifted in
int wrap = 1 << 32; // 1, not 0: int shift distance is taken mod 32
long big = 1L << 40; // use a long literal for shifts >= 31
boolean wrong = (5 & 1) == 1; // parenthesize: == binds tighter than &
if (arith != -4 || logical != 2147483644 || wrap != 1 || big != 1099511627776L || !wrong)
throw new AssertionError();
// Integer.bitCount(x), Integer.toBinaryString(x), Integer.highestOneBit(x),
// Integer.numberOfTrailingZeros(x) are built in and O(1).
}
Counting set bits. x & (x - 1) removes one set bit per iteration, so the loop runs once per set bit (Brian Kernighan's method). Integer.bitCount(x) does it in O(1). Counting Bits (LC 338) for all numbers up to n uses DP: bits[i] = bits[i >> 1] + (i & 1).
Power of two. A positive power of two has exactly one set bit: n > 0 && (n & (n - 1)) == 0. The n > 0 check excludes 0 and Integer.MIN_VALUE.
15.3XOR patterns
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
XOR has three properties that do all the work: x ^ x = 0, x ^ 0 = x, and it is commutative and associative (order does not matter).
// LC 136. Pairs cancel: a ^ a = 0, and XOR is order-independent.
static int singleNumber(int[] nums) {
int x = 0;
for (int v : nums) x ^= v;
return x;
}
// LC 191. Each n & (n - 1) removes one set bit: loop runs popcount times.
static int hammingWeight(int n) {
int count = 0;
while (n != 0) { // != 0, not > 0: works for negative (unsigned) inputs
n &= n - 1;
count++;
}
return count;
}
// LC 260. a ^ b != 0 has some set bit; that bit splits a and b into different groups.
static int[] singleNumberIII(int[] nums) {
int xorAll = 0;
for (int v : nums) xorAll ^= v;
int diffBit = xorAll & -xorAll;
int a = 0, b = 0;
for (int v : nums) {
if ((v & diffBit) != 0) a ^= v;
else b ^= v;
}
return new int[]{Math.min(a, b), Math.max(a, b)};
}
| Problem | XOR idea |
|---|---|
| Single Number (LC 136) | XOR everything; pairs cancel |
| Missing Number (LC 268) | XOR all indices 0..n with all values; the missing one remains |
| Single Number III (LC 260) | xorAll = a ^ b has a set bit where a and b differ; split by that bit and XOR each group |
| Single Number II (LC 137) | XOR does not cancel triples; count each bit position modulo 3 instead |
| Count Triplets with Equal XOR (LC 1442) | prefix[i] == prefix[k + 1] means the XOR of i..k is 0: prefix XOR plus counting |
| Sum of Two Integers (LC 371) | a ^ b is the sum without carries; (a & b) << 1 is the carry; repeat until no carry |
15.4Bitmasks as small sets
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
An int can represent a subset of up to 31 items (a long, 63): bit i set means item i is in the set. Union is |, intersection &, membership (mask >> i) & 1, and enumerating all subsets is a loop from 0 to (1 << n) - 1.
// Enumerate all subsets of n items: mask bit i = 1 means item i is chosen.
static List<List<Integer>> subsetsByMask(int[] nums) {
int n = nums.length;
List<List<Integer>> all = new ArrayList<>();
for (int mask = 0; mask < (1 << n); mask++) { // 2^n masks; fine for n <= ~20
List<Integer> subset = new ArrayList<>();
for (int i = 0; i < n; i++)
if ((mask & (1 << i)) != 0) subset.add(nums[i]);
all.add(subset);
}
return all;
}
Use bitmasks when n is at most about 20 and you need to store or compare subsets compactly: DP over subsets (assignment problems), or carrying a set of seen characters or parities along a path (Pseudo-Palindromic Paths, LC 1457: toggle bit val at each node, and a path can form a palindrome if at most one bit is set at the leaf).
15.5Java-specific pitfalls
In Java, what are -8 >> 1, -8 >>> 1, and 1 << 32?
Reveal the answer
-8 >> 1 = -4 (the sign bit is copied in). -8 >>> 1 = 2147483644 (a zero is shifted in, so the result is a large positive number). 1 << 32 = 1, because the shift distance of an int is taken modulo 32; use 1L << k for large k.
- Precedence:
==binds tighter than&,|,^. Write(x & 1) == 1, neverx & 1 == 1(which does not compile for ints, but does odd things with booleans). - Shift distance is taken modulo 32 for
int(modulo 64 forlong):1 << 32 == 1. Use1L << kwhen k can be 31 or more. >>vs>>>on negative numbers:-8 >> 1 == -4, but-8 >>> 1 == 2147483644.- Loops on negative inputs:
while (n > 0)never runs for a negative n treated as unsigned (LC 191); usewhile (n != 0). - Overflow in shifts:
1 << 31isInteger.MIN_VALUE, a negative number. - Built-ins:
Integer.bitCount,Integer.highestOneBit,Integer.numberOfTrailingZeros,Integer.toBinaryString(useful for debugging).
Bit manipulation
10 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 371Sum of Two Integers XOR is sum without carry; AND << 1 is the carry | Medium | Medium | |
| 137Single Number II Count each bit position modulo 3 | Medium | Medium | |
| 260Single Number III Split the group by any set bit of a ^ b | Medium | Medium | |
| 201Bitwise AND of Numbers Range Common prefix of left and right | Medium | Medium | |
| 1318Minimum Flips to Make a OR b Equal to c Reason bit by bit independently | Medium | Medium | |
| 477Total Hamming Distance Count ones per bit position: pairs = ones * zeros | Medium | High | |
| 1310XOR Queries of a Subarray Prefix XOR answers any range in O(1) | Medium | High | |
| 1442Count Triplets That Can Form Two Arrays of Equal XOR Prefix XOR equality means a zero-XOR range | Medium | Medium | |
| 2401Longest Nice Subarray Sliding window with an OR mask of used bits | Medium | Medium | |
| 1239Maximum Length of a Concatenated String with Unique Characters Represent each word as a bitmask; skip overlapping masks | Medium | Medium |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 16: Essential mathematics
Interview math is a short list: GCD and LCM, primes, modular arithmetic, fast exponentiation, overflow-safe arithmetic, and a little counting. Each one has a standard implementation worth knowing exactly.
- Signal
- Divisibility, primes, 'answer modulo 10^9 + 7', huge exponents, counting arrangements.
- Required condition
- Integer arithmetic with known identities (Euclid, Fermat, Pascal).
Mental model, core operation, limits, and what to use instead
- Mental model
- Clock arithmetic: after 12 comes 1; only the remainder matters.
- Core operation
- gcd via Euclid, sieve for many primes, fast power by squaring, reduce modulo after each operation.
- Complexity benefit
- O(log n) gcd and power instead of O(n); sieve gives all primes up to n in O(n log log n).
- Limitations
- Overflow before the modulo; negative remainders in Java; division under a modulus needs an inverse.
- Alternative
- BigInteger for exact big values (slow); long arithmetic with careful ordering.
16.1GCD and LCM
Compute gcd(84, 36) with the Euclidean algorithm, one step per line. Then compute lcm(84, 36).
Reveal the answer
gcd(84, 36) = gcd(36, 12) = gcd(12, 0) = 12. lcm = 84 / 12 * 36 = 252. Divide before multiplying to keep the intermediate small.
Divisibility relationships: simplifying ratios, grouping by common periods, repeating patterns (GCD of Strings, LC 1071), counting multiples (LCM inside inclusion-exclusion).
Euclid's algorithm. gcd(a, b) = gcd(b, a mod b), with gcd(a, 0) = a. Every two steps the larger number at least halves, so it runs in O(log min(a, b)).
// Euclid: gcd(a, b) = gcd(b, a mod b). O(log min(a, b)).
static long gcd(long a, long b) {
while (b != 0) {
long t = a % b;
a = b;
b = t;
}
return Math.abs(a);
}
// Divide BEFORE multiplying so the intermediate value stays small.
static long lcm(long a, long b) {
return a / gcd(a, b) * b;
}
lcm(a, b) = a / gcd(a, b) * b: divide first, or a * b can overflow before the division.
16.2Primes and the Sieve of Eratosthenes
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
Checking one number. A composite n has a divisor at most sqrt(n), so trial division up to sqrt(n) is O(sqrt n). Loop with d * d <= n in long (or d <= n / d) to avoid overflow.
All primes below n. The sieve crosses out multiples of each prime, starting at p * p (smaller multiples were already crossed out by smaller primes). Total work is O(n log log n), effectively linear.
// Trial division up to sqrt(n): a composite n has a factor <= sqrt(n).
static boolean isPrime(int n) {
if (n < 2) return false;
for (long d = 2; d * d <= n; d++) // long: d * d can overflow int
if (n % d == 0) return false;
return true;
}
// LC 204. Sieve of Eratosthenes: O(n log log n). Count primes strictly less than n.
static int countPrimes(int n) {
if (n < 3) return 0;
boolean[] composite = new boolean[n];
int count = 0;
for (int i = 2; i < n; i++) {
if (composite[i]) continue;
count++;
for (long j = (long) i * i; j < n; j += i) // smaller multiples were crossed already
composite[(int) j] = true;
}
return count;
}
n = 20 p = 2: cross 4, 6, 8, 10, 12, 14, 16, 18 p = 3: cross 9, 15 (6, 12, 18 already crossed) p = 5: 25 > 20, stop crossing primes: 2 3 5 7 11 13 17 19
16.3Modular arithmetic
In Java, what is -7 % 3? How do you get a remainder in the range 0 to 2 instead?
Reveal the answer
-1: Java's % takes the sign of the left operand. Use ((a % m) + m) % m, which gives 2 here, or Math.floorMod(a, m).
Counting problems whose answers are huge ("return the answer modulo 10^9 + 7"). Reduce after every operation so values stay small.
Rules. (a + b) mod m = ((a mod m) + (b mod m)) mod m, and the same for subtraction and multiplication. Division does not work this way; it needs a modular inverse (for a prime m, a^(m - 2) mod m by Fermat's little theorem), which is rarely needed in interviews.
static final int MOD = 1_000_000_007;
static void modularRules() {
long a = 999_999_999L, b = 888_888_888L;
long sum = (a + b) % MOD; // (a + b) mod m
long product = (a % MOD) * (b % MOD) % MOD; // operands < 2^30, product < 2^60: fits in long
long diff = ((a - b) % MOD + MOD) % MOD; // Java % can be negative: add MOD back
int negMod = -7 % 3; // -1 in Java, not 2
int fixed = Math.floorMod(-7, 3); // 2
if (sum < 0 || product < 0 || diff < 0 || negMod != -1 || fixed != 2) throw new AssertionError();
}
// LC 7. Check for overflow BEFORE the multiply-and-add.
static int reverse(int x) {
int result = 0;
while (x != 0) {
int digit = x % 10; // negative for negative x
if (result > Integer.MAX_VALUE / 10 || result < Integer.MIN_VALUE / 10) return 0;
result = result * 10 + digit;
x /= 10;
}
return result;
}
Why 10^9 + 7: it is prime, and any two residues below it multiply to less than about 10^18, which fits in a long (max about 9.2 x 10^18). Two int-sized residues multiplied as int overflow; always multiply in long.
16.4Fast exponentiation
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
x^n for huge n (Pow(x, n), LC 50; Count Good Numbers, LC 1922) in O(log n) multiplications instead of n.
Idea. Write n in binary. x^13 = x^8 * x^4 * x^1 because 13 = 1101. Square the base each step; multiply it into the result when the current bit of the exponent is 1.
// Fast exponentiation: square the base, use the bits of the exponent. O(log exp).
static long modPow(long base, long exp, long mod) {
long result = 1;
base %= mod;
while (exp > 0) {
if ((exp & 1) == 1) result = result * base % mod; // this bit contributes base^(2^k)
base = base * base % mod;
exp >>= 1;
}
return result;
}
// LC 50. Handle negative n with a long: -Integer.MIN_VALUE does not fit in an int.
static double myPow(double x, int n) {
long e = n;
if (e < 0) { x = 1 / x; e = -e; }
double result = 1;
while (e > 0) {
if ((e & 1) == 1) result *= x;
x *= x;
e >>= 1;
}
return result;
}
The myPow version shows two classic edge cases: a negative exponent becomes 1 / x with a positive exponent, and -Integer.MIN_VALUE does not fit in an int, so the exponent is widened to long before negating.
16.5Safe arithmetic and integer overflow
int a = 50000, b = 50000; long c = a * b; What is c?
Reveal the answer
-1,794,967,296. a * b is computed in int and overflows before it is widened to long. Write (long) a * b.
| Danger | Example | Fix |
|---|---|---|
| Sum of many ints | 10^5 values of 10^9 | Accumulate in long |
| Product of two ints | a * b with a, b up to 10^5 |
(long) a * b (cast before multiplying) |
| Midpoint | (lo + hi) / 2 near 2^31 |
lo + (hi - lo) / 2 |
| Negation | -Integer.MIN_VALUE is still negative |
Widen to long, or handle separately |
| Comparator subtraction | a - b |
Integer.compare(a, b) |
| Building a number digit by digit | result * 10 + d |
Check result > (MAX - d) / 10 first |
| Absolute value | Math.abs(Integer.MIN_VALUE) is negative |
Widen first |
Math.addExact, Math.multiplyExact, and Math.toIntExact throw on overflow, which is useful while debugging.
16.6Basic combinatorics
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
| Question | Formula | Example |
|---|---|---|
| Ordered arrangements of n distinct items | n! | Permutations |
| Choose k of n, order irrelevant | C(n, k) = n! / (k! (n - k)!) | Unique Paths: C(m + n - 2, m - 1) |
| Distribute n identical items into k groups (stars and bars) | C(n + k - 1, k - 1) | Count Sorted Vowel Strings (LC 1641): C(n + 4, 4) |
| Pairs among n items | n (n - 1) / 2 | Counting unreachable pairs, equal pairs |
| Count with an exclusion | inclusion-exclusion: |A or B| = |A| + |B| - |A and B| | Ugly Number III (LC 1201): multiples of a, b, or c up to x |
// nCr with Pascal's rule C(n, r) = C(n-1, r-1) + C(n-1, r), modulo MOD.
static long[][] pascal(int n) {
long[][] c = new long[n + 1][n + 1];
for (int i = 0; i <= n; i++) {
c[i][0] = 1;
for (int r = 1; r <= i; r++) c[i][r] = (c[i - 1][r - 1] + c[i - 1][r]) % MOD;
}
return c;
}
// Exact small nCr without overflow-prone factorials: multiply and divide alternately.
static long nCr(int n, int r) {
r = Math.min(r, n - r);
long result = 1;
for (int i = 1; i <= r; i++) result = result * (n - r + i) / i; // always an integer
return result;
}
Computing C(n, k) by multiplying and dividing alternately keeps every intermediate value an exact integer and avoids computing huge factorials. For many binomials modulo a prime, build Pascal's triangle (O(n^2)) or precompute factorials and inverse factorials.
Factorial trailing zeros (LC 172) is a counting question in disguise: each zero needs a factor 10 = 2 x 5, and 2s are plentiful, so count factors of 5: n/5 + n/25 + n/125 + ....
Essential mathematics
10 Medium 0 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 2447Number of Subarrays With GCD Equal to K Running gcd over subarrays; stop when it drops below k | Medium | Medium | |
| 365Water and Jug Problem Reachable amounts are multiples of gcd(x, y) | Medium | Medium | |
| 204Count Primes Sieve of Eratosthenes starting at i * i | Medium | High | |
| 2523Closest Prime Numbers in Range Sieve once, then scan neighbors | Medium | Medium | |
| 50Pow(x, n) Fast exponentiation; n = Integer.MIN_VALUE edge case | Medium | High | |
| 1922Count Good Numbers Modular fast power on long values | Medium | Medium | |
| 7Reverse Integer Detect overflow before multiplying by 10 | Medium | Medium | |
| 172Factorial Trailing Zeroes Count factors of 5, not the factorial | Medium | Medium | |
| 1641Count Sorted Vowel Strings Stars and bars: C(n + 4, 4) | Medium | Medium | |
| 1201Ugly Number III Inclusion-exclusion with LCM inside binary search | Medium | Medium |
Mastery checklist
Tick an item only when it is true with your notes closed.
Module 17: Data structure design
Design questions give you a list of operations and a time limit for each. No single structure meets every limit, so you combine two or three, and keep them consistent on every update. This module does not reteach the structures; it shows how they combine.
17.1How to design from a list of operations
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
- Signal
- 'Design a class' with required time limits per operation: get/put in O(1), getRandom in O(1), min in O(1).
- Required condition
- List every operation and its target complexity first; each operation needs some structure that supports it.
Mental model, core operation, limits, and what to use instead
- Mental model
- A kitchen with a pantry (lookup), a conveyor (order), and a scoreboard (extremes): each station does one job fast.
- Core operation
- Combine structures so each operation is fast in at least one of them, and keep them consistent on every update.
- Complexity benefit
- Meets per-operation bounds that no single structure can.
- Limitations
- Every write must update all structures; inconsistent updates are the main bug.
- Alternative
- Built-ins: LinkedHashMap (LRU), TreeMap (ordered), PriorityQueue with lazy deletion.
Follow these steps before writing a class:
- List every operation and its required complexity. Write them as a table. "get in O(1), put in O(1), evict the least recently used in O(1)."
- For each operation, name the structure that makes it fast. Lookup by key: hash map. Order by time: linked list or queue. Current min/max: heap or a stack of running mins. Random element: array. Sorted access: TreeMap.
- Decide how the structures point at each other. Usually the map stores a reference or index into the other structure, so finding and then moving or deleting an element are both O(1).
- Walk through every operation and list every structure it must update. Inconsistency between structures is the main source of bugs.
- Handle the edge cases explicitly: empty structure, capacity 1, updating an existing key, deleting the last element.
| Required operation | Structure that gives it |
|---|---|
| Find by key in O(1) | HashMap |
| Remove an element you already hold, in O(1) | Doubly linked list node (or swap-with-last in an array) |
| Maintain insertion or usage order | Linked list, deque, LinkedHashMap |
| Uniform random element in O(1) | ArrayList (index access) |
| Current minimum or maximum | Heap (with lazy deletion), or running min stored per stack entry |
| Smallest key >= x, ordered iteration | TreeMap / TreeSet |
| Value at a past version or time | Per-key list of (time, value) + binary search |
17.2Min Stack: carry the answer with each element
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
push, pop, top, getMin, all O(1). A single "current min" variable fails on pop: after removing the minimum, you cannot recover the previous one. Store the minimum as of each push alongside the value. Popping restores the previous minimum automatically.
// LC 155. Each entry remembers the minimum of everything at or below it.
static class MinStack {
private final Deque<int[]> stack = new ArrayDeque<>(); // {value, minSoFar}
void push(int val) {
int min = stack.isEmpty() ? val : Math.min(val, stack.peek()[1]);
stack.push(new int[]{val, min});
}
void pop() { stack.pop(); }
int top() { return stack.peek()[0]; }
int getMin() { return stack.peek()[1]; }
}
The same "store the aggregate with each element" idea gives a max stack, or a stack that answers "sum of everything below".
17.3Queue from stacks, stack from queues
Build a queue from two stacks, in and out. Push goes to in. How does pop work, and what does it cost on average over many operations?
Reveal the answer
Pop from out; when out is empty, move everything from in to out first, which reverses the order. Each element is moved at most once in its lifetime, so pop is O(1) amortized even though a single pop can cost O(n).
Queue from two stacks. Push into in. To pop, take from out; when out is empty, pour all of in into it, which reverses the order so the oldest element is on top. Each element is moved at most once in its lifetime, so the amortized cost per operation is O(1) even though a single pop can cost O(n).
Stack from one queue. After each push, rotate the older elements behind the new one so it reaches the front: O(n) push, O(1) pop.
// LC 232. Push to 'in'. Pop from 'out'; refill 'out' only when it is empty.
// Each element moves at most once: amortized O(1) per operation.
static class MyQueue {
private final Deque<Integer> in = new ArrayDeque<>(), out = new ArrayDeque<>();
void push(int x) { in.push(x); }
int pop() { peek(); return out.pop(); }
int peek() {
if (out.isEmpty())
while (!in.isEmpty()) out.push(in.pop()); // reverses order: oldest on top
return out.peek();
}
boolean empty() { return in.isEmpty() && out.isEmpty(); }
}
// LC 225. After pushing x, rotate the older elements behind it: x is now at the front.
static class MyStack {
private final Deque<Integer> q = new ArrayDeque<>();
void push(int x) {
q.offer(x);
for (int i = 0; i < q.size() - 1; i++) q.offer(q.poll()); // O(n) push
}
int pop() { return q.poll(); }
int top() { return q.peek(); }
boolean empty() { return q.isEmpty(); }
}
These two show the trade-off you should state in an interview: amortized O(1) with occasional O(n) spikes, or a consistent O(n) on one operation.
17.4LRU cache: HashMap + doubly linked list
Stuck after 15 minutes? The guided solve in this lesson walks through this exact problem, one question at a time.
Show a hint
get(key) and put(key, value) in O(1); when full, evict the least recently used key.
| Need | Structure |
|---|---|
| Find a key's entry in O(1) | HashMap<key, Node> |
| Move an entry to "most recent" in O(1) | Doubly linked list (unlink and relink a node) |
| Find and remove the least recent in O(1) | The node at the tail end of the list |
| Remove that key from the map | The node stores its own key |
Guided solveLC 146. LRU CacheFive questions, each with a reveal
Problem: 146LRU CacheMedium. Answer each question on paper first, then reveal.
- 1Understand the problem
Say in one sentence what is asked. Which constraint sets the time budget?
Reveal step 1
Problem decoding
Fixed-capacity key-value cache.
getreturns the value or -1 and marks the key as used.putinserts or updates and marks it used; if the cache is over capacity, evict the least recently used key.Input analysis
Up to 2 * 10^5 calls; O(1) per call is required.
- 2Brute force, then a pattern
What is the simplest correct approach and what does it cost? Which pattern could beat it, and why does it apply?
Reveal step 2
Pattern discovery
Two requirements pull in different directions: lookup by key (hash map) and order by recency with O(1) moves (linked list). The design is their combination.
Brute force
A list ordered by recency:
getis O(n) to find. A map with timestamps: eviction is O(n) to find the oldest. - 3The key observation
What single fact removes the repeated work? Trace a tiny input on paper until you see it.
Reveal step 3
Key idea. the map finds nodes, the list orders them
Key observation
If the map's value is the list node itself, finding a node is O(1) via the map, and moving or deleting it is O(1) via its
prevandnextlinks. A singly linked list is not enough, because unlinking needs the predecessor.Visualization
capacity 2 put(1,1) list: head <-> 1 <-> tail put(2,2) list: head <-> 2 <-> 1 <-> tail get(1) -> 1 list: head <-> 1 <-> 2 <-> tail (1 moved to the front) put(3,3) full: evict tail.prev = 2 list: head <-> 3 <-> 1 <-> tail get(2) -> -1
- 4Build it
Write the algorithm in three or four lines, then code it. Reveal after you have run one small input by hand.
Reveal step 4
Algorithm
get: look up the node; if present, unlink it, add it after head, return its value.put: if the key exists, update the value and move it to the front. Otherwise, if full, unlinktail.prevand remove its key from the map; then create a node, map it, and add it to the front.Java implementation
Java// LC 146. get/put in O(1). // HashMap: key -> node (find in O(1)). Doubly linked list: recency order (move/remove in O(1)). // Most recent sits right after head; least recent right before tail. static class LRUCache { private static class Node { int key, value; Node prev, next; Node(int key, int value) { this.key = key; this.value = value; } } private final int capacity; private final Map<Integer, Node> map = new HashMap<>(); private final Node head = new Node(0, 0), tail = new Node(0, 0); // sentinels LRUCache(int capacity) { this.capacity = capacity; head.next = tail; tail.prev = head; } int get(int key) { Node node = map.get(key); if (node == null) return -1; unlink(node); addFront(node); // used -> most recent return node.value; } void put(int key, int value) { Node node = map.get(key); if (node != null) { // update existing node.value = value; unlink(node); addFront(node); return; } if (map.size() == capacity) { // evict least recent Node lru = tail.prev; unlink(lru); map.remove(lru.key); // the node stores its key for exactly this } Node fresh = new Node(key, value); map.put(key, fresh); addFront(fresh); } private void unlink(Node n) { n.prev.next = n.next; n.next.prev = n.prev; } private void addFront(Node n) { n.next = head.next; n.prev = head; head.next.prev = n; head.next = n; } }Dry run
The trace above. The test harness also checks capacity 1 with an update to an existing key, which must not evict anything.
- 5Check it
State time and space with a reason, name three inputs that could break the code, and say what this problem teaches beyond itself.
Reveal step 5
Complexity
O(1) per operation, O(capacity) space.
Edge cases
Updating an existing key (no eviction, but it becomes most recent), capacity 1,
geton a missing key (no change to order).Transfer lesson
Java's
LinkedHashMapwithaccessOrder = trueand an overriddenremoveEldestEntryis an LRU cache in a few lines. Mention it, but expect to build the manual version. LFU Cache (LC 460) extends the idea: a map from frequency to an LRU list, plus aminFreqpointer.
17.5Insert, delete, getRandom in O(1)
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
insert, remove, and return a uniformly random element, all O(1). An ArrayList gives random access; a hash map from value to index gives "where is it". To delete from the middle of the array in O(1), move the last element into the hole and fix its index in the map.
// LC 380. ArrayList gives O(1) random access; the map gives O(1) "where is it?".
// Delete in O(1) by moving the last element into the deleted slot.
static class RandomizedSet {
private final List<Integer> values = new ArrayList<>();
private final Map<Integer, Integer> indexOf = new HashMap<>();
private final Random random = new Random();
boolean insert(int val) {
if (indexOf.containsKey(val)) return false;
indexOf.put(val, values.size());
values.add(val);
return true;
}
boolean remove(int val) {
Integer idx = indexOf.get(val);
if (idx == null) return false;
int last = values.get(values.size() - 1);
values.set(idx, last); // move last into the hole
indexOf.put(last, idx);
values.remove(values.size() - 1); // removing the last index is O(1)
indexOf.remove(val);
return true;
}
int getRandom() { return values.get(random.nextInt(values.size())); }
}
Order of updates in remove is the bug magnet: write the last element into the hole, update the last element's index, then remove the last slot, then remove the deleted value from the map. This order also works when the removed element is itself the last one.
17.6Other combinations worth recognizing
Any correct brute force counts. Note where you got stuck: the lesson below is about exactly that.
Show a hint
| Problem | Combination | Why |
|---|---|---|
| Time Based Key-Value Store (LC 981) | Map<key, List<(time, value)>> + binary search |
Times arrive in increasing order, so each list is sorted |
| Snapshot Array (LC 1146) | Per-index list of (snapId, value) + binary search | Store only changes, not full copies per snapshot |
| Design Underground System (LC 1396) | Map of check-ins by id + map of (from, to) totals | Separate in-progress state from aggregated state |
| Food Rating System (LC 2353) | Map food -> rating, map cuisine -> TreeSet ordered by (rating desc, name) |
Update = remove, change, re-insert |
| Design Twitter (LC 355) | Map user -> tweets, map user -> followees, heap to merge feeds | K-way merge of the most recent tweets |
| Stock Price / leaderboards | Map + TreeMap<value, count> |
Ordered values with deletions |
| LFU Cache (LC 460) | Map key -> node, map freq -> LRU list, minFreq |
Two levels of ordering |
Common mistakes
- Updating one structure and forgetting the other (the map still points at an evicted node).
- Using a heap where deletions by key are needed without lazy deletion.
- Storing values in the list but not keys, so eviction cannot clean the map.
- Not stating amortized vs worst-case bounds when they differ.
Data structure design
9 Medium 1 Hard| Problem and what it teaches | Level | Priority | Status |
|---|---|---|---|
| 155Min Stack Store the running min alongside each value | Medium | High | |
| 1472Design Browser History Array with a cursor and a logical end replaces a stack pair | Medium | High | |
| 146LRU Cache HashMap for lookup + doubly linked list for recency | Medium | High | |
| 380Insert Delete GetRandom O(1) Array + index map; delete by swapping with the last | Medium | High | |
| 1396Design Underground System Two maps: in-progress trips and aggregated stats | Medium | Medium | |
| 2353Design a Food Rating System Map + sorted set per cuisine (or heap with lazy deletion) | Medium | Medium | |
| 2349Design a Number Container System Map index -> number plus per-number heaps with lazy deletion | Medium | Medium | |
| 1146Snapshot Array Per-index history plus binary search on snapshot id | Medium | Medium | |
| 355Design Twitter Per-user lists merged with a heap for the feed | Medium | Medium | |
| 460LFU Cache Frequency buckets of LRU lists plus a minFreq pointer | Hard | High |
Mastery checklist
Tick an item only when it is true with your notes closed.
Pattern cheat sheets
Quick-reference pages for revision. Use them after you have studied a module, to check recall, not instead of the module. For choosing between two similar techniques, see the decision tables.
Signal to pattern
Read the problem, find the matching signals, then verify the required condition before committing. A signal is a hypothesis, not a decision.
| If the problem has ... | First try | Verify that ... |
|---|---|---|
| Pair or complement lookup in unsorted data | Hash map | Order does not matter; O(n) memory is acceptable |
| Groups of equivalent items | Hash map with a canonical key | You can build a key that is equal exactly for items in the same group |
| Range sums, many queries, or "subarray sum equals k" | Prefix sums (+ hash map) | The operation is invertible (sum, count, XOR) |
| Many range updates, read once | Difference array | Updates come before reads |
| Best contiguous sum or product | Kadane | Contiguity is required |
| Sorted array, pairs or triplets with a target | Two pointers | Moving a pointer safely discards candidates |
| Partition in place (colors, pivot) | Read/write or three pointers | The relative order requirement (stable or not) |
| Every window of size k | Fixed sliding window | State can be updated on add and remove |
| Longest or shortest contiguous range with a condition | Variable sliding window | Shrinking a valid window keeps it valid (no negatives in sums) |
| "Exactly k" in a window | atMost(k) - atMost(k - 1) | "At most" is monotonic |
| Sorted or rotated data, find a position | Binary search on data | A test at mid tells you which half to discard |
| "Minimum X such that ...", "minimize the maximum" | Binary search on the answer | Feasibility is monotonic and cheap to check |
| Overlaps, merges, rooms, schedules | Sort intervals; sweep line | Endpoint semantics (closed or half-open) |
| Keep the most non-overlapping intervals | Greedy by earliest end | Unweighted (weighted needs DP) |
| Reverse, reorder, or merge a linked list | Pointer rewiring with a dummy | You save next before overwriting |
| Cycle, middle, kth from end, repeating sequence | Fast and slow pointers | There is a single "next" function |
| Matching brackets, nested encodings, undo | Stack | The item you need is the most recent unresolved one |
| Next or previous greater or smaller element, spans | Monotonic stack | Beaten elements can never be an answer again |
| Max or min of every window | Monotonic deque | Elements expire from the front |
| "Generate all", "list every" | Backtracking | n is small (about 20 for subsets, 10 for permutations) |
| Tree height, diameter, balance, LCA, path through a node | Bottom-up tree recursion | You know what each node returns to its parent |
| Root-to-node paths, ancestors' values | Top-down parameters | You undo mutable state on the way back |
| Level by level, nearest node | Tree BFS | Snapshot the queue size per level |
| BST, sorted order, kth smallest | Inorder traversal or bounds | Strictness of the BST definition |
| Top k, kth largest, repeatedly take the extreme | Heap (size k) | Data arrives over time or k is small |
| Running median | Two heaps | Sizes stay balanced |
| Merge k sorted sources | Heap of fronts | Each source is sorted |
| Locally obvious best choice | Greedy | An exchange argument holds; no small counterexample |
| Grid regions, connectivity, reachability | BFS or DFS | Visited marking |
| Fewest steps, equal costs | BFS (multi-source if many starts) | All moves cost the same |
| Prerequisites, ordering constraints | Topological sort | Directed; detect cycles |
| Two groups with no conflicting pair together | Bipartite check | Undirected conflict graph |
| Cheapest path, weighted | Dijkstra | No negative weights |
| Path with at most k edges | Bellman-Ford rounds | Read only the previous round |
| Merging groups as connections arrive | Union-Find | No deletions |
| Connect everything cheaply | Kruskal or Prim | Undirected, weighted |
| Many prefix queries, autocomplete, many words in a grid | Trie | Keys are sequences sharing prefixes |
| Count ways, min or max cost, feasibility with interacting choices | DP | Subproblems repeat; the state captures the future |
| Pairs canceling, single odd element, small subsets | Bits (XOR, masks) | Fixed width, Java shift rules |
| GCD, primes, huge powers, answer modulo 10^9 + 7 | Number theory | Overflow before the modulo |
| "Design a class" with per-operation limits | Combine structures | Every update keeps all structures consistent |
Pattern summary
Every pattern card from the modules, condensed to the three fields you check first.
| Pattern | Signal | Must be true | If it is not |
|---|---|---|---|
| Hashing (lookup and counting) Module 2 | You keep asking 'have I seen X?', 'how many X?', or 'where is X?' inside a loop. Pairs that must sum or match. Grouping by an equivalence (anagrams, same pattern). | A key that identifies what you are looking for. Order of elements does not matter for the lookup itself. | Sorting + two pointers (O(1) extra space), TreeMap for ordered or range queries, a counting array for small value ranges. |
| Prefix sums Module 2 | Sum (or count, XOR) over a contiguous range, asked many times. 'Subarray sum equals / divisible by k'. Negative numbers allowed. | An invertible combine operation (sum, XOR, count): range = prefix[r+1] - prefix[l]. Max and min are NOT invertible. | Sliding window when all values are non-negative; difference array for many range updates; Fenwick or segment tree when the array changes. |
| Kadane (best subarray ending here) Module 2 | Maximum (or minimum) sum or product of a contiguous subarray. 'Best time to buy and sell once'. | The best answer ending at i can be built from the best answer ending at i - 1 plus nums[i]. | Prefix sums with a running minimum prefix (same idea), DP with extra states for deletions or circular arrays. |
| Two pointers Module 3 | Sorted array or string; pairs or triplets meeting a target; palindromes; in-place removal or partitioning. | Moving one pointer must safely discard candidates. Usually needs sorted order or a symmetric structure. | Hash map for unsorted pair search; sliding window when the pair bounds a contiguous range with a running condition. |
| Fixed-size sliding window Module 3 | 'Every subarray/substring of length k', anagram or permutation of a pattern, averages over k items. | Window state can be updated in O(1) when one element enters and one leaves. | Monotonic deque for window max/min; prefix sums for window sums with random access. |
| Variable-size sliding window Module 3 | Longest or shortest contiguous subarray/substring satisfying a condition; 'at most k' distinct, zeros, or replacements. | Monotonic validity: shrinking a valid window keeps it valid (or growing an invalid one keeps it invalid). Typically fails with negative numbers in sum conditions. | Prefix sums + hash map (exact sums with negatives); monotonic deque over prefix sums (shortest sum >= k with negatives); exactly(k) = atMost(k) - atMost(k-1). |
| Binary search on data Module 4 | Sorted or rotated-sorted input, 'first/last position', insertion point, peak, or any O(log n) requirement. | A predicate over indices that is false...false true...true (monotonic). Sorting gives one for free. | Linear scan when unsorted and queried once; hash set for membership only; TreeMap floor/ceiling for dynamic data. |
| Binary search on the answer Module 4 | 'Minimum largest', 'maximum smallest', 'least capacity/speed/time so that...'. You can check a candidate answer easily but cannot compute it directly. | feasible(x) is monotonic: if x works, every larger (or smaller) x also works. | Direct formula, greedy, or DP when the answer can be constructed; heap-based simulation for some scheduling problems. |
| Intervals and sweep line Module 5 | Pairs [start, end]: overlaps, merging, free time, rooms needed, max concurrent events. | Sort order exposes the structure: by start for merging, by end for selecting, by event time for counting. | Min-heap of end times for rooms; difference array when coordinates are small integers; TreeMap for dynamic calendars. |
| Linked list pointer manipulation Module 6 | Reverse, reorder, merge, delete, or split a linked list in place. | You can only move forward (singly linked); O(1) extra space is usually expected. | Copy into an ArrayList when space is allowed and the logic gets simpler; recursion for clarity (O(n) stack). |
| Fast and slow pointers Module 6 | Cycle detection, middle element, kth from end, a sequence that might repeat (happy number), duplicates in [1..n]. | A function next(x) that defines a path; a cycle exists if the path revisits a state. | HashSet of visited states (simpler, O(n) memory). |
| Stack (most recent unmatched item) Module 7 | Matching pairs (brackets, tags), undo, nested structure (decode string, calculators), collisions that resolve against the latest item. | The element you need next is always the most recently added unresolved one (LIFO). | Recursion (an implicit stack), a counter when only one bracket type exists. |
| Monotonic stack Module 7 | Next/previous greater or smaller element, span/width bounded by a taller or shorter element, 'how far until warmer'. | Once a bigger (or smaller) element arrives, the elements it beats can never be the answer for anything later. | Monotonic deque when the window also expires from the left; sorting + DSU for some offline variants. |
| Monotonic deque (window max/min) Module 7 | Max or min of every sliding window; DP where dp[i] needs the best of the last k states. | Old elements expire from the front; new elements make worse older ones useless from the back. | Heap with lazy deletion (O(n log n)); TreeMap of counts for any order statistic. |
| Recursion and divide and conquer Module 8 | The problem on input n can be stated using the same problem on a smaller input (n - 1, halves, subtrees, splits). | A base case that stops the recursion and a smaller subproblem each call. | Iteration with an explicit stack for deep inputs; memoization or DP for overlapping subproblems. |
| Backtracking Module 8 | 'All', 'every', 'generate', 'list all valid' combinations, permutations, partitions, placements. Small n (about 10 to 20). | The answer is built by a sequence of choices; partial choices can be undone; output size itself may be exponential. | DP (count or optimize), bitmask enumeration for n <= 20, greedy when a choice is provably safe. |
| Tree recursion (bottom-up return values) Module 9 | Height, diameter, balance, path sums through a node, LCA, subtree properties, 'information from children'. | A node's answer can be computed from its children's answers plus its own value. | Top-down parameters for ancestor information; BFS for level-based questions; parent pointers + BFS for distance-k. |
| Tree BFS (level order) Module 9 | 'Level by level', 'each row', right side view, minimum depth, width, zigzag. | The answer depends on depth or on nodes that share a level. | DFS with a depth parameter and a list indexed by depth. |
| Binary search tree Module 9 | 'BST' in the statement, sorted order, kth smallest, floor/ceiling, validate, range sum. | Every left-subtree value < node < every right-subtree value (strict, unless stated). | TreeMap / TreeSet in Java (red-black tree, guaranteed O(log n)); sorted array + binary search for static data. |
| Heap / priority queue Module 10 | Top k, kth largest/smallest, 'repeatedly take the smallest/largest', merging sorted sources, scheduling by time or priority. | You need fast access to the current extreme while elements keep arriving or changing. | Sorting when all data is known up front; quickselect for one kth element (O(n) average); TreeMap when you need removal by key. |
| Two heaps and k-way merge Module 10 | Running median, balancing two halves, merging k sorted lists, smallest pairs from sorted arrays. | The answer is defined by a boundary between two halves, or by the next smallest among k sorted frontiers. | TreeMap multiset; divide-and-conquer pairwise merging for k-way merge. |
| Greedy Module 11 | Optimize one quantity with a natural order (earliest end, largest value per unit, furthest reach); local decision seems obviously safe. | Greedy-choice property: some optimal solution starts with the greedy choice. Prove it (exchange argument) or find a counterexample. | DP when choices interact; heap-based greedy when the best choice changes as you go. |
| Graph traversal (BFS / DFS) Module 12 | Connections, reachability, islands, regions, 'can you get from A to B', components. Grids are graphs. | You can list each node's neighbors. Mark visited to avoid infinite loops. | Union-Find for connectivity with incremental edges; iterative DFS with an explicit stack. |
| BFS shortest path (unweighted, multi-source) Module 12 | Fewest steps, minimum moves, nearest X from every cell, spreading per minute. All moves cost the same. | Every edge has equal weight. (0/1 weights: 0-1 BFS with a deque.) | Dijkstra for non-negative weights; Bellman-Ford rounds for edge-count limits or negative weights. |
| Topological sort and cycle detection Module 12 | Prerequisites, dependencies, build order, 'is it possible to finish', ordering with constraints. | Directed graph. A valid order exists if and only if there is no directed cycle. | DFS three-color for cycle detection; DP over the topological order for longest paths in a DAG. |
| Dijkstra (weighted shortest path) Module 12 | Minimum total cost, time, or effort over weighted edges; 'cheapest', 'fastest', 'least effort path'. | All edge weights are non-negative, and the path cost never decreases as you extend a path. | BFS when weights are equal; 0-1 BFS for weights 0/1; Bellman-Ford for negatives or k-edge limits; Floyd-Warshall for all pairs with small V. |
| Union-Find (disjoint set union) Module 12 | Grouping by connections that arrive over time, 'are A and B connected', counting components, redundant edge, Kruskal. | Only unions happen (no edge deletions). Undirected relationships. | BFS/DFS when the graph is static and queried once. |
| Trie (prefix tree) Module 13 | Many prefix queries, autocomplete, 'words starting with', dictionary with wildcards, word search with many words, max XOR. | Keys are sequences (strings, bits) and queries share prefixes. | HashSet for exact matches; sorted list + binary search for prefix ranges; HashSet of all prefixes for small inputs. |
| Dynamic programming Module 14 | Count the ways, min/max cost, 'is it possible', with choices at each step where later choices depend on earlier ones. A brute-force recursion re-solves the same subproblems. | Optimal substructure (the best answer is built from best sub-answers) and overlapping subproblems. | Greedy when a choice is provably safe; backtracking when you need all solutions; graph shortest path when states form a weighted graph. |
| Bit manipulation Module 15 | 'Without extra space', pairs canceling out, powers of two, subsets of a small set, flags, XOR in the statement. | Values fit in a fixed width (32 or 64 bits); operations act on each bit independently. | HashSet or boolean arrays when clarity matters more than space. |
| Number theory essentials Module 16 | Divisibility, primes, 'answer modulo 10^9 + 7', huge exponents, counting arrangements. | Integer arithmetic with known identities (Euclid, Fermat, Pascal). | BigInteger for exact big values (slow); long arithmetic with careful ordering. |
| Data structure design Module 17 | 'Design a class' with required time limits per operation: get/put in O(1), getRandom in O(1), min in O(1). | List every operation and its target complexity first; each operation needs some structure that supports it. | Built-ins: LinkedHashMap (LRU), TreeMap (ordered), PriorityQueue with lazy deletion. |
Complexity reference
| Algorithm or operation | Time | Extra space |
|---|---|---|
| Hash map get / put | O(1) average | O(n) total |
| Sorting | O(n log n) | O(log n) to O(n) |
| Binary search | O(log n) | O(1) |
| Two pointers, sliding window | O(n) | O(1) or O(alphabet) |
| Prefix sums build / query | O(n) / O(1) | O(n) |
| Monotonic stack or deque | O(n) | O(n) |
| Heap push / pop, top-k | O(log n), O(n log k) | O(k) |
| Tree traversal | O(n) | O(h) |
| BST search / insert / delete | O(h) | O(h) recursive |
| BFS / DFS on a graph | O(V + E) | O(V) |
| Topological sort | O(V + E) | O(V) |
| Dijkstra (binary heap) | O((V + E) log V) | O(V + E) |
| Bellman-Ford | O(V * E) | O(V) |
| Floyd-Warshall | O(V^3) | O(V^2) |
| Union-Find (both optimizations) | O(alpha(n)) amortized per op | O(n) |
| Kruskal / Prim (heap) | O(E log E) / O(E log V) | O(V + E) |
| Trie insert / search | O(L) | O(total characters x alphabet) |
| Subsets / permutations | O(2^n * n) / O(n! * n) | O(n) plus output |
| DP | states x transitions | states (often reducible) |
| Sieve of Eratosthenes | O(n log log n) | O(n) |
| GCD, fast power | O(log n) | O(1) |
Java quick reference
| Task | Code |
|---|---|
| Count with a map | freq.merge(x, 1, Integer::sum) |
| Group into lists | map.computeIfAbsent(key, k -> new ArrayList<>()).add(v) |
| Default value | map.getOrDefault(key, 0) |
| Stack / queue / deque | Deque<Integer> d = new ArrayDeque<>(); stack: push/pop/peek; queue: offer/poll/peek |
| Min-heap / max-heap | new PriorityQueue<>(), new PriorityQueue<>(Collections.reverseOrder()) |
| Heap of arrays by first element | new PriorityQueue<int[]>((a, b) -> Integer.compare(a[0], b[0])) |
| Sort 2D array by column 0 | Arrays.sort(arr, (a, b) -> Integer.compare(a[0], b[0])) |
| Sort with tie-break | Comparator.comparingInt((int[] a) -> a[0]).thenComparingInt(a -> a[1]) |
| Ordered map queries | treeMap.floorKey(x), ceilingKey(x), firstKey(), pollFirstEntry() |
| Fill / copy arrays | Arrays.fill(a, -1), a.clone(), Arrays.copyOfRange(a, l, r) |
| Array to string for debugging | Arrays.toString(a), Arrays.deepToString(grid) |
| Build strings | StringBuilder sb = new StringBuilder(); sb.append(c); sb.reverse(); sb.toString() |
| Char to index and back | c - 'a', (char) ('a' + i), Character.isDigit(c) |
| Integer limits | Integer.MAX_VALUE (2^31 - 1), Long.MAX_VALUE; cast before multiplying: (long) a * b |
| Bits | Integer.bitCount(x), x & (x - 1), x & -x, 1L << k |
| List to int array | list.stream().mapToInt(Integer::intValue).toArray() |
Java templates
Short, reusable skeletons for the core patterns. Each one lists when to use it, the invariant that makes it correct, its cost, the common mistakes, and the lines that change from problem to problem (marked CHANGE in the code). Every template compiles and is covered by tests.
Use templates to check your own code after writing it, not to copy before thinking. If you cannot rebuild a template from its invariant, go back to its module.
Binary search: first true
| Aspect | Detail |
|---|---|
| Use when | Searching a sorted array for a boundary, or searching an answer range with a monotonic feasibility check |
| Invariant | The first true lies in [left, right]; [left, right) is the live search space |
| Cost | O(log range) calls to the predicate |
| Mistakes | Non-monotonic predicate; bounds that exclude the answer; overflow in the predicate |
| Changes | The predicate and the bounds; "last true" = first true of the negation, minus 1 |
// Find the FIRST x in [lo, hi] where ok(x) is true, given ok is false...false true...true.
// Returns hi + 1 if ok is never true. Works for indices (data) and for answers.
static int firstTrue(int lo, int hi, IntPredicate ok) {
int left = lo, right = hi + 1; // search space [left, right)
while (left < right) {
int mid = left + (right - left) / 2;
if (ok.test(mid)) right = mid; // mid might be the first true
else left = mid + 1; // mid and everything before are false
}
return left;
}
// CHANGE: the predicate and the bounds.
// lower bound of t in a: firstTrue(0, n - 1, i -> a[i] >= t)
// min feasible answer: firstTrue(minAns, maxAns, x -> feasible(x))
// max feasible answer (true...true false...false): firstTrue(lo, hi, x -> !feasible(x)) - 1
Sliding window and two pointers
| Aspect | Detail |
|---|---|
| Use when | Window: contiguous range with a condition that stays true when a valid window shrinks. Two pointers: pairs in sorted data |
| Invariant | After the inner loop, [left, right] is the longest valid window ending at right |
| Cost | O(n): each index enters and leaves once |
| Mistakes | if instead of while when shrinking; updating the answer in the wrong place for shortest vs longest; negative numbers |
| Changes | The state, the add and remove steps, the invalid condition, where the answer is updated |
// Variable window: longest valid window. The window must stay valid after shrinking,
// and adding elements can only make it "less valid" (monotonic).
static int longestValidWindow(int[] nums, int limit) {
int left = 0, best = 0;
long state = 0; // CHANGE: sum, counts map, distinct count...
for (int right = 0; right < nums.length; right++) {
state += nums[right]; // CHANGE: add nums[right] to state
while (state > limit) { // CHANGE: "window invalid" condition
state -= nums[left++]; // CHANGE: remove nums[left] from state
}
best = Math.max(best, right - left + 1); // longest: update after shrinking
}
return best;
}
// Shortest valid window: move the update INSIDE the while loop, before shrinking.
// Counting windows: add (right - left + 1) after shrinking (for "at most" conditions).
// Opposite-direction two pointers on sorted input.
static boolean pairWithSum(int[] sorted, int target) {
int left = 0, right = sorted.length - 1;
while (left < right) {
int sum = sorted[left] + sorted[right];
if (sum == target) return true;
if (sum < target) left++; else right--; // CHANGE: which pointer and why
}
return false;
}
Prefix sums with a hash map
| Aspect | Detail |
|---|---|
| Use when | Counting subarrays with an exact sum, remainder, or balance, including negative values |
| Invariant | Before processing index i, the map counts prefix values for all earlier positions (including the empty prefix) |
| Cost | O(n) time, O(n) space |
| Mistakes | Missing the 0 -> 1 seed; inserting before querying; negative remainders in Java |
| Changes | The transform of each element; count vs first index (longest) |
// Count subarrays whose sum satisfies prefix(i) - prefix(j) == k.
static int countSubarrays(int[] nums, int k) {
Map<Long, Integer> count = new HashMap<>();
count.put(0L, 1); // empty prefix: subarrays starting at 0
long prefix = 0;
int result = 0;
for (int x : nums) {
prefix += x; // CHANGE: transform x (0 -> -1, x % k ...)
result += count.getOrDefault(prefix - k, 0); // query first
count.merge(prefix, 1, Integer::sum); // then record
}
return result;
}
// Longest instead of count: store the FIRST index of each prefix, never overwrite.
Linked list reversal and cycle detection
| Aspect | Detail |
|---|---|
| Use when | Reversing all or part of a list; detecting cycles or finding middles |
| Invariant | prev heads the reversed part, head heads the rest; fast is twice as far as slow |
| Cost | O(n) time, O(1) space |
| Mistakes | Overwriting next before saving it; forgetting the dummy when the head can change |
| Changes | The range to reverse; the stopping condition for the middle |
static ListNode reverse(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
static boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
// Always: dummy node when the head can change; save next before rewiring; draw it first.
Monotonic stack
| Aspect | Detail |
|---|---|
| Use when | Next or previous greater or smaller element; ranges bounded by nearest larger or smaller values |
| Invariant | Values at stack indices are monotonic; every popped index has found its answer |
| Cost | O(n): each index pushed and popped once |
| Mistakes | Storing values instead of indices; strict vs non-strict comparisons with duplicates; forgetting leftovers |
| Changes | The comparison direction; what is recorded on pop; previous (stack top) vs next (current element) |
// Next greater element to the right (value), -1 if none.
static int[] nextGreater(int[] nums) {
int[] result = new int[nums.length];
Arrays.fill(result, -1);
Deque<Integer> stack = new ArrayDeque<>(); // indices; values decreasing
for (int i = 0; i < nums.length; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) // CHANGE: < vs <=, > for smaller
result[stack.pop()] = nums[i]; // nums[i] resolves the popped index
stack.push(i);
}
return result;
}
// Previous greater: the element left on the stack top after popping is the answer for i.
Tree DFS and BFS
| Aspect | Detail |
|---|---|
| Use when | DFS: answers built from children (bottom-up) or from ancestors (parameters). BFS: per-level questions |
| Invariant | DFS returns exactly what the parent needs; BFS processes one full level per round |
| Cost | O(n) time; O(h) DFS stack, O(width) BFS queue |
| Mistakes | Returning the final answer instead of the parent's need; reading queue.size() inside the loop |
| Changes | Base value, combine step, global update, return value |
// DFS with a return value: combine children's answers (bottom-up).
static int dfs(TreeNode node) {
if (node == null) return 0; // CHANGE: base value
int left = dfs(node.left), right = dfs(node.right);
// CHANGE: update a global answer using left, right, node.val if needed
return 1 + Math.max(left, right); // CHANGE: what the parent needs
}
// BFS by levels.
static List<List<Integer>> bfs(TreeNode root) {
List<List<Integer>> levels = new ArrayList<>();
Deque<TreeNode> q = new ArrayDeque<>();
if (root != null) q.offer(root);
while (!q.isEmpty()) {
List<Integer> level = new ArrayList<>();
for (int size = q.size(); size > 0; size--) {
TreeNode n = q.poll();
level.add(n.val);
if (n.left != null) q.offer(n.left);
if (n.right != null) q.offer(n.right);
}
levels.add(level);
}
return levels;
}
// Top-down state (ancestors): pass it as a parameter, e.g. dfs(node, maxSoFar).
Backtracking
| Aspect | Detail |
|---|---|
| Use when | Enumerating subsets, permutations, combinations, partitions, placements |
| Invariant | path is exactly the current partial choice; it is restored after each recursive call |
| Cost | (nodes in the decision tree) x (work per node), usually exponential |
| Mistakes | Adding path without copying; forgetting to undo; wrong start index (i vs i + 1 vs 0) |
| Changes | Goal test, duplicate skip, pruning, the recursion's next start |
static void backtrack(int[] items, int start, List<Integer> path, List<List<Integer>> out) {
// CHANGE: if (goal reached) { out.add(new ArrayList<>(path)); return; }
out.add(new ArrayList<>(path)); // subsets: every node is an answer
for (int i = start; i < items.length; i++) {
// CHANGE: if (i > start && items[i] == items[i - 1]) continue; // duplicates (sorted)
// CHANGE: if (cannot lead to an answer) break or continue; // pruning
path.add(items[i]); // choose
backtrack(items, i + 1, path, out); // explore (i for reuse, 0 + used[] for permutations)
path.remove(path.size() - 1); // un-choose
}
}
Heap: top k
| Aspect | Detail |
|---|---|
| Use when | k largest or smallest, kth element, streams |
| Invariant | The heap holds the k best seen so far; its top is the worst of them |
| Cost | O(n log k) time, O(k) space |
| Mistakes | Max-heap for k largest (it should be a min-heap); expecting sorted iteration |
| Changes | Heap direction, comparator, what is stored (value, pair, entry) |
// K largest: min-heap of size k. (K smallest: max-heap of size k.)
static List<Integer> kLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>(); // CHANGE: comparator for objects
for (int x : nums) {
heap.offer(x);
if (heap.size() > k) heap.poll();
}
List<Integer> out = new ArrayList<>(heap);
Collections.sort(out, Collections.reverseOrder());
return out;
}
Graph BFS and DFS
| Aspect | Detail |
|---|---|
| Use when | Reachability, components, grid regions; BFS for fewest steps with equal costs |
| Invariant | A node is marked when it is first discovered, so it is processed once; BFS processes distance d before d + 1 |
| Cost | O(V + E), or O(R x C) on a grid |
| Mistakes | Marking on dequeue (duplicates in the queue); recursion depth on big grids; forgetting multi-source starts |
| Changes | Sources, what blocks a move, what is recorded per node |
static final int[][] DIRS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
// Grid BFS shortest path from (sr, sc) to (tr, tc); -1 if unreachable.
static int gridShortestPath(int[][] grid, int sr, int sc, int tr, int tc) {
int rows = grid.length, cols = grid[0].length;
boolean[][] seen = new boolean[rows][cols];
Deque<int[]> q = new ArrayDeque<>();
q.offer(new int[]{sr, sc}); // CHANGE: multi-source = offer all sources
seen[sr][sc] = true;
for (int steps = 0; !q.isEmpty(); steps++) {
for (int size = q.size(); size > 0; size--) {
int[] cell = q.poll();
if (cell[0] == tr && cell[1] == tc) return steps;
for (int[] d : DIRS) {
int r = cell[0] + d[0], c = cell[1] + d[1];
if (r < 0 || c < 0 || r >= rows || c >= cols) continue;
if (seen[r][c] || grid[r][c] == 1) continue; // CHANGE: what blocks a move
seen[r][c] = true; // mark on ENQUEUE
q.offer(new int[]{r, c});
}
}
}
return -1;
}
// Adjacency-list DFS (recursive). Use an explicit stack if depth can exceed ~10^4.
static void dfs(List<List<Integer>> adj, int u, boolean[] seen) {
seen[u] = true;
for (int v : adj.get(u)) if (!seen[v]) dfs(adj, v, seen);
}
Topological sort (Kahn)
| Aspect | Detail |
|---|---|
| Use when | Ordering with dependencies; detecting cycles in directed graphs |
| Invariant | Every node in the queue has all its prerequisites already output |
| Cost | O(V + E) |
| Mistakes | Edge direction reversed; treating "fewer than V nodes output" as success |
| Changes | Edge construction; what you do with each node as it is output (DP in topological order) |
// Kahn's algorithm. Returns an order, or an empty list if there is a cycle.
static List<Integer> topoSort(int n, int[][] edges) { // edge u -> v
List<List<Integer>> adj = new ArrayList<>();
for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
int[] indeg = new int[n];
for (int[] e : edges) { adj.get(e[0]).add(e[1]); indeg[e[1]]++; }
Deque<Integer> q = new ArrayDeque<>();
for (int i = 0; i < n; i++) if (indeg[i] == 0) q.offer(i);
List<Integer> order = new ArrayList<>();
while (!q.isEmpty()) {
int u = q.poll();
order.add(u);
for (int v : adj.get(u)) if (--indeg[v] == 0) q.offer(v);
}
return order.size() == n ? order : List.of();
}
Union-Find
| Aspect | Detail |
|---|---|
| Use when | Dynamic connectivity, grouping by shared keys, redundant edges, Kruskal |
| Invariant | Two nodes are in the same set exactly when find returns the same root |
| Cost | Effectively O(1) amortized per operation with path compression and union by size |
| Mistakes | Comparing parent[a] == parent[b] instead of find(a) == find(b); forgetting to initialize parents |
| Changes | How items map to indices (use a HashMap for strings); what is counted on union |
static int[] parent;
static int find(int x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
}
static boolean union(int a, int b) {
int ra = find(a), rb = find(b);
if (ra == rb) return false;
parent[ra] = rb; // add union by size/rank for guaranteed bounds
return true;
}
// init: parent = new int[n]; for (int i = 0; i < n; i++) parent[i] = i;
The full version with union by size and a component counter is in Module 12.
Dijkstra
| Aspect | Detail |
|---|---|
| Use when | Single-source shortest paths with non-negative weights |
| Invariant | When a node is popped with its current distance, that distance is final |
| Cost | O((V + E) log V) |
| Mistakes | Negative weights; not skipping stale entries; using int when sums can overflow |
| Changes | How a path's cost combines (sum, max, product), the target, extra state in the node |
// adj.get(u) holds {v, w}. Non-negative weights only.
static long[] dijkstra(List<List<int[]>> adj, int src) {
long[] dist = new long[adj.size()];
Arrays.fill(dist, Long.MAX_VALUE);
dist[src] = 0;
PriorityQueue<long[]> pq = new PriorityQueue<>((a, b) -> Long.compare(a[0], b[0]));
pq.offer(new long[]{0, src});
while (!pq.isEmpty()) {
long[] top = pq.poll();
int u = (int) top[1];
if (top[0] > dist[u]) continue; // stale
for (int[] e : adj.get(u)) {
long nd = dist[u] + e[1]; // CHANGE: max(dist, w) for bottleneck paths
if (nd < dist[e[0]]) { dist[e[0]] = nd; pq.offer(new long[]{nd, e[0]}); }
}
}
return dist;
}
Core DP patterns
| Aspect | Detail |
|---|---|
| Use when | Counting, optimizing, or deciding with overlapping subproblems |
| Invariant | Every state is computed only after the states it depends on |
| Cost | (number of states) x (transitions per state) |
| Mistakes | Incomplete state; wrong iteration order; wrong initialization for min problems; knapsack loop direction |
| Changes | State definition, transition, base cases, answer cell |
// 1. Top-down: write the recurrence, cache it.
static long[] memo;
static long solve(int i, int[] a) {
if (i >= a.length) return 0; // base case
if (memo[i] != Long.MIN_VALUE) return memo[i];
long take = a[i] + solve(i + 2, a), skip = solve(i + 1, a); // CHANGE: the choices
return memo[i] = Math.max(take, skip);
}
// 2. 0/1 knapsack (each item once): capacity loop goes DOWN.
static int knapsack01(int[] weight, int[] value, int cap) {
int[] dp = new int[cap + 1];
for (int i = 0; i < weight.length; i++)
for (int c = cap; c >= weight[i]; c--)
dp[c] = Math.max(dp[c], dp[c - weight[i]] + value[i]);
return dp[cap];
}
// 3. Unbounded knapsack (items reusable): capacity loop goes UP.
static int knapsackUnbounded(int[] weight, int[] value, int cap) {
int[] dp = new int[cap + 1];
for (int i = 0; i < weight.length; i++)
for (int c = weight[i]; c <= cap; c++)
dp[c] = Math.max(dp[c], dp[c - weight[i]] + value[i]);
return dp[cap];
}
// 4. Two sequences: dp[i][j] over prefixes a[0..i), b[0..j).
static int twoSequences(String a, String b) {
int[][] dp = new int[a.length() + 1][b.length() + 1]; // CHANGE: base row/col
for (int i = 1; i <= a.length(); i++)
for (int j = 1; j <= b.length(); j++)
dp[i][j] = a.charAt(i - 1) == b.charAt(j - 1) ? dp[i - 1][j - 1] + 1
: Math.max(dp[i - 1][j], dp[i][j - 1]);
return dp[a.length()][b.length()];
}
Trie
| Aspect | Detail |
|---|---|
| Use when | Prefix queries, autocomplete, wildcard search, many-word grid search, max XOR |
| Invariant | The path from the root to a node spells a prefix; end marks complete words |
| Cost | O(L) per insert or lookup |
| Mistakes | Treating a prefix as a word; using arrays for large alphabets |
| Changes | Child storage (array vs map) and what each node records |
static class TrieNode {
TrieNode[] next = new TrieNode[26]; // CHANGE: Map<Character, TrieNode> for big alphabets
boolean end; // CHANGE: count, word, or aggregated value
}
static void insert(TrieNode root, String w) {
TrieNode n = root;
for (char ch : w.toCharArray()) {
if (n.next[ch - 'a'] == null) n.next[ch - 'a'] = new TrieNode();
n = n.next[ch - 'a'];
}
n.end = true;
}
Interview framework
A repeatable 13-step process for an unfamiliar coding question, with the exact questions to ask yourself at each step, and a recovery playbook for when you get stuck.
The 13 steps
The decode method on its own page is how you think. This framework is how you think out loud, under time pressure, with someone watching. A typical 45-minute round leaves 35 to 40 minutes for one Medium problem or two shorter ones.
| Phase | Steps | Rough budget (one Medium in 40 min) |
|---|---|---|
| Understand | 1 to 3 | 5 minutes |
| Design | 4 to 10 | 10 to 12 minutes |
| Implement | 11 | 12 to 15 minutes |
| Verify | 12 to 13 | 5 minutes |
Details1. Clarify the input, output, constraints, and edge cases
Ask, then write the answers in a comment at the top of the editor.
- What are the sizes? (They set the complexity target; see Module 1.)
- Can values be negative, zero, duplicated? Can the input be empty?
- Is the input sorted? Can I modify it?
- What should I return if there is no answer? Any answer, or a specific one (smallest index, lexicographically first)?
- For strings: which characters? For graphs: directed? connected? self-loops?
Details2. Translate the question into a simpler statement
Say it back in one sentence, without the story: "So I need the length of the shortest contiguous subarray whose sum is at least k, and values are positive." The interviewer will correct a misunderstanding now instead of in 20 minutes.
Details3. Work through a small example by hand
Pick a 4 to 6 element example that is not the given one, and solve it out loud. Notice what you track in your head; that is usually the state of the final algorithm.
Details4. Identify the input structure and the required operation
"Contiguous, positive values, minimize length." "Dependencies between items." "Many queries on a fixed array." Name the properties from the structure table.
Details5. Formulate a straightforward correct approach
"The simplest correct approach is to try every start and extend until the sum reaches k." Say it even if you already see better; it shows you can always produce a correct answer and gives a baseline to improve.
Details6. Analyze its time and space
"That is O(n^2) time and O(1) space. With n up to 10^5 that is 10^10 operations, too slow."
Details7. Identify repeated work or useful properties
Ask the optimization questions one by one: What is recomputed? Can order discard candidates? Can a window be maintained? Is checking easier than computing?
Details8. Derive the pattern and explain why it applies
"Because all values are positive, shrinking a valid window keeps the sum non-increasing, so once the window is valid I can move left without missing anything. That is a variable sliding window." State the required condition and why it holds.
Details9. Visualize the algorithm and track its state
Draw the array with two pointers, the tree with return values, or the DP table, for your small example. Walk two or three steps. This is where many bugs are found before they exist.
The figures in this handbook use conventions you can reproduce in a shared editor or on a whiteboard in under a minute:
| Structure | How to draw it | What to update as you step |
|---|---|---|
| Array, pointers, window | Values in a row, indices underneath, pointer names above with an arrow, a bracket under the window | Move the pointer labels; cross out discarded cells |
| Prefix sums | A second row under the array, one cell longer, starting with 0 | Circle the two cells you subtract |
| Linked list | Boxes with arrows; write prev, curr, next above the nodes |
Erase one arrow and draw its replacement per step |
| Stack, queue, deque | A vertical column (stack) or a horizontal row (queue); write indices, not only values | Write each push and pop as its own line, with the reason for the pop |
| Recursion, backtracking | A call tree: arguments in each node, return value next to it; mark pruned branches with an X | Fill in return values bottom-up |
| Tree | Nodes and edges; next to each node, what it returns to its parent | Annotate on the way up |
| Graph BFS | Nodes with distances written beside them; the queue as a row below | Strike nodes off the queue as they are processed |
| DP | A table with labeled rows and columns; an arrow from each cell to the cells it reads | Fill cells in the loop order you will code |
Details10. State the invariant or reasoning that guarantees correctness
One sentence: "After the inner loop, the window is the shortest valid window ending at right." If you cannot say it, keep working on step 9.
Details11. Implement clean Java
- Write the function signature and the edge-case returns first.
- Use descriptive names (
left,windowSum,best), noti2,tmp. - Narrate briefly: "Now I shrink while the window is valid."
- If a helper would make the main logic clearer (a feasibility check, a neighbor iterator), write it.
- Do not optimize prematurely; finish a correct version first.
Details12. Dry-run on a normal case and a boundary case
Trace the code, not your intention, line by line with your small example, writing variable values beside the code. Then run one boundary case: empty input, one element, all equal, no answer.
Details13. State and justify the final complexity
"Time O(n): each index enters and leaves the window once. Space O(1)." Mention trade-offs and possible follow-ups ("If values could be negative, I would switch to prefix sums with a monotonic deque").
Questions to ask yourself at each stage
| Stage | Ask yourself |
|---|---|
| Before designing | What would make this problem trivial? (Sorted input? Small n? Only positive values?) Which of those do I actually have? |
| Brute force | What exactly does my brute force recompute? Can I name the inner loop's job in words? |
| Optimizing | Which candidates can I prove irrelevant? What is monotonic here? What would I need to know in O(1) to skip the inner loop? |
| Choosing a pattern | What is its required condition, and does my small example satisfy it? What would break it? |
| Before coding | What are my variables and what does each mean exactly? What is the loop invariant? When do I update the answer? |
| After coding | What happens on the first iteration and the last? What if the loop never runs? Where can an index go out of bounds? Where can an int overflow? |
Recovery playbook
Details"I cannot identify the pattern"
- Go back to the small example and solve it by hand, slowly. Write down what you track.
- Classify the task type (existence, counting, optimization, search, ordering, connectivity, construction) and look at the families for it in the task type table.
- Look at the constraints: n = 20 allows exponential search; n = 10^5 demands O(n log n); a value range of 10^9 hints at binary search on the answer.
- Say the brute force out loud and ask what it repeats. The optimization is almost always hiding there.
- Ask the interviewer: "I am considering X and Y; I am not sure X's condition holds because of Z. Does that seem like the right direction?" Asking well is a positive signal.
Details"It looks like several patterns"
Write the required condition of each candidate and test it against your small example and one adversarial case. The one whose condition holds is the one. The decision tables list the deciding question for the common pairs.
Details"My brute force is too slow"
- Identify the innermost loop and name its job ("find the previous smaller element").
- Ask what structure answers that job faster: hash map (lookup), sorted order (binary search), monotonic stack (nearest larger or smaller), heap (current extreme), prefix sum (range totals), memo table (repeated subproblem).
- If the outer loop is the expensive part, ask whether order lets you skip candidates (two pointers, binary search on the answer).
Details"I understand solutions when I read them but cannot derive them"
This is the most common plateau, and the fix is in how you review:
- After reading a solution, close it and write down the one observation that made it work, in one sentence.
- Re-derive the solution on a blank page the next day, starting from the brute force, without looking.
- Mark the problem "Solved with hints" honestly and let the revision queue bring it back.
- For each new problem, before searching for the solution, spend 20 minutes on the decode method and write down where you got stuck. That note is what to compare with the editorial.
Details"My code passes the examples but fails hidden tests"
Check, in this order:
- Edge sizes: empty input, one element, two elements.
- Extreme values: negative numbers, zeros,
Integer.MAX_VALUEandMIN_VALUE, overflow in sums and products. - Duplicates: equal values in sorting, binary search, and backtracking.
- Boundaries in loops:
<vs<=,nvsn - 1, the last window, the final flush of a stack. - State reset: static or shared variables reused across test cases (LeetCode runs many cases on one instance).
- Misread requirements: subsequence vs subarray, strictly vs non-strictly increasing, 0-based vs 1-based.
Then write a brute-force checker and compare on small random inputs.
Details"Time Limit Exceeded" or "Memory Limit Exceeded"
| Symptom | Likely cause | Fix |
|---|---|---|
| TLE with an O(n^2) idea at n = 10^5 | Wrong algorithm | Return to step 7 |
| TLE with the right algorithm | Hidden O(n) operations inside loops: list.contains, list.remove(0), substring, string +, PriorityQueue.remove(Object) |
Use a set, ArrayDeque, indices, StringBuilder, lazy deletion |
| TLE in recursion | Missing memoization, or memo keyed by an object that is rebuilt each call | Memoize on primitive state; use arrays instead of maps where possible |
| TLE in BFS | Nodes enqueued many times | Mark visited when enqueuing |
| MLE | A full 2D DP table where one row suffices; storing all paths; huge recursion | Roll the table; store parents instead of paths; iterate |
| Stack overflow | Deep recursion on 10^5 nodes or large grids | Explicit stack or BFS |
Details"I am stuck in the interview and the clock is running"
- Say where you are. "I have a correct O(n^2) approach. I believe there is an O(n) one because ..., but I have not found the key observation yet." Silence is the worst option.
- Offer to code the brute force if 15 minutes or less remain. A working simpler solution beats an unfinished optimal one.
- Simplify the problem: solve it for sorted input, for positive values only, or for k = 1, then generalize.
- Use the hint well. When the interviewer gives a hint, repeat it in your own words and connect it to your example before changing code.
- Keep the code clean even when rushed; it is easier to fix and easier to discuss.
Communication checklist
- Restate the problem and confirm assumptions before designing.
- Say the brute force and its complexity before optimizing.
- Explain the key observation and the invariant before coding.
- Narrate while coding at the level of intent, not syntax.
- Test out loud with a small example and a boundary case.
- State the final complexity and one realistic follow-up or trade-off.
Roadmap and revision
The modules in prerequisite order, grouped into five phases, with a study loop that works at any pace, a revision system built into the problem statuses, and checkpoints that tell you when to move on.
The five phases
The phases follow the prerequisite map. Move to the next phase when you pass its checkpoint, not when a calendar says so. Faster is not better if the checkpoint is not honestly passed.
| Phase | Modules | Focus | Why here |
|---|---|---|---|
| 1. Foundations | 1, Decode, 2, 3, 4 | Complexity, hashing, prefix sums, two pointers, windows, binary search | Highest-frequency Medium patterns; everything later stores data in arrays and maps |
| 2. Linear structures and recursion | 5, 6, 7, 8 | Intervals, pointers, stacks, monotonic structures, recursion, backtracking | Pointer discipline and recursion are prerequisites for trees |
| 3. Trees and priorities | 9, 10, 11, 13 | Tree recursion, BSTs, heaps, greedy proofs, tries | Trees are the most common onsite topic; heaps and greedy reuse sorting |
| 4. Graphs and DP | 12, 14 | Traversal, shortest paths, ordering, connectivity, the DP derivation process | Both build on recursion and trees; they decide many Hard and late-round questions |
| 5. Integration | 15, 16, 17, mixed practice, mocks | Bits, math, design, interleaved review, timed rounds | Short modules plus the transfer from "I know the pattern" to "I can do it under time pressure" |
Modules 15 and 16 are short and depend only on Module 2. If you want variety during a long phase, slot them in anywhere after Phase 1.
The study session loop
Every session, whatever its length, follows the same order:
- Revision queue first. Re-solve everything due (shown on the home page and in the tracker). Start from a blank editor. Update each status.
- One lesson, practice first. Attempt its "Try it first" problem, then read the short part, step through the figure, and work the guided solve. Tick "Studied" when you could teach it.
- New problems from the current bank, in order. Use the time limits below.
- Write one line per problem (in your own notes): the key observation, in one sentence.
Time limits per attempt
| Difficulty | Think before coding | Total before a hint | After the hint |
|---|---|---|---|
| Medium | 10 to 15 minutes | 30 minutes | Read only the key idea, close it, implement yourself |
| Hard | 15 to 20 minutes | 45 minutes | Read the approach, re-derive it the next day |
Hint ladder. Do not jump to the full solution. In order: re-read the pattern card for the bank; look only at the problem's topic tags; read the first paragraph of an editorial; read the full approach; read code last, if at all.
The revision system
Problem statuses are the revision system. The five study categories map to statuses, and each status schedules the next review:
| Category | Status to set | Review again in | What the review should be |
|---|---|---|---|
| Not solved independently | Not solved independently | 2 days | Re-derive from the brute force; do not look at the old code |
| Solved with hints | Solved with hints | 4 days | Solve clean, then explain the observation out loud |
| Solved independently | Solved independently | 10 days | Timed re-solve (Medium in 20 minutes) |
| Needs revision | Needs revision | 1 day | A previously solved problem that went badly on review |
| Pattern mastered | Pattern mastered | 30 days | Quick check that it is still automatic |
Promotion rules. Move a problem up only after a clean re-solve at its review date: hints -> independently -> mastered (mastered requires two clean solves at least a week apart, plus being able to state the invariant). Move it to "Needs revision" whenever a review fails, regardless of its previous status.
Why spaced review works. Re-solving after a gap forces you to rebuild the reasoning instead of recognizing the code. The intervals grow because each successful retrieval makes the next forgetting slower.
Mixed practice and timed sets
Banks are grouped by pattern, which makes recognition easy: you already know the pattern when you open the bank. Interviews do not tell you. From Phase 3 onward, add interleaved practice:
- Once a week, pick 3 to 4 problems from different earlier modules using the tracker (filter by priority High, any module, status "Solved independently" or "Not started"). Do not look at which module they came from before attempting.
- Classify each problem before solving it: write the pattern and the required condition, then check.
Timed sets. From Phase 2 onward, once a week, do two Mediums in 45 minutes with the full 13-step interview framework, talking out loud. Record yourself or work with a partner.
Mock interviews
| When | How often | Format |
|---|---|---|
| Phase 3 | Once a week | 45 minutes, one Medium, with a peer or a recording; review against the communication checklist |
| Phase 4 | Once or twice a week | Mix one Medium with one harder follow-up |
| Phase 5 | Two or more a week | Full loop practice: two coding rounds back to back |
After each mock, write three lines: what slowed you down, which step of the framework you skipped, and one thing to practice. Feed missed problems into the tracker as "Not solved independently".
Revision checkpoints
Tick each item only when it is true with notes closed. These checkpoints save with your progress.
Phase 1 checkpoint
Phase 2 checkpoint
Phase 3 checkpoint
Phase 4 checkpoint
Phase 5 checkpoint
Practice tracker
All 407 problems in one place, Medium and Hard only. Filter by module, status, difficulty, or priority; choose "Due for revision" to see today's review list. Changing a status here updates it everywhere in the handbook.
Topic mastery overview
The module cards on the home page show solved problems, studied lessons, and mastery items per module. A module is ready for interviews when its lessons are studied, about 70% of its bank is solved independently (including at least one challenge problem), and all eight mastery items are ticked.
Backup and restore
Progress saves automatically. Use this to move progress between browsers when you are not signed in, or to keep a copy. Restoring merges: the newer entry wins for each problem and checkbox.
SDE-2 readiness check
Use this page a few weeks before interviews and again the week before. Every item should be true without notes. If an item is not, the linked module or framework page is where to go.
Pattern recognition
Derivation
Implementation in Java
Interview performance
Evidence from your tracker
When every box is ticked, keep the revision queue running and shift most of your time to mock interviews and mixed sets until the interview.