Chapter 3 Β· Design & Analysis of Algorithms

Divide &
Conquer

Break the problem into parts, solve each recursively, combine the results. A deceptively simple idea that yields some of the most beautiful algorithms in computer science.

πŸ”€ Inversions O(n log n) πŸ“ˆ Stock O(n) πŸ”‘ Master Theorem βœ– Karatsuba O(n¹·⁡⁹) πŸ”’ Strassen O(n²·⁸¹) γ€° FFT O(n log n)
Γ·

01 The Divide & Conquer Paradigm

"Divide and conquer refers to a class of algorithmic techniques in which one breaks the input into several parts, solves the problem on each part recursively, and then combines the solutions to these subproblems into an overall solution."

βœ‚οΈ
1 Β· Divide
Split the input into smaller subproblems, usually of roughly equal size.
πŸ”
2 Β· Conquer
Solve each subproblem recursively. The base case is small enough to solve directly.
πŸ”—
3 Β· Combine
Merge the sub-solutions into a solution for the original problem.
Analysis method
The running time of a D&C algorithm is naturally expressed as a recurrence relation: T(n) = aT(n/b) + f(n), where a subproblems of size n/b are solved, and combining takes f(n) time. The Master Theorem often gives a closed-form solution.

02 Counting Inversions

This problem arises directly from collaborative filtering β€” matching users by taste. The key insight: two rankings are "similar" when few pairs are out of order. How many pairs need to be swapped?

Definition
Inversion

Given an array A[1:n], two indices i < j form an inversion if A[i] > A[j]. The number of inversions measures how far the array is from being sorted. A perfectly sorted array has 0 inversions; the worst case has C(n,2) = n(nβˆ’1)/2 inversions.

Why Not NaΓ―ve O(nΒ²)?

Checking every pair takes O(n²). Connection to insertion sort: each swap in insertion sort eliminates exactly one inversion, so its runtime is O(n + I) where I = inversions. But in the worst case, I = Θ(n²).

The Trick: Modified Merge Sort

During the merge step, when we pick an element from the right subarray over an element from the left, all remaining left elements form inversions with it. We count these "cross-inversions" for free during the merge.

Algorithm β€” O(n log n)
NumberOfInversions
NumberOfInversions(A[p:r]): inv = 0 if p < r: q = ⌊(p+r)/2βŒ‹ inv += NumberOfInversions(A[p:q]) // left inversions inv += NumberOfInversions(A[q+1:r]) // right inversions inv += NumberOfInversionsWithMerge(A[p:q], A[q+1:r]) // cross-inversions return inv // During merge: if A[i'] > A[j'], count (q βˆ’ i' + 1) new inversions // because all remaining left elements are > A[j']

