Divide-and-Conquer and Dynamic Programming as General Strategies
What This Concept Is
Divide-and-conquer (D&C) solves a problem by splitting it into smaller subproblems of the same shape, solving them independently, and combining the results. It works when subproblems are roughly independent and the combining step is cheap.
Dynamic programming (DP) is the cousin strategy for problems with overlapping subproblems and optimal substructure. Instead of recomputing the same subproblem many times, you store the answer (memoisation) or build up answers in a table (tabulation).
Both are heuristics for shape recognition, not algorithm categories. Seeing "this problem has a recursive structure with shared subproblems" is the move. The specific algorithm follows from the shape.
The dividing line between D&C and DP is one word: overlap.
- No overlap, independent subproblems $\to$ D&C. Naive recursion is efficient.
- Overlap, shared subproblems $\to$ DP. Naive recursion is exponential; memoise or tabulate.
A secondary distinction is about optimisation: DP typically targets optimal solutions (longest, shortest, max, min), leveraging optimal substructure. D&C is often for any solution of a correct shape (sort, search, discrete Fourier transform).
Common DP flavors you will meet later:
- Top-down (memoised recursion): natural when the recursion is clear; $O(\text{states} \times \text{transition cost})$ time.
- Bottom-up (tabulation): iterative, cache-friendlier; requires a dependency order on states.
- Dimension reduction: if the current state depends only on the last $k$ rows, keep $O(k)$ space instead of $O(n)$.
Why It Matters Here
These two strategies account for a large fraction of efficient algorithms in CS. Learning to recognise their fit in a problem is more valuable than memorising their templates.
In CS specifically:
- D&C applications: sorting (mergesort, quicksort), searching (binary search), geometric algorithms (closest pair), FFT, matrix multiplication (Strassen), parallel algorithms (many), tree recursions.
- DP applications: shortest paths (Bellman-Ford, Floyd-Warshall), edit distance, matrix chain multiplication, knapsack variants, sequence alignment (bioinformatics), scheduling, text layout.
- Mixed: many graph optimisation problems combine D&C decomposition with DP at the leaves.
Many competition problems reward contestants who spot one of these shapes immediately. Many production systems bottleneck on naive recursion that a DP table would fix.
Forward pipeline: Semester 2 (full chapters on each), Semester 3 (memoisation as a refactoring pattern), Semester 4 (recognising exponential recursion as a bug to fix via DP), Semester 6 (distributed D&C via MapReduce), Semester 10 (capstone performance optimisation).
Concrete Examples
Example 1 -- binary search (divide-and-conquer)
Given a sorted array $a[0..n)$ and a target $t$, decide whether $t \in a$.
The problem splits: "is $t$ in the left half?" or "is $t$ in the right half?" Each subproblem has the same shape and half the size. The combining step is trivial (one comparison).
Recurrence: $T(n) = T(n/2) + O(1)$. Master theorem gives $T(n) = O(\log n)$.
def bsearch(a, t):
lo, hi = 0, len(a)
while lo < hi:
mid = (lo + hi) // 2
if a[mid] == t:
return mid
if a[mid] < t:
lo = mid + 1
else:
hi = mid
return -1
Key D&C marker: the two subproblems never overlap. The left half and right half have no shared elements. No DP needed.
Example 2 -- longest common subsequence (dynamic programming)
Given two strings $x, y$ of lengths $m, n$, find the length of the longest common subsequence (LCS).
State. $\text{lcs}(i, j) = $ LCS length of $x[0..i)$ and $y[0..j)$.
Recurrence.
$$\text{lcs}(i, j) = \begin{cases} 0 & \text{if } i = 0 \text{ or } j = 0 \ \text{lcs}(i-1, j-1) + 1 & \text{if } x[i-1] = y[j-1] \ \max(\text{lcs}(i-1, j), \text{lcs}(i, j-1)) & \text{otherwise} \end{cases}$$
Naive recursion is $O(2^{m+n})$ because the same $(i, j)$ states recur across the recursion tree.
DP observation: there are only $(m+1)(n+1)$ distinct states. Tabulate:
def lcs(x, y):
m, n = len(x), len(y)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if x[i-1] == y[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[m][n]
Time $O(mn)$. Space $O(mn)$, reducible to $O(\min(m, n))$ by keeping only the previous row. The key insight was optimal substructure -- an LCS is built from an LCS of shorter prefixes -- plus overlap -- the same prefix pair recurs in many naive-recursion branches.