Chapter 9. Divide and Conquer
Fixed-point reformulations of classical divide-and-conquer problems on lattices.
This page: LLP forms. View classical forms »
Setting
A divide-and-conquer algorithm reduces a problem of size $n$ to a small number of independent subproblems of size $n/b$, solves each recursively, and combines the answers in $O(f(n))$ time. The combine step often dominates and the running time satisfies a recurrence of the form $T(n) = a \cdot T(n/b) + f(n)$. The recursive subproblems are independent, so a parallel evaluator can run them concurrently — captured in LL by Dijkstra-style parallel composition $[ A \, [] \, B ]$.
The Master Theorem
Let $a \geq 1$, $b > 1$, and $c \geq 0$, and suppose that $T(n) = a\,T(n/b) + O(n^c)$ with $T(1) = O(1)$. Then
$$ T(n) = \begin{cases} O(n^{\log_b a}) & \text{if } c < \log_b a, \\ O(n^c \log n) & \text{if } c = \log_b a, \\ O(n^c) & \text{if } c > \log_b a. \end{cases} $$Here $a$ is the number of sub-problems, $b$ is the factor by which the sub-problem size shrinks, and $O(n^c)$ is the cost of work done outside the recursive calls.
Applications:
- Merge Sort. $T(n) = 2T(n/2) + O(n)$: $a = 2$, $b = 2$, $c = 1 = \log_2 2$; case 2 gives $T(n) = O(n \log n)$.
- Binary Search. $T(n) = T(n/2) + O(1)$: $a = 1$, $b = 2$, $c = 0 = \log_2 1$; case 2 gives $T(n) = O(\log n)$.
- Strassen. $T(n) = 7T(n/2) + O(n^2)$: $a = 7$, $b = 2$, $c = 2 < \log_2 7 \approx 2.81$; case 1 gives $T(n) = O(n^{2.81})$.
- Karatsuba. $T(n) = 3T(n/2) + O(n)$: $a = 3$, $b = 2$, $c = 1 < \log_2 3 \approx 1.58$; case 1 gives $T(n) = O(n^{1.58})$.
Closest pair of points (Euclidean nearest neighbours)
Given $n$ points in the plane, find the pair with the smallest Euclidean distance. Naive $O(n^2)$. Divide-and-conquer in $O(n \log n)$: pre-sort by $x$-coordinate, recurse on the left and right halves, then scan a "strip" of width $\delta$ (the minimum distance found in either half) around the median $x$-line. The strip scan is $O(n)$ thanks to a beautiful geometric argument: any candidate pair that improves $\delta$ has its $y$-coordinates within $\delta$, so each strip point compares against only a constant number of neighbours.
Counting inversions
An inversion in an array $A$ is a pair $(i, j)$ with $i < j$ and $A[i] > A[j]$. The number of inversions measures how far $A$ is from sorted. The merge step of MergeSort counts cross-half inversions for free: when we pull from the right half, every element still pending in the left half is the larger half of an inversion. Total time: $O(n \log n)$.
Other classical examples
- Planar Convex Hull. Divide the points by $x$-coordinate; recursively hull the halves; merge by computing the upper and lower tangents in $O(n)$. Total $O(n \log n)$.
- Karatsuba multiplication. Multiplies two $n$-digit integers in $\Theta(n^{\log_2 3}) \approx \Theta(n^{1.585})$ time using three half-size multiplications instead of four.
- Strassen multiplication. Multiplies two $n \times n$ matrices in $\Theta(n^{\log_2 7}) \approx \Theta(n^{2.81})$ time using seven half-size multiplications instead of eight.
- Fast Fourier Transform. Evaluates a degree-$n$ polynomial at the $n$-th roots of unity in $\Theta(n \log n)$ via the Cooley-Tukey decomposition, enabling $\Theta(n \log n)$ polynomial multiplication.
The LLP perspective
Divide-and-conquer programs are not naturally fixed-point algorithms; they are recipes
that compose recursive calls with a combine step. However, several divide-and-conquer
problems admit lattice-linear reformulations that recover the correct answer as the
least fixed point of a forbidden/advance rule. These
reformulations do not match the asymptotic running time of the sharpest
divide-and-conquer algorithms, but they expose the parallelism in the problem and give
a uniform template that a parallel scheduler can exploit.
Three problems from this chapter have natural LLP reformulations:
- LLP-NearestNeighbor. For each point $p_j$, $G[j]$ holds an upper bound on the nearest-neighbour distance. Forbidden: some other point $k$ is closer than $G[j]$. Advance: set $G[j]$ to the exact minimum. Lattice-linear on the reverse-ordered reals. Total work $O(n^2)$.
- LLP-CountInversions. $G[j]$ counts inversions at index $j$ (elements to its left that are strictly larger). Forbidden: the count is too low. Advance: compute the exact count. Each index advances at most once. Total work $O(n^2)$.
- LLP-ConvexHull. $G[j] \in \{0,1\}$ indicates whether $p_j$ is a hull candidate. Forbidden: $p_j$ lies inside a triangle of three other candidates. Advance: $G[j] := 0$. The lattice is the reverse Boolean cube. Total work $O(n^4)$.
Integer multiplication (Karatsuba) and matrix multiplication (Strassen) compute a uniquely determined output via algebraic identities that reduce recursive multiplications; they do not search a lattice for a predicate-satisfying element and consequently do not admit natural LLP reformulations.
LLP-NearestNeighbor
For each point $p_j$, the vector $G[j]$ holds an upper bound on the distance from $p_j$ to its nearest neighbour. The forbidden condition fires when some other point $k$ is closer than $G[j]$; the advance sets $G[j]$ to the exact minimum distance over all other points. At the least fixed point, $G[j] = \min_{k \neq j} d(p_j, p_k)$ for every $j$, and the global nearest-pair distance is $\min_j G[j]$.
Time complexity: $O(n^2)$, where $n$ is the number of points.
Lattice-linear on the reverse-ordered real lattice: lowering $G[j]$ never invalidates other resolved indices. Total work $O(n^2)$; with $n$ processors, $O(n)$ parallel time.
LLP-CountInversions
Given an array $A[0..n{-}1]$, $G[j]$ counts the number of indices to the left of $j$ whose value is strictly greater than $A[j]$. Index $j$ is forbidden when $G[j]$ is strictly less than the true count; the advance sets $G[j]$ to the exact count. The total number of inversions is $\sum_j G[j]$.
Time complexity: $O(n^2)$, where $n$ is the size of the array.
Each index is advanced at most once and the advance scans $j$ left neighbours, so total work is $O(n^2)$. With $n$ processors all indices can advance in parallel for $O(n)$ parallel time.
LLP-ConvexHull
The convex hull of a point set is expressed on a Boolean lattice. $G[j] = 1$ means $p_j$ is a hull candidate; $G[j] = 0$ means it has been eliminated. A point $p_j$ is forbidden when $G[j] = 1$ and $p_j$ lies strictly inside a triangle formed by three other hull candidates. The advance sets $G[j] := 0$. At the least fixed point, $G$ is the characteristic vector of the convex hull.
Time complexity: $O(n^4)$, where $n$ is the number of points.
Each forbidden check considers $O(n^3)$ triangles, for $O(n^4)$ total work — far worse than the $O(n \log n)$ divide-and-conquer algorithm. The LLP formulation returns the hull as a membership vector; recovering the cyclic vertex order requires a post-processing step.
Looking ahead
Divide-and-conquer recurrences underpin Chapter 10 (dynamic programming, where overlapping subproblems force memoisation), Chapter 11 (network flow, where augmenting-path methods recurse on residual graphs), and the parallel-prefix / tree-contraction primitives that appear throughout the book's parallel chapters.
LLP-ConvexHull
Eliminate interior points: $j$ is forbidden when it lies inside some triangle formed by three other hull-candidate points; the advance removes it from the candidate set.
// LLP-ConvexHull: eliminate interior points to find convex hull membership.
// px[j], py[j] are x,y coordinates of point j.
class LLPConvexHull {
void LLPConvexHull(double[] px, double[] py) {
boolean[] G = true;
forbidden (j) : G[j] && isInterior(j, px, py, G) =>
advance : G[j] = false;
}
boolean isInterior(int j, double[] px, double[] py, boolean[] G) {
int i = 0;
while (i < px.length) {
if (i != j && G[i]) {
int k = i + 1;
while (k < px.length) {
if (k != j && G[k]) {
int l = k + 1;
while (l < px.length) {
if (l != j && G[l]) {
if (insideTriangle(px, py, j, i, k, l)) {
return true;
}
};
l = l + 1;
}
};
k = k + 1;
}
};
i = i + 1;
};
return false;
}
boolean insideTriangle(double[] px, double[] py, int j, int a, int b, int c) {
double d1 = sign(px[j], py[j], px[a], py[a], px[b], py[b]);
double d2 = sign(px[j], py[j], px[b], py[b], px[c], py[c]);
double d3 = sign(px[j], py[j], px[c], py[c], px[a], py[a]);
boolean hasNeg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
boolean hasPos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
return !hasNeg || !hasPos;
}
double sign(double x1, double y1, double x2, double y2, double x3, double y3) {
return (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3);
}
}
LLP-CountInversions
Count inversions to the left of each index: $j$ is forbidden when $G[j]$ is less than the number of elements to the left of $j$ that are larger than $A[j]$.
// LLP-CountInversions: count inversions to the left of each index.
class LLPCountInversions {
void LLPCountInversions(int[] A) {
int[] G = 0;
forbidden (j) : G[j] < countLeft(A, j) =>
advance : G[j] = countLeft(A, j);
}
int countLeft(int[] A, int j) {
int count = 0;
int i = 0;
while (i < j) {
if (A[i] > A[j]) { count = count + 1; };
i = i + 1;
};
return count;
}
}
LLP-NearestNeighbor
Find the nearest-neighbor distance for each point: $j$ is forbidden when $G[j]$ exceeds the actual nearest distance; the advance corrects it.
// LLP-NearestNeighbor: find nearest neighbor distance for each point.
// px[j], py[j] are x,y coordinates of point j.
class LLPNearestNeighbor {
void LLPNearestNeighbor(double[] px, double[] py) {
double[] G = infinity;
forbidden (j) : G[j] > nearestDist(px, py, j) =>
advance : G[j] = nearestDist(px, py, j);
}
double nearestDist(double[] px, double[] py, int j) {
double best = infinity;
int k = 0;
while (k < px.length) {
if (k != j) {
double d = dist(px, py, j, k);
if (d < best) { best = d; }
};
k = k + 1;
};
return best;
}
double dist(double[] px, double[] py, int j, int k) {
double dx = px[j] - px[k];
double dy = py[j] - py[k];
return dx * dx + dy * dy;
}
}