Algorithm Search⌘K

Proofs: Suffix trie

These are the human proofs of Suffix trie, at full length.

The suffix trie holds exactly the substrings, with their counts and ends full

This proof shows: correctness.

Let S=S[0…n−1]S = S[0 \ldots n-1], and write Si=S[i…n−1]S_i = S[i \ldots n-1]. For a node xx, str(x)str(x) is the string spelled by the path from the root to xx. Edges out of a node carry distinct symbols, because line L5 creates a child for cc only when none exists. So for each string ww there is at most one node xx with str(x)=wstr(x) = w.

Lemma (build). For 0≤k≤n0 \le k \le n, after the passes i=0,…,k−1i = 0, \ldots, k-1 of line L2:

  1. the strings str(x)str(x), over all nodes xx, are exactly the distinct prefixes of S0,…,Sk−1S_0, \ldots, S_{k-1}, together with ε\varepsilon;
  2. for each node x≠rootx \ne root, x.count=∣{ i<k:str(x) is a prefix of Si }∣x.count = |\{\, i < k : str(x) \text{ is a prefix of } S_i \,\}|;
  3. for each node xx, x.endsx.ends is the list, in increasing order, of the i<ki < k with Si=str(x)S_i = str(x).

Proof. By induction on kk. For k=0k = 0, line L1 makes only the root, str(root)=εstr(root) = \varepsilon, and the claims hold.

Step from kk to k+1k + 1. The pass i=ki = k starts at the root (line L3). We show by induction on jj that before the iteration jj of line L4, xx is the node of S[k…j−1]S[k \ldots j-1]. This holds for j=kj = k. In the iteration jj, line L5 makes sure that a child for S[j]S[j] exists, creating it if the string S[k…j]S[k \ldots j] had no node, and line L6 moves to it, so xx becomes the node of S[k…j]S[k \ldots j]. Hence the pass visits the nodes of the nonempty prefixes S[k…j]S[k \ldots j], k≤j≤n−1k \le j \le n-1, each one time, and adds 1 to each of their counts. The only new nodes are those of prefixes of SkS_k that were missing, which proves claim 1 for k+1k + 1. A node x≠rootx \ne root gets +1+1 exactly when str(x)str(x) is a nonempty prefix of SkS_k, which proves claim 2. After the loop, xx is the node of SkS_k (the root if SkS_k were empty, which does not happen since k≤n−1k \le n - 1), and line L7 appends kk to its list. Since kk is larger than all earlier positions, the list stays increasing, which proves claim 3. □\square

Theorem. After SUFFIX-TRIE-BUILD(S)(S), the invariants (ST1), (ST2) and (ST3) of the page hold. For each string PP, FIND(P)(P) returns the node of PP if PP is a substring of SS, and NIL otherwise. For a nonempty PP, the count of that node is the number of occurrences of PP, and the union of the end lists in its subtree is the set of positions where PP occurs.

Proof. Apply the lemma with k=nk = n.

(ST1). A string ww is a substring of SS if and only if w=S[i…j−1]w = S[i \ldots j-1] for some 0≤i≤j≤n0 \le i \le j \le n. If i<ni < n, this is a prefix of SiS_i with i≤n−1i \le n-1. If i=ni = n, then w=εw = \varepsilon. So the substrings of SS are exactly ε\varepsilon and the prefixes of S0,…,Sn−1S_0, \ldots, S_{n-1}, and claim 1 says that these are exactly the strings str(x)str(x). With the uniqueness shown at the start, x↦str(x)x \mapsto str(x) is a bijection.

(ST2). For a nonempty ww, "ww is a prefix of SiS_i" holds if and only if i+∣w∣≤ni + |w| \le n and S[i…i+∣w∣−1]=wS[i \ldots i + |w| - 1] = w, that is, if and only if ww occurs at ii. With claim 2 this gives (ST2).

(ST3). This is claim 3. Each i<ni < n is appended exactly once, in pass ii.

FIND. The loop of lines L9 to L11 keeps the statement "xx is the node of the prefix of PP read so far". This holds at line L8, since the root is the node of ε\varepsilon. If the node of the prefix PcPc exists, it is the child of xx for cc, because str(x.next[c])=str(x) cstr(x.next[c]) = str(x)\,c and nodes are unique. If no such node exists, then PcPc is not a substring by (ST1), so PP is not one either (a substring of SS has all its prefixes as substrings), and line L11 returns NIL correctly. If the loop ends, line L12 returns the node of PP.

Occurrences. Let xx be the node of a nonempty PP. A node yy is in the subtree of xx if and only if str(x)str(x) is a prefix of str(y)str(y). By (ST3), position ii is in the list of the node yy with str(y)=Sistr(y) = S_i, so ii is collected if and only if PP is a prefix of SiS_i, that is, if and only if PP occurs at ii. Each position is in one list, so no position is reported twice. ■\blacksquare

Back to Suffix trie