PROJECT · 75 minutes
A repeatable four-move method for unfamiliar problems — decompose, reason from examples, time-box, debug systematically — with a full worked walkthrough.
Knowing every algorithm in this path and actually cracking an unfamiliar problem are two different skills. The gap is not knowledge — it is method. I have watched strong engineers stall on problems well within their ability, not because they lacked the technique, but because they had no repeatable procedure for the moment when the path forward is not obvious.
The last time I hit this hard was when I was debugging a subtle ordering issue in my orchestration framework's DAG executor. Jobs were completing in the wrong sequence on certain diamond-shaped dependency graphs. I had all the right primitives — topological sort, priority queues, concurrency locks — but the bug eluded me for two hours because I kept patching symptoms without forming a hypothesis about the root cause.
When I finally stopped and forced myself to draw the smallest possible diamond graph on paper, trace each step by hand, and ask "what assumption in my topological sort is wrong?", the bug was obvious in five minutes. That is the method: decompose the problem, reason from a concrete example, and debug with a hypothesis — not with hope.
This module treats hard problems as a process you can rehearse. The method has four moves. The rest of the module is organized around them.
Restate the problem in your own words. Then ask: what smaller problems do I already know how to solve that this reduces to? Most hard problems are composed of two or three easy sub-problems, each of which you have solved before under a different name.
Example decomposition: "Find all substrings of S that are permutations of P" decomposes into:
That is three patterns from this path. None of them is new. The novelty is only in how they compose.
Work two or three concrete inputs by hand before writing code. Not toy examples — ones that include the edge cases the problem describes.
The goal is not to verify your solution. It is to discover the algorithm. The pattern you find by tracing a specific input is almost always the one that generalizes.
What to trace:
Set a budget for any single approach. When the timer expires, step back and try a different decomposition — do not push harder on a failing idea. For a medium-difficulty problem in a 60-minute session, 15-20 minutes per approach is a reasonable box.
The failure mode this prevents is rabbit-holing: committing to the first idea and spending the entire session patching it. Rabbit-holing feels like progress because code is being written. It is not progress if the approach is fundamentally wrong.
When this bites: the most common rabbit-hole is an O(N^2) brute-force that almost works. You can often get it to pass on small test cases, which makes the approach feel close. The time-box forces you to ask "is this the right algorithm?" before "why is it failing on this test case?"
When a solution is wrong, form one hypothesis about why, test it against a small input, and narrow the search. Never guess-and-tweak.
The hypothesis loop:
flowchart TD
A["Problem arrives"] --> B["Decompose into sub-problems"]
B --> C["Reason from 2-3 concrete examples"]
C --> D["Implement first approach"]
D --> E{"Correct?"}
E -->|Yes| F["Done"]
E -->|No, time remaining| G["Form one hypothesis\nabout why it's wrong"]
G --> H["Trace smallest failing input"]
H --> I{"Hypothesis confirmed?"}
I -->|Yes| J["Fix the specific bug"]
I -->|No| G
J --> E
E -->|No, time-box expired| K["Step back — try\ndifferent decomposition"]
K --> B
style F fill:#4ade80Let's apply the four-move method to a genuinely hard problem.
Problem: Given heights of bars in a histogram, compute how much water can be trapped after rain. heights = [0, 1, 0, 2, 1, 0, 1, 3, 2, 1, 2, 1] — answer: 6.
Restate: at each position, how much water sits above it? Water level = min(max_left, max_right) - height[i]. That decomposes into three sub-problems:
max(0, min(left_max[i], right_max[i]) - heights[i]) for each position.Each sub-problem is a linear scan. The hard problem is three easy ones composed.
Trace heights = [3, 0, 2] by hand.
Total: 2. Draw it: a bar of height 3 on the left, empty in the middle, a bar of height 2 on the right. The middle cell collects water up to height 2.
After the two-pass solution works, you might wonder: can you reduce to O(1) space? This is where reasoning from examples pays off again.
The insight: when left_max < right_max, the water at the left pointer is determined solely by left_max — the right side is already taller, so whatever the actual right maximum is, it cannot reduce the water below left_max. You can process the left pointer without knowing the exact right maximum.
The progression pattern: easy decomposition → working O(N) space solution → insight from tracing → optimized O(1) space. This is almost always the right order. Jumping to the optimized solution first is usually a dead end because the insight that enables the optimization only becomes clear after you understand the simpler version.
Try these from memory:
left_max[i] record the max of heights[0..i] rather than the max of heights[0..i-1]?Reflective prompt: The four-move method is designed for constraint-filled situations — a timer, a blank editor, an unfamiliar problem. How does your usual debugging process at work differ from this? Where do you rabbit-hole, and what personal signal tells you it's time to step back? Naming your own failure modes is what makes the recovery moves automatic rather than aspirational.
Questions on the four-move method, failure modes, and systematic debugging
1. You have spent 45 minutes on one approach that isn't working, and you keep finding ways to patch it. Which failure mode is this, and what is the recovery move?
Correct Answer: B
Committing to one approach and refusing to abandon it despite evidence it isn't working is rabbit-holing. The recovery move is to stop, acknowledge the time-box has expired, and start a different decomposition — not to patch the failing approach further.
2. Before writing any code for a new problem, the most effective first step is:
Correct Answer: B
Working concrete examples by hand forces you to simulate the algorithm you haven't yet written. Patterns and constraints that are invisible in the abstract problem statement become obvious when you trace specific inputs. This is faster than staring at the problem and faster than debugging a premature implementation.
3. You are given Trapping Rain Water: for each cell, the trapped water equals the minimum of (max height to the left, max height to the right) minus the cell's height. What is the key sub-problem decomposition?
Correct Answer: B
The sub-problem decomposition: (1) build left_max[i] = max of heights[0..i]; (2) build right_max[i] = max of heights[i..n-1]; (3) water[i] = max(0, min(left_max[i], right_max[i]) - heights[i]). Each sub-problem is a simple linear scan — the hard problem becomes three easy ones.
4. Your solution passes 49 of 50 test cases. The failing case is `[3, 0, 2, 0, 4]`. What is the most effective debugging approach?
Correct Answer: B
Systematic debugging starts with a hypothesis: what specific thing might be wrong? Trace the failing input through your code with that hypothesis in mind. Confirm or refute it — if refuted, form a new hypothesis. This is always faster than either scattering print statements everywhere or staring at the code hoping to spot the bug.
5. You have 60 minutes for a problem. A reasonable time-box for your first approach before stepping back is:
Correct Answer: C
A time-box of 15-20 minutes is enough to attempt a complete solution on a medium-difficulty problem. If you are not making progress after that time, you have evidence the approach is wrong — not just slow. Stepping back at this point costs little but can save the remaining 40+ minutes.
6. The two-pointer approach for Trapping Rain Water uses left and right pointers converging from both ends. When left_max < right_max, you process the left side. Why?
Correct Answer: B
Water at position left = min(left_max, right_max) - heights[left]. When left_max < right_max, the bottleneck is left_max — you already know right_max is at least left_max, so the actual right maximum (which we haven't seen fully) doesn't matter. You can safely compute the water and advance the left pointer.
This is a multi-step exercise. Do not skip the decomposition and reasoning steps — they are the practice.
Problem: Container With Most Water. Given heights[i] (heights of vertical lines), find two lines that together with the x-axis form a container holding the most water. Return the maximum water volume.
Step 1: Decompose in writing (before any code). The water in a container formed by lines at index i and j is:
Your task: find the pair (i, j) that maximizes this.
Step 2: Reason from examples. Trace heights = [1, 8, 6, 2, 5, 4, 8, 3, 7] for the pairs (1, 8) and (0, 8) by hand. Which gives more water?
Hint 1: Use two pointers starting at both ends. The area shrinks as pointers converge (width decreases). To maximize, you want the height to increase — so move the pointer pointing at the shorter line.
Hint 2: Exchange argument: if you move the pointer at the taller line, you keep the bottleneck (the shorter line) and lose width — the area can only decrease. Moving the shorter line at least gives the area a chance to increase if you find a taller line.
Nice work. Here's what's next.
Back to this pathOr book the diagnostic call