Eliminating Rows and Columns in the Pair Search Matrix
Why does the converging two-pointer algorithm for sorted 2-Sum ($A[L] + A[R] = T$) never miss a valid pair? Visualize all possible index pairs $(L, R)$ with $0 \le L < R < N$ as an upper-triangular grid inside an $N \times N$ matrix, starting at the top-right corner $(L = 0, R = N - 1)$:
- If $A[L] + A[R] < T$, since the array is sorted ($A[k] \le A[R]$ for all $k < R$), pairing the current $L$ with any smaller right index $k < R$ yields $A[L] + A[k] \le A[L] + A[R] < T$. Thus the entire row $L$ is provably invalid, and incrementing $L \leftarrow L + 1$ safely discards $R - L$ pairs in $O(1)$ time.
- If $A[L] + A[R] > T$, since $A[k] \ge A[L]$ for all $k > L$, pairing the current $R$ with any larger left index $k > L$ yields $A[k] + A[R] \ge A[L] + A[R] > T$. Thus the entire column $R$ is provably invalid, and decrementing $R \leftarrow R - 1$ safely discards $R - L$ pairs in $O(1)$ time.
The exact same pruning proof governs the Container With Most Water problem ($\max (R - L) \cdot \min(H[L], H[R])$): if $H[L] \le H[R]$, then for every $k \in (L, R)$, the width $(k - L) < (R - L)$ and the height $\min(H[L], H[k]) \le H[L] = \min(H[L], H[R])$. Therefore, $L$ can never form a larger container with any $k < R$, justifying $L \leftarrow L + 1$.
To partition an array $A[0 \dots N-1]$ around a pivot $p$ into three contiguous regions ($< p$, $== p$, and $> p$) in a single $O(N)$ pass with $O(1)$ auxiliary space, maintain three pointers $\text{low}$, $\text{mid}$, and $\text{high}$ satisfying the four-region loop invariant at the start of every iteration:
- $\forall k \in [0, \text{low} - 1]: A[k] < p$ (strictly less than pivot)
- $\forall k \in [\text{low}, \text{mid} - 1]: A[k] == p$ (equal to pivot)
- $\forall k \in [\text{mid}, \text{high}]: A[k]$ is unclassified / unknown
- $\forall k \in [\text{high} + 1, N - 1]: A[k] > p$ (strictly greater than pivot)
Potential Function (Termination Proof): Define $\Phi = \text{high} - \text{mid} + 1$, the size of the unclassified region. Initially $\text{low} = 0, \text{mid} = 0, \text{high} = N - 1$, so $\Phi = N$ and regions 1, 2, and 4 are vacuously empty. Each step inspects $A[\text{mid}]$ and either increments $\text{mid}$ or decrements $\text{high}$, strictly decreasing $\Phi$ by 1 until $\Phi = 0$ ($\text{mid} > \text{high}$), terminating in exactly $N$ comparisons.
Taxonomy of Multi-Pointer Paradigms
| Paradigm | Pointer Geometry | Core Loop Invariant | Time / Space | Canonical Problems |
|---|---|---|---|---|
| Converging Pointers | $L = 0 \to \leftarrow R = N - 1$ | All pairs outside $[L, R]$ have been provably pruned | $O(N)$ / $O(1)$ | Sorted 2-Sum, 3-Sum, Container With Most Water, Trapping Rain Water |
| Reader / Writer Compaction | $0 \le \text{write} \le \text{read} < N$ | $A[0 \dots \text{write}-1]$ contains compacted valid prefix of $A[0 \dots \text{read}-1]$ | $O(N)$ / $O(1)$ | std::remove_if, Dedup Sorted Array, Move Zeroes |
| 3-Way Partition (DNF) | $0 \le \text{low} \le \text{mid} \le \text{high}+1 \le N$ | 4 regions: $[0..\text{low}), [\text{low}..\text{mid}), [\text{mid}..\text{high}], (\text{high}..N)$ | $O(N)$ / $O(1)$ | Sort Colors (0/1/2), Quicksort 3-Way Partition (duplicate keys) |
Visualizing Dijkstra's 4-Region Dutch National Flag State
Dutch National Flag State Transitions
while mid <= high"] --> B{"Compare A[mid]
with pivot p"} B -- "A[mid] < p" --> C["swap(A[low], A[mid])
low++, mid++"] B -- "A[mid] == p" --> D["mid++"] B -- "A[mid] > p" --> E["swap(A[mid], A[high])
high-- (DO NOT increment mid!)"]
Modern x86-64 (Intel Golden Cove / AMD Zen 4) and ARM Neoverse L2 stream prefetchers track both ascending (+1 stride) and descending (-1 stride) memory accesses per 4 KB page independently. Consequently, converging two-pointer scans ($L$ moving up from $0$, $R$ moving down from $N-1$) sustain near-100% L1 data cache hit rates. However, inside the Dutch National Flag loop, the 3-way branch on random data causes ~50% branch misprediction penalties (15–20 cycles per mispredict). High-performance sorting libraries (such as pdqsort, Pattern-Defeating Quicksort in Rust's standard library) eliminate this branch bottleneck using BlockQuicksort: evaluating comparisons into small branchless offset buffers via conditional moves (cmov) before executing a tight swap loop.
Trace variable-length sliding window [L..R] and Dutch National Flag 3-way partitioning. Both L and R advance monotonically, guaranteeing at most 2N pointer movements.
Invariant & State
Production Implementation: Dijkstra's 3-Way Partition & O(1)-Space Trapping Rain Water
#include <algorithm>
#include <cstdint>
#include <utility>
#include <vector>
// Returns {low, high} bounds of the equal-to-pivot range [low .. high]
template <typename T>
std::pair<std::size_t, std::size_t> dutch_flag_partition(std::vector<T>& a, const T& pivot) noexcept {
if (a.empty()) return {0, 0};
std::size_t low = 0;
std::size_t mid = 0;
std::size_t high = a.size(); // Use half-open [mid, high) to avoid unsigned underflow!
while (mid < high) {
if (a[mid] < pivot) {
std::swap(a[low], a[mid]);
++low;
++mid;
} else if (a[mid] == pivot) {
++mid;
} else {
--high;
std::swap(a[mid], a[high]);
// Critical: do NOT increment mid; the element swapped from a[high] is unclassified!
}
}
return {low, high};
}
// O(N) Time, O(1) Space Converging Two-Pointer Trapping Rain Water
// Invariant: if height[left] <= height[right], then max_left <= max_right (or height[right]
// acts as a guaranteed right-hand wall >= height[left]), so water at left is bounded
// exclusively by max_left.
std::int64_t trap_rain_water(const std::vector<int>& height) noexcept {
if (height.size() < 3) return 0;
std::size_t left = 0, right = height.size() - 1;
int max_left = height[left], max_right = height[right];
std::int64_t trapped = 0;
while (left < right) {
if (max_left <= max_right) {
++left;
max_left = std::max(max_left, height[left]);
trapped += (max_left - height[left]);
} else {
--right;
max_right = std::max(max_right, height[right]);
trapped += (max_right - height[right]);
}
}
return trapped;
}
mid After Swapping with high & Unsigned UnderflowIn the Dutch National Flag partition, the #1 bug is incrementing mid in the A[mid] > pivot branch after swap(A[mid], A[high]). When you swap A[low] and A[mid], you already know A[low] == pivot (unless low == mid), so mid can safely advance. When you swap A[mid] and A[high], the value coming from A[high] was inside the unclassified region—incrementing mid skips inspecting it! Furthermore, if high is a size_t initialized to N - 1 and every element exceeds the pivot, --high underflows to SIZE_MAX. Always maintain the unclassified region as a half-open interval [mid, high) where high starts at N.