The previous three sections gave us a definition of algorithm, the machinery of asymptotic analysis, and the discipline of abstraction. This section is the capstone: a methodology for attacking any algorithmic problem — homework, an interview, an on-call incident, a research notebook. You are about to read the same playbook used by Olympiad finalists, FAANG interviewers, ICPC coaches, and Site Reliability Engineers running an outage. The technical content of this book is the vocabulary; this section is the grammar.
The Polya Framework: Four Phases
In 1945 the Hungarian mathematician George Polya published How to Solve It, a slim book that distilled the problem-solving process into four phases. It was written for high-school geometry but holds up unreasonably well for modern algorithm design. The four phases, in order, are:
| Phase | Question | Output |
|---|---|---|
| 1. Understand | What exactly am I being asked? | A precise restatement and a list of edge cases. |
| 2. Plan | What approach will work? Why? | An algorithm sketched in words, with a target complexity. |
| 3. Execute | How do I turn the plan into code? | Working code that compiles and runs. |
| 4. Reflect | Did I really solve it? Could I do better? | Tested code, an invariant, a Big-O, and a generalization. |
Most beginners spend 5% on phase 1, 10% on phase 2, 80% on phase 3, and 5% on phase 4. Most experts invert this: roughly 30 / 40 / 20 / 10. They write less code because they think harder before writing. The cost of a bug found in phase 1 is a corrected sentence; the cost of a bug found in phase 3 is a debugging session; the cost of a bug found in phase 4 is sometimes a postmortem. Every minute spent in the upper phases pays compound interest.
The 30 / 40 / 20 / 10 split
Phase 1 — Understand the Problem
The single most common interview failure mode — and the single most common production bug — is solving the wrong problem. Understand-the-problem is not optional warm-up; it is half the work.
Restate it in your own words
Read the problem. Now close it and write down what it asks in one sentence, without using any of the original wording. If you cannot do this, you do not yet understand the problem. This is the engineering analogue of the Feynman technique.
Pin down the contract
An algorithm is a contract. You owe the spec: given input of type Tin satisfying constraint C, return output of type Tout satisfying relation R. Until you can fill in those four slots, you cannot start.
| Question | Why it matters |
|---|---|
| What are the input types and ranges? | An array of int32 vs. an array of arbitrary-precision integers changes which operations are O(1). |
| Can n be 0? 1? 10⁹? | Empty inputs and very large inputs both reshape the algorithm. |
| Is the input sorted? unique? non-negative? | Sorted-ness alone unlocks binary search, two pointers, sweep line, and merge. |
| Are there ties? duplicates? | Sometimes the spec depends on the choice — return the first match, the last, all of them? |
| Is it one query or many? | Preprocess once + query many = different complexity budget than single-shot. |
| What is the output exactly? | An index? a value? a count? all witnesses? Read this five times. |
Run small examples by hand
Take the smallest non-trivial input you can imagine, simulate the problem on paper, and check that your interpretation of the spec matches the expected output. If you cannot produce the expected output by hand, you cannot produce it in code.
Hunt edge cases
Edge cases are not bugs to fix later; they are part of the specification. Name them now, in writing.
- Empty input ().
- Single element ().
- All elements identical.
- Already sorted / reverse sorted.
- Negatives, zero, max int, min int.
- Duplicates of the target.
- No solution exists.
- Multiple solutions exist (which one to return?).
- Numerical: overflow at on 32-bit. Famous binary-search bug fixed in Java's standard library in 2006, present in textbooks since 1962.
Clarifying questions are not weakness
Phase 2 — Plan an Approach
With the spec pinned, the second phase is a deliberate, structured search through algorithm design. Three moves, in order.
Move 1: brute force first
Always have a baseline. Brute force is what you would do with infinite time and unbounded compute — try every pair, enumerate every permutation, simulate every step. Two reasons to write it down:
- It anchors correctness. The brute force is almost always trivially correct and gives you something to test against.
- It exposes the structure to optimize. Looking at "for every i, for every j, check sum" tells you exactly what you must remove.
Move 2: spot the structure
Real algorithm design is not invention; it is recognition. The same twenty patterns cover an astonishingly large fraction of problems. The question to ask is: what hidden structure does the input have, and what algorithmic technique exploits it?
| Hidden structure | Technique |
|---|---|
| Sorted (or sortable) input | Binary search, two pointers, sweep line |
| Monotone predicate over the answer space | Binary search on the answer |
| Need pair / triple summing to target | Hash map of complements; two pointers if sorted |
| Window with a sliding constraint | Sliding window (two pointers same direction) |
| Repeated subproblems | Memoization / dynamic programming |
| Optimal-substructure decision | DP or greedy (greedy if exchange argument holds) |
| Local choice + undo on failure | Backtracking |
| Shortest path, unweighted | BFS |
| Shortest path, non-negative weights | Dijkstra (heap) |
| Connectivity / cycles in undirected graph | DFS with colors, or Union-Find |
| Topological order in a DAG | DFS post-order, or Kahn's algorithm |
| k-th smallest / largest | Heap of size k, or quickselect |
| Range queries on a static array | Prefix sums, sparse tables |
| Range queries with updates | Segment tree, Fenwick tree |
| Substring with no repeats | Sliding window with hash set |
| Next greater / smaller element | Monotonic stack |
| Interval problems | Sort by start or end, sweep |
Move 3: estimate complexity from the constraints
Constraints leak the intended complexity. If you see , an solution will time out and the setter is hinting at . If you see , the setter is winking at bitmask DP. The next table is the practical heuristic that competitive programmers carry in their heads.
The Constraint-to-Complexity Table
Assume a generous one billion () simple operations per second — a healthy ceiling for compiled languages, an optimistic one for Python. With a one-second budget, the following complexity classes fit within these input sizes:
| n up to | Acceptable complexity | Typical technique |
|---|---|---|
| ~11 | O(n!) | Permutation enumeration, brute-force TSP |
| ~25 | O(2ⁿ × n) | Bitmask DP over subsets (Held-Karp) |
| ~500 | O(n³) | Floyd-Warshall, n × n × n DP, naive matrix multiply |
| ~5,000 | O(n²) | Quadratic DP, all-pairs scan, naive 2D |
| ~10⁵ | O(n log n) or O(n √n) | Sorting, heap-based, segment tree, Mo's algorithm |
| ~10⁶ | O(n) or O(n log n) | Linear scan, hash map, prefix sums, two-pointer |
| ~10⁸ | O(n) | Tight linear scan, must avoid Python |
| ~10⁹ – 10¹⁸ | O(log n) or O(1) | Binary search on the answer, closed-form |
Read this table backward as well. If the only algorithm you know is quadratic and the constraint is , do not start coding — you are guaranteed to time out. Stop, return to phase 2, and find a better technique. The constraints are the problem author telling you what class of solution they expect.
Python tax
sorted, collections.Counter) erase most of the gap because they run in C under the hood.Quick Check
A problem says n ≤ 100,000 and asks for the answer in one second. Which complexity class should you target?
Pattern Recognition: A Working Catalogue
Phase 2 is largely pattern recognition. The cue in the problem statement maps to the technique. After your first hundred problems the mapping becomes unconscious; until then, this catalogue is your cheat-sheet. Read the "cue" column with care — these are the verbal patterns that should fire your alarm.
| Cue in problem statement | Technique |
|---|---|
| "subarray sum equals K" / "count subarrays with sum K" | Prefix sums + hash map of seen prefixes |
| "longest substring without repeating characters" | Sliding window + hash set |
| "minimum window substring containing all of T" | Sliding window + frequency map |
| "two numbers in array summing to target" | Hash map of complements |
| "3-sum / 4-sum" | Sort, then fix one pointer + two-pointer |
| "k-th largest" | Heap of size k, or quickselect (O(n) average) |
| "all permutations / all subsets / all combinations" | Backtracking with explicit choice / un-choice |
| "number of ways to climb / partition / pay" | DP — usually 1D or 2D |
| "is there a path / shortest path in unweighted graph" | BFS |
| "shortest path with positive weights" | Dijkstra |
| "detect cycle in directed graph" | DFS with three colors (white / gray / black) |
| "detect cycle in undirected graph" | DFS with parent tracking, or Union-Find |
| "merge / sort / order intervals" | Sort by start; sweep with a heap of active ends |
| "next greater element" | Monotonic stack, scan right-to-left |
| "maximum / minimum in every window of size k" | Monotonic deque |
| "edit distance / longest common subsequence" | 2D DP, O(n × m) |
| "minimum number of X to satisfy Y" | Binary search on the answer + feasibility check |
The mantra
Quick Check
You see: 'Find the longest substring of s that contains no repeating characters.' Constraint: |s| ≤ 5 × 10⁴. Which technique fires first?
Phase 3 — Execute the Plan
With a plan in hand, execution is mostly transcription. The pitfalls are mechanical, not intellectual.
- Off-by-one. Half-open vs. closed intervals. Decide once: do you use
[lo, hi)or[lo, hi]? Be consistent for the whole function. - Mutation while iterating. Modifying a list while looping over it is a guaranteed bug in Python and a footgun in JS.
- Integer overflow. In Java/C++ the midpoint
(lo + hi) / 2overflows for . The fix islo + (hi - lo) / 2. Python ints are unbounded so this bug is invisible there. - Hash collisions on adversarial inputs. Some competitive judges deliberately construct inputs that blow up Python's default
dicthashing. Wrapping keys with a random salt fixes it. - Recursion depth. Python defaults to 1000. A backtracking on a 10000-node tree will
RecursionError— raise the limit or convert to a stack. - Floating point. Never compare floats with
==; use a tolerance.
Name variables for meaning
i and j are fine for short loops. For anything longer, left, right, complement, seen, frontier, visited read like English and surface bugs by themselves. A misnamed variable is a half-discovered bug.Worked Example: Two Sum in Python
Let us walk the entire Polya cycle on the canonical interview problem.
Two Sum. Given an array of integers and a target integer , return indices with such that . You may assume exactly one such pair exists.
Phase 1 (Understand). Input: an integer array, a target integer. Output: a pair of indices. Constraints, by clarification: lengths up to ; values fit in 32-bit; exactly one solution; the same index cannot be used twice. Edge cases: duplicates ( — the two 3s have different indices, so this is valid); negatives; zero target. Hand example: → .
Phase 2 (Plan). Brute force: nested loop over all pairs, . At that is operations — too slow. Improvement: for each , the partner we need is . Looking up "have we seen already?" in a hash map is . So one pass suffices: time, space.
Phase 3 (Execute).
Two Sum in TypeScript
The same algorithm in TypeScript. Notice that the algorithmic content is identical — only the syntax and the choice of Map over dict changes.
The same algorithm, twice
Phase 4 — Reflect
Most students stop at "it passes the examples." The fourth phase is what separates engineers from coders. Five questions, in order:
- Does it handle every edge case I listed in phase 1? Empty input, single element, duplicates, negatives, max int, no solution. Run them.
- What is the loop invariant? For Two Sum: at the top of iteration ,
seencontains exactly{A[k]: k}for , and no answer has been found among . Combined with the loop's exit (we return as soon as a partner is found), this implies correctness. - What is the Big-O? Two Sum is time, space — one pass, one hash entry per element in the worst case.
- Could it be simpler? Faster? Less memory? Faster: no — we must read every element, so is a lower bound. Less memory: if the input is sorted, we can use two pointers from the ends and drop the hash map — extra space, still time. That is a different algorithm for a different precondition.
- What does the solution generalize to? The pattern "hash the complement" generalizes to: for any predicate over pairs that decomposes into independent functions of each element, replace nested loops with one pass + a hash lookup. 4-Sum, count-of-pairs-with-difference-K, subarray sum equals K — all the same idea.
Always write the invariant
Interactive: The Problem-Solving Cockpit
The widget below ties the four phases to a live trace and a complexity meter. The left column lets you flip between phases and see the checklist for each. The middle column steps through the Two Sum algorithm on : watch the hash map fill, watch the hit fire on iteration 1. The right column lets you slide across nine orders of magnitude and see how , , and diverge in absolute time.
- ✓Restate the problem in one sentence.
- ✓Pin down input type, output type, value ranges.
- ✓Run two small examples by hand.
- ✓List edge cases: empty, single, duplicates, negatives, max int.
- ✓Clarify ambiguity (interview habit).
Bar widths are on a log10 scale so all three classes stay visible. Times assume a generous 109 simple operations per second.
Two things to look for. First, slide from to : at the small end every algorithm is instant; somewhere around the quadratic class crosses one millisecond; by the quadratic class is many minutes; by the quadratic class is centuries. The asymptotic class is not academic — it is the difference between "ships" and "does not ship."
Second, step through the Two Sum trace. On iteration , the hash map is empty so the lookup misses; we store . On iteration , we look up , find it at index , and return immediately. The whole algorithm visits each element at most once, exactly as the loop invariant predicts.
Debugging Strategies
Even the best methodology produces buggy code on the first attempt. These are the techniques senior engineers use, in roughly the order they reach for them.
1. Print invariants, not values
Beginners print the value of every variable. Experts print the invariant: a single line per iteration that says "here is what should be true right now." If the invariant prints false, you have found the bug.
1# Beginner debugging
2print("i =", i, "x =", x, "seen =", seen)
3
4# Expert debugging — print the invariant
5assert all(seen[A[k]] == k for k in range(i)), \
6 f"invariant violated at i={i}, seen={seen}"2. Binary-search the bug
If you have a 200-line function and one of them is wrong, do not re-read the file. Comment out the second half and re-run; if the bug is gone, the bug is in the second half. Repeat. Five rounds of binary search localizes the bug to a single line.
3. Minimize the failing input
A failure on a 10,000-element array is unreadable. Find the smallest input that still fails. Halve, quarter, eighth — usually a 2-element input reproduces the bug. The minimal input is also the regression test you commit.
4. Rubber-duck
Explain the code, line by line, to an inanimate object. The act of verbalizing what each line is supposed to do surfaces the gap between intent and implementation. The duck has been a member of software engineering teams since at least 1986 (Brian Kernighan, The Practice of Programming).
5. Re-read the spec
After two failed debugging attempts, stop debugging. Go back to phase 1 and re-read the problem statement. Roughly half of all "impossible" bugs are in fact correct code solving the wrong problem.
What does NOT work
Where This Methodology Lives
Polya's framework is not just for Olympiad problems. It is the same loop senior engineers use across every corner of computing.
| Domain | What 'Understand' means | What 'Reflect' means |
|---|---|---|
| Competitive programming | Re-read constraints, hand-trace small cases. | Stress-test against brute force; analyze worst-case input. |
| Technical interviews | Ask clarifying questions; restate the problem. | Walk through edge cases; state Big-O; discuss trade-offs. |
| SRE / incident response | Read alerts, gather logs, define the user-visible symptom. | Postmortem: what allowed this? What invariant was missing? |
| Scientific computing | Pin down the equations; identify boundary conditions. | Validate against analytic limits; check conservation laws. |
| ML pipeline debugging | Inspect input batches; check shapes, dtypes, ranges. | Track loss curves, gradient norms; ablate one component at a time. |
| Database query tuning | EXPLAIN the plan; identify which join blew up. | Re-EXPLAIN; verify cardinality estimates match reality. |
| Distributed systems design | Enumerate failure modes; pin the consistency model. | Replay traces; check linearizability or chosen weaker model. |
Notice the universality of the four phases. The artefact at each phase changes — a clarifying question in an interview, a timeline in an outage, a derivation in a physics simulation — but the loop is the same. Spend time on phase 1, structure phase 2, execute carefully in phase 3, learn from the result in phase 4. The engineers who do this consistently are the engineers who ship.
From here, the rest of the book is fuel for phase 2. Every chapter introduces structures and algorithms; what makes them useful is the ability, in phase 2, to recognize when one of them is the right tool. That recognition is what we will build, problem by problem, for the next 14 chapters.