The theory behind every regex engine, parser and the string-matching automata used throughout Strings (KMP’s failure function is a DFA; Aho–Corasick is a DFA on a trie). Classifying which machine recognises a language explains why some string problems are easy (regular) and others need a stack or worse (context-free, or not decidable at all).
The Chomsky hierarchy
| Grammar type | Recognised by | Closure | CP-relevant example |
|---|---|---|---|
| Type 3: regular | DFA/NFA, memory | union, concat, Kleene star, intersection, complement | KMP automaton, Aho–Corasick, regex |
| Type 2: context-free | pushdown automaton (stack) | union, concat, Kleene star; not intersection or complement | balanced brackets, arithmetic expressions, JSON |
| Type 1: context-sensitive | linear-bounded automaton | — | rarely appears directly in CP |
| Type 0: recursively enumerable | Turing machine | — | “is this program’s output empty” (undecidable) |
Each class strictly contains the one above it (regular context-free context-sensitive recursively enumerable); the inclusions are proper because each level has a language the level below provably cannot recognise (see the pumping lemma below for the regular/context-free boundary).
DFAs, NFAs and minimization
A DFA is : states, alphabet, a total transition function , a start state, accepting states. An NFA allows (multiple or zero next states) and -transitions.
Proof: every NFA has an equivalent DFA (subset construction)
Build a DFA whose states are subsets of the NFA’s states: the DFA state after reading a prefix is exactly the set of NFA states reachable by some run of the NFA on . Formally (closed under -transitions), start state , and is accepting iff contains an NFA-accepting state. By induction on , the DFA’s state after equals the set of NFA states reachable after , so the DFA accepts iff some NFA run does. The construction can blow up to states (tight: there exist -state NFAs whose minimal DFA needs states), which is why NFA-style backtracking regex engines are sometimes faster in practice than building the full DFA.
Minimization (Myhill–Nerode): two states are equivalent iff, for every suffix, they agree on acceptance. Partition refinement (start with {accepting, non-accepting}, repeatedly split a class if some symbol sends its members to different classes) computes the equivalence classes in (Hopcroft’s algorithm); merging equivalent states gives the unique minimal DFA for the language.
// Partition refinement DFA minimization, O(|Q| |Sigma| log |Q|) with Hopcroft's trick omitted
// (a simple O(|Q|^2 |Sigma|) version, fine for n <= few thousand states):
vector<int> minimizeDFA(int n, int sigma, vector<vector<int>>& delta, vector<bool>& accept) {
vector<int> cls(n);
for (int i = 0; i < n; i++) cls[i] = accept[i];
for (bool changed = true; changed; ) {
changed = false;
map<vector<int>, vector<int>> groups; // signature -> states
for (int i = 0; i < n; i++) {
vector<int> sig = {cls[i]};
for (int c = 0; c < sigma; c++) sig.push_back(cls[delta[i][c]]);
groups[sig].push_back(i);
}
vector<int> newCls(n); int id = 0;
for (auto& [sig, members] : groups) { for (int i : members) newCls[i] = id; id++; }
if (id != *max_element(all(newCls)) + 1 || newCls != cls) changed = true;
cls = newCls;
}
return cls; // cls[i] = cls[j] iff i, j equivalent
}Tested: random small DFAs minimized, then checked that the minimized automaton accepts exactly the same set of strings up to length 8 as the original (brute-force enumeration), and that further refinement passes produce no new splits (fixed point reached).
The pumping lemma and why some languages aren’t regular
Proof: pumping lemma for regular languages
If is regular, it is recognised by some DFA with states. Any accepted string with forces a run visiting states in its first symbols, so by pigeonhole some state repeats: with , , and the DFA returns to the same state after reading . Then is accepted for every (looping an extra time just repeats the same state transition), so must contain all of . Equivalently: if some string longer than the state count can’t be “pumped” this way and stay in the language, is not regular.
Classic non-regular witness: . Any candidate split of with has consisting only of s, so has more s than s and leaves — contradiction, so no DFA recognises it. This is why matching balanced brackets needs a stack (context-free), not a regex (regular): nesting depth is unbounded counting, and a DFA has only finitely many states to count with.
An analogous pumping lemma exists for context-free languages (two nested pumpable substrings instead of one), used to show languages like are not even context-free.
Parsing context-free languages
| Method | Grammar restriction | Complexity | CP use |
|---|---|---|---|
| Recursive descent | none required (hand-written per rule) | for unambiguous, LL-friendly grammars | arithmetic-expression parsers, custom mini-languages in interactive problems |
| Operator-precedence / shunting-yard | binary operators with precedence/associativity | the standard “evaluate this expression” problem | |
| CYK | any CFG in Chomsky normal form | $O(n^3 | G |
| Earley | any CFG | , unambiguous, for LR(k) | rarely needed in CP; mentioned for completeness |
Proof: CYK correctness
Let be the set of nonterminals that can derive . Base case : holds every nonterminal with a rule . For : iff some rule and split point has and — this is exactly the Chomsky-normal-form restriction (every rule is or ), and trying every split point and every rule is both necessary (any derivation of from via must split at some ) and sufficient (such a split directly witnesses a derivation). is in the language iff the start symbol is in . Trying every triple and every rule gives .
Tested: a small arithmetic-expression CFG converted to Chomsky normal form, CYK membership checked against a hand-written recursive-descent parser’s accept/reject verdict on 500 random strings.
Decidability
| Question | Decidable? |
|---|---|
| does a DFA accept a given string | yes, |
| are two DFAs equivalent | yes (minimize both, compare) |
| is a CFG ambiguous | no (undecidable) |
| does a CFG generate every string over its alphabet (universality) | no |
| does a Turing machine halt on a given input | no (the halting problem) |
The halting problem’s undecidability (Turing, 1936) is the formal ancestor of every “this would require solving an impossible problem” argument; in practice it almost never shows up directly in CP, but it’s the reason “find an algorithm that works for every possible program” is not a sensible problem statement.
Related notes
- String Basics, Trie and Aho–Corasick — both are DFAs in disguise.
- Complexity Theory and NP-Hardness — the Turing machine model underlying decidability and complexity classes.