Computation Theory · Automata
Where the 2^n Comes From: Subset Construction and the Limits of Determinization
Why determinizing an n-state NFA can require exponentially many states, how to exhibit a language that forces the blow-up, and which partial determinization schemes keep it bounded in practice.
Determinization is the step that turns a machine which may guess into a machine which cannot, and its cost is the most quoted exponential in automata theory. The construction itself takes a paragraph: the states of the new machine are sets of states of the old one. The interesting part is not the construction but the reason it is sometimes unavoidable, and the engineering that keeps it from being unavoidable in the cases that matter.
The construction, stated precisely
Given N = (Q, Σ, δ, q₀, F) with δ ⊆ Q × (Σ ∪ {ε}) × Q, define D = (Q_D, Σ, δ_D, S₀, F_D) where:
Q_Dis the set of subsets ofQthat are reachable fromS₀underδ_D.S₀ = ε-closure({q₀}).δ_D(S, a) = ε-closure(⋃_{q ∈ S} δ(q, a)).F_D = { S ∈ Q_D | S ∩ F ≠ ∅ }.
Reachability matters. The powerset has 2^|Q| members, but the construction only ever materializes the
ones reachable from the start set, and for many machines that is a small fraction. A subset that is not
reachable is not a state of the determinized machine, and building it would be wasted work.
The empty set is a legitimate state and is almost always reachable. It represents “no path survives”,
and it is a trap: δ_D(∅, a) = ∅ for every a. Implementations often add it explicitly rather than
discovering it, because the determinized machine is then total and its transition table has no holes.
Adding it costs one state and one row.
from collections import deque
def determinize(start_set, delta, alphabet, accepting):
start = frozenset(start_set)
index = {start: 0}
order, queue = [start], deque([start])
trans = []
while queue:
s = queue.popleft() # FIFO keeps trans rows aligned with index
row = []
for a in alphabet:
# frozenset().union(*[]) returns an empty *set*, not a frozenset, so
# the empty subset needs its own branch to stay hashable and comparable.
t = frozenset().union(*[delta(q, a) for q in s]) if s else frozenset()
if t not in index:
index[t] = len(order)
order.append(t)
queue.append(t)
row.append(index[t])
trans.append(row)
accept = [bool(s & accepting) for s in order]
return order, trans, accept
delta(q, a) must itself return a frozenset, or the union produces a plain set that will not compare
equal to the keys already in index. The dictionary keyed by frozenset is the whole algorithm: it is
simultaneously the visited set and the state numbering. Each distinct subset is processed once, and the
number of subsets processed is exactly the number of states of the result. The cost per subset is |Σ|
unions over its members, so the total work is proportional to the number of resulting transitions, not to
2^|Q|.
A language that forces every subset
The bound is tight, and the standard witness makes the reason visible. Fix k and let
L_k = { w ∈ {0,1}* : the k-th symbol from the end of w is 1 }, with |w| ≥ k.
An NFA with k + 1 states decides L_k: it stays in the start state on every symbol, and on each
symbol it also spawns a branch that checks whether the remaining input is exactly k - 1 symbols long
and whether the current symbol was 1. The branches are guesses about where the last k symbols begin.
A DFA cannot guess, so it must remember the last k symbols it has seen. Two inputs that agree on their
last k symbols are indistinguishable by any continuation, and two inputs that disagree on any of those
k positions are distinguished by a continuation of the right length. The number of distinct k-bit
suffixes is 2^k, and every one of them must lead to a different state, so the minimal DFA has 2^k
states.
The gap is therefore exponential in the worst case and cannot be closed by a better construction. It is
a property of the language, not of the algorithm: any deterministic machine for L_k needs 2^k
states, because a state must encode the entire relevant suffix. The Myhill-Nerode argument behind that
claim is developed in
distinguishing strings and the Myhill-Nerode theorem.
What the blow-up looks like in a table
The size of the determinized table is |Q_D| · |Σ| entries. For k = 16, L_k has 17 NFA states and
65,536 DFA states. With a 2-symbol alphabet and 4-byte state indices, each row is 8 bytes, so the table is
512 KB, adding one row for the empty-set trap state. That is far past any first-level data cache, which is
tens of kilobytes on current x86 cores. For k = 20 the same arithmetic gives 1,048,576 states and an
8 MB table, which exceeds the second-level cache of most x86 cores. For k = 24 it is 128 MB, past the
point where many hosts will allocate the table contiguously.
That progression is why tools that determinize set limits. The limit is not a matter of taste: the result is a data structure whose size is exponential in a parameter the user controls, and past some point the table cannot be built at all. A tool that refuses to determinize beyond a state budget is reporting a real constraint, not being conservative.
The NFA for the same language stays at k + 1 states regardless of k, which is the asymmetry that
motivates the set-based simulation in
simulating an NFA without determinizing it.
That simulation runs in O(|Q|) per symbol and never materializes the exponential table, trading the
per-symbol constant for memory.
Partial and hybrid determinization
Full determinization is rarely what a lexer or a matcher actually needs. Several weaker schemes preserve most of the speed while bounding the state count.
Determinize only the prefix. A lexer must commit to a token boundary before it can continue, so it needs determinism only up to the point where the alternatives are resolved. Beyond that point the machine may carry a set, and the set is usually small. The construction runs until a state budget is hit, then stops expanding and keeps the remaining subsets as simulator states.
Determinize on demand with a cache. The lazy simulation of the previous article is this scheme in its purest form: subsets become states as they are encountered, and the transition table is a memo table that grows with observed input rather than with the theoretical worst case. A workload that exercises a small part of the language builds a small table.
Delay the expensive decision. For matching with submatches, a fully determinized machine loses the
information needed to report where a group started, because several alternatives have been merged into
one state. Tagged determinization attaches a tag to each transition recording which alternative survived,
which restores the information at the cost of more states and more work per transition. The state count
is bounded by the number of distinct tag configurations, not by 2^|Q|, but the constant is large.
Keep the NFA and accept the per-symbol loop. When the machine is large and the input is short, the simulation wins outright. The crossover depends on the ratio of live-set size to state count and on whether the determinized table would fit in cache; there is no universal threshold.
| Scheme | States | Work per symbol | Submatch info |
|---|---|---|---|
| Full determinization | up to 2^n | one table lookup | lost |
| Lazy subsets | observed subsets | loop over live states | kept |
| Budgeted determinization | capped | lookup, fallback to set | partly kept |
| Tagged determinization | tag configurations | lookup plus tag merge | kept |
Correctness details that bite
The closure must be applied to the initial set, not just to transitions. A machine whose start state has
an epsilon path to an accepting state accepts the empty string, and a construction that begins from
{q₀} without closing will miss that.
The accepting predicate is intersection with F, not containment. A subset containing one accepting
state and five non-accepting ones is accepting, because acceptance in the NFA is existential. Getting
this backwards produces a machine that accepts the intersection of the alternatives, which is a
different and usually much smaller language.
The alphabet must be the full alphabet of the NFA, including symbols that appear on no transition from the current subset. Skipping them produces a partial table, which is fine if the caller treats a missing entry as the dead state, and wrong if the caller treats it as “stay put”.
State identity must be by set equality, not by set hash alone. Two distinct subsets can collide in a
hash table, and using the hash as the key silently merges them, which changes the language. The
frozenset key above compares by content; an integer-keyed implementation must store the sets and
compare them on collision.
Why determinization is still the default
Despite the exponential worst case, determinization is the standard first step in lexer generation and in many protocol validators, because the languages involved are small and the resulting machine is the fastest thing available: one dependent load per input symbol, no interpretation, no allocation, and no per-input state beyond a single integer. The exponential is a bound on the worst case, and the worst case is a language designed to require unbounded memory of the past.
The practical rule is to measure the determinized size before building the table, cap it, and fall back to the set simulation when the cap is exceeded. Both paths accept the same language; the difference is where the cost is paid, in memory before the run or in time during it. Deciding which cost is acceptable requires knowing the input length distribution, which is the one thing the theory does not supply.
The exponential is also the entry point to a larger question about descriptions rather than machines. Adding universal branching to the transition relation keeps the language class unchanged while allowing descriptions that are exponentially smaller than the smallest nondeterministic one, at the price of determinizing into a doubly exponential form when a table is required. The subset construction is the first rung of that ladder, not the whole of it.
References
- Introduction to Automata Theory, Languages, and Computation — Hopcroft, Motwani, and Ullman; Addison-Wesley
- Finite Automata and Their Decision Problems — Rabin and Scott, IBM Journal of Research and Development, 1959
- Introduction to the Theory of Computation — Sipser; Cengage Learning