Chapter 1
18 min read
Section 4 of 261

How to Approach Any Problem

Introduction to Algorithmic Thinking

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:

PhaseQuestionOutput
1. UnderstandWhat exactly am I being asked?A precise restatement and a list of edge cases.
2. PlanWhat approach will work? Why?An algorithm sketched in words, with a target complexity.
3. ExecuteHow do I turn the plan into code?Working code that compiles and runs.
4. ReflectDid 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

For a 60-minute interview problem, that is roughly 18 minutes thinking and writing on paper before you touch a keyboard. Counter-intuitive, but it is the dominant strategy.

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.

QuestionWhy 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 (n=0n = 0).
  • Single element (n=1n = 1).
  • 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 (lo+hi)/2(\text{lo} + \text{hi})/2 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

In an interview, asking "Can the input be empty? Are values unique?" signals that you have done this before. In production, the engineers who ask the most questions ship the fewest incidents. The cost of a clarifying sentence is two seconds; the cost of misreading the spec is hours.

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:

  1. It anchors correctness. The brute force is almost always trivially correct and gives you something to test against.
  2. 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 structureTechnique
Sorted (or sortable) inputBinary search, two pointers, sweep line
Monotone predicate over the answer spaceBinary search on the answer
Need pair / triple summing to targetHash map of complements; two pointers if sorted
Window with a sliding constraintSliding window (two pointers same direction)
Repeated subproblemsMemoization / dynamic programming
Optimal-substructure decisionDP or greedy (greedy if exchange argument holds)
Local choice + undo on failureBacktracking
Shortest path, unweightedBFS
Shortest path, non-negative weightsDijkstra (heap)
Connectivity / cycles in undirected graphDFS with colors, or Union-Find
Topological order in a DAGDFS post-order, or Kahn's algorithm
k-th smallest / largestHeap of size k, or quickselect
Range queries on a static arrayPrefix sums, sparse tables
Range queries with updatesSegment tree, Fenwick tree
Substring with no repeatsSliding window with hash set
Next greater / smaller elementMonotonic stack
Interval problemsSort by start or end, sweep

Move 3: estimate complexity from the constraints

