Named algorithms that matter for the theory (or for production libraries) more than for contests: what each achieves, how, and what to write instead. Asymptotic complexity is not running time: several entries are galactic (faster only for inputs larger than anything that will exist), and Dinic, Kruskal, Hungarian, KMP and NTT beat their “faster” relatives at every contest size.
| Area | Algorithm | Result | Write instead |
|---|---|---|---|
| primality | AKS | PRIMES P, | Miller–Rabin |
| primality | Lucas–Lehmer | Mersenne in squarings | (it is the practical test) |
| factoring | Lenstra ECM | , depends on the smallest factor | Pollard’s rho |
| factoring | quadratic sieve | Pollard’s rho | |
| sorting | Han; Han–Thorup | ; expected | radix sort |
| multiplication | Toom–Cook, Schönhage–Strassen, Fürer, Harvey–van der Hoeven | , , , | Karatsuba / NTT |
| DFT | Good–Thomas, Rader | no twiddles for coprime factors; prime lengths via convolution | radix-2 FFT / NTT |
| max flow | Karzanov, MPM, Ahuja–Orlin, Goldberg–Tarjan, Orlin | , , , , | Dinic |
| min-cost flow | Goldberg–Tarjan cost scaling | SSP with potentials | |
| matching | Gabow scaling, Gabow–Tarjan, Micali–Vazirani | , , | Hungarian, Hopcroft–Karp, blossom |
| MST | Fredman–Tarjan, Chazelle, Karger–Klein–Tarjan, Pettie–Ramachandran | , , expected, optimal | Kruskal |
| dynamic MST / trees | Frederickson (topology trees) | per update | offline segment tree + rollback DSU |
| LCA | Harel–Tarjan, Tarjan / Mehlhorn offline | ; | binary lifting, Euler tour + sparse table |
| planar min cut | Reif | , now | dual shortest path (Dijkstra) |
| string matching | Apostolico–Giancarlo, Galil–Seiferas, Crochemore–Perrin (Two-Way) | comparisons; space; space | KMP / Z |
| multi-pattern | Commentz–Walter, Wu–Manber | sublinear on average | Aho–Corasick |
| suffix trees | Weiner, McCreight, Farach | ; ; for integer alphabets | suffix automaton / suffix array |
Number theory and arithmetic
AKS primality (Agrawal, Kayal, Saxena, 2002)
For : is prime . Checking this directly costs coefficients; AKS checks it modulo the small polynomial for a bounded range of . Algorithm: (1) if with , composite; (2) find the smallest with ; (3) if for some , composite; (4) if , prime; (5) for , if , composite; (6) prime. in the paper, with Lenstra–Pomerance, polynomial space. It resolved an old question (primality in P unconditionally; Miller had it only under the generalized Riemann hypothesis) but nobody uses it: deterministic Miller–Rabin with fixed bases takes under a microsecond for 64-bit inputs where AKS takes seconds, and ECPP produces certificates far faster for huge .
| Test | Time | Deterministic? | Practical? |
|---|---|---|---|
| trial division | yes | tiny | |
| Miller–Rabin, fixed bases | yes for 64-bit | yes: Primality and Factorization | |
| Miller–Rabin, random bases | error | yes | |
| Baillie–PSW | no counterexample known | yes | |
| ECPP | heuristic | yes, with a certificate | huge |
| AKS | yes | no |
Proof: the polynomial identity behind AKS
In , . If is prime, for (the numerator has the factor , the denominator does not), and by Fermat, so . If is composite with a prime factor , , then is not divisible by (the factor in is cancelled by the in , and no other factor of appears), so the coefficient of is when . AKS’s contribution, quoted here, is that checking the identity modulo for the small range of still certifies primality.
Lucas–Lehmer (Mersenne primes)
With , : for an odd prime , is prime . Reduction mod needs no division (: fold the high bits onto the low ones). squarings of -bit numbers: schoolbook, with FFT / IBDWT multiplication; around this is days of GPU time per candidate, which is what GIMPS distributes. Facts: prime needs prime (), the converse fails (); 52 Mersenne primes are known; the largest known prime has been a Mersenne prime almost continuously since 1952; even perfect numbers are exactly with prime (Euclid–Euler). Relatives: Lucas–Lehmer–Riesel for , Pépin’s test for Fermat numbers .
bool lucasLehmer(int p) { // M_p = 2^p - 1 prime? p prime, p <= 61 (i128 products)
if (p == 2) return true;
const ull M = (1ULL << p) - 1; ull s = 4;
auto modM = [&](i128 x) { while (x > (i128)M) x = (x & M) + (x >> p); return (ull)(x == M ? 0 : x); }; // 2^p = 1
for (int i = 0; i < p - 2; i++) s = modM((i128)s * s + M - 2); // + M keeps the value non-negative
return s == 0;
}Proof: if s_(p-2) is divisible by M_p, then M_p is prime
Let , , . By induction (square: ). Suppose and let be a prime factor of with if were composite. In (a ring with elements), ; multiply by : , so and has order exactly in the unit group of , which has at most elements. Then : a contradiction, so has no prime factor , i.e. it is prime. The converse (prime divisible) uses that 3 is a non-residue mod () and a Frobenius argument; quoted. The code:
modMkeeps the residue class because . Checked against Miller–Rabin on for all primes : exactly 2, 3, 5, 7, 13, 17, 19, 31, 61 give primes.
Lenstra’s elliptic curve method (ECM)
Pollard’s : if is -smooth then and reveals , but is fixed. Lenstra: replace by the group of an elliptic curve mod , whose order lies in (Hasse) and differs from curve to curve: unlimited retries. Pick a random curve and point (choose and set ), compute with ; point addition needs inverses mod , and a failed inversion hands out , a factor: arithmetic “mod ” is a group modulo each prime simultaneously, and the failure happens when modulo but not modulo another factor, i.e. when the curve’s order mod is -smooth. Running time in the smallest prime factor , times per curve operation: the best method for medium factors (up to 60–70 digits) of huge numbers. Practice: Montgomery curves and the ladder (no inversions in the inner loop), stage 2 (one extra prime between and ; roughly doubles effectiveness).
| Factor digits | Curves | |
|---|---|---|
| 15 | 2 000 | ~25 |
| 20 | 11 000 | ~90 |
| 30 | 250 000 | ~700 |
| 40 | 3 000 000 | ~2 400 |
| 50 | 44 000 000 | ~7 500 |
Quadratic sieve
Find with ; then is a proper factor (Fermat’s idea). (1) Factor base: primes up to a bound with (otherwise never divides ). (2) is small for small , hence often smooth. (3) Sieve: on arithmetic progressions (roots by Tonelli–Shanks); subtract along them and keep entries reaching : the entire speed advantage. (4) Collect smooth relations, exponent vectors mod 2. (5) A dependency over exists ( vectors in dimension ), found by block Lanczos or Wiedemann (huge sparse matrix). (6) Its product is a square on both sides; a trivial gcd happens about half the time, then try another dependency. Time , factor base about primes. Variants: MPQS (many polynomials), SIQS (self-initialising, the fastest QS). The idea “build relations, then linear algebra over ” recurs in XOR bases and subset-parity problems.
| Digits of | Best method |
|---|---|
| (64-bit) | Pollard’s rho |
| 20–60 | ECM (especially if some factor is small) |
| 60–100 | quadratic sieve |
| general number field sieve, |
Integer sorting in the word RAM
The bound holds for comparison sorting; the word RAM (arithmetic, bit operations and indexing on -bit words in ) is a stronger model. Techniques: packed sorting (several keys per word, sorted by a constant number of word operations, a bitonic network with masks and one multiplication), signature sorting (sort short hashed signatures, then repair ties), fusion trees (Fredman–Willard: branching factor , a node compared in by “sketch and multiply”; sorting), range reduction (halve the key length recursively: the ). None is usable in practice, but the ideas are: LSD radix sort beats std::sort 3–5 times on large 32-bit arrays (worth a template for ), word packing is bitset optimization, and “sort by a cheap key, then repair ties” is how prefix doubling works.
| Algorithm | Time | Space |
|---|---|---|
| comparison sort | – | |
| counting sort | ||
| radix sort, base | ||
| fusion trees | ||
| Han (2002) | deterministic | |
| Han–Thorup | expected | |
| ? | open |
void radixSort(vector<uint32_t>& a) { // LSD, 4 passes of 8 bits, stable
vector<uint32_t> b(sz(a));
for (int shift = 0; shift < 32; shift += 8) {
int cnt[257] = {};
for (uint32_t x : a) cnt[((x >> shift) & 255) + 1]++;
for (int i = 0; i < 256; i++) cnt[i + 1] += cnt[i];
for (uint32_t x : a) b[cnt[(x >> shift) & 255]++] = x;
swap(a, b);
}
}Proof: LSD radix sort is correct
Induction on passes: after the pass on byte , the array is sorted by the low bits. Counting sort places elements by byte and is stable (equal bytes keep their previous order, which by induction is sorted by the lower bytes); so elements are ordered by byte first and by the lower bytes among ties. After 4 passes they are sorted by all 32 bits. Checked against
std::sort.
Multiplication: Toom–Cook, Schönhage–Strassen, Fürer
Toom–Cook treats each operand as a polynomial in the split radix with pieces, evaluates both at small points ( = leading coefficient), multiplies pointwise ( recursive products of size ), interpolates the coefficients by a fixed sequence of additions and exact small divisions, and recomposes with carries: . Karatsuba is (), Toom-3 gives , Toom-4 , and approaches 1 while the interpolation constant explodes, so libraries stop around Toom-4 to Toom-8 and switch to FFT. FFT is the same evaluate–multiply–interpolate at roots of unity, where evaluation and interpolation themselves cost ; unbalanced Toom variants handle operands of very different sizes.
// Polynomial product by Toom-3 (exact integer coefficients), schoolbook below 32 terms.
vector<ll> toom3(vector<ll> a, vector<ll> b) {
int n = max(sz(a), sz(b));
if (n <= 32) {
vector<ll> c(sz(a) + sz(b) - 1, 0);
for (int i = 0; i < sz(a); i++) for (int j = 0; j < sz(b); j++) c[i + j] += a[i] * b[j];
return c;
}
int k = (n + 2) / 3; int na = sz(a), nb = sz(b);
a.resize(3 * k, 0); b.resize(3 * k, 0);
auto part = [&](const vector<ll>& v, int i) { return vector<ll>(v.begin() + i * k, v.begin() + (i + 1) * k); };
auto lin = [&](const vector<ll>& x, ll cx, const vector<ll>& y, ll cy, const vector<ll>& z, ll cz) {
vector<ll> r(k); for (int i = 0; i < k; i++) r[i] = cx * x[i] + cy * y[i] + cz * z[i]; return r;
};
auto a0 = part(a, 0), a1 = part(a, 1), a2 = part(a, 2), b0 = part(b, 0), b1 = part(b, 1), b2 = part(b, 2);
auto r0 = toom3(a0, b0); // x = 0
auto r1 = toom3(lin(a0, 1, a1, 1, a2, 1), lin(b0, 1, b1, 1, b2, 1)); // x = 1
auto rm = toom3(lin(a0, 1, a1, -1, a2, 1), lin(b0, 1, b1, -1, b2, 1)); // x = -1
auto r2 = toom3(lin(a0, 1, a1, 2, a2, 4), lin(b0, 1, b1, 2, b2, 4)); // x = 2
auto ri = toom3(a2, b2); // x = infinity
int L = 2 * k - 1; vector<ll> c(6 * k - 1, 0);
for (int i = 0; i < L; i++) { // interpolate c0..c4 of degree-4 C(x) at 0, 1, -1, 2, inf
ll v0 = r0[i], v1 = r1[i], vm = rm[i], v2 = r2[i], vi = ri[i];
ll c0 = v0, c4 = vi;
ll e = (v1 + vm) / 2 - c0 - c4; // c2
ll o = (v1 - vm) / 2; // c1 + c3
ll t = (v2 - c0 - 4 * e - 16 * c4) / 2; // c1 + 4 c3
ll c3 = (t - o) / 3, c1 = o - c3;
ll cs[5] = {c0, c1, e, c3, c4};
for (int j = 0; j < 5; j++) c[i + j * k] += cs[j];
}
c.resize(na + nb - 1);
return c;
}Schönhage–Strassen (1971): an FFT over , where is a -th root of unity (), so every twiddle multiplication is a shift; split into blocks, transform with shifts only ( bit operations), multiply pointwise by recursion on -bit numbers, inverse transform, carry: (the recursion depth is the ). GMP uses it above roughly 30 000–100 000 bits (Toom variants below): the only sub-quadratic asymptotic improvement that is both production code and asymptotically strong. The contest instinct it teaches: choose an algebraic setting where the expensive operation becomes cheap (an NTT-friendly prime like 998244353).
Fürer (2007): in the ring , which has both cheap roots of unity (powers of : shifts) and expensive ordinary ones, arranged so that only levels use the expensive ones. below , but it was never competitive: a galactic algorithm. Harvey and van der Hoeven (2019) reached , the conjectured optimum, building on Fürer’s mix of cheap and expensive roots; it beats Schönhage–Strassen only beyond about bits.
| Year | Multiplication | Time |
|---|---|---|
| antiquity | schoolbook | |
| 1960 | Karatsuba | |
| 1963 | Toom–Cook | |
| 1971 | Schönhage–Strassen | |
| 2007 | Fürer | |
| 2019 | Harvey–van der Hoeven |
| Operand size (limbs, GMP-style) | Method |
|---|---|
| schoolbook | |
| 30–300 | Karatsuba |
| 300–1000 | Toom-3 |
| 1000–3000 | Toom-4 / 6.5 / 8.5 |
| Schönhage–Strassen / FFT |
| Galactic algorithm | Beats the practical option only for | Practical? |
|---|---|---|
| Fürer multiplication | astronomically large | no |
| Harvey–van der Hoeven | no | |
| Coppersmith–Winograd-type matrix multiplication | enormous matrices | no |
| Pettie–Ramachandran MST | unanalysable | no |
| Chen et al. max flow | not yet | no |
| AKS primality | never | no |
Proof: the Toom-3 interpolation
(per coefficient block). The five products are , , , , . Hence gives ; ; ; subtracting gives , then . All divisions are exact (they divide integers that are multiples by construction). Block of the product contributes to positions , which is the recomposition. Checked against schoolbook multiplication for random polynomials up to degree 500.
Transforms
Good–Thomas (prime-factor algorithm)
For with , map the input index by the CRT () and the output index by the Ruritanian map (); then with no cross terms, and the 1-D DFT becomes a 2-D DFT on an grid: transform the columns, then the rows. Strictly fewer multiplications than Cooley–Tukey (no twiddle stage) at the price of cache-unfriendly index permutations. FFTW’s planner combines Good–Thomas (coprime factors), Cooley–Tukey (non-coprime factors, with twiddles), Rader (prime factors), Bluestein (the general fallback, Convolution Variants) and hard-coded codelets, which is why it is fast at lengths like . The reusable idea is the CRT index map: a problem of size with coprime factors becomes independent multi-dimensional pieces (modular arithmetic split into prime powers, counting over , multiplicative functions). Winograd’s FFT pushes the multiplication count lower with minimal convolution algorithms for small sizes.
Rader (prime lengths)
For a prime with primitive root , re-index , (): , a cyclic convolution of length of with , computed by an FFT (or Bluestein if factors badly), plus : . Reusable move: re-index by a primitive root to turn multiplication into addition (discrete logarithms, Pohlig–Hellman, convolution over under multiplication: Primitive Roots).
| Rader | Bluestein | |
|---|---|---|
| requires | a primitive root mod | nothing |
| convolution length | (exact, cyclic) | (padded, linear) |
| accuracy | better | chirp factors grow, slightly worse |
| generality | prime lengths | any length |
| used by FFTW for | moderate primes | large or awkward primes |
using cd = complex<double>;
vector<cd> naiveDFT(const vector<cd>& x) {
int n = sz(x); vector<cd> X(n); const double PI = acos(-1.0);
for (int k = 0; k < n; k++) for (int j = 0; j < n; j++) X[k] += x[j] * polar(1.0, -2 * PI * j * k / n);
return X;
}
vector<cd> goodThomasDFT(const vector<cd>& x, int n1, int n2) { // gcd(n1, n2) = 1; sub-DFTs done naively here
int n = n1 * n2; vector<vector<cd>> g(n1, vector<cd>(n2));
for (int j = 0; j < n; j++) g[j % n1][j % n2] = x[j]; // CRT input map
for (int r = 0; r < n1; r++) g[r] = naiveDFT(g[r]); // length-n2 DFTs
for (int c = 0; c < n2; c++) { vector<cd> col(n1); for (int r = 0; r < n1; r++) col[r] = g[r][c]; col = naiveDFT(col); for (int r = 0; r < n1; r++) g[r][c] = col[r]; }
vector<cd> X(n);
for (int k1 = 0; k1 < n1; k1++) for (int k2 = 0; k2 < n2; k2++) X[(k1 * n2 + k2 * n1) % n] = g[k1][k2]; // Ruritanian output map
return X;
}
vector<cd> raderDFT(const vector<cd>& x, int g) { // prime length p, primitive root g; cyclic convolution done naively
int p = sz(x); const double PI = acos(-1.0);
vector<int> pw(p - 1); pw[0] = 1; for (int i = 1; i < p - 1; i++) pw[i] = (ll)pw[i - 1] * g % p;
vector<cd> A(p - 1), B(p - 1), X(p);
for (int a = 0; a < p - 1; a++) A[a] = x[pw[a]];
for (int c = 0; c < p - 1; c++) B[c] = polar(1.0, -2 * PI * pw[c] / p); // omega^(g^c)
for (int j = 0; j < p; j++) X[0] += x[j];
for (int b = 0; b < p - 1; b++) { // X[g^{-b}] = x0 + sum_a A[a] B[a - b]
cd s = 0; for (int a = 0; a < p - 1; a++) s += A[a] * B[((a - b) % (p - 1) + (p - 1)) % (p - 1)];
X[pw[(p - 1 - b) % (p - 1)]] = x[0] + s;
}
return X;
}Proof: the Good–Thomas and Rader re-indexings
Good–Thomas. Write , (a bijection by the CRT) and (also a bijection: modulo it is with invertible, modulo it is ). With , , and likewise . Hence : the length- DFT is exactly the 2-D DFT of the array, read out at the Ruritanian positions. Rader. the exponent with is a bijection of onto ; for , , so , which depends on modulo : a cyclic correlation, computed in the code directly. Both functions agree with the direct DFT on random inputs (, primes up to 31).
Flows and matching
Karzanov (1974) introduced the preflow (inflow outflow, vertices may hold excess): within each level graph, saturate the source edges, push excess forward by increasing level, return what cannot reach by decreasing level, until no excess remains (a blocking flow); per blocking flow, total, space with a matrix. Goldberg–Tarjan’s push-relabel (1986) kept the preflow and replaced the level graph by a height function with purely local operations (parallelizable and, with highest-label and gap heuristics, the fastest family in practice). Karzanov also proved the bound for unit-capacity Dinic and worked on multicommodity flow.
Malhotra–Kumar–Maheshwari (MPM, 1978): blocking flow in via vertex potentials in the level graph (, count one side). The minimum-potential vertex can push its whole forward to and pull it back from greedily (no vertex can block that amount); then it is saturated and deleted with all zero-potential vertices. Each vertex is deleted once, each push saturates an edge or exhausts , and potential updates cost per deletion with a matrix: per phase, total. Dinic’s is famously pessimistic; MPM’s value is the guarantee on dense graphs with large capacities.
| Graph | Dinic | MPM |
|---|---|---|
| sparse, | ||
| dense, | ||
| unit capacities, bipartite matching |
Capacity scaling works in phases , augmenting only along residual capacities : Ford–Fulkerson becomes , Dinic . Ahuja–Orlin scales the excess instead: in phase process only nodes with excess , push from the large-excess node of smallest height, and never push more than into ; this caps non-saturating pushes at per phase: . Goldberg–Tarjan: push-relabel with dynamic trees pushes along whole paths in : . Orlin (2013) with King–Rao–Tarjan: for all densities (Orlin for by compaction of already-determined parts, excess scaling, dynamic trees and an abundance argument; KRT for denser graphs). Chen, Kyng, Liu, Peng, Probst Gutenberg, Sachdeva (2022): for max flow and min-cost flow, via interior-point methods with dynamic data structures; purely theoretical.
| Max flow | Time |
|---|---|
| Ford–Fulkerson | , pseudo-polynomial |
| Edmonds–Karp | |
| capacity-scaling Ford–Fulkerson | |
| Dinic | ; unit capacities , unit networks |
| Karzanov, MPM, push-relabel FIFO | |
| push-relabel highest label | |
| Ahuja–Orlin | |
| Goldberg–Tarjan (dynamic trees) | |
| Orlin + King–Rao–Tarjan | |
| Chen et al. |
Min-cost flow by cost scaling (Goldberg–Tarjan): with potentials , reduced costs ; a flow is -optimal if every residual arc has (0-optimal = optimal by complementary slackness). Start with and the zero flow, refine to -optimality with a push-relabel loop where “admissible” means , and stop when : integer reduced costs summed around a cycle of at most arcs cannot reach , so the flow is optimal. phases, each with dynamic trees ( without).
| Min-cost flow | Time | Notes |
|---|---|---|
| cycle cancelling | simple, pseudo-polynomial | |
| min-mean cycle cancelling | strongly polynomial (Goldberg–Tarjan, using Karp’s min-mean cycle) | |
| SSP with potentials (Bellman–Ford once, then Dijkstra on reduced costs) | write this: Min-Cost Flow | |
| capacity-scaling SSP | ||
| Goldberg–Tarjan cost scaling | fast general purpose | |
| network simplex | exponential worst case | often fastest in practice |
Gabow’s bit scaling: with , solve for the top bits ; since , a doubled optimal solution is within of optimal and augmentations repair it; after rounds it is exact. replaces a factor (pseudo-polynomial becomes weakly polynomial). It works whenever doubling preserves near-optimality and repair is cheap: max weight perfect matching in general graphs (versus or blossom), bipartite ; Goldberg’s scaling shortest paths with negative weights , and the near-linear of Bernstein, Nanongkai and Wulff-Nilsen (2022). The contest idea that survives: solve the high bits first, then refine (build an answer from the most significant bit down, checking feasibility).
Gabow–Tarjan: the assignment problem by cost scaling in : maintain -optimal dual prices (), halve each phase and repair with Hopcroft–Karp-style phases of vertex-disjoint augmenting paths (the ), stop at . Separately, linear-time union-find when the union tree is known in advance: micro-trees of vertices solved by table lookup (four Russians), ordinary DSU on the macro nodes; this makes Tarjan’s offline LCA truly linear and underlies linear MST verification.
| Assignment | Time |
|---|---|
| all permutations | |
| bitmask DP | , best for with irregular costs |
| Hungarian | , ~40 lines, well under a second for : Assignment Problem |
| MCMF with potentials | |
| Gabow–Tarjan |
Micali–Vazirani (1980): maximum matching in general graphs in , matching Hopcroft–Karp: phases of maximal sets of vertex-disjoint shortest augmenting paths (after phases paths are longer than , and at most disjoint ones remain), each phase . Blossoms break the layering; instead of contracting them it keeps evenlevel and oddlevel per vertex, detects blossoms as bridges (edges whose endpoint levels are inconsistent with a layering) and runs a “double depth-first search”. One of the hardest correct algorithms to implement: the original proof was incomplete, a full proof came in 1994 (Vazirani), and few correct implementations exist.
| Matching | Algorithm | Time |
|---|---|---|
| bipartite, small | Kuhn | |
| bipartite | Hopcroft–Karp or Dinic | |
| bipartite, weighted | Hungarian | |
| general | blossom (randomized order) | or : General Matching |
| general | Micali–Vazirani | |
| general, weighted | Blossom V (Kolmogorov) | , practical |
| perfect matching exists? | Tutte matrix, random determinant | randomized, 15 lines |
Trees and graphs
MST. Chazelle (2000): deterministic, using soft heaps (insert, meld, extract-min in amortized while corrupting at most an fraction of keys, i.e. increasing their stored values), Borůvka contractions, a hierarchy of contractible subgraphs guided by a -ary tree chosen so corruption stays controlled, and recursive clean-up of the corrupted “bad” edges. Soft heaps also give a clean selection algorithm. Pettie–Ramachandran (2002): provably optimal, running in , the unknown decision-tree complexity of MST (): partition into subgraphs of vertices (Borůvka plus soft heaps), look up a precomputed optimal decision tree for each (all found by brute force in because is tiny), contract and recurse. The open problem is now to analyse it, and to find a deterministic algorithm; MST verification is deterministic (King). Its “table lookup on tiny subproblems” is the four-Russians idea of Farach-Colton–Bender RMQ, Boolean matrix multiplication and block-precomputed bitset DP.
| Year | MST | Time |
|---|---|---|
| 1926 | Borůvka | , parallelizable |
| 1956/57 | Kruskal, Prim | ; Prim with Fibonacci heap |
| 1975 | Yao | |
| 1984 | Fredman–Tarjan | |
| 1986 | Gabow, Galil, Spencer, Tarjan | |
| 1995 | Karger–Klein–Tarjan | expected |
| 2000 | Chazelle | |
| 2002 | Pettie–Ramachandran | optimal, value unknown |
Fredman–Tarjan: Prim with a Fibonacci heap, but each growth phase stops when the heap exceeds a threshold , the grown trees are contracted, and the next pass uses threshold : passes. Frederickson: topology trees cluster a bounded-degree tree into -vertex clusters recursively ( levels): path and subtree aggregates, link and cut, plus non-local information such as “minimum edge crossing a cut”. Dynamic MST: insertion of adds the edge or swaps it with the heaviest edge on the tree path (path max, ); deleting a tree edge needs the minimum replacement across the cut, which the ambivalent data structure finds in ; sparsification (Eppstein, Galil, Italiano, Nissenzweig) improves this to .
| Dynamic connectivity / MST | Best known | Technique |
|---|---|---|
| incremental connectivity | amortized | DSU |
| decremental connectivity on trees | amortized | Euler tour tricks |
| fully dynamic connectivity | amortized | Holm–de Lichtenberg–Thorup |
| fully dynamic MST | amortized | HDT extension |
| Frederickson | worst case | topology trees |
| offline dynamic connectivity | segment tree over time + rollback DSU: ~60 lines, write this: Dynamic Connectivity |
LCA. Harel–Tarjan (1984) gave the first / LCA: heavy-path decomposition, embedding into a complete binary tree where LCA of inorder labels comes from the highest differing bit (one XOR and one clz), and per-node words recording which path levels contain an ancestor. Then Schieber–Vishkin (1988, simplified and parallelizable), Berkman–Vishkin (1993, reduction to RMQ), Farach-Colton–Bender (2000, RMQ + four Russians: the standard exposition; 2004 further simplified with a sparse table on blocks). Tarjan’s offline LCA (presented in Mehlhorn’s textbook) answers known queries in with a DSU; if the tree grows by leaves, use an Euler tour in a balanced BST; under link and cut, link-cut trees. “Sort the queries so a cheap incremental structure suffices” generalizes far beyond LCA (Mo’s algorithm, offline BIT sweeps, offline dynamic connectivity). Related: level ancestor in (ladder decomposition), tree distance , virtual trees (LCA of consecutive nodes in DFS order).
| LCA need | Method | Cost |
|---|---|---|
| queries known in advance | offline union-find | |
| online, also -th ancestor and path aggregates | binary lifting (the default) | / |
| queries | Euler tour + sparse table | / |
| preprocessing | Farach-Colton–Bender | / |
| path updates and queries | HLD + segment tree | |
| tree grows by leaves / link and cut | Euler tour in a BST / link-cut tree |
// Tarjan's offline LCA: queries[u] = {(v, id)}; iterative DFS from root, DSU tagged with the current ancestor.
vector<int> offlineLCA(int n, const vector<vector<int>>& adj, int root, const vector<pair<int, int>>& qs) {
vector<int> dsu(n), anc(n), ans(sz(qs)), parent(n, -1), it(n, 0); vector<char> done(n, 0), seen(n, 0);
iota(all(dsu), 0);
auto find = [&](int x) { while (dsu[x] != x) x = dsu[x] = dsu[dsu[x]]; return x; };
vector<vector<pair<int, int>>> at(n);
for (int i = 0; i < sz(qs); i++) at[qs[i].first].push_back({qs[i].second, i}), at[qs[i].second].push_back({qs[i].first, i});
vector<int> st{root}; seen[root] = 1; anc[root] = root;
while (!st.empty()) {
int u = st.back();
if (it[u] < sz(adj[u])) {
int v = adj[u][it[u]++];
if (!seen[v]) { seen[v] = 1; parent[v] = u; anc[v] = v; st.push_back(v); }
} else { // u finished
done[u] = 1;
for (auto [v, id] : at[u]) if (done[v]) ans[id] = anc[find(v)];
st.pop_back();
if (parent[u] >= 0) { int p = parent[u]; dsu[find(u)] = find(p); anc[find(p)] = p; }
}
}
return ans;
}Proof: offline LCA
Invariant: when finishes, every finished vertex belongs to the DSU set of the deepest ancestor of that is still on the DFS stack, and that set is tagged with (finished subtrees are merged into their parent, retagged with the parent, at the moment they finish). For a query answered when the later of the two finishes, say with already finished, the deepest stack ancestor of is an ancestor of too (the stack is the root-to- path), and it is the deepest common one: . With path compression (union by size is omitted for brevity) the cost is near-linear. Checked against a brute-force LCA on random trees.
Reif: planar minimum cut via planar duality: cuts in correspond to cycles in the dual (one vertex per face, an edge per primal edge), so the minimum cut is the minimum-weight dual cycle, found by divide and conquer on a planar separator ( vertices, balanced halves; a minimum cycle avoids it or crosses it, handled by shortest paths from separator vertices): , later (Chalermsook–Fakcharoenphol–Nanongkai; Italiano et al.). The - version is the contest one: an - min cut in a planar graph is a shortest path in the dual between the two outer faces, so “block every path from left to right at minimum cost” on a grid is Dijkstra on the dual grid; image segmentation by graph cuts started here.
| Graph | Min cut |
|---|---|
| general | Stoer–Wagner ; Karger randomized : Global Min Cut |
| planar, undirected | |
| planar - max flow | (Borradaile–Klein) |
| Primal | Dual |
|---|---|
| cut | cycle |
| - min cut | shortest path between two faces |
| spanning tree | the complement edges form a spanning tree of the dual |
| face | vertex |
Strings
Boyer–Moore family. Plain Boyer–Moore can re-compare text characters ( for aaaa in aaa…a). Apostolico–Giancarlo stores , the match length that ended at text position in an earlier alignment, and the pattern’s self-overlap table (longest common suffix of pattern[0..j] and the pattern): if a mismatch is certain, if a match of length is certain and skipped, if equal resume comparing: comparisons, preprocessing, space (the array is the price, unwelcome for streams), sublinear on typical text. Horspool and Sunday (bad-character rule only) are faster in practice.
| Variant | Worst case | Extra space | Notes |
|---|---|---|---|
| Boyer–Moore | the original | ||
| Boyer–Moore–Horspool | bad character only; fastest in practice | ||
| Sunday (quick search) | looks one past the window | ||
| Boyer–Moore–Galil | linear for all-occurrence reporting | ||
| Apostolico–Giancarlo | comparisons | general guarantee | |
| KMP | never skips, always linear |
Constant-space matching. Galil–Seiferas (1983): linear time, extra space (no failure function), comparisons, via a decomposition with highly periodic (a mismatch in is resolved by the period, a single integer) and short, computed with the critical factorisation theorem: every string has a position where the local period equals the global period. Crochemore–Perrin Two-Way (1991): the practical descendant, used by glibc’s strstr and memmem: find a critical position from the maximal suffixes under both alphabet orders ( time, space), scan the right part left to right (mismatch at offset : shift by ), then the left part right to left (mismatch: shift by the period), remembering one integer of verified prefix so characters are not re-compared: , at most comparisons. Karp–Rabin is space but only expected linear. The same periodicity theory underlies runs, Lyndon factorisation and linear-time maximal repetitions; constant-space matching matters for embedded and streaming settings.
// Crochemore-Perrin Two-Way: all occurrences of x in y with O(1) extra space.
vector<int> twoWaySearch(const string& x, const string& y) {
int m = sz(x), n = sz(y); vector<int> res;
if (m == 0) { for (int j = 0; j <= n; j++) res.push_back(j); return res; }
auto maxSuf = [&](bool rev, int& per) { // maximal suffix under < (rev: under >), and its period
int ms = -1, j = 0, k = 1; per = 1;
while (j + k < m) {
char a = x[j + k], b = x[ms + k];
if (rev ? a > b : a < b) { j += k; k = 1; per = j - ms; }
else if (a == b) { if (k != per) k++; else { j += per; k = 1; } }
else { ms = j; j = ms + 1; k = per = 1; }
}
return ms;
};
int p, q, i = maxSuf(false, p), jj = maxSuf(true, q);
int ell = i > jj ? i : jj, per = i > jj ? p : q; // critical position and its local period
if (ell + 1 + per <= m && x.compare(0, ell + 1, x, per, ell + 1) == 0) { // periodic pattern
int j = 0, memory = -1;
while (j <= n - m) {
int t = max(ell, memory) + 1;
while (t < m && x[t] == y[t + j]) t++;
if (t >= m) {
t = ell;
while (t > memory && x[t] == y[t + j]) t--;
if (t <= memory) res.push_back(j);
j += per; memory = m - per - 1;
} else { j += t - ell; memory = -1; }
}
} else {
per = max(ell + 1, m - ell - 1) + 1;
int j = 0;
while (j <= n - m) {
int t = ell + 1;
while (t < m && x[t] == y[t + j]) t++;
if (t >= m) {
t = ell;
while (t >= 0 && x[t] == y[t + j]) t--;
if (t < 0) res.push_back(j);
j += per;
} else j += t - ell;
}
}
return res;
}Crochemore’s repetitions algorithm (1981): all maximal repetitions (runs: maximal substrings of period ) in by refining the partition of positions by length- substrings (the idea of prefix doubling for suffix arrays and of Hopcroft’s DFA minimisation); classes with positions at distance reveal repetitions. Main–Lorentz finds all squares in by divide and conquer, more easily. The runs theorem (Bannai et al., 2015): fewer than runs, all found in via Lyndon roots (Runs and Periodicity). Repetition detection is the core of LZ-family compression.
Multi-pattern matching. Commentz–Walter (1979): a trie of the reversed patterns walked right to left within a window of the shortest pattern length, with two generalized good-suffix shift functions and a bad-character table: sublinear on average, worst case , space ; used by early fgrep; superseded. Wu–Manber: with the shortest pattern length and blocks of – characters, a SHIFT table says how far the window may jump for each block ( if the block occurs in no pattern prefix of length , else its rightmost end position), a HASH table lists patterns whose length- prefix ends with the block, and a PREFIX filter rejects candidates cheaply. Average about accesses (no character inside a jump is read), worst case , tables of size . Behind agrep (by Wu and Manber), grep -F with many patterns, Snort and antivirus scanners. The shortest pattern rules everything: one 2-character pattern destroys the skipping, so real tools route short patterns to a separate Aho–Corasick pass.
| Approach | Representative | Guarantee | When |
|---|---|---|---|
| automaton, left to right | Aho–Corasick | contests, always: Aho–Corasick | |
| trie + skipping, right to left | Commentz–Walter | none | historical |
| block hashing + skipping | Wu–Manber | none | long patterns, huge texts |
| bit-parallel | Bitap variants | few short patterns | |
| hashing | multi-pattern Rabin–Karp | expected | equal-length patterns |
// Wu-Manber with block size B = 2 over bytes: all (position, pattern id) occurrences.
vector<pair<int, int>> wuManber(const vector<string>& pats, const string& t) {
const int B = 2; int m = INT_MAX; for (auto& p : pats) m = min(m, sz(p));
vector<pair<int, int>> res;
if (m < B) { // too short to skip: plain search
for (int id = 0; id < sz(pats); id++) for (size_t pos = t.find(pats[id]); pos != string::npos; pos = t.find(pats[id], pos + 1)) res.push_back({(int)pos, id});
sort(all(res)); return res;
}
auto key = [&](const string& s, int i) { return (unsigned char)s[i - 1] * 256 + (unsigned char)s[i]; }; // block ending at i
vector<int> shift(1 << 16, m - B + 1); vector<vector<int>> hashTab(1 << 16);
for (int id = 0; id < sz(pats); id++) {
for (int i = B - 1; i < m; i++) shift[key(pats[id], i)] = min(shift[key(pats[id], i)], m - 1 - i);
hashTab[key(pats[id], m - 1)].push_back(id);
}
for (int i = m - 1; i < sz(t);) { // i = text index of the window's last character
int k = key(t, i);
if (shift[k] > 0) { i += shift[k]; continue; }
int start = i - m + 1;
for (int id : hashTab[k]) if (t.compare(start, sz(pats[id]), pats[id]) == 0) res.push_back({start, id});
i++;
}
sort(all(res));
return res;
}Proof: the Wu–Manber shift is safe, and the Two-Way test
Wu–Manber. An occurrence of pattern starting at makes the window ending at equal to ‘s length- prefix. If the current window ends at and an occurrence ends its prefix at with , then the block ending at is the block of ‘s prefix ending at position , so the minimum over patterns and positions of (end position of that block), which is exactly
shift. Hence jumping byshiftskips no window that could end an occurrence; when the shift is 0 the block ends some pattern’s prefix and all candidates are verified directly. Two-Way. Correctness rests on the critical factorisation theorem (the local period at the critical position equals the global period, so mismatches in the right part allow the shift and full matches allow the period shift); the full proof is Crochemore and Perrin’s (1991). Both functions were checked against naive search on random strings over small alphabets (periodic and non-periodic patterns).
Suffix trees. Weiner (1973), “the algorithm of the year” (Knuth): the first linear construction, prepending characters right to left, with per-node tables (does occur?) and (the node of ), i.e. reverse suffix links; space was the problem. Its view is that of the suffix automaton of the reversed string: the suffix-link tree of the suffix automaton of is the suffix tree of . McCreight (1976): inserts suffixes from longest to shortest with suffix links (follow the link from the parent of the last leaf instead of restarting) and skip/count (jump whole edges by length, comparing one character per edge); the head/tail bookkeeping () telescopes to , like Kasai’s LCP; with balanced children; matching statistics fall out. Ukkonen (1995) is online (left to right). Farach (1997): linear for integer alphabets by odd/even divide and conquer: pair characters, radix sort and rename, recurse on the half-length string to get the odd-suffix tree, derive the even tree in (one radix sort), then a hard “over-merge” corrected by an amortized argument: . It inspired DC3/skew (thirds instead of halves, an easy merge) and SA-IS (induced sorting replaces the merge). (The same Farach, as Farach-Colton with Bender, gave RMQ/LCA.) In contests write none of them: a suffix automaton (30 lines) or a suffix array with LCP covers almost every suffix-tree application (Suffix Tree).
| Year | Linear suffix structures |
|---|---|
| 1973 | Weiner: first linear suffix tree, space |
| 1976 | McCreight: linear time, space, offline |
| 1983 | Blumer et al.: the DAWG / suffix automaton |
| 1990 | Manber–Myers: suffix arrays in , far less memory |
| 1995 | Ukkonen: online linear construction |
| 1997 | Farach: linear for integer alphabets |
| 2003 | Kärkkäinen–Sanders DC3: simple linear suffix array |
| 2009 | SA-IS (Nong, Zhang, Chan; after Ko–Aluru): fast linear suffix array, the current standard |