11. Divide and Conquer#
Theory#
Divide and conquer solves a problem by:
- Base case — return directly when the subproblem is small enough.
- Divide — split the input into smaller independent (or nearly independent) subproblems.
- Conquer — solve each subproblem recursively.
- Combine — merge subproblem results into the final answer.
Classic recurrence: T(n) = a·T(n/b) + f(n). Merge sort is the template: split array in half, sort each half, merge.
def solve(lo, hi):
if lo >= hi: # base case
return base_value
mid = (lo + hi) // 2 # divide
left = solve(lo, mid)
right = solve(mid + 1, hi)
return combine(left, right)
When to use: sorted output from unsorted input, counting cross-boundary events (inversions), geometric problems with plane sweep strips, fast polynomial/matrix multiply, or tree queries that split left/right subtrees.
Not D&C: binary search on an answer value (feasibility predicate), pure tabulation DP, backtracking enumeration, or preprocessing tricks like binary lifting — those belong in Binary Search, Dynamic Programming, Backtracking, or Tree docs.
See also: Recursion, Searching.
Problems at a glance#
| LC | Problem | |
|---|---|---|
| 33 | Search in Rotated Sorted Array | ↗ |
| 236 | Lowest Common Ancestor in a Binary Tree | ↗ |
| 222 | Count Nodes in Complete Binary Tree | ↗ |
| 509 | Fibonacci Number with Memoization | ↗ |
| 96 | Unique Binary Search Trees | ↗ |
1. Classic Divide and Conquer (Recursive Decomposition)#
Problems that split input in half (or into fixed parts), recurse, then combine.
Problem 1: Merge Sort (GeeksforGeeks)#
Problem Statement
Given an array of integers, sort it in ascending order using merge sort.
Example 1:
Input: arr = [38, 27, 43, 3, 9, 82, 10]
Output: [3, 9, 10, 27, 38, 43, 82]
Example 2:
Input: arr = [5, 1, 4, 2, 8]
Output: [1, 2, 4, 5, 8]
Constraints:
1 <= arr.length <= 10^5-10^9 <= arr[i] <= 10^9
Code and Explanation
- Base case: arrays of length 0 or 1 are already sorted — return as-is.
- Divide: split
arratmidinto left and right halves. - Conquer: recursively sort each half.
- Combine: two-pointer merge of sorted halves into one sorted array.
Time: O(n log n) | Space: O(n) for merge buffers (O(log n) recursion stack).
- Base case: length ≤ 1 — nothing to do.
- Divide: process subarrays of doubling width
size(1, 2, 4, …) instead of recursion. - Conquer: each pass merges adjacent sorted blocks of length
size. - Combine: in-place write-back of merged block into
a[lo:hi].
Time: O(n log n) | Space: O(n) temporary buffer per merge.
Problem 2: Quick Sort (GeeksforGeeks)#
Problem Statement
Given an array of integers, sort it in ascending order using quick sort.
Example 1:
Input: arr = [10, 7, 8, 9, 1, 5]
Output: [1, 5, 7, 8, 9, 10]
Example 2:
Input: arr = [4, 2, 6, 1, 3]
Output: [1, 2, 3, 4, 6]
Constraints:
1 <= arr.length <= 10^5-10^9 <= arr[i] <= 10^9
Code and Explanation
- Base case: subarray of length 0 or 1 (
lo >= hi) is sorted. - Divide: pick a pivot (randomized here) and partition so elements ≤ pivot are left, greater are right.
- Conquer: recursively sort
[lo, p-1]and[p+1, hi]. - Combine: nothing — elements are placed in final position by partitioning.
Time: O(n log n) average, O(n²) worst | Space: O(log n) stack.
- Base case:
lo >= hi— subarray has at most one element. - Divide: Hoare partition scans from both ends, swapping out-of-place pairs until pointers cross.
- Conquer: recurse on
[lo, p]and[p+1, hi](pivot may not be at final index). - Combine: implicit — in-place swaps place elements correctly across recursive calls.
Time: O(n log n) average | Space: O(log n) stack. Fewer swaps than Lomuto on average.
Problem 3: Maximum Subarray Sum — Divide and Conquer (GeeksforGeeks)#
Problem Statement
Given an integer array nums, find the contiguous subarray with the largest sum and return that sum.
Example 1:
Input: nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
Output: 6
Explanation: subarray [4, -1, 2, 1] has sum 6.
Example 2:
Input: nums = [1]
Output: 1
Constraints:
1 <= nums.length <= 10^5-10^4 <= nums[i] <= 10^4
Code and Explanation
- Base case: single element
nums[lo]is the best subarray in that range. - Divide: split at
midinto left and right halves. - Conquer: recursively find best subarray fully in left half and fully in right half.
- Combine: compute crossing sum — best suffix of left plus best prefix of right; take max of all three.
Time: O(n log n) | Space: O(log n) stack.
- Base case: single-element ranges update
bestdirectly. - Divide: explicit stack pushes left and right sub-ranges (avoids recursion depth).
- Conquer: each popped range computes its crossing contribution.
- Combine: track global maximum across all crossing sums and base cases.
Time: O(n log n) | Space: O(n) stack frames in worst case.
Problem 4: Count Inversions in an Array (GeeksforGeeks)#
Problem Statement
Given an array arr[], count the number of inversion pairs: indices i < j where arr[i] > arr[j].
Example 1:
Input: arr = [2, 4, 1, 3, 5]
Output: 3
Explanation: pairs (2,1), (4,1), (4,3).
Example 2:
Input: arr = [5, 4, 3, 2, 1]
Output: 10
Constraints:
1 <= arr.length <= 10^51 <= arr[i] <= 10^9
Code and Explanation
- Base case: single-element range has 0 inversions.
- Divide: split array at
mid. - Conquer: count inversions in left half and right half recursively.
- Combine: during merge, when
a[j] < a[i], all elementsa[i..mid]invert witha[j]— addmid - i + 1.
Time: O(n log n) | Space: O(n).
- Base case: length ≤ 1 returns sorted copy with 0 inversions.
- Divide: split into left and right subarrays.
- Conquer: recursively sort-count each half.
- Combine: merge sorted halves, counting cross-half inversions when right element is smaller.
Time: O(n log n) | Space: O(n) per recursion level (functional style).
Problem 5: Closest Pair of Points (GeeksforGeeks)#
Problem Statement
Given n points in a 2D plane, find the minimum Euclidean distance between any two distinct points.
Example 1:
Input: points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)]
Output: 1.41421 (distance between (2,3) and (3,4))
Example 2:
Input: points = [(0, 0), (1, 1)]
Output: 1.41421
Constraints:
2 <= n <= 2×10^5- Coordinates fit in 32-bit integers
Code and Explanation
- Base case: ≤ 3 points — brute-force all pairs.
- Divide: sort by x, split into left and right halves at
mid. - Conquer: recursively find closest distance
dlanddrin each half. - Combine: check strip within
dof midline; only compare points withindvertically (at most 7 neighbors per point).
Time: O(n log² n) naive strip sort; O(n log n) with careful merge-by-y | Space: O(n).
- Base case: ≤ 3 points — brute force.
- Divide: split x-sorted list; partition y-sorted list into left/right by x-coordinate.
- Conquer: recurse on both halves with pre-partitioned y-lists.
- Combine: scan y-ordered strip near midline — avoids re-sorting strip each level.
Time: O(n log n) | Space: O(n).
Problem 6: Integer Multiplication — Karatsuba (GeeksforGeeks)#
Problem Statement
Multiply two large non-negative integers faster than the O(n²) schoolbook method using Karatsuba divide and conquer.
Example 1:
Input: x = 1234, y = 5678
Output: 7006652
Example 2:
Input: x = 999, y = 99
Output: 98901
Constraints:
0 <= x, y <= 10^1000(big integers)
Code and Explanation
- Base case: single-digit numbers — multiply directly.
- Divide: split each number into high/low halves
a,bandc,dat10^m. - Conquer: three recursive multiplies:
ac,bd, and(a+b)(c+d). - Combine:
ac·10^(2m) + (ad+bc)·10^m + bdusingad+bc = (a+b)(c+d) - ac - bd.
Time: O(n^log₂3) ≈ O(n^1.585) | Space: O(log n) stack.
- Base case: numbers below
THRESHOLDuse hardware multiply (constant time). - Divide: same half-split as classic Karatsuba.
- Conquer: recurse until subproblems are small.
- Combine: identical formula — hybrid reduces overhead for small subproblems.
Time: O(n^log₂3) asymptotically | Space: O(log n).
Problem 7: Search in Rotated Sorted Array (Leetcode:33)#
Problem Statement
There is an integer array nums sorted in ascending order (with distinct values).
Prior to being passed to your function, nums is possibly left rotated at an unknown index k (1 <= k < nums.length) such that the resulting array is [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]] (0-indexed). For example, [0,1,2,4,5,6,7] might be left rotated by 3 indices and become [4,5,6,7,0,1,2].
Given the array nums after the possible rotation and an integer target, return the index of target if it is in nums, or -1 if it is not in nums.
You must write an algorithm with O(log n) runtime complexity.
Example 1:
Input: nums = [4,5,6,7,0,1,2], target = 0
Output: 4
Example 2:
Input: nums = [4,5,6,7,0,1,2], target = 3
Output: -1
Example 3:
Input: nums = [1], target = 0
Output: -1
Constraints:
1 <= nums.length <= 5000-10^4 <= nums[i] <= 10^4- All values of
numsare unique.numsis an ascending array that is possibly rotated.-10^4 <= target <= 10^4
Code and Explanation
- Base case: empty range (
lo > hi) → target not found. - Divide: pick
mid, identify which half is sorted (no rotation pivot inside). - Conquer: recurse only on the half that can contain
targetby range check. - Combine: return result from the chosen half (or
-1).
Time: O(log n) | Space: O(log n) stack.
- Base case: loop ends when
lo > hi— return-1. - Divide: compute
mid, determine sorted half. - Conquer: shrink search space to the viable half each iteration.
- Combine: direct return on match; otherwise implicit via narrowed bounds.
Time: O(log n) | Space: O(1).
2. Divide and Conquer on Data Structures#
Recursive decomposition on trees, arrays, or hierarchical structures.
Problem 1: Lowest Common Ancestor in a Binary Tree (Leetcode:236)#
Problem Statement
Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
According to the definition of LCA on Wikipedia: "The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself)."
Example 1:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
Output: 3
Explanation: The LCA of nodes 5 and 1 is 3.
Example 2:
Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
Output: 5
Explanation: The LCA of nodes 5 and 4 is 5, since a node can be a descendant of itself according to the LCA definition.
Example 3:
Input: root = [1,2], p = 1, q = 2
Output: 1
Constraints:
- The number of nodes in the tree is in the range
[2, 10^5].-10^9 <= Node.val <= 10^9- All
Node.valare unique.p != qpandqwill exist in the tree.
Code and Explanation
- Base case:
None→ not found; ifnodeisporq, return it upward. - Divide: recurse into left and right subtrees independently.
- Conquer: each subtree returns its first matching ancestor of
p/q(orNone). - Combine: if both sides return non-null, current
nodeis LCA; else bubble up the non-null side.
Time: O(n) | Space: O(h) recursion stack.
- Base case: both
pandqlocated in parent map. - Divide: DFS stack explores left/right children, recording parent pointers.
- Conquer: mark when
porqis found. - Combine: walk
pancestors; first ancestor shared withq's upward walk is LCA.
Time: O(n) | Space: O(n). Iterative alternative when recursion depth is a concern.
Problem 2: Count Nodes in Complete Binary Tree (Leetcode:222)#
Problem Statement
Given the root of a complete binary tree, return the number of the nodes in the tree.
According to Wikipedia, every level, except possibly the last, is completely filled in a complete binary tree, and all nodes in the last level are as far left as possible. It can have between 1 and 2^h nodes inclusive at the last level h.
Design an algorithm that runs in less than O(n) time complexity.
Example 1:
Input: root = [1,2,3,4,5,6]
Output: 6
Example 2:
Input: root = []
Output: 0
Example 3:
Input: root = [1]
Output: 1
Constraints:
- The number of nodes in the tree is in the range
[0, 5 * 10^4].0 <= Node.val <= 5 * 10^4- The tree is guaranteed to be complete.
Code and Explanation
- Base case: empty tree → 0 nodes.
- Divide: compare left-subtree height vs right-subtree height (via left-spine walks).
- Conquer: if equal, left subtree is a perfect tree of
2^left_h - 1nodes — recurse only on right; else mirror on left. - Combine:
2^h + 1(root + perfect side) plus count from the recursive call.
Time: O(log² n) | Space: O(log n) stack.
- Base case: empty root → 0.
- Divide: compute tree height
h; last level indices[0, 2^(h-1)-1]form a search space. - Conquer: binary search for rightmost existing index using path-from-root checks.
- Combine: full levels
(2^(h-1) - 1)pluslonodes on the partial last level.
Time: O(log² n) | Space: O(1).
Problem 3: Segment Tree Construction (GeeksforGeeks)#
Problem Statement
Given an array arr[] of length n, build a segment tree that supports range-sum queries. Construct the tree recursively.
Example 1:
Input: arr = [1, 3, 5, 7, 9, 11]
Build tree → query sum(1, 3) = 3 + 5 + 7 = 15
Example 2:
Input: arr = [2, 4, 6]
query sum(0, 2) = 12
Constraints:
1 <= n <= 10^5-10^9 <= arr[i] <= 10^9
Code and Explanation
- Base case: leaf node (
lo == hi) storesarr[lo]. - Divide: split index range
[lo, hi]atmidinto left/right children. - Conquer: recursively build child subtrees.
- Combine: internal node = sum of children's values. Query recursively merges overlapping ranges.
Time: O(n) build, O(log n) query | Space: O(4n).
- Base case: leaves at indices
[size, size+n)hold raw array values. - Divide: implicit — heap indices
ihave children2iand2i+1. - Conquer: bottom-up loop from
size-1down to 1 fills parents. - Combine: each parent = sum of two children; query walks up the tree merging partial nodes.
Time: O(n) build, O(log n) query | Space: O(2n).
3. Master Theorem / Recurrence Analysis#
Analyzing divide-and-conquer recurrences and advanced matrix/polynomial splits.
Problem 1: Analyze Merge Sort Time Complexity (Conceptual)#
Problem Statement
Derive the time complexity of merge sort using recurrence relations and the Master Theorem.
Example 1:
Input size n = 1024
Output: Θ(n log n) = Θ(1024 × 10) comparisons (approx.)
Example 2:
Recurrence T(n) = 2T(n/2) + Θ(n)
Output: T(n) = Θ(n log n)
Code and Explanation
- Base case:
T(1) = Θ(1)— single element needs no merge work. - Divide: one array → two halves of size
n/2. - Conquer: two recursive calls each costing
T(n/2). - Combine: merge step scans all
nelements →Θ(n)per level;log nlevels → Θ(n log n).
Time: analysis only | Space: O(log n) recursion depth.
- Base case: Master Theorem applies when subproblems shrink uniformly (
n/b). - Divide:
a = 2subproblems of sizen/2. - Conquer: recursive cost
2·T(n/2). - Combine:
f(n) = Θ(n)matchesn^(log_b a)exactly (Case 2) → multiply by extralog nfactor.
Result: merge sort is Θ(n log n) time, Θ(n) auxiliary space for merging.
Problem 2: Solve Recurrence T(n) = 2T(n/2) + O(n) (Conceptual)#
Problem Statement
Solve the general divide-and-conquer recurrence T(n) = 2T(n/2) + O(n) that appears in merge sort, counting inversions, and many combine-linear algorithms.
Example 1:
T(n) = 2T(n/2) + n → T(n) = Θ(n log n)
Example 2:
T(n) = 2T(n/2) + n log n → T(n) = Θ(n log² n) (Case 2 with polylog factor)
Code and Explanation
- Base case:
T(1) = O(1)when subproblem size reaches 1. - Divide: each level doubles subproblem count while halving size.
- Conquer: unrolling gives
2^k · T(n/2^k)plusk·ncombine cost. - Combine: substitute
k = log₂ n→T(n) = n + n log n = Θ(n log n).
Time: Θ(n log n) | Applies to any algorithm with this recurrence shape.
- Base case:
T(1) = Θ(1)standard assumption. - Divide: identify
a(subproblem count) andb(shrink factor). - Conquer: compute critical exponent
log_b a. - Combine: compare
f(n)ton^(log_b a)— equality (Case 2) addslog nfactor → Θ(n log n).
Key insight: any D&C algorithm that splits in half twice and does linear combine work is Θ(n log n).
Problem 3: Strassen's Matrix Multiplication (GeeksforGeeks)#
Problem Statement
Multiply two n × n matrices using Strassen's divide-and-conquer algorithm in sub-cubic time.
Example 1:
Input: A = [[1,2],[3,4]], B = [[5,6],[7,8]]
Output: [[19,22],[43,50]]
Example 2:
Input: 1×1 matrices A = [[3]], B = [[7]]
Output: [[21]]
Constraints:
nis a power of 2 (pad if needed)1 <= n <= 512
Code and Explanation
- Base case: 1×1 matrices — single scalar multiply.
- Divide: partition each matrix into four
n/2 × n/2quadrants. - Conquer: seven recursive multiplies on combinations of quadrants (Strassen's trick).
- Combine: assemble
C11..C22fromM1..M7formulas; stitch into result matrix.
Time: O(n^log₂7) ≈ O(n^2.81) | Space: O(n²) per recursion level.
- Base case: same 1×1 scalar multiply after padding.
- Divide: pad to next power of 2 so quadrants split evenly.
- Conquer: run recursive Strassen on padded matrices.
- Combine: crop result back to original
n × ndimensions.
Time: O(n^log₂7) | Space: O(n²) with padding overhead.
4. Divide and Conquer with Memoization#
Recursive decomposition where overlapping subproblems are cached.
Problem 1: Fibonacci Number with Memoization (Leetcode:509)#
Problem Statement
The Fibonacci numbers, commonly denoted F(n) form a sequence, called the Fibonacci sequence, such that each number is the sum of the two preceding ones, starting from 0 and 1. That is,
F(0) = 0, F(1) = 1
F(n) = F(n - 1) + F(n - 2), for n > 1.
Given n, calculate F(n).
Example 1:
Input: n = 2
Output: 1
Explanation: F(2) = F(1) + F(0) = 1 + 0 = 1.
Example 2:
Input: n = 3
Output: 2
Explanation: F(3) = F(2) + F(1) = 1 + 1 = 2.
Example 3:
Input: n = 4
Output: 3
Explanation: F(4) = F(3) + F(2) = 2 + 1 = 3.
Constraints:
0 <= n <= 30
Code and Explanation
- Base case:
F(0) = 0,F(1) = 1. - Divide: split
ninto subproblemsn-1andn-2. - Conquer: recursively compute each (cached after first call).
- Combine: sum the two sub-results. Memoization prunes exponential recomputation to O(n) calls.
Time: O(n) | Space: O(n) memo + stack.
- Base case:
n <= 1returnndirectly. - Divide: implicit — build answers for sizes 2, 3, …, n in order.
- Conquer: each iteration solves one subproblem using two prior results.
- Combine:
F(i) = F(i-1) + F(i-2)rolling forward.
Time: O(n) | Space: O(1).
Problem 2: Unique Binary Search Trees (Leetcode:96)#
Problem Statement
Given an integer n, return the number of structurally unique BST's (binary search trees) which has exactly n nodes of unique values from 1 to n.
Example 1:
Input: n = 3
Output: 5
Example 2:
Input: n = 1
Output: 1
Constraints:
1 <= n <= 19
Code and Explanation
- Base case: empty range (
lo > hi) → one tree (empty subtree). - Divide: choose each value
rootin[lo, hi]as the tree root. - Conquer: count unique left BSTs and right BSTs independently.
- Combine: multiply left × right counts (Catalan convolution); sum over all root choices.
Time: O(n²) with memo on (lo, hi) | Space: O(n²).
- Base case:
dp[0] = dp[1] = 1(empty tree and single node). - Divide: for
nodestotal keys, try eachrootposition. - Conquer:
dp[root-1]left subtrees,dp[nodes-root]right subtrees. - Combine:
dp[nodes] += left × right— same Catalan recurrence, bottom-up.
Time: O(n²) | Space: O(n).
Problem 3: Matrix Chain Multiplication (GeeksforGeeks)#
Problem Statement
Given dimensions of n matrices in an array dims where matrix i has dimensions dims[i-1] × dims[i], find the minimum number of scalar multiplications needed to compute the product.
Example 1:
Input: dims = [1, 2, 3, 4, 3] (matrices: 1×2, 2×3, 3×4, 4×3)
Output: 30
Example 2:
Input: dims = [10, 20, 30] (one product: 10×20 × 20×30)
Output: 6000
Constraints:
2 <= len(dims) <= 1001 <= dims[i] <= 500
Code and Explanation
- Base case: single matrix (
i == j) needs 0 multiplications. - Divide: choose split point
kbetween matricesi..kandk+1..j. - Conquer: recursively find optimal cost for left and right chains.
- Combine: add left cost + right cost + merge cost
dims[i-1] × dims[k] × dims[j]; minimize overk.
Time: O(n³) | Space: O(n²) memo.
- Base case:
dp[i][i] = 0for single-matrix intervals. - Divide: iterate interval lengths from 2 to
n(smallest chains first). - Conquer: for each
[i, j], try every splitk. - Combine:
dp[i][j] = min(dp[i][k] + dp[k+1][j] + merge cost)— interval D&C tabulated.
Time: O(n³) | Space: O(n²).