Constraints leak the intended complexity. If you see n2×105n \leq 2 \times 10^5, an O(n2)O(n^2) solution will time out and the setter is hinting at O(nlogn)O(n \log n). If you see n20n \leq 20, 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 (10910^9) 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 toAcceptable complexityTypical technique
~11O(n!)Permutation enumeration, brute-force TSP
~25O(2ⁿ × n)Bitmask DP over subsets (Held-Karp)
~500O(n³)Floyd-Warshall, n × n × n DP, naive matrix multiply
~5,000O(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 n=106n = 10^6, 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

Python is roughly 30× slower than C++ for tight loops. The practical ceilings shrink: n=105n = 10^5 with O(nlogn)O(n \log n) is comfortable; n=106n = 10^6 with O(nlogn)O(n \log n) starts to hurt. NumPy and built-ins (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 statementTechnique
"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

Before writing code, finish this sentence: "This is a ____ problem; the structure I will exploit is ____; my target complexity is ____." If any blank is empty, return to phase 1 or 2.

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) / 2 overflows for lo+hi>231\text{lo} + \text{hi} > 2^{31}. The fix is lo + (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 dict hashing. 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 AA of integers and a target integer tt, return indices (i,j)(i, j) with iji \neq j such that A[i]+A[j]=tA[i] + A[j] = t. 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 10510^5; values fit in 32-bit; exactly one solution; the same index cannot be used twice. Edge cases: duplicates (A=[3,3],t=6A = [3, 3], t = 6 — the two 3s have different indices, so this is valid); negatives; zero target. Hand example: A=[2,7,11,15],t=9A = [2, 7, 11, 15], t = 9 (0,1)(0, 1).

Phase 2 (Plan). Brute force: nested loop over all pairs, O(n2)O(n^2). At n=105n = 10^5 that is 101010^{10} operations — too slow. Improvement: for each x=A[i]x = A[i], the partner we need is txt - x. Looking up "have we seen txt - x already?" in a hash map is O(1)O(1). So one pass suffices: O(n)O(n) time, O(n)O(n) space.

Phase 3 (Execute).

Two Sum in O(n) with a hash map
🐍two_sum.py
3Function signature

List of ints in, tuple of two indices out. The type hints document the contract you wrote down in phase 1.

5Hash map of seen values

We map each value we have already passed to its index. The choice of dict (not list, not set) is dictated by the lookup we need: 'is value v in my history, and if so where?'

EXAMPLE
After one step on A = [2,7,11,15]: seen = {2: 0}.
6Single pass over the array

enumerate gives us (index, value) pairs. Crucially we do NOT iterate twice — each element is visited exactly once.

EXAMPLE
Iteration order on [2,7,11,15]: (0,2), (1,7), (2,11), (3,15).
7Compute the complement

The plan from phase 2: if x is in the answer, the other element must be t - x. So we ask: have we seen t - x already? This converts an O(n²) pair search into O(n) lookups.

EXAMPLE
i = 1, x = 7, t = 9 -> need = 9 - 7 = 2.
8Lookup in O(1) average time

`need in seen` is amortized O(1) for Python dicts (open addressing with random hashing). This is the line that buys us the speedup.

EXAMPLE
seen = {2: 0}. need = 2 is in seen — hit!
9Return both indices

seen[need] is the index where the partner appeared (necessarily earlier in the array, so smaller). i is the current index. We return them in left-to-right order — this is the most common spec convention. The check seen[need] != i is automatic because we look up BEFORE storing x — so the current x is never matched against itself.

EXAMPLE
seen[2] = 0, i = 1 -> return (0, 1). Verify: A[0] + A[1] = 2 + 7 = 9 = t.
10Otherwise: remember this value

If the partner has not appeared yet, it might appear LATER. So we record the current value with its index. The next iterations may use this entry as a partner.

EXAMPLE
After i = 0: seen = {2: 0}. After i = 1 we would write seen[7] = 1, but we already returned.
11Defensive raise

The spec promises a solution exists. In production, never trust callers — raise on the impossible case so a violation surfaces loudly instead of returning silently wrong indices.

15First test: the canonical example

Trace by hand: i=0,x=2,need=7,not in {},store seen={2:0}. i=1,x=7,need=2,2 IS in seen -> return (0,1). One miss, one hit, two iterations total.

16Test with the answer at the end

A = [3,2,4], t = 6. i=0,x=3,need=3,not in {} (we have not stored 3 yet — this is why we look up BEFORE storing). store seen={3:0}. i=1,x=2,need=4,not in seen,store seen={3:0,2:1}. i=2,x=4,need=2,2 IS in seen -> return (1,2).

17Duplicate-aware test

A = [3,3], t = 6. i=0,x=3,need=3,not in {} (the lookup-before-store ordering matters here — if we stored first, x would match itself with the same index!). store seen={3:0}. i=1,x=3,need=3,3 IS in seen at index 0 -> return (0,1). Correct: A[0]+A[1] = 3+3 = 6.

6 lines without explanation
1from typing import List, Tuple
2
3def two_sum(A: List[int], t: int) -> Tuple[int, int]:
4    """Indices (i, j) with A[i] + A[j] == t, i != j."""
5    seen: dict[int, int] = {}             # value -> index seen so far
6    for i, x in enumerate(A):
7        need = t - x                      # the partner we are looking for
8        if need in seen:                  # has it appeared earlier?
9            return (seen[need], i)        # found: indices in left-to-right order
10        seen[x] = i                       # otherwise remember x for the future
11    raise ValueError("no two-sum pair")   # spec says this never fires
12
13
14# Worked example
15print(two_sum([2, 7, 11, 15], 9))   # -> (0, 1)
16print(two_sum([3, 2, 4], 6))        # -> (1, 2)
17print(two_sum([3, 3], 6))           # -> (0, 1) — duplicate-aware

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.

Two Sum, TypeScript edition
📘twoSum.ts
1TypeScript signature with a tuple return

We return a fixed-length tuple `[number, number]` rather than a generic `number[]`. The type system now enforces 'exactly two elements' — a small win against accidental array mutation.

3Map instead of plain object

JS plain objects coerce keys to strings; Map preserves number keys and offers O(1) average has/get/set with no prototype-pollution surprises. For numeric-keyed lookup tables, Map is the right default in modern JS.

EXAMPLE
After one step on [2,7,11,15]: seen = Map(1) { 2 => 0 }.
5Index-based for loop

We need both index and value, so a classic for loop is cleanest. for...of would give us values only; entries() works but is wordier.

EXAMPLE
Iteration order on [2,7,11,15]: i = 0,1,2,3.
6Read the current value

x is the value at the current index. Captured into a const so we cannot accidentally reassign it inside the body.

EXAMPLE
i = 1 -> x = A[1] = 7.
7Compute the complement

Same plan as Python: partner = target - x.

EXAMPLE
x = 7, t = 9 -> need = 2.
9Has the partner already appeared?

Map.has is the JS analogue of `in dict`: average O(1).

EXAMPLE
seen.has(2) on { 2 => 0 } -> true.
11Return as a tuple, with the non-null assertion

TypeScript cannot prove that has(need) implies get(need) is defined, so we use the post-fix `!`. A safer alternative: const j = seen.get(need); if (j !== undefined) return [j, i].

EXAMPLE
seen.get(2) = 0, i = 1 -> [0, 1].
13Otherwise remember the current value

Same pattern as Python: store AFTER the lookup so the current x is never matched with itself.

EXAMPLE
seen.set(7, 1) -> Map { 2 => 0, 7 => 1 }.
16Defensive throw

If the spec promised a solution and we did not find one, the input violated the contract. Throw, do not silently return [-1, -1] — a wrong answer is worse than a loud crash.

12 lines without explanation
1function twoSum(A: number[], t: number): [number, number] {
2  // Map<value, index>: average O(1) get/set, ordered insertion.
3  const seen = new Map<number, number>();
4
5  for (let i = 0; i < A.length; i++) {
6    const x = A[i];
7    const need = t - x;
8
9    if (seen.has(need)) {
10      // Non-null assertion: has(need) guarantees get(need) is defined.
11      return [seen.get(need)!, i];
12    }
13    seen.set(x, i);
14  }
15
16  throw new Error("no two-sum pair");
17}
18
19console.log(twoSum([2, 7, 11, 15], 9));  // -> [0, 1]
20console.log(twoSum([3, 2, 4], 6));        // -> [1, 2]
21console.log(twoSum([3, 3], 6));           // -> [0, 1]

The same algorithm, twice

The two listings above are the same algorithm: the sequence of operations on the input is identical, only the host language differs. Big-O claims about the algorithm transfer to both. This is the algorithm-vs-program distinction from section 1 made concrete: one idea, two encodings.

Phase 4 — Reflect

Most students stop at "it passes the examples." The fourth phase is what separates engineers from coders. Five questions, in order:

  1. Does it handle every edge case I listed in phase 1? Empty input, single element, duplicates, negatives, max int, no solution. Run them.
  2. What is the loop invariant? For Two Sum: at the top of iteration ii, seen contains exactly {A[k]: k} for 0k<i0 \leq k < i, and no answer has been found among A[0..i1]A[0..i-1]. Combined with the loop's exit (we return as soon as a partner is found), this implies correctness.
  3. What is the Big-O? Two Sum is O(n)O(n) time, O(n)O(n) space — one pass, one hash entry per element in the worst case.
  4. Could it be simpler? Faster? Less memory? Faster: no — we must read every element, so Ω(n)\Omega(n) is a lower bound. Less memory: if the input is sorted, we can use two pointers from the ends and drop the hash map — O(1)O(1) extra space, still O(n)O(n) time. That is a different algorithm for a different precondition.
  5. 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

The loop invariant is a single sentence that, if maintained, proves correctness. The discipline of writing it forces you to confront questions like "what if the array is empty?" and "why does the lookup happen before the store?" before they become bugs.

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 A=[2,7,11,15],t=9A = [2, 7, 11, 15], t = 9: watch the hash map fill, watch the hit fire on iteration 1. The right column lets you slide nn across nine orders of magnitude and see how O(n)O(n), O(nlogn)O(n \log n), and O(n2)O(n^2) diverge in absolute time.

Problem-Solving Cockpit
Polya phases · Two Sum trace · complexity meter
Phase
  • 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).
Two Sum tracenums = [2, 7, 11, 15], target = 9
i=02
i=17
i=211
i=315
1 / 2
read: x = nums[0] = 2
need: target - x = 9 - 2 = 7
seen: {}
miss. store seen[2] = 0
Complexity metern = 100,000
O(n)1.0e+5 ops · 100.0 µs
O(n log n)1.7e+6 ops · 1.7 ms
O(n²)1.0e+10 ops · 10.00 s

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 nn from 1010 to 10910^9: at the small end every algorithm is instant; somewhere around n=104n = 10^4 the quadratic class crosses one millisecond; by n=106n = 10^6 the quadratic class is many minutes; by n=109n = 10^9 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 i=0i = 0, the hash map is empty so the lookup misses; we store (2,0)(2, 0). On iteration i=1i = 1, we look up 97=29 - 7 = 2, find it at index 00, 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.

🐍python
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

Random tweaks. Adding +1 here, swapping < for <= there, guessing. If you cannot articulate why a change should fix the bug, the change will not fix it — or it will paper over one bug while introducing another.

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.

DomainWhat 'Understand' meansWhat 'Reflect' means
Competitive programmingRe-read constraints, hand-trace small cases.Stress-test against brute force; analyze worst-case input.
Technical interviewsAsk clarifying questions; restate the problem.Walk through edge cases; state Big-O; discuss trade-offs.
SRE / incident responseRead alerts, gather logs, define the user-visible symptom.Postmortem: what allowed this? What invariant was missing?
Scientific computingPin down the equations; identify boundary conditions.Validate against analytic limits; check conservation laws.
ML pipeline debuggingInspect input batches; check shapes, dtypes, ranges.Track loss curves, gradient norms; ablate one component at a time.
Database query tuningEXPLAIN the plan; identify which join blew up.Re-EXPLAIN; verify cardinality estimates match reality.
Distributed systems designEnumerate 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.

Loading comments...