Algorithm Search⌘K

Proofs: Generalized suffix tree

These are the human proofs of Generalized suffix tree, at full length.

The stack build gives the generalized suffix tree full

This proof shows: correctness, termination, time.worst.

Notation. The strings are S0,…,Sm−1S_0, \ldots, S_{m-1}, and Z=S0$0S1$1⋯Sm−1$m−1Z = S_0 \$_0 S_1 \$_1 \cdots S_{m-1} \$_{m-1} has length NN. The separators are distinct, each occurs once in ZZ, and $0<⋯<$m−1\$_0 < \cdots < \$_{m-1} are smaller than all letters. Positions are 0-based. For a position pp that holds a letter, let e(p)e(p) be the position of the first separator at or after pp, and let Xp=Z[p…e(p)]X_p = Z[p \ldots e(p)], the suffix of its string followed by its separator. The generalized suffix tree is the compacted trie of the set X={Xp}\mathcal{X} = \{X_p\}: a rooted tree whose edges are labeled with nonempty strings, where the labels of the edges out of a node start with different symbols, each internal node other than the root has at least two children, and the leaves are exactly the strings of X\mathcal{X}.

Lemma 1 (terminated suffixes). Let p≠qp \ne q be letter positions. Then lcp(Zp,Zq)<min⁡(∣Xp∣,∣Xq∣)\mathrm{lcp}(Z_p, Z_q) < \min(|X_p|, |X_q|), and Zp<ZqZ_p < Z_q if and only if Xp<XqX_p < X_q. In particular, no string of X\mathcal{X} is a prefix of another.

Proof. Let h=lcp(Zp,Zq)h = \mathrm{lcp}(Z_p, Z_q) and suppose h≥∣Xp∣h \ge |X_p|. Then Z[p+∣Xp∣−1]=Z[q+∣Xp∣−1]Z[p + |X_p| - 1] = Z[q + |X_p| - 1], and the left side is the separator at e(p)e(p). A separator occurs once in ZZ, so p=qp = q, a contradiction. The same holds for qq. So the first difference of ZpZ_p and ZqZ_q is at an offset hh inside both XpX_p and XqX_q, with the same two symbols, and the orders agree. If XpX_p were a prefix of XqX_q, then lcp(Zp,Zq)≥∣Xp∣\mathrm{lcp}(Z_p, Z_q) \ge |X_p|. □\square

Lemma 2 (ranks). The ranks 0,…,m−10, \ldots, m-1 of the suffix array of ZZ hold the mm suffixes that start with a separator, and the ranks m,…,N−1m, \ldots, N-1 hold the letter positions. For i≥m+1i \ge m + 1, LCP[i]=lcp(XSA[i−1],XSA[i])LCP[i] = \mathrm{lcp}(X_{SA[i-1]}, X_{SA[i]}), and LCP[m]=0LCP[m] = 0.

Proof. A suffix that starts with a separator is smaller than each suffix that starts with a letter, because the separators are smaller than all letters. For i≥m+1i \ge m + 1 both suffixes start at letter positions, and Lemma 1 shows that their common prefix lies inside both terminated suffixes. The suffix of rank m−1m - 1 starts with a separator and that of rank mm with a letter, so LCP[m]=0LCP[m] = 0. □\square

So the letter positions in the order SA[m],…,SA[N−1]SA[m], \ldots, SA[N-1] list X\mathcal{X} in increasing order, with the LCP of each adjacent pair. Write xt=XSA[m+t]x_t = X_{SA[m + t]} and ht=LCP[m+t]h_t = LCP[m + t] for 0≤t<N−m0 \le t < N - m.

Invariant. After the loop of lines L3 to L10 has handled x0,…,xtx_0, \ldots, x_t:

(G) The tree is the compacted trie CtC_t of {x0,…,xt}\{x_0, \ldots, x_t\}, with the leaf of xsx_s labeled by its string and offset. The stack holds, from bottom to top, the nodes of the path from the root to the leaf of xtx_t, in order of increasing string depth.

Initialization. Before the loop, the tree is the root alone and the stack is [root]. For t=0t = 0, h0=0h_0 = 0: line L5 pops nothing, since the root has depth 0, line L6 fails, and lines L9 and L10 hang the leaf of x0x_0 below the root with the edge label x0x_0. This is C0C_0, and the stack is the path to the leaf.

Maintenance. Assume (G) for t−1t - 1 and handle xtx_t with h=hth = h_t. Because the list is sorted, xtx_t branches off the path to xt−1x_{t-1} at string depth hh: the common prefix of xtx_t with each earlier xsx_s has length at most hh, by the sandwich property of sorted strings, lcp(xs,xt)=min⁡(hs+1,…,ht)≤h\mathrm{lcp}(x_s, x_t) = \min(h_{s+1}, \ldots, h_t) \le h. So in CtC_t the leaf of xtx_t hangs from the point at depth hh on the path to xt−1x_{t-1}, and no other part of Ct−1C_{t-1} changes. By Lemma 1, h<∣xt−1∣h < |x_{t-1}|, the depth of the leaf on top of the stack. Line L5 pops the nodes of depth more than hh; at least the leaf is popped, so lastlast is defined, and lastlast is the shallowest popped node, a child of the new top. If the new top has depth hh, that node is the branch point. Otherwise its depth is less than hh and the point at depth hh lies inside the edge from the top to lastlast; lines L7 and L8 split this edge with a new node vv of depth hh. The key of lastlast below vv is the symbol at depth hh of its path label, and the key of vv below the top is the same as the old key of lastlast, so the edge to lastlast is replaced. Lines L9 and L10 hang the leaf of xtx_t below the branch point, with the key xt[h]x_t[h]. This key differs from the key of the child on the path to xt−1x_{t-1}, because hh is the exact LCP. Each new internal node has two children, and the stack is the path to the new leaf. So (G) holds for tt.

Termination and result. The loop runs N−mN - m times and each pop removes a node pushed earlier, so it ends. By (G) the result is the compacted trie of X\mathcal{X}, which is the generalized suffix tree. Each suffix of each SkS_k that is nonempty is one leaf. □\square

Lemma 3 (search). For a nonempty pattern PP over the letters, the occurrences of PP in the strings are the labels of the leaves below the highest node whose path label starts with PP.

Proof. PP occurs at offset jj of SkS_k if and only if PP is a prefix of XpX_p with pp the position of Sk[j]S_k[j], because PP has no separator. The strings of X\mathcal{X} with prefix PP are the leaves below the locus of PP in the compacted trie. □\square

Theorem (time). Given the suffix array and the LCP array of ZZ, lines L2 to L10 run in O(N)O(N) time.

Proof. Each iteration pushes at most two nodes, so there are at most 2(N−m)2(N - m) pushes and so at most as many pops in total. The other lines take O(1)O(1) time each, with the children of a node in a hash map keyed by a symbol, and the string of each position is found in O(1)O(1) time from a table of the string of each position, or in O(log⁡m)O(\log m) time by a binary search on the start positions. With a linear-time suffix array construction for an integer alphabet and Kasai's algorithm for the LCP array, the whole build is O(N)O(N). ■\blacksquare

Back to Generalized suffix tree