The previous stages gave you tools. This one is about recognising which tool an unfamiliar problem calls for.
Weeks 16 to 32 built a toolbox. This stage is about diagnosis — looking at a problem you have never seen and knowing, within a few minutes, what kind of problem it is.
Why this stage exists
This is the part that actually transfers to engineering work. Real problems do not arrive labelled. The skill of recognising “this is a graph problem wearing a disguise” is what separates knowing the from being able to use them.
NeetCode 150 · complete category
Two Pointers practice lab
Use two coordinated positions to eliminate impossible choices without repeatedly scanning the same range.
5 problems
Pattern first
Two pointers: discard impossible pairs
1 + 12 = 13 — if too small move L; if too large move R.
Naive
All-pairs testing repeats comparisons sorted order has already made impossible.
Why this works
If a sorted pair is too small, only a larger left value helps; if too large, only a smaller right value helps.
Check whether A man, a plan, a canal: Panama is a palindrome.
Picture itTwo proofreaders start at opposite covers, ignoring punctuation and comparing the next real letters.
Conceptual blueprint
Move inward from both ends, skip non-alphanumeric characters, compare lowercase letters, and stop on a mismatch.
Animated dry-runFrame 1
1 / 6
0A▲ L
1m
2a
3n
4a
5P
6a
7n
8a
9m
10a▲ R
L at index 0, R at index 10
Lowercased A on the left equals the final a; punctuation between displayed letters is skipped by lines 4–5.
paira = a
1def is_palindrome(text):
2 left, right = 0, len(text) - 1
3 while left < right:
4 while left < right and not text[left].isalnum(): left += 1
5 while left < right and not text[right].isalnum(): right -= 1
6 if text[left].lower() != text[right].lower(): return False
7 left += 1
8 right -= 1
9 return True
Check yourself before reading onCost it first
What does the in-place two-pointer check trade compared with cleaning and reversing?
Find the bug
Which missing guard can read outside the string?
def is_palindrome(text):
left, right = 0, len(text) - 1
while left < right:
while not text[left].isalnum(): left += 1
while not text[right].isalnum(): right -= 1
if text[left].lower() != text[right].lower(): return False
left += 1
right -= 1
return True
Predict the next step
After the a pair matches, which significant pair is compared next?
Fill in the blanks
Recall each hidden line before revealing it. 3 left.
1def is_palindrome(text):
2 left, right = 0, len(text) - 1
3 while left < right:
4
5
6
7 left += 1
8 right -= 1
9 return True
Why the naive approach falls short
Build a cleaned lowercase copy and reverse it, which is simple but allocates memory proportional to the text.
Why the optimized approach works
Every significant character has exactly one mirrored partner, so inward pointers verify pairs without copying the string.
Time
O(n) — each pointer crosses the text once.
Space
O(1) — comparisons happen in place.
NeetCode 150 · complete category
Sliding Window practice lab
When the answer is about a contiguous run, move one boundary forward and repair the other instead of rebuilding the run from scratch.
2 problems
Pattern first
A window that only ever moves forward
Naive
Re-examining every substring or subarray from its own start repeats work the previous window already did.
Why this works
Both boundaries only advance, so each element enters the window once and leaves once — 2n moves, not n² of them.
Picture itA tour guide keeps a rope around a group; when a duplicate face appears, the rope's back end jumps forward past the earlier copy.
Conceptual blueprint
Remember where each character was last seen; when one repeats inside the window, move the start just past its previous position.
Animated dry-runFrame 1
1 / 8
0a▲ start i
1b
2c
3a
4b
5c
6b
7b
start at index 0, i at index 0
The window is just "a"; record that a was last seen at index 0.
best1
Lookup structure
a → 0
1def length_of_longest(s):
2 last_seen = {}
3 start = best = 0
4 for i, ch in enumerate(s):
5 if ch in last_seen and last_seen[ch] >= start:
6 start = last_seen[ch] + 1
7 last_seen[ch] = i
8 best = max(best, i - start + 1)
9 return best
Check yourself before reading onCost it first
What does storing last-seen positions buy over restarting the scan at each index?
Find the bug
On "abba" this reports 3. Which character is it wrongly counting?
def length_of_longest(s):
last_seen = {}
start = best = 0
for i, ch in enumerate(s):
if ch in last_seen:
start = last_seen[ch] + 1
last_seen[ch] = i
best = max(best, i - start + 1)
return best
Predict the next step
The a at index 3 has just moved start to 1. When the b at index 4 is read next, where does start go?
Fill in the blanks
Recall each hidden line before revealing it. 3 left.
1def length_of_longest(s):
2 last_seen = {}
3 start = best = 0
4 for i, ch in enumerate(s):
5
6
7 last_seen[ch] = i
8
9 return best
Why the naive approach falls short
Start a fresh scan at every index and stop at the first repeat, re-reading almost the whole string each time.
Why the optimized approach works
The start boundary never moves backwards, so every character is entered and left exactly once.
Time
O(n) — each index is read once and start only moves forward.
Space
O(k) — one entry per distinct character.
NeetCode 150 · complete category
1-D Dynamic Programming practice lab
When the answer at position i depends only on a couple of earlier answers, carry those answers forward instead of recomputing them.
2 problems
Pattern first
A recurrence you can hold in two variables
Naive
Plain recursion rebuilds the same subproblem an exponential number of times.
Why this works
Each position's answer is a fixed function of the ones just before it, so a single forward pass carrying the last two values is enough.
Picture itWalking a decision tree with a rucksack: you put an item in, walk deeper, then take it out again on the way back.
Conceptual blueprint
Record the current path, then for each remaining index add it, recurse from the next index, and remove it.
Animated dry-runFrame 1
1 / 8
01
12
23
The empty path is recorded first — the empty set is a subset too.
path[]start0
Lookup structure
result → [[]]
1def subsets(nums):
2 result = []
3 path = []
4 def explore(start):
5 result.append(path[:])
6 for i in range(start, len(nums)):
7 path.append(nums[i])
8 explore(i + 1)
9 path.pop()
10 explore(0)
11 return result
Check yourself before reading onCost it first
Why is `path[:]` a copy rather than just `path`?
Find the bug
The pop is missing. What does the path look like by the end?
def subsets(nums):
result = []
path = []
def explore(start):
result.append(path[:])
for i in range(start, len(nums)):
path.append(nums[i])
explore(i + 1)
explore(0)
return result
Predict the next step
3 has just been popped and the path is [1, 2]. What happens next?
Fill in the blanks
Recall each hidden line before revealing it. 3 left.
1def subsets(nums):
2 result = []
3 path = []
4 def explore(start):
5 result.append(path[:])
6 for i in range(start, len(nums)):
7
8
9
10 explore(0)
11 return result
Why the naive approach falls short
Generate strings of choices and re-parse them into lists afterwards.
Why the optimized approach works
Every subset is a distinct path through the choice tree, and recursing from i + 1 fixes the order so no subset appears twice.
Time
O(n · 2ⁿ) — 2ⁿ subsets, each copied once.
Space
O(n) working space beyond the output — one path and one frame per level.
NeetCode 150 · complete category
2-D Dynamic Programming practice lab
When a subproblem is identified by two indices rather than one, the table becomes a grid — and often only its last row is needed.
2 problems
Pattern first
Two moving parts, one table
Naive
Recursing on both indices recomputes the same (i, j) pair enormously often.
Why this works
Each cell depends only on neighbours already computed, so filling the grid in order answers every subproblem exactly once.
Picture itWater poured on the top-left corner, where each cell pools everything arriving from above and from the left.
Conceptual blueprint
Every cell's count is the cell above plus the cell to its left; the first row and first column are all 1.
Animated dry-runFrame 1
1 / 6
01
11
21
One row of cells is reused for each row of the grid. There is exactly one way to reach any cell in the top row: keep moving right.
meaningthe grid's top row
Lookup structure
row → [1, 1, 1]
1def unique_paths(rows, cols):
2 row = [1] * cols
3 for _ in range(rows - 1):
4 for c in range(1, cols):
5 row[c] += row[c - 1]
6 return row[-1]
Check yourself before reading onCost it first
Why is one row enough instead of the full rows × cols table?
Find the bug
The inner loop starts at 0. What breaks?
def unique_paths(rows, cols):
row = [1] * cols
for _ in range(rows - 1):
for c in range(cols):
row[c] += row[c - 1]
return row[-1]
Predict the next step
The row reads [1, 2, 3] after the first pass. What is the middle cell after the next pass?
Fill in the blanks
Recall each hidden line before revealing it. 3 left.
1def unique_paths(rows, cols):
2
3 for _ in range(rows - 1):
4
5
6 return row[-1]
Why the naive approach falls short
Enumerate every sequence of moves, which is exponential in the grid size.
Why the optimized approach works
A path reaching a cell arrived from exactly one of two neighbours, and those two sets of paths never overlap.
Time
O(rows × cols) — each cell is written once.
Space
O(cols) — only one row is kept, not the whole grid.
What you are learning — 7 topics
Brute force
every candidate the rules allow and keeps the ones that work.
Reach for it whenUse brute force instead of a clever algorithm when the candidate space is small or as a correctness oracle; do not use it when combinations grow beyond the available time.
Write the obvious, exhaustive version first. It is quick to write and almost certainly correct, and that makes it your reference: when you write the fast version, you can run both on random inputs and check they agree. Optimised code is where bugs hide, and a known-correct baseline is the cheapest testing you will ever do.
It also tells you whether optimising is needed at all. If the input is never more than fifty items, the brute-force version may simply be the right answer, and time spent making it clever is time wasted.
Syntax you can use
for left in range(len(nums)):
for right in range(left + 1, len(nums)):
if nums[left] + nums[right] == target:
return left, right
Checks every pair, a clear first solution for two-sum.
from itertools import combinations
Imports a generator for choosing groups without writing nested loops.
for pair in combinations(nums, 2):
Visits every two-item combination.
best = min(candidates, key=score)
Chooses the best answer after trying every candidate.
Notice how quadratic work separates from a linear scan; brute force is a baseline whose feasible input size must be measured.
Several ways to see it
Why it existsThe obvious complete search is the first correctness oracle
Trying every candidate makes few structural assumptions and is therefore hard to misunderstand. Even when too slow for production, it supplies expected answers for small random cases used to test an optimized solution.
What it costsThe exponent decides when hardware stops helping
Quadratic enumeration may be fine for hundreds of items, while `2^n` can become impossible around a few dozen. Estimate candidate count times work per candidate before optimizing inner syntax.
Against the alternativePruning keeps completeness while avoiding doomed branches
A pruned search is still exhaustive over candidates that could succeed. Greedy or heuristic methods may skip valid candidates entirely, gaining speed by giving up either certainty or generality.
In the wild
Hashcat — can enumerate password candidates from masks and test each candidate against a target hash.
pytest parametrize — runs the same test body across an explicitly enumerated set of input cases.
Commonly gets misread as
"Brute force is always bad engineering." Correction: for small bounds it is simple, auditable, and often fastest enough.
"Faster hardware fixes exponential growth." Correction: one extra input dimension can multiply the search beyond reach.
splits one large job into similar smaller jobs, solves them, then combines their answers.
Reach for it whenUse it instead of one shared-state loop when subproblems are independent and combine cleanly; avoid it when splitting duplicates most of the work.
Merge sort is the canonical example: split in half, sort each half, merge. Quicksort is another. The shape recurs constantly once you look for it.
The Master Theorem is a formula for working out the Big-O of a divide-and-conquer algorithm from three facts: how many pieces you split into, how much smaller each piece is, and how much work the combining takes. It is worth being able to apply, and not worth memorising the proof of.
Syntax you can use
mid = len(items) // 2
Finds a midpoint for splitting a problem in two.
left = solve(items[:mid])
right = solve(items[mid:])
Solves two smaller halves recursively.
return combine(left, right)
Combines the two solved halves into one answer.
if len(items) <= 1:
return items
Stops when the problem is already small enough to answer directly.
Notice that balanced splitting creates only three levels for eight items; each level still covers all eight during merging.
Several ways to see it
Its shapeThe recursion tree reveals both depth and duplicated work
Balanced halves create about log n levels, while the total work across each level determines the final cost. Merge sort does O(n) combining per level and therefore totals O(n log n).
What the machine doesThe combine step is where separate answers become useful
Splitting alone merely creates smaller inputs. The algorithm succeeds because sorted halves can be merged linearly, or independently solved regions can be reconciled without reopening every internal decision.
How it breaksUneven splits can erase the logarithmic depth
Quicksort with a consistently extreme pivot produces subproblems of sizes zero and n minus one. The tree becomes a chain and comparison work becomes quadratic.
In the wild
Hadoop MapReduce — runs map work across input splits and combines grouped results in reduce tasks.
Python multiprocessing Pool — distributes independent input chunks to worker processes and gathers their results.
Commonly gets misread as
"Splitting makes the total work disappear." Correction: partitioning and combining still cost time.
"Every recursive algorithm is divide and conquer." Correction: the subproblems must be meaningfully separate and recombined.
A commits to the best-looking move now without reopening earlier choices.
Reach for it whenUse greedy instead of only when an exchange argument or invariant proves local choices lead to a global optimum; otherwise test for counterexamples.
Giving change is the intuitive example: to make 87p, take the largest coin that fits and repeat. With normal currency this always gives the fewest coins. With a made-up currency it does not, and that is exactly the danger.
A greedy approach is correct only when the problem has two properties. The greedy choice property: taking the locally best option never rules out the overall best answer. And optimal substructure: the best answer contains the best answers to its sub-parts. If you cannot argue that both hold, do not trust a greedy solution — test it against brute force on small random inputs.
Syntax you can use
intervals.sort(key=lambda interval: interval[1])
Orders intervals by earliest finishing time for interval scheduling.
if start >= last_end:
chosen.append((start, end))
last_end = end
Takes the next compatible interval after choosing the earliest finish.
for coin in sorted(coins, reverse=True):
Considers the largest available coin first.
take = min(remaining // coin, available)
Takes as much of a locally best choice as the constraints allow.
Several ways to see it
What the machine doesA greedy step permanently narrows the future
At each state the algorithm takes the locally best admissible move and never revisits it. The proof burden is showing that some optimal solution can always be rearranged to include that move.
Against the alternativeDynamic programming pays to preserve competing futures
Greedy keeps one promising choice and is usually simpler and lighter. Dynamic programming retains values for multiple states when a locally worse choice may enable a better total later.
How it breaksCoin systems expose how plausible rules fail
With coins 1, 3, and 4, taking the largest coin first makes 6 as 4 + 1 + 1. The optimum is 3 + 3, so a familiar greedy rule needs a proof tied to the input structure, not confidence from examples.
In the wild
gzip — builds Huffman codes by repeatedly combining the two least-frequent nodes.
NetworkX Kruskal MST — takes the cheapest safe edge next while skipping edges that would make a cycle.
Commonly gets misread as
"Choosing the current best usually gives the final best." Correction: it does only for problems with the right structure.
"Greedy means no ." Correction: heaps and union-find often make the next safe choice efficient.
builds one possible answer, abandons it as soon as it cannot work, and returns to the last choice point.
Reach for it whenUse backtracking instead of flat brute force when partial choices can be rejected early; use dynamic programming when many branches repeat the same subproblem.
Think of it likeSolving a maze. Walk down a corridor, and reverse out the instant you hit a dead end rather than continuing to stare at the wall.
It is exhaustive search with pruning. You still explore possibilities, but you cut off entire branches as soon as they are provably hopeless, which can remove astronomically many combinations without ever checking them.
Solving Sudoku is the standard example: place a digit, check whether the grid is still valid, and if not, undo it immediately rather than filling the remaining eighty squares first.
Syntax you can use
def search(path):
if complete(path):
answers.append(path.copy())
return
Ends one branch when it forms a complete answer.
for choice in choices(path):
path.append(choice)
search(path)
path.pop()
The choose, explore, un-choose shape for exhaustive search.
if not valid(path):
return
Prunes a branch as soon as it cannot lead to a valid answer.
answers.append(path.copy())
Saves the current answer without sharing the mutable path.Appending path itself saves repeated references to one list, which later pops will empty.
Notice that rejecting the 10 prefix removes both completions below it; pruning saves entire subtrees, not single guesses.
Several ways to see it
Its shapeThe search space is a tree of partial decisions
Each level commits one more choice and each child represents an alternative. A leaf is either a complete solution or a dead end; pruning cuts a whole subtree before enumerating its leaves.
What the machine doesChoose, recurse, and undo must be symmetric
The algorithm mutates a shared partial solution before descending and reverses exactly that mutation afterward. A missing undo leaks one branch's state into its siblings and produces failures far from the faulty line.
What it costsA strong constraint is an exponential optimization
Rejecting one impossible prefix can eliminate millions of complete candidates beneath it. Check cheap, selective constraints early and choose the next variable with the fewest legal options when possible.
In the wild
CPython re — uses a backtracking regex engine that can revisit earlier match choices when a later token fails.
OR-Tools CP-SAT — searches combinatorial assignments while propagating constraints and backtracking from conflicts.
Commonly gets misread as
"Backtracking is just recursion." Correction: recursion carries the search, but pruning invalid partial choices is the point.
"It makes exponential search polynomial." Correction: pruning helps typical cases but worst-case growth can remain exponential.
Dynamic programming solves each repeated smaller problem once and reuses its answer wherever it appears again.
Reach for it whenUse it instead of backtracking when branches revisit the same state and the full answer is built from optimal smaller answers; avoid it when states do not repeat.
Think of it likeWorking out a long sum and writing intermediate totals in the margin, rather than re-adding the same column every time you need it.
Go back to the recursive Fibonacci you wrote in week 15. To compute Fibonacci of 40 it computes Fibonacci of 38 twice, Fibonacci of 37 three times, and so on — hundreds of millions of calls for an answer involving forty numbers. Add a dictionary that remembers each answer the first time and it returns instantly. That is the entire idea.
There are two ways to write it. is top-down: keep the natural recursion and cache the results. In Python, adding @functools.cache above a function does this for you in one line, which makes it a very cheap experiment. Tabulation is bottom-up: work out the smallest cases first and build up in a table, avoiding recursion entirely.
Syntax you can use
from functools import cache
Imports memoisation for a top-down dynamic programme.
@cache
def ways(n):
if n <= 1:
return 1
return ways(n - 1) + ways(n - 2)
Turns repeated recursive subproblems into one cached result each.
dp = [0] * (n + 1)
Allocates a table with one answer slot per subproblem.
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
Builds answers bottom-up after their dependencies exist.
dp = [[0] * cols for _ in range(rows)]
Creates independent rows for a two-dimensional table.[[0] * cols] * rows repeats the same row object, so one write appears in every row.
Notice that both branches request F(3); caching turns the second exponential subtree into one lookup.
Several ways to see it
Why it existsRepeated subproblems turned recursion into wasted work
Naive recursive Fibonacci recomputes the same smaller values through many branches. Dynamic programming names each distinct state and solves it once, replacing an exponential call tree with a table or cache.
Against the alternativeMemoization follows demand; tabulation controls order
A memoized recursion calculates only states reached from the query and often mirrors the recurrence clearly. Bottom-up tabulation avoids call overhead and makes dependencies and memory reuse explicit, but may fill states the query never needs.
How it breaksThe wrong state loses information the future needs
If two partial solutions share a table cell but have different consequences for later choices, the recurrence cannot be correct. Define state as the smallest summary that makes every future decision independent of the path used to reach it.
In the wild
GNU diff — uses Myers' sequence-difference algorithm to reuse progress across edit paths while finding a short edit script.
SQLite spellfix1 — computes edit distance to rank vocabulary words against a misspelled query.
Commonly gets misread as
"Dynamic programming means writing a table." Correction: the table or cache only stores answers to a repeated-state recurrence.
"Memoization makes every recursion fast." Correction: it helps only when different calls reach the same state.
and windows keep a few moving boundaries so one pass can reuse what the previous position already knew.
Reach for it whenUse them instead of nested scans on ordered sequences or contiguous ranges when adding and removing boundary items updates the answer; use prefix sums for many fixed range totals.
keeps track of two positions at once. On sorted data, to find a pair adding to a target, start one pointer at each end: if the sum is too big move the right one in, if too small move the left one out. One pass instead of checking every pair.
A looks at a stretch of the list and moves it along, adding the item entering and removing the item leaving, rather than re-examining the whole stretch each time. Any question about the best run of k consecutive items is this.
Syntax you can use
left, right = 0, len(nums) - 1
Starts two pointers at opposite ends of a sorted list.
while left < right:
total = nums[left] + nums[right]
if total < target:
left += 1
else:
right -= 1
Moves the useful pointer after comparing a pair sum with the target.
left = 0
for right, value in enumerate(nums):
Grows a sliding window one item at a time.
while window_sum > limit:
window_sum -= nums[left]
left += 1
Shrinks a window until it satisfies its constraint.
Notice that moving the window right reuses the three middle values; only one value leaves and one enters.
Several ways to see it
Its shapeAll three methods reuse overlap between neighbouring questions
Pointers mark changing boundaries, a window carries the aggregate between those boundaries, and a prefix stores every aggregate from the start. Each avoids recounting the unchanged middle of nearby ranges.
Against the alternativeWindows move online; prefix sums answer later
A sliding window is ideal when one pass can maintain exactly the current range and its validity. Prefix sums spend O(n) memory up front so arbitrary static range sums become constant-time queries.
How it breaksNegative values can destroy monotonic window logic
Shrinking a sum-too-large window works for non-negative values because removing from the left cannot increase the sum. With negatives, expansion and contraction no longer move the condition predictably, so a prefix-sum map may be required.
In the wild
TCP — uses a sliding receive window to advertise which byte range the receiver can currently accept.
Transformer context windows — limit attention and retained tokens to a bounded span when processing long sequences.
Commonly gets misread as
"A sliding window must have fixed length." Correction: its ends can move independently to maintain a condition.
"Two pointers means two nested loops." Correction: each pointer usually advances only forward, keeping total work linear.
What it costsEach typically reduces an O(n²) approach to O(n).
Bit manipulation treats an integer as a row of on-off switches that can be tested or changed together.
Reach for it whenUse bit masks instead of many booleans when flags are compact and combinations matter; use named fields when readability and future expansion matter more.
A useful first one: exclusive-or has the property that a number combined with itself cancels to zero. So combining every item in a list where everything appears twice except one leaves exactly that one, using no extra memory at all.
Bitmasks let a single number carry many yes/no flags at once — one bit per permission, per feature toggle, per option. Compact and fast to check.
Syntax you can use
n & 1
Reads the last bit: 1 for odd n, 0 for even n.
n >> 1
Shifts bits right by one, like floor-dividing a non-negative integer by two.
n | (1 << bit)
Sets a numbered bit to 1.
n & ~(1 << bit)
Clears a numbered bit to 0.
n ^ (1 << bit)
Flips a numbered bit.
n & (n - 1)
Clears the lowest set bit, useful for counting set bits.
Notice that one integer stores four independent yes-or-no flags; AND can inspect any chosen position without unpacking the others.
Several ways to see it
Its shapeA bitmask is a row of independent switches
Bit position i can encode whether option i is present. OR turns switches on, AND tests or filters them, XOR toggles differences, and a shift moves the whole pattern across positions.
What the machine doesTwo's complement makes negative bits conceptually unbounded
Python integers do not have a fixed visible word size, so negative values behave as if sign bits continue forever. Mask with `(1 << width) - 1` when modelling a 32-bit register or protocol field.
Against the alternativeA set is clearer until representation is the problem
For ordinary membership logic, Python's set names the intent and handles arbitrary elements. A bitmask earns its density when the universe is small and fixed, or when hardware flags, permissions, subsets, or binary formats are the subject.
In the wild
Unix file permissions — stores read, write, and execute flags as bits combined into a mode value.
PostgreSQL bitmap scans — combines bitmap index results before visiting the matching heap pages.
Commonly gets misread as
"Bit tricks are automatically faster." Correction: clarity and the compiler often matter more than replacing ordinary arithmetic.
"A bit mask can store any number of flags." Correction: it is limited by the chosen integer width unless expanded.
Reading about this stage is not the same as finishing it. Type every one of these from a blank file rather than copying — the writing is the part that teaches.
The dynamic programming ladder
Work through Fibonacci, climbing stairs, coin change, 0/1 knapsack, longest common subsequence, longest increasing subsequence and edit distance — in that order. Write each one both memoised and tabulated. This is the single biggest time investment of the stage and it is correctly placed.
Write a tiny diff tool
Use longest common subsequence to compare two text files and print which lines were added and removed. You will have written a simplified version of what git shows you every day.
Break a greedy algorithm on purpose
Write greedy coin change, then invent a currency where it gives the wrong answer, and confirm it by comparing against a brute-force solution. Experiencing greedy failing is what stops you trusting it blindly.
Sudoku solver
Backtracking, end to end. Choose, explore, un-choose. Then add better pruning — always filling the most constrained square next — and measure how much faster it gets.
You are done with this stage when
Exit checkGiven a problem you have never seen, you can name the likely approach within five minutes and justify why. Around 300 problems in total — though see the note below on volume.
If you cannot do this, repeat the stage rather than moving on. Nothing later gets easier by skipping it.
Worth knowing before you start
Dynamic programming is where people quit. It is normal for it to take several attempts before it clicks. Work the ladder in order rather than jumping to hard problems.
The 300-problem figure is calibrated for interview preparation. For general engineering skill, around 150 well-understood problems beat 300 rushed ones. Depth over volume.
This stage corresponds to DSA Phase 5 — Problem-Solving Paradigms in the source roadmap.