Divide and Conquer
Divide and conquer has three steps, and only one of them is interesting:
- Divide the problem into independent subproblems — usually trivial.
- Conquer each recursively — also trivial, it is just a recursive call.
- Combine the results — this is the algorithm.
Merge sort is not interesting because it splits in half; it is interesting
because of merge. And the reason the pattern is worth its own page is that the
combine step can compute things far beyond the stated problem: while merging two
sorted halves, you can count cross-half relationships for free, which turns
several problems into .
What you’ll learn
Section titled “What you’ll learn”- The three-step skeleton, and how to reason about its cost with the Master Theorem.
- Why merge sort counts things — the technique behind inversion counting.
- Divide and conquer on expressions rather than arrays, where the split point is a choice.
- When D&C is the wrong tool, and what to use instead.
- Three real LeetCode problems solved in the browser: 912, 241, 493.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”Merge sort is the canonical divide and conquer, and watching it makes the recurrence concrete: the splits cost nothing, the merges are where all the work is, and there are levels of them.
Count the comparisons at each level rather than in total -- every level touches all n elements once, and there are log n levels, which is the whole derivation of O(n log n). The counting variants on this page (inversions, reverse pairs) piggyback on exactly these merges, which is why they are also O(n log n).
The skeleton
Section titled “The skeleton”def solve(problem):
if is_base_case(problem): # small enough to answer directly
return answer(problem)
left, right = split(problem) # 1. DIVIDE
left_result = solve(left) # 2. CONQUER
right_result = solve(right)
return combine(left_result, right_result) # 3. COMBINE -- the real workReasoning about the cost
Section titled “Reasoning about the cost”The Master Theorem
gives the complexity from the recurrence
, where a is the number of subproblems, b the
shrink factor, and f(n) the cost of combining.
| Algorithm | Recurrence | Result |
|---|---|---|
| Binary search | ||
| Merge sort | ||
| Naive matrix multiply | ||
| Strassen |
Strassen is instructive: it changes only a, from 8 subproblems to 7, and
that alone beats the textbook cubic bound. When you want a D&C algorithm to be
faster, you either reduce the number of subproblems or cheapen the combine.
Merge sort, and its real value
Section titled “Merge sort, and its real value”def merge_sort(a):
if len(a) <= 1:
return a
mid = len(a) // 2
left, right = merge_sort(a[:mid]), merge_sort(a[mid:])
out, i, j = [], 0, 0
while i < len(left) and j < len(right):
if left[i] <= right[j]: # <= keeps the sort STABLE
out.append(left[i]); i += 1
else:
out.append(right[j]); j += 1
out.extend(left[i:]) # one side may remain
out.extend(right[j:])
return out time and space, and stable thanks to the <=.
Counting during the merge
Section titled “Counting during the merge”Here is the idea worth taking from this page. When merging sorted left and
sorted right, if left[i] > 2 * right[j], then every remaining element of
left also exceeds 2 * right[j], because left is sorted ascending.
So instead of checking pairs one at a time, you count them in blocks:
def count_reverse_pairs(a):
"""Count pairs i < j with a[i] > 2 * a[j]."""
def sort_count(a):
if len(a) <= 1:
return a, 0
mid = len(a) // 2
left, cl = sort_count(a[:mid])
right, cr = sort_count(a[mid:])
count = cl + cr
j = 0 # a pointer that NEVER resets
for x in left: # both halves are sorted here
while j < len(right) and x > 2 * right[j]:
j += 1
count += j # x beats the first j elements
merged, i, k = [], 0, 0 # then merge as usual
while i < len(left) and k < len(right):
if left[i] <= right[k]:
merged.append(left[i]); i += 1
else:
merged.append(right[k]); k += 1
merged.extend(left[i:]); merged.extend(right[k:])
return merged, count
return sort_count(a)[1]Every pair (i, j) with i in the left half and j in the right half is
counted exactly once, at the merge that first separates them. Pairs within a half
are counted by the recursive calls. So every pair is accounted for exactly once,
which is why the total is correct.
Divide and conquer on expressions
Section titled “Divide and conquer on expressions”For LC 241 the split point is not the midpoint — it is every operator in turn, and each choice yields a different set of results:
from functools import lru_cache
def diff_ways_to_compute(expression):
@lru_cache(maxsize=None) # memoise: substrings recur a lot
def solve(expr):
if expr.lstrip("-").isdigit(): # base case: a bare number
return (int(expr),)
results = []
for i, ch in enumerate(expr):
if ch in "+-*":
for a in solve(expr[:i]): # every left result
for b in solve(expr[i + 1:]): # crossed with every right
results.append(a + b if ch == "+" else
a - b if ch == "-" else a * b)
return tuple(results)
return list(solve(expression))The combine step is a cross product: every result from the left combined with every result from the right. That is characteristic of “all possible ways” problems.
Dry run
Section titled “Dry run”LC 493 — reverse pairs in [1, 3, 2, 3, 1], i.e. pairs i < j with a[i] > 2·a[j]. The
count is accumulated inside the merges, bottom-up:
| depth | left (sorted) | right (sorted) | pairs found here | running total |
|---|---|---|---|---|
| 1 | [1] | [3] | 0 — 1 > 6 is false | 0 |
| 2 | [3] | [1] | 1 — 3 > 2·1 ✓ | 1 |
| 1 | [2] | [1, 3] | 0 — 2 > 2 is false | 1 |
| 0 | [1, 3] | [1, 2, 3] | 1 — 3 > 2·1 ✓, and 1 beats nothing | 2 |
Answer 2: the pairs are (3, 1) from indices 1 and 4, and (3, 1) from indices 3 and 4.
Four things this makes concrete:
- Both halves are already sorted when the counting runs, and that is the entire reason the
block-counting works.
leftascending means oncex > 2·right[j]fails, it fails for every laterxtoo — sojsweeps forward once instead of restarting per element. jmust be declared outside thefor x in leftloop. Reset it per element and the counting becomes per merge — still correct, but the whole point was to avoid that. This is the same amortised-pointer argument as a sliding window.- Sorting destroys the original indices, and it does not matter. Pairs are counted before
the two halves are merged, at the moment when every element of
leftgenuinely precedes every element ofrightin the original array. That ordering guarantee is what licenses the count. - The recursion counts each pair exactly once — at the unique level where its two elements first end up in different halves. The depth-0 merge finds one pair the deeper merges could not see, and re-counts none of theirs.
Complexity
Section titled “Complexity”| Problem | Recurrence | Result |
|---|---|---|
| Merge sort | time, space | |
| Reverse pairs / inversions | — counting rides along free | |
| Binary search | ||
| Quick sort, balanced pivots | average, worst | |
| Quickselect | average | |
| Karatsuba multiplication | ||
| Different Ways to Add Parentheses | non-uniform splits | Catalan-many results, exponential |
The Master Theorem in one line: for , compare with — the larger exponent wins, and they tie into an extra factor. Merge sort is the tie case (, , so ), which is exactly why it lands on rather than or . Karatsuba beats naive multiplication precisely by changing from 4 to 3.
The variant map
Section titled “The variant map”| Variant | The combine step | Canonical problem |
|---|---|---|
| Sort | Merge two sorted halves | 912 · 148 |
| Count cross-half pairs | Count during the merge | 493 · 315 · 327 |
| All possible results | Cross product of both sides | 241 · 95 |
| Best of left, right, or crossing | Compare three candidates | 53 (D&C variant) · 1763 |
| Discard half of one input | Binary search on the split position | 4 Median of Two Sorted Arrays |
| Closest pair of points | Check the strip near the dividing line | classic geometry |
Practice — real LeetCode problems
Section titled “Practice — real LeetCode problems”LC 912 — Sort an Array · Medium
Section titled “LC 912 — Sort an Array · Medium”Problem. Sort an array of integers ascending. You must not use any built-in sorting function, and the solution should be with the lowest possible space complexity.
Constraints. 1 <= len(nums) <= 5 * 10^4,
-5 * 10^4 <= nums[i] <= 5 * 10^4.
Examples. [5,2,3,1] gives [1,2,3,5] · [5,1,1,2,0,0] gives
[0,0,1,1,2,5]
Editorial — approach, complexity, follow-ups
Split in half, sort each half, merge. The merge walks both halves once, always taking the smaller front element.
Time — levels, merging per level. Space for the temporary lists, plus recursion.
<= rather than < makes the sort stable: equal elements keep their relative
order. It makes no visible difference for plain integers, but it matters the
moment you sort records by a key, and it is the correct default. LeetCode’s own
[5,1,1,2,0,0] example contains duplicates specifically to exercise the merge.
Note this problem explicitly asks for “the smallest space complexity possible”, which is a nudge toward heap sort ( extra space) or an in-place quicksort with a randomised pivot. Worth mentioning both:
- Quicksort is average and extra, but worst case — and LC 912 has adversarial tests that kill a fixed pivot, so randomise.
- Heap sort is worst case and extra, but not stable.
Merge sort is the safe answer for a D&C discussion; naming the trade-offs is what makes it a complete one. See Merge Sort and Quick Sort.
Follow-ups you should expect: “Sort a linked list (LC 148)?” — merge sort is
the natural fit, since splitting needs no random access; find the middle with
fast/slow pointers
and merge with a
dummy head.
“Do it in place?” — in-place merging is possible but fiddly and slower; prefer
heap sort if space is required. “Why does Python’s sorted use Timsort?”
— it exploits existing runs in real data, giving on nearly-sorted input.
LC 241 — Different Ways to Add Parentheses · Medium
Section titled “LC 241 — Different Ways to Add Parentheses · Medium”Problem. Given a string expression of numbers and the operators +, -
and *, return all possible results from computing it with every valid
parenthesisation. Results may be returned in any order.
Constraints. 1 <= len(expression) <= 20, non-negative integers under 10,
and the number of results does not exceed 10^4.
Examples. "2-1-1" gives [0, 2] · "2*3-4*5" gives
[-34,-14,-10,-10,10] (note the repeated -10) · "11" gives [11]
Editorial — approach, complexity, follow-ups
Each operator is a candidate root of the expression tree. Choosing it splits the string into a left and a right subexpression, each of which has its own set of possible values. The results for this choice are every left value combined with every right value.
Time exponential in general — the count of distinct parenthesisations follows the Catalan numbers — but bounded here by the promise of at most results, and cut substantially by memoisation. Space plus the cache.
Two details:
"11"must give[11]. The base case has to handle multi-digit numbers, so it tests the whole substring rather than a single character.isdigit()on the string does that.- Do not deduplicate.
"2*3-4*5"yields-10from two different parenthesisations and the expected output lists it twice. Wrapping the result in asetis a plausible-looking mistake that fails this exact example.
Returning a tuple from the memoised function is deliberate: a cached mutable list could be modified by a caller and corrupt every later lookup.
Without memoisation this is still accepted at len <= 20, so it is optional here
— but recognising that the subproblems overlap is the observation that
connects D&C to
dynamic programming.
When they overlap heavily, memoising is what stops D&C from being exponential.
Follow-ups you should expect: “Return the expressions, not just the values?”
— build strings alongside the numbers in the combine step. “Handle division?” —
add the operator and guard against dividing by zero. “Count the ways rather than
list them?” — the n-th Catalan number over the operator count, no enumeration
needed. “Generate all BSTs from 1..n (LC 95)?” — structurally identical: each
value is a candidate root, and the combine is a cross product of left and right
subtrees.
LC 493 — Reverse Pairs · Hard
Section titled “LC 493 — Reverse Pairs · Hard”Problem. A reverse pair is a pair (i, j) with i < j and
nums[i] > 2 * nums[j]. Return the number of reverse pairs.
Constraints. 1 <= len(nums) <= 5 * 10^4,
-2^31 <= nums[i] <= 2^31 - 1.
Examples. [1,3,2,3,1] gives 2 · [2,4,3,5,1] gives 3 ·
[5,4,3,2,1] gives 4
Editorial — approach, complexity, follow-ups
Every reverse pair has its two indices either within one half or across the split. Recursion handles the within-half pairs; the merge step counts the cross-half ones. So every pair is counted exactly once, at the level where the two indices first end up in different halves.
The counting is cheap because both halves are already sorted: for ascending x
from left, the set of qualifying right elements only grows, so one
forward-only pointer suffices.
Time — the counting adds per merge level, the same order as the merge itself. Space .
Two details that decide whether this works:
joutside the loop. Resetting it makes the count step and the whole thing — worse than brute force. The amortised forward-only-pointer argument is the same one behind sliding window.- Count before merging. The counting loop relies on
leftandrightbeing separate sorted lists. Once merged, the split information is gone.
Test-case notes: [5,4,3,2,1] gives 4 — pairs (5,2), (5,1), (4,1),
(3,1), since e.g. 5 > 2*2. [1,1,1] gives 0, confirming the strict
inequality. And note the constraints allow the full 32-bit range, so 2 * nums[j]
can exceed 32 bits — a non-issue in Python, but in C++ or Java you would need a
64-bit cast, which is a good detail to raise.
Follow-ups you should expect:
- “Count plain inversions (
nums[i] > nums[j])?” The same algorithm with the factor of 2 removed — the classic inversion count. - “Count of smaller numbers after self (LC 315)?” Same merge-sort-with-counting idea, but you must track original indices to attribute counts per element.
- “Do it with a Fenwick tree instead?” Yes: compress the values and query prefix counts as you sweep right to left. too, and often easier to extend. Worth naming as the alternative.
- “Why not brute force?” is operations at — far too slow.
LeetCode problem set
Section titled “LeetCode problem set”Generated from the problem database, so each entry carries its sheet membership and reported companies. Tick them off as you go — progress is saved in this browser, and the Export button writes it to a file you can keep.
Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 108Convert Sorted Array to Binary Search Treeeasy
- 148Sort ListmediumMerge sort on a linked list -- no random access needed
- 241Different Ways to Add ParenthesesmediumSplit at every operator; combine is a cross product; do **not** deduplicate
- 427Construct Quad Treemedium
- 912Sort an ArraymediumMerge sort; `<=` for stability
- 4Median of Two Sorted ArrayshardBinary search the split position in the **shorter** array
- 315Count of Smaller Numbers After SelfhardSame idea, but attribute counts to original indices
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Where is the actual work?” | Understanding the pattern | In the combine step; divide and conquer are usually trivial |
| “Derive the complexity” | Master Theorem fluency | ; merge sort is |
| “How does merge sort count inversions?” | The key technique | Cross-half pairs are counted at the merge with a forward-only pointer, so per level |
| “Why must the pointer not reset?” | Amortised reasoning | Both halves are sorted, so it only moves forward — resetting makes the step |
| “What if the subproblems overlap?” | Knowing the boundary | Plain D&C recomputes exponentially; memoise, i.e. use DP |
| “Merge sort or quicksort?” | Judgement | Merge for stability, predictable splits and linked lists; quicksort for space in-place, with a randomised pivot |
| “Alternative to merge-counting?” | Breadth | A Fenwick tree over compressed values, sweeping from the right |
Edge-case checklist
Section titled “Edge-case checklist”- Single element — the base case for every recursion here.
- Empty input — guard
len(a) <= 1covers it. - All identical —
[3,3,3]sorts fine and yields0reverse pairs (strict inequality). - Already sorted / reverse sorted — the extremes for counting;
[5,4,3,2,1]gives 4. - Negative values — legal in 912 and 493;
2 * nums[j]gets smaller for negatives, so do not assume monotonic behaviour. - 32-bit overflow in
2 * nums[j]— a non-issue in Python, real elsewhere. - Multi-digit numbers (LC 241) —
"11"must parse as one number. - Duplicate results (LC 241) — must be kept, not deduplicated.
- Recursion depth — for merge sort, so never a problem; but expression recursion is bounded by string length.
Self-check
Section titled “Self-check”-
Why is merge sort O(n log n) rather than O(n) or O(n²)?
Drawing the recursion tree and multiplying 'work per level × number of levels' is faster and more convincing than quoting the Master Theorem — and it also explains the O(n) space and O(log n) stack depth.
pch.quizShowAnswer
B — Because each of the log n levels does O(n) work: level k has 2^k subproblems of size n/2^k, so every level touches all n elements once — Drawing the recursion tree and multiplying 'work per level × number of levels' is faster and more convincing than quoting the Master Theorem — and it also explains the O(n) space and O(log n) stack depth.
-
In the reverse-pairs count, why can `j` sweep forward without ever resetting?
Declaring j inside the loop keeps the answer correct but makes each merge O(n²), which defeats the purpose. Same amortised-pointer argument as a sliding window.
pch.quizShowAnswer
B — Because `left` is sorted ascending, so once x > 2·right[j] fails it fails for every later x too — the pointer is amortised across the whole merge — Declaring j inside the loop keeps the answer correct but makes each merge O(n²), which defeats the purpose. Same amortised-pointer argument as a sliding window.
-
Sorting during the count destroys the original indices. Why is the answer still right?
Each pair is also counted exactly once, at the unique recursion level where its two elements first land in different halves. That is why no deduplication is needed.
pch.quizShowAnswer
B — Because pairs are counted BEFORE the halves merge — at that moment every element of `left` genuinely precedes every element of `right` in the original array — Each pair is also counted exactly once, at the unique recursion level where its two elements first land in different halves. That is why no deduplication is needed.
-
For T(n) = aT(n/b) + O(n^d), how do you get the answer quickly?
Karatsuba is the memorable non-tie case: changing a from 4 to 3 with b=2 moves the exponent from n² to n^1.585 with no change to d.
pch.quizShowAnswer
B — Compare d with log_b(a): the larger exponent dominates, and a tie adds a log n factor — merge sort is the tie case (a=b=2, d=1) — Karatsuba is the memorable non-tie case: changing a from 4 to 3 with b=2 moves the exponent from n² to n^1.585 with no change to d.
-
Quick sort has the same recurrence as merge sort but O(n²) worst case. Why?
The recurrence describes the balanced case. Divide and conquer's guarantee depends entirely on the division actually being balanced, which merge sort gets structurally and quick sort only probabilistically.
pch.quizShowAnswer
B — Because the recurrence only holds when pivots split evenly — a bad pivot makes the tree n levels deep instead of log n, and O(n) work at each of n levels is O(n²) — The recurrence describes the balanced case. Divide and conquer's guarantee depends entirely on the division actually being balanced, which merge sort gets structurally and quick sort only probabilistically.
-
Which problems on this page are NOT improved by divide and conquer?
That distinction is the boundary with the whole DP phase: independent subproblems → divide and conquer; overlapping ones → cache them and it becomes DP.
pch.quizShowAnswer
B — Ones where the subproblems overlap — then it is exponential re-computation and you want DP (memoisation) instead; divide and conquer assumes independent subproblems — That distinction is the boundary with the whole DP phase: independent subproblems → divide and conquer; overlapping ones → cache them and it becomes DP.
Recall card
Section titled “Recall card”- Cue — the problem splits into independent subproblems of the same shape, and the results combine cheaply: sorting, counting pairs across halves, searching a halved space, expression parenthesisation.
- Skeleton — base case → split → recurse on each part → combine. The combine step is where the real work (and the complexity) lives.
- Cost — recursion tree: work per level × number of levels. Master Theorem for : compare with , larger wins, ties add .
- Merge sort — time, space, stack; the tie case of the theorem.
- Counting during the merge — inversions and reverse pairs ride along free, because both halves are sorted at that moment. Keep the counting pointer outside the loop.
- Balance is the whole guarantee — quick sort shares the recurrence and degrades to when pivots are bad.
- Boundary — overlapping subproblems mean this is the wrong frame; cache them and it becomes DP.
- Divide and conquer is split, solve, combine — and the combine step is the algorithm.
- Use the Master Theorem to get the complexity from the recurrence; to speed a D&C algorithm up, reduce the number of subproblems or cheapen the combine.
- Merge sort counts things. Cross-half relationships can be tallied during the merge in per level, turning pair-counting into . The counting pointer must never reset.
- Every pair is counted exactly once — at the level where its indices first fall into different halves.
- For “all possible ways” problems, split at every valid point and make the combine a cross product. Memoise when substrings recur.
- If subproblems overlap, plain D&C is exponential — that is DP’s territory.
<=in the merge keeps the sort stable.
Next: the dynamic programming phase — what to do when the subproblems overlap.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading