Skip to content

Divide and Conquer

Divide and conquer has three steps, and only one of them is interesting:

  1. Divide the problem into independent subproblems — usually trivial.
  2. Conquer each recursively — also trivial, it is just a recursive call.
  3. 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 O(n2)O(n^2) problems into O(nlogn)O(n \log n).

  • 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.

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 logn\log n levels of them.

sortMerge sort: O(n) work per level, log n levelsT(n) = 2T(n/2) + O(n)
3802714323394825106
setupMerge sort is bottom-up in effect: split until every piece is length 1 (trivially sorted), then merge sorted pieces pairwise. The merge is where all the work happens.
1/14

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).

divide_conquer.py
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 work

The Master Theorem gives the complexity from the recurrence T(n)=aT(n/b)+f(n)T(n) = a \cdot T(n/b) + f(n), where a is the number of subproblems, b the shrink factor, and f(n) the cost of combining.

AlgorithmRecurrenceResult
Binary searchT(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)O(logn)O(\log n)
Merge sortT(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n)
Naive matrix multiplyT(n)=8T(n/2)+O(n2)T(n) = 8T(n/2) + O(n^2)O(n3)O(n^3)
StrassenT(n)=7T(n/2)+O(n2)T(n) = 7T(n/2) + O(n^2)O(n2.807)O(n^{2.807})

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.py
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

O(nlogn)O(n \log n) time and O(n)O(n) space, and stable thanks to the <=.

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:

count_during_merge.py
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.

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:

diff_ways_to_compute.py
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.

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:

depthleft (sorted)right (sorted)pairs found hererunning total
1[1][3]0 — 1 > 6 is false0
2[3][1]1 — 3 > 2·1 ✓1
1[2][1, 3]0 — 2 > 2 is false1
0[1, 3][1, 2, 3]1 — 3 > 2·1 ✓, and 1 beats nothing2

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. left ascending means once x > 2·right[j] fails, it fails for every later x too — so j sweeps forward once instead of restarting per element.
  • j must be declared outside the for x in left loop. Reset it per element and the counting becomes O(n2)O(n^2) 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 left genuinely precedes every element of right in 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.
ProblemRecurrenceResult
Merge sortT(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n) time, O(n)O(n) space
Reverse pairs / inversionsT(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n) — counting rides along free
Binary searchT(n)=T(n/2)+O(1)T(n) = T(n/2) + O(1)O(logn)O(\log n)
Quick sort, balanced pivotsT(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n)O(nlogn)O(n \log n) average, O(n2)O(n^2) worst
QuickselectT(n)=T(n/2)+O(n)T(n) = T(n/2) + O(n)O(n)O(n) average
Karatsuba multiplicationT(n)=3T(n/2)+O(n)T(n) = 3T(n/2) + O(n)O(nlog23)O(n1.585)O(n^{\log_2 3}) \approx O(n^{1.585})
Different Ways to Add Parenthesesnon-uniform splitsCatalan-many results, exponential

The Master Theorem in one line: for T(n)=aT(n/b)+O(nd)T(n) = aT(n/b) + O(n^d), compare dd with logba\log_b a — the larger exponent wins, and they tie into an extra logn\log n factor. Merge sort is the tie case (a=b=2a = b = 2, d=1d = 1, so log22=1=d\log_2 2 = 1 = d), which is exactly why it lands on nlognn \log n rather than nn or n2n^2. Karatsuba beats naive multiplication precisely by changing aa from 4 to 3.

VariantThe combine stepCanonical problem
SortMerge two sorted halves912 · 148
Count cross-half pairsCount during the merge493 · 315 · 327
All possible resultsCross product of both sides241 · 95
Best of left, right, or crossingCompare three candidates53 (D&C variant) · 1763
Discard half of one inputBinary search on the split position4 Median of Two Sorted Arrays
Closest pair of pointsCheck the strip near the dividing lineclassic geometry

Problem. Sort an array of integers ascending. You must not use any built-in sorting function, and the solution should be O(nlogn)O(n \log n) 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 O(nlogn)O(n \log n)logn\log n levels, O(n)O(n) merging per level. Space O(n)O(n) for the temporary lists, plus O(logn)O(\log n) 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 (O(1)O(1) extra space) or an in-place quicksort with a randomised pivot. Worth mentioning both:

  • Quicksort is O(nlogn)O(n \log n) average and O(1)O(1) extra, but O(n2)O(n^2) worst case — and LC 912 has adversarial tests that kill a fixed pivot, so randomise.
  • Heap sort is O(nlogn)O(n \log n) worst case and O(1)O(1) 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 O(1)O(1) space is required. “Why does Python’s sorted use Timsort?” — it exploits existing runs in real data, giving O(n)O(n) 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 10410^4 results, and cut substantially by memoisation. Space O(results)O(\text{results}) 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 -10 from two different parenthesisations and the expected output lists it twice. Wrapping the result in a set is 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.

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 O(nlogn)O(n \log n) — the counting adds O(n)O(n) per merge level, the same order as the merge itself. Space O(n)O(n).

Two details that decide whether this works:

  • j outside the loop. Resetting it makes the count step O(n2)O(n^2) and the whole thing O(n2logn)O(n^2 \log n) — 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 left and right being 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. O(nlogn)O(n \log n) too, and often easier to extend. Worth naming as the alternative.
  • “Why not brute force?” O(n2)O(n^2) is 2.5×1092.5 \times 10^9 operations at n=5×104n = 5 \times 10^4 — far too slow.

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.

7 problems
1 easy4 medium2 hard

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.

They askWhat they’re checkingThe answer
“Where is the actual work?”Understanding the patternIn the combine step; divide and conquer are usually trivial
“Derive the complexity”Master Theorem fluencyT(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n); merge sort is 2T(n/2)+O(n)=O(nlogn)2T(n/2) + O(n) = O(n \log n)
“How does merge sort count inversions?”The key techniqueCross-half pairs are counted at the merge with a forward-only pointer, so O(n)O(n) per level
“Why must the pointer not reset?”Amortised reasoningBoth halves are sorted, so it only moves forward — resetting makes the step O(n2)O(n^2)
“What if the subproblems overlap?”Knowing the boundaryPlain D&C recomputes exponentially; memoise, i.e. use DP
“Merge sort or quicksort?”JudgementMerge for stability, predictable splits and linked lists; quicksort for O(1)O(1) space in-place, with a randomised pivot
“Alternative to merge-counting?”BreadthA Fenwick tree over compressed values, sweeping from the right
  • Single element — the base case for every recursion here.
  • Empty input — guard len(a) <= 1 covers it.
  • All identical[3,3,3] sorts fine and yields 0 reverse 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 depthlogn\log n for merge sort, so never a problem; but expression recursion is bounded by string length.
pch.quizTag Divide and conquer — self-check
  1. Why is merge sort O(n log n) rather than O(n) or O(n²)?

    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.

  2. In the reverse-pairs count, why can `j` sweep forward without ever resetting?

    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.

  3. Sorting during the count destroys the original indices. Why is the answer still right?

    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.

  4. For T(n) = aT(n/b) + O(n^d), how do you get the answer quickly?

    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.

  5. Quick sort has the same recurrence as merge sort but O(n²) worst case. Why?

    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.

  6. Which problems on this page are NOT improved by divide and conquer?

    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.

  • 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 T(n)=aT(n/b)+O(nd)T(n) = aT(n/b) + O(n^d): compare dd with logba\log_b a, larger wins, ties add logn\log n.
  • Merge sortO(nlogn)O(n \log n) time, O(n)O(n) space, O(logn)O(\log n) 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 O(n2)O(n^2) 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 O(n)O(n) per level, turning O(n2)O(n^2) pair-counting into O(nlogn)O(n \log n). 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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading