Algorithm Search⌘K

Proofs: Thompson's NFA simulation

These are the human proofs of Thompson's NFA simulation, at full length.

Thompson's NFA simulation decides membership and search in O(mn) time full

This proof shows: correctness.

Statement. Let rr be a regular expression of size mm, let NN be its Thompson NFA with start q0q_0 and accepting state qfq_f, and let w=a1⋯anw = a_1 \cdots a_n. THOMPSON-MATCH(r,w)(r, w) returns true if and only if w∈L(r)w \in L(r), and it runs in O(mn)O(mn) time and O(m)O(m) extra space. The search version returns true if and only if some substring of ww is in L(r)L(r), within the same bounds.

Notation. For a set SS of states, E(S)E(S) is the set of states reachable from a state of SS by a path of zero or more ε\varepsilon-moves. For a string vv, R(v)R(v) is the set of states qq such that some path from q0q_0 to qq reads vv (the symbols on its moves spell vv, and ε\varepsilon-moves read nothing). By definition, NN accepts vv if and only if qf∈R(v)q_f \in R(v). Two facts about RR follow from splitting a path at its last symbol move: R(ε)=E({q0})R(\varepsilon) = E(\{q_0\}), and R(va)=E({p:q∈R(v), q→ap})R(va) = E(\{p : q \in R(v),\ q \xrightarrow{a} p\}).

Lemma 1 (ADD). Suppose that before a call ADD(L,q,i)(L, q, i) the states with mark ii are exactly the states of LL, and the set MM of these states is closed under ε\varepsilon-moves (every ε\varepsilon-target of a state of MM is in MM). After the call, the states with mark ii are exactly the states of LL, LL holds each of them once, and as a set L=M∪E({q})L = M \cup E(\{q\}).

Proof. A state is appended at line L14 only when its mark is not ii, and its mark becomes ii at the same moment, so no state is appended twice and the marks match LL throughout. Every appended state is in E({q})E(\{q\}), by induction on the order of pushes: qq is, and every other pushed state is an ε\varepsilon-target of an appended state. Conversely, let p∈E({q})p \in E(\{q\}) with a path q=p0→p1→⋯→pk=pq = p_0 \to p_1 \to \cdots \to p_k = p of ε\varepsilon-moves. If p0∈Mp_0 \in M, the whole path is in MM because MM is closed. Otherwise p0p_0 is pushed and, when popped, appended. Whenever some pjp_j is appended, all its ε\varepsilon-targets are pushed, so pj+1p_{j+1} is pushed and is popped before the stack empties; at that point it is in LL, either appended now or earlier. By induction on jj, pp ends in LL. The loop ends because each state is appended at most once and each appended state pushes at most two states. □\square

After the call, the set LL is again closed under ε\varepsilon-moves, since MM and E({q})E(\{q\}) are. So the hypothesis of Lemma 1 holds before each later call with the same ii and LL. Before the first call with a given ii, no state has mark ii, because marks only take the values −1,0,…,i−1-1, 0, \ldots, i - 1 before step ii, and LL is empty, so the hypothesis holds with M=∅M = \emptyset.

Lemma 2 (invariant). After line L2, and after line L7 of pass ii, the list CC holds each state of R(a1⋯ai)R(a_1 \cdots a_i) exactly once and nothing else, and mark[q]=i\mathit{mark}[q] = i exactly for the states qq of CC.

Proof. For i=0i = 0, Lemma 1 gives C=E({q0})=R(ε)C = E(\{q_0\}) = R(\varepsilon). Assume the claim for i−1i - 1. In pass ii, the loop of lines L5 and L6 calls ADD(D,p,i)(D, p, i) for each move q→aipq \xrightarrow{a_i} p with q∈Cq \in C; there is at most one such move per state. By Lemma 1, applied call after call, DD ends as the union of E({p})E(\{p\}) over these pp, each state once, which is E({p:q∈R(a1⋯ai−1), q→aip})=R(a1⋯ai)E(\{p : q \in R(a_1 \cdots a_{i-1}),\ q \xrightarrow{a_i} p\}) = R(a_1 \cdots a_i). The marks with value ii are exactly the states of DD. Line L7 sets C←DC \leftarrow D. □\square

Theorem (match version). If line L8 returns false at pass ii, then R(a1⋯ai)=∅R(a_1 \cdots a_i) = \emptyset. Every path that reads ww has a prefix that reads a1⋯aia_1 \cdots a_i and ends in a state of R(a1⋯ai)R(a_1 \cdots a_i), so no path reads ww and w∉L(N)w \notin L(N). Otherwise the loop runs to the end, and by Lemma 2 the test mark[qf]=n\mathit{mark}[q_f] = n at line L9 holds if and only if qf∈R(w)q_f \in R(w), that is, w∈L(N)w \in L(N). The Thompson construction gives L(N)=L(r)L(N) = L(r). So the answer is correct.

Search version. Write Si=⋃j≤iR(aj+1⋯ai)S_i = \bigcup_{j \le i} R(a_{j+1} \cdots a_i): the states in which some run that started at a position j≤ij \le i can be after reading up to position ii. The search version adds ADD(D,q0,i)(D, q_0, i) at the end of pass ii. Since ADD(C,q0,0)(C, q_0, 0) gives S0=R(ε)S_0 = R(\varepsilon), and the same argument as in Lemma 2 gives, after pass ii,

C=E({p:q∈Si−1, q→aip})∪E({q0})=⋃j≤i−1R(aj+1⋯ai)∪R(ε)=Si.C = E(\{p : q \in S_{i-1},\ q \xrightarrow{a_i} p\}) \cup E(\{q_0\}) = \bigcup_{j \le i - 1} R(a_{j+1} \cdots a_i) \cup R(\varepsilon) = S_i.

The version returns true at the first ii with qf∈Siq_f \in S_i, which means that some substring aj+1⋯aia_{j+1} \cdots a_i is in L(r)L(r). If it never returns true, then qf∉Siq_f \notin S_i for every ii, so no substring is in L(r)L(r). With the start state added at each step, CC always contains q0q_0, so the early exit of line L8 does not apply.

Complexity. By Thompson's construction, NN has at most 2m2m states, at most 4m4m moves, and each state has at most two outgoing moves; building it costs O(m)O(m). In one pass, Lemma 1 shows that each state is appended at most once, so all the calls of ADD in the pass together append at most 2m2m states. Each push at line L15 is charged to the appended state that makes it, so there are at most 2⋅2m2 \cdot 2m pushes from line L15 and one push per call at line L10. The calls at line L6 number at most ∣C∣≤2m|C| \le 2m, and the search version makes one more. Each pop costs O(1)O(1), so a pass costs O(m)O(m), and nn passes cost O(mn)O(mn). The NFA, the arrays mark\mathit{mark}, the lists CC and DD and the stack KK each have O(m)O(m) entries, which gives O(m)O(m) extra space. □\square

Back to Thompson's NFA simulation