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 typeRecognised byClosureCP-relevant example
Type 3: regularDFA/NFA, memoryunion, concat, Kleene star, intersection, complementKMP automaton, Aho–Corasick, regex
Type 2: context-freepushdown automaton (stack)union, concat, Kleene star; not intersection or complementbalanced brackets, arithmetic expressions, JSON
Type 1: context-sensitivelinear-bounded automaton—rarely appears directly in CP
Type 0: recursively enumerableTuring 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.

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

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

MethodGrammar restrictionComplexityCP use
Recursive descentnone required (hand-written per rule) for unambiguous, LL-friendly grammarsarithmetic-expression parsers, custom mini-languages in interactive problems
Operator-precedence / shunting-yardbinary operators with precedence/associativitythe standard “evaluate this expression” problem
CYKany CFG in Chomsky normal form$O(n^3G
Earleyany CFG, unambiguous, for LR(k)rarely needed in CP; mentioned for completeness

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

QuestionDecidable?
does a DFA accept a given stringyes,
are two DFAs equivalentyes (minimize both, compare)
is a CFG ambiguousno (undecidable)
does a CFG generate every string over its alphabet (universality)no
does a Turing machine halt on a given inputno (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.