Frequency Tables, Alphabet Compression, and Prefix Counts
Use a fixed-size frequency array when the alphabet is small and known, such as lowercase English letters or bytes. Map each symbol to an integer index and update counts in constant time. For arbitrary Unicode symbols or sparse alphabets, use a hash map so storage grows only with the number of distinct symbols encountered.
Alphabet compression combines these approaches: sort or otherwise enumerate the distinct symbols, assign each a dense identifier in $[0,\sigma-1]$, and then use compact arrays indexed by those identifiers. When many substring-frequency queries must be answered, precompute a prefix-count matrix where $P[i][c]$ counts occurrences of symbol $c$ in $S[0 \dots i-1]$. The frequency of $c$ in the half-open range $S[l \dots r)$ is then $P[r][c]-P[l][c]$.
| Representation | Lookup or update | Space | Alphabet requirement | Best use | Main tradeoff |
|---|---|---|---|---|---|
| Fixed array | $O(1)$ | $O(\sigma)$ | Small, known alphabet | Anagrams, byte counts, lowercase letters | Wastes space when few symbols occur |
| Hash map | Expected $O(1)$ | $O(d)$ | Arbitrary or sparse alphabet | Unicode text and unknown symbol sets | Hashing overhead and unordered traversal |
| Compressed-alphabet array | $O(1)$ after mapping | $O(d)$ | Symbols can be mapped to dense IDs | Repeated algorithms on a fixed input | Requires an $O(N \log d)$ sorting step or equivalent mapping pass |
| Sorted symbol-count pairs | $O(\log d)$ | $O(d)$ | Comparable symbols | Compact deterministic storage | Updates may require shifting entries |
| Prefix-count matrix | $O(1)$ per symbol-range query | $O(N\sigma)$ | Usually small or compressed alphabet | Many substring-frequency queries | High preprocessing and memory cost |
Two Pointers and Sliding Windows
A two-pointer algorithm maintains an interval $S[L\dots R)$ and updates its summary as either boundary moves. It is useful when the problem admits a monotone decision: after extending the right boundary, repeatedly moving the left boundary must be sufficient to restore feasibility or minimize the current feasible interval.
A fixed-size window has a predetermined length $k$. Add $S[R]$, remove $S[R-k]$, and inspect the updated summary. Anagram search uses this pattern with a frequency-difference array: a window is an anagram when every difference is zero. Tracking the number of nonzero entries avoids comparing all $\sigma$ counters at every position.
A variable-size window expands until a condition becomes satisfied or violated, then contracts while the relevant invariant permits it:
- For the longest substring with at most $k$ distinct symbols, advance $R$, then move $L$ while the distinct count exceeds $k$.
- For a minimum covering substring, expand until every required multiplicity is satisfied, then contract while coverage remains valid.
- For a longest substring with replacement budget $k$, maintain the window length and the maximum symbol frequency.
- For additive constraints such as “sum at most $k$,” the usual monotone window generally requires nonnegative contributions; negative values can invalidate the method.
The window summary must encode multiplicity, not merely membership. Covering the target "AABC" requires two copies of A, so a set of symbols is insufficient.
Trace the Window
Find the shortest substring of ADOBECODEBANC containing every symbol in ABC.
Maintain required counts A:1, B:1, and C:1. Let formed be the number of required symbols whose window count currently meets its required multiplicity. The window is valid when formed = 3.
| Event | Current window | Relevant counts | formed | Decision |
|---|---|---|---|---|
| Extend through index 5 | ADOBEC | A:1, B:1, C:1 | 3 | Record the first valid window, length 6 |
| Remove index 0 | DOBEC | A:0, B:1, C:1 | 2 | Coverage breaks; resume expansion |
| Extend through index 10 | DOBECODEBA | A:1, B:2, C:1 | 3 | Valid again; begin contraction |
| Contract to index 5 | CODEBA | A:1, B:1, C:1 | 3 | Still valid, length 6 |
| Remove index 5 | ODEBA | A:1, B:1, C:0 | 2 | Coverage breaks |
| Extend through index 12 | ODEBANC | A:1, B:1, C:1 | 3 | Valid; contract repeatedly |
| Contract to index 9 | BANC | A:1, B:1, C:1 | 3 | Record the new best, length 4 |
| Remove index 9 | ANC | A:1, B:0, C:1 | 2 | Coverage breaks; stop |
The minimum covering window is BANC. Irrelevant symbols can be removed freely, whereas removing a required symbol whose count is exactly at its threshold destroys validity.
At every step, the counters must describe exactly the half-open window $S[L\dots R)$. Expand until the target condition is reached, then advance $L$ only while that condition remains valid; this monotonicity ensures that no better candidate ending at the same right boundary is skipped.
Subsequence Dynamic Programming
Prefix DP is useful when a decision depends on progressively longer prefixes of two strings. For strings $A$ and $B$, define $\operatorname{LCS}[i][j]$ as the length of a longest common subsequence of $A[0 \dots i)$ and $B[0 \dots j)$. The base row and column are zero. If $A[i-1]=B[j-1]$, the matching symbol extends an optimal solution for the preceding prefixes; otherwise, discard one final symbol and take the better remaining state:
$$ \operatorname{LCS}[i][j]= \begin{cases} 1+\operatorname{LCS}[i-1][j-1], & A[i-1]=B[j-1],\\ \max(\operatorname{LCS}[i-1][j],\operatorname{LCS}[i][j-1]), & A[i-1]\ne B[j-1]. \end{cases} $$
A shortest common supersequence follows from the same prefix structure: matching final symbols are emitted once, while unequal symbols require choosing which prefix to shorten. Its optimal length is $|A|+|B|-\operatorname{LCS}(A,B)$.
For distinct-subsequence counting, let $D[i][j]$ count the ways the first $j$ symbols of target $T$ can be formed from the first $i$ symbols of source $S$. Every state may skip $S[i-1]$; when $S[i-1]=T[j-1]$, it may also use that occurrence:
$$ D[i][j]=D[i-1][j]+[S[i-1]=T[j-1]]D[i-1][j-1]. $$
The empty target has one realization, so $D[i][0]=1$, while a non-empty target cannot be formed from an empty source. For interleaving, state $(i,j)$ means that $A[0 \dots i)$ and $B[0 \dots j)$ form the first $i+j$ symbols of a third string; transitions consume the next matching symbol from either source.
| Problem | State | Transition | Time | Reducible space | Reconstruction |
|---|---|---|---|---|---|
| Longest common subsequence | $L[i][j]$: best length for prefixes $A[0 \dots i)$ and $B[0 \dots j)$ | Match: $1+L[i-1][j-1]$; otherwise $\max(L[i-1][j],L[i][j-1])$ | $O(nm)$ | $O(\min(n,m))$ for length only | Keep the full table, parent choices, or use Hirschberg’s algorithm |
| Shortest common supersequence | $Q[i][j]$: minimum supersequence length for two prefixes | Match: $1+Q[i-1][j-1]$; otherwise $1+\min(Q[i-1][j],Q[i][j-1])$ | $O(nm)$ | $O(\min(n,m))$ for length only | Backtrack through a full table, emitting matched or selected symbols |
| Distinct subsequences | $D[i][j]$: number of ways $T[0 \dots j)$ occurs in $S[0 \dots i)$ | Skip $S[i-1]$; also use it when final symbols match | $O(nm)$ | $O(m)$ with right-to-left updates | Usually a count only; witnesses require additional state and may be exponentially numerous |
| Interleaving strings | $I[i][j]$: whether two prefixes form $C[0 \dots i+j)$ | Arrive from $(i-1,j)$ or $(i,j-1)$ when the consumed symbol matches $C[i+j-1]$ | $O(nm)$ | $O(\min(n,m))$ for feasibility | Store a parent choice to recover one interleaving |
Segmentation and Interval DP
Prefix DP partitions a string at boundaries. In Word Break, define reachable[i] to mean that $S[0\dots i)$ can be segmented into dictionary words:
$$ \operatorname{reachable}[i] = \bigvee_{0\leq j\lt i} \left( \operatorname{reachable}[j] \land S[j\dots i)\in D \right), $$
with $\operatorname{reachable}[0]=\mathrm{true}$. A parent array storing one successful boundary $j$ reconstructs a segmentation. Traversing a trie from each reachable boundary avoids constructing and hashing every possible candidate substring.
Interval DP uses states indexed by both ends of a substring. For the minimum number of insertions needed to make $S[i\dots j]$ a palindrome, define
$$ E[i][j] = \begin{cases} 0, & i\geq j,\\ E[i+1][j-1], & S[i]=S[j],\\ 1+\min(E[i+1][j],E[i][j-1]), & S[i]\neq S[j]. \end{cases} $$
Evaluate states by increasing interval length so that every shorter dependency has already been computed.
For palindrome partitioning, first precompute pal[i][j], indicating whether $S[i\dots j]$ is a palindrome. Then let pieces[i] be the minimum number of palindromic pieces covering $S[0\dots i)$. Initialize pieces[0] = 0; whenever $S[j\dots i)$ is palindromic, update from pieces[j] + 1. For a non-empty string, the minimum number of cuts is pieces[N] - 1.
flowchart TD Empty["Boundary 0: reachable"] PrefixJ["Reachable prefix ending at j"] Word["Dictionary word S[j..i)"] PrefixI["Reachable prefix ending at i"] Inner["Shorter interval state"] Pal["Palindrome state pal[i][j]"] Partition["Best partition ending at i"] ``` Empty --> PrefixJ PrefixJ --> PrefixI Word --> PrefixI Inner --> Pal Pal --> Partition PrefixJ --> Partition ```
Prefix states naturally use boundaries from $0$ through $N$. Interval states must explicitly state whether endpoints are inclusive or half-open; mixing those conventions is a common source of incorrect base cases and off-by-one errors.
Space Optimization, Reconstruction, and Testing
When a two-dimensional recurrence depends only on the preceding row and the current row, retain two rows instead of the full table. This reduces LCS length computation from $O(nm)$ space to $O(\min(n,m))$ while preserving its $O(nm)$ running time. A one-row implementation is also possible when cells are updated in an order that preserves every dependency.
Reconstruction requires more information. A full LCS table permits backtracking from $(n,m)$: matching symbols move diagonally and enter the answer, while unequal symbols move toward a neighboring state with the same optimal value. Parent pointers encode the same decisions explicitly. Hirschberg’s algorithm recovers an LCS using linear auxiliary space by combining forward and backward DP rows with divide and conquer.
Tests should cover empty strings, identical strings, disjoint alphabets, repeated symbols, ties with multiple valid witnesses, and highly unequal lengths. In Python, iteration operates on Unicode code points rather than user-perceived grapheme clusters; normalize text and segment graphemes explicitly when the application defines a “character” as a visible symbol.
Rolling storage preserves an optimal value but normally discards the decisions needed to reconstruct a witness. Do not replace the full DP table until you know that only the value is required or that reconstruction will use a linear-space method such as Hirschberg’s algorithm.
Next in the Series
In Exact Pattern Matching, we'll explore From naive comparison to KMP, Z, rolling hashes, Boyer-Moore, Two-Way, and bit-parallel matching.