The key correctness argument: when A[i'] > A[j'] during merge, all elements A[i'], A[i'+1], …, A[q] in the left half are also > A[j'] (since the left is sorted). So we count exactly q βˆ’ i' + 1 new inversions in one step.

πŸ”’ Inversion Counter Interactive
Array: [4, 3, 1, 2]. Press Next to count inversions via merge sort.
Inversions found: 0
Phase: β€”
Inversions log will appear here…
\[T(n) = 2T(n/2) + O(n) \implies T(n) = O(n \log n)\]

Same recurrence as merge sort β€” because it IS a modified merge sort.

03 Maximum Profit (Investment Problem)

Given stock prices A[1:n] over n days, find buy day i and sell day j β‰₯ i maximizing profit A[j] βˆ’ A[i].

D&C Strategy β€” 3 Cases
MaximalIncrease

Split A[1:n] at midpoint m. The optimal (buy, sell) pair must fall into exactly one of three cases:

⬅️
Case 1 β€” Left only
Both i, j ≀ m. Solve recursively on A[1:m].
➑️
Case 2 β€” Right only
Both i, j > m. Solve recursively on A[m+1:n].
↔️
Case 3 β€” Crossing
i ≀ m < j. Buy at the minimum of A[1:m], sell at the maximum of A[m+1:n].

Two Versions

Basic Version β€” O(n log n)

Each call scans subarrays for min/max: O(n) combine step β†’ T(n) = 2T(n/2) + O(n) β†’ O(n log n).

\[T(n) = 2T(n/2) + O(n)\]
Upgraded Version β€” O(n)

Return min and max recursively alongside the best pair β€” so combine is O(1). Unrolling the recurrence gives linear time.

\[T(n) = 2T(n/2) + O(1) = O(n)\]

Upgrade Trick β€” Returning Extra Info

UpgradedMaximalIncrease(A[l:r]) returns four values: the best (buy, sell) pair, and the indices of the overall min and max in A[l:r]. The crossing case then uses the left's min and the right's max directly β€” no scan needed. Combining min/max of both halves is O(1).

Explicit derivation (n = 2ᡏ)
Expanding T(n) = c₁ + 2T(n/2): after k = log n unrollings we get T(n) = c₁(2α΅βˆ’1) + 2ᡏT(1) = c₁(nβˆ’1) + cβ‚‚n = O(n).

04 The Master Theorem

A systematic tool for solving recurrences of the form T(n) = aT(n/b) + f(n).

Master Theorem
T(n) = aT(n/b) + f(n) β€” where a β‰₯ 1, b > 1 constants, f(n) positive

Compare f(n) to the watershed function nlogba:

Case 1 β€” f dominated by watershed

If f(n) = O(nlogba βˆ’ Ξ΅) for some Ξ΅ > 0, then T(n) = Θ(nlogba)

Case 2 β€” f matches watershed

If f(n) = Θ(nlogba), then T(n) = Θ(nlogba log n)

Case 3 β€” f dominates watershed

If f(n) = Ξ©(nlogba + Ξ΅) for some Ξ΅ > 0, AND af(n/b) ≀ cf(n) for some c < 1, then T(n) = Θ(f(n))

Four Worked Examples from the Notes

Recurrencea, b, f(n)Watershed nlogbaCaseResult
T(n) = 9T(n/3) + n a=9, b=3, f=n nlog₃9 = nΒ² Case 1 (Ξ΅=1) Θ(nΒ²)
T(n) = T(2n/3) + 1 a=1, b=3/2, f=1 nlog₃/β‚‚1 = 1 Case 2 Θ(log n)
T(n) = 3T(n/4) + n log n a=3, b=4, f=n log n nlogβ‚„3 β‰ˆ n0.79 Case 3 (Ξ΅β‰ˆ0.2) Θ(n log n)
T(n) = 2T(n/2) + n log n a=2, b=2, f=n log n nlogβ‚‚2 = n ⚠ Master fails! Θ(n logΒ²n)
Edge case β€” Master Theorem fails
For T(n)=2T(n/2)+n log n: f(n) = n log n grows faster than n, but only by a log factor β€” not polynomially. Since f(n)/nlogβ‚‚2 = log n = o(nΞ΅), Case 3 doesn't apply. The Master Theorem has no answer here. A refined analysis gives T(n) = Θ(n logΒ² n).

05 Interactive Master Theorem Calculator

πŸ”‘ Master Theorem Calculator Interactive

Setting f(n) exponent = k means f(n) = nᡏ (for log n terms use Case 2 directly). Examples preloaded above use Merge Sort defaults (a=4, b=2, f=n¹).

06 Karatsuba's Fast Integer Multiplication

Multiplying two n-bit integers naΓ―vely: O(nΒ²). Can divide-and-conquer help?

The Obvious Split β€” Still O(nΒ²)

Write x = x₁·2n/2 + xβ‚€ and y = y₁·2n/2 + yβ‚€. Then:

\[xy = x_1y_1 \cdot 2^n + (x_1y_0 + x_0y_1) \cdot 2^{n/2} + x_0y_0\]

Four n/2-bit multiplications β†’ T(n) = 4T(n/2) + O(n). By Master Theorem: T(n) = O(nlogβ‚‚4) = O(nΒ²). No improvement!

Karatsuba's Trick β€” Three Recursive Calls

Observe that x₁yβ‚€ + xβ‚€y₁ = (x₁+xβ‚€)(y₁+yβ‚€) βˆ’ x₁y₁ βˆ’ xβ‚€yβ‚€. The three products on the right side reuse x₁y₁ and xβ‚€yβ‚€ which we compute anyway!

Karatsuba

P₁ = x₁y₁           β† recursive call 1
Pβ‚‚ = xβ‚€yβ‚€           β† recursive call 2
P₃ = (x₁+xβ‚€)(y₁+yβ‚€)  β† recursive call 3
xy = P₁·2ⁿ + (P₃ βˆ’ P₁ βˆ’ Pβ‚‚)Β·2n/2 + Pβ‚‚

\[T(n) = 3T(n/2) + O(n) \implies T(n) = O(n^{\log_2 3}) = O(n^{1.585})\]
Naive schoolbook
O(nΒ²)
D&C 4-way
O(nΒ²) still!
Karatsuba (3-way)
O(n¹·⁡⁸⁡)
The pattern
Saving one recursive call (4β†’3) saves a factor of n0.415 β€” substantial for large n. This same trick of "reducing recursions by algebra" drives both Karatsuba and Strassen.

07 Strassen's Fast Matrix Multiplication

Standard nΓ—n matrix multiply needs nΒ³ operations. Partition A, B, C into 2Γ—2 blocks of n/2Γ—n/2 submatrices:

\[\begin{pmatrix}A_{11}&A_{12}\\A_{21}&A_{22}\end{pmatrix}\begin{pmatrix}B_{11}&B_{12}\\B_{21}&B_{22}\end{pmatrix}=\begin{pmatrix}C_{11}&C_{12}\\C_{21}&C_{22}\end{pmatrix}\]
Standard Blocking β€” 8 multiplications

C₁₁ = A₁₁B₁₁ + A₁₂B₂₁
C₁₂ = A₁₁B₁₂ + A₁₂Bβ‚‚β‚‚
C₂₁ = A₂₁B₁₁ + Aβ‚‚β‚‚B₂₁
Cβ‚‚β‚‚ = A₂₁B₁₂ + Aβ‚‚β‚‚Bβ‚‚β‚‚

T(n) = 8T(n/2) + O(nΒ²) β†’ O(nΒ³) β€” still cubic!

Strassen β€” 7 multiplications

P₁ = A₁₁(Bβ‚β‚‚βˆ’Bβ‚‚β‚‚)
Pβ‚‚ = (A₁₁+A₁₂)Bβ‚‚β‚‚
P₃ = (A₂₁+Aβ‚‚β‚‚)B₁₁
Pβ‚„ = Aβ‚‚β‚‚(Bβ‚‚β‚βˆ’B₁₁)
Pβ‚… = (A₁₁+Aβ‚‚β‚‚)(B₁₁+Bβ‚‚β‚‚)
P₆ = (Aβ‚β‚‚βˆ’Aβ‚‚β‚‚)(B₂₁+Bβ‚‚β‚‚)
P₇ = (Aβ‚β‚βˆ’A₂₁)(B₁₁+B₁₂)

Recombining with 7 Products

C₁₁ = Pβ‚… + Pβ‚„ βˆ’ Pβ‚‚ + P₆
C₁₂ = P₁ + Pβ‚‚
C₂₁ = P₃ + Pβ‚„
Cβ‚‚β‚‚ = Pβ‚… + P₁ βˆ’ P₃ βˆ’ P₇

\[T(n) = 7T(n/2) + O(n^2) \implies T(n) = O(n^{\log_2 7}) \approx O(n^{2.807})\]
MethodMultiplicationsAdditionsTotal Ops
Standard 8 Γ— (n/2)Β² 4 Γ— (n/2)Β² O(nΒ³)
Strassen 7 Γ— (n/2)Β² 18 Γ— (n/2)Β² O(n²·⁸⁰⁷)
Trade-off
Strassen needs 18 additions vs. standard's 4 β€” it trades multiplications for additions. Since multiplications are slower (especially for large matrices), this is worthwhile asymptotically. For small n, the constant overhead makes it slower than the standard algorithm.
Non-powers of 2
For general n, pad A and B with zeros to the smallest power of 2 β‰₯ n, run Strassen, then extract the top-left nΓ—n block. This gives O(m²·⁸¹) = O((2n)²·⁸¹) = O(n²·⁸¹).

08 Fast Polynomial Multiplication (FFT)

Given polynomials A(x) of degree mβˆ’1 and B(x) of degree nβˆ’1, compute C(x) = A(x)Β·B(x) of degree m+nβˆ’2, where cβ‚– = Ξ£α΅’β‚Šβ±Όβ‚Œβ‚– aα΅’bβ±Ό.

Naive convolution: O(mn). FFT does it in O(n log n).

Key Concept
Evaluation-Multiplication-Interpolation

Instead of working with coefficients directly, we convert to "point-value" form β€” evaluate both polynomials at 2n points, multiply the values pointwise, then convert back to coefficients (interpolation).

πŸ“Š
1. Evaluate β€” O(n log n)
Use FFT to evaluate A(x) and B(x) at all 2n-th complex roots of unity Ο‰β±Ό = e2Ο€ij/2n.
βœ–οΈ
2. Multiply β€” O(n)
Compute C(Ο‰β±Ό) = A(Ο‰β±Ό)Β·B(Ο‰β±Ό) for each j. Pointwise multiplication is trivially linear.
πŸ”™
3. Interpolate β€” O(n log n)
Run the inverse FFT to recover the coefficients cβ‚€,…,c₂ₙ₋₁ from the point values.

The FFT Divide & Conquer β€” Even/Odd Split

Any polynomial A(x) can be split into its even-index and odd-index coefficients:

\[A(x) = A_{\text{even}}(x^2) + x\, A_{\text{odd}}(x^2)\]

where Aeven(x) = aβ‚€ + aβ‚‚x + aβ‚„xΒ² + … and Aodd(x) = a₁ + a₃x + aβ‚…xΒ² + …

Evaluating at Ο‰β±ΌΒ² = e2Ο€ij/n (the n-th roots of unity) means we've reduced evaluating one degree-(nβˆ’1) polynomial at 2n points to evaluating two degree-(n/2βˆ’1) polynomials at n points.

\[T(n) = 2T(n/2) + O(n) \implies T(n) = O(n \log n)\]

The Inverse FFT (IFFT)

To recover coefficients from point values, we exploit a beautiful algebraic fact: evaluating the polynomial D(x), whose coefficients are the C-values at roots of unity, at the roots of unity again gives us back 2n times each coefficient cβ‚œ. Dividing by 2n recovers C. The IFFT has the same structure as the FFT, so it also runs in O(n log n).

Key property β€” roots of unity sum to zero
For any (2n)-th root of unity Ο‰ β‰  1: Ξ£β‚›β‚Œβ‚€Β²βΏβ»ΒΉ Ο‰Λ’ = 0. This cancellation is what makes IFFT work β€” all cross-terms vanish.
Why this matters beyond polynomials
Fast polynomial multiplication is a building block for fast integer multiplication (integers are polynomials with digit coefficients). Combined with Karatsuba, FFT-based multiplication achieves O(n log n log log n) β€” nearly linear!

09 Closest Pair of Points

Given n points in the plane, find the pair with smallest distance. NaΓ―ve: O(nΒ²). D&C achieves O(n log n) β€” the first sub-quadratic algorithm for this fundamental geometry problem, due to Shamos and Hoey (early 1970s).

Algorithm β€” O(n log n)
ClosestPair
Pre-sort all points by x-coord (array X) and by y-coord (array Y). Recursive step on set Q, with sorted arrays X, Y: if |Q| ≀ 3: brute-force all pairs Split Q into Q_L (left half) and Q_R (right half) via vertical line β„“ Ξ΄_L ← ClosestPair(Q_L) // min distance in left half Ξ΄_R ← ClosestPair(Q_R) // min distance in right half Ξ΄ ← min(Ξ΄_L, Ξ΄_R) // Cross-strip: find pairs with one point in Q_L, one in Q_R Y' ← points within distance Ξ΄ of line β„“, sorted by y-coord for each point p in Y': compare p to next 7 points in Y' // at most 7! update Ξ΄ if closer pair found return Ξ΄

Why Check Only 7 Points?

This is the geometric key. If two points in the strip Y' are within distance Ξ΄, they both lie in a rectangle of width 2Ξ΄ and height Ξ΄. This rectangle splits on the dividing line β„“ into two δ×δ halves (one for each side). Within each δ×δ half, no two same-side points are closer than Ξ΄ (by the recursive guarantee). So each δ×δ box holds at most one point. There are 4 boxes total on each side β†’ 8 boxes, 8 points max, so at most 7 others to check per point.

\[T(n) = 2T(n/2) + O(n) \implies T(n) = O(n \log n)\]
Correctness
The O(n) combine step: splitting X and Y into halves, building Y', and doing the 7-point comparisons all take O(n). The recurrence is T(n) = 2T(n/2) + O(n) β€” the classic merge sort recurrence β€” giving O(n log n). Adding the initial sorts (O(n log n)) keeps the total at O(n log n).

Chapter Summary

ProblemKey InsightRecurrenceRuntime
Counting Inversions Count cross-inversions during merge 2T(n/2) + O(n) O(n log n)
Max Profit (basic) 3-case split: left / right / cross 2T(n/2) + O(n) O(n log n)
Max Profit (upgraded) Return min/max alongside pair 2T(n/2) + O(1) O(n)
Karatsuba 3 recursive calls instead of 4 3T(n/2) + O(n) O(n¹·⁡⁸⁡)
Strassen 7 recursive calls instead of 8 7T(n/2) + O(n²) O(n²·⁸⁰⁷)
FFT Even/odd split at roots of unity 2T(n/2) + O(n) O(n log n)
Closest Pair Strip argument: only 7 checks 2T(n/2) + O(n) O(n log n)
The master insight
Many of these recurrences are T(n) = 2T(n/2) + O(n) β€” the same as merge sort! The magic of D&C is that a linear combine step on top of binary recursion yields O(n log n). Saving even one recursive call (4β†’3 for Karatsuba, 8β†’7 for Strassen) yields dramatic asymptotic improvements.