前言
本博客仅记录个人对后缀自动机的一些理解,没有入门详细推导等内容。
可以参考这两篇博客:
后缀自动机 (SAM) 学习笔记
后缀自动机 (SAM)
理解
后缀自动机到底记录了什么?由于一个字符串的任意一个子串可以表达为某个前缀的后缀这样的形式,所以后缀自动机其实以一种高度压缩的形式保存了字符串所有子串的信息。
一点性质,后缀自动机最多只有\(2*n-1\)个状态,\(3n-4\)条转移,这可能在开数组大小,或者多组测试数据清空的时候要注意到。
回顾一些知识:
\(1.\) \(SAM\)的状态和转移构成了一张有向无环图,称为\(DAWG\)。
\(2.\) \(SAM\)的状态和后缀链接构成了一棵树,称为\(parent\)树。
\(3.\) 后缀自动机有唯一一个起始节点\(s\),代表空串,起始节点\(s\)到任意一个终止状态路径上的所有转移恰好可以表示原串的一个后缀,原串的每一个后缀同样都可以被表示。
\(4.\) 后缀自动机每一个状态本质上代表了一个\(endpos\)等价类,又称为\(right\)集合。
\(5.\) 引理: 两个非空子串\(|u|\)和\(|w|\) (假设\(|u|<|w|\))的\(endpos\)相同,当且仅当字符串\(u\)是\(w\)的后缀。
\(6.\) 引理: 两个非空子串\(|u|\)和\(|w|\) (假设\(|u|<|w|\)),要么\(endpos(u)\cap endpos(w)=\emptyset\),要么\(endpos(w)\subseteq endpos(u)\),取决于\(u\)是否为\(w\)的一个后缀。
\(7.\) 考虑一个\(endpos\)等价类,将类中的所有子串按长度非递增的顺序排序。每个子串都不会比它前一个子串长,与此同时每个子串也是它前一个子串的后缀。换句话说,对于同一等价类的任一两子串,较短者为较长者的后缀,且该等价类中的子串长度恰好覆盖一段联系的区间。
\(8.\) 一个 后缀链接 \(link\)连接到对应于\(w\)的最长后缀的另一个\(endpos\)等价类的状态。也就是说后缀链接连接的状态所代表的的子串集是上一个区间的相邻区间,同样也是连续区间。
\(9.\) 从节点\(v\)通过后缀链接到根的路径中每一个状态的\(substrings\)恰好不重不漏地表示了节点\(v\)所代表的最长子串的每一个后缀。
好了,如果这些知识在脑中都已经有了印象,那么我们就已经对后缀自动机初步理解了。
后缀自动机的实际运用
统计实质不同的子串个数 (HihoCoder1445)
只要统计每个状态包含的子串总数就可以了,不用担心普通统计中重复的问题。
答案就是\(\sum maxlen[i]-minlen[i]+1\)。
\(Code:\)
#include <bits/stdc++.h> using namespace std; const int N = 1e6+20; struct SuffixAutomation { int maxlen[N*2],link[N*2],trans[N*2][26],tot,last; SuffixAutomation () { tot = last = 1; } inline void extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for (p=last;p&&!trans[p][c];p=link[p]) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); link[cl] = link[q]; for (;p&&trans[p][c]==q;p=link[p]) trans[p][c] = cl; link[cur] = link[q] = cl; } } last = cur; } }; SuffixAutomation T; char s[N]; int n; long long ans; int main(void) { scanf("%s",s+1); n = strlen( s + 1 ); for (int i=1;i<=n;i++) T.extend( s[i] - 'a' ); for (int i=2;i<=T.tot;i++) ans += T.maxlen[i] - T.maxlen[T.link[i]]; printf("%lld\n",ans); return 0; }
任意子串出现次数 (HihoCoder1449)
任意子串的出现次数其实就是对应子串状态的\(endpos\)集合大小。考虑在构建\(SAM\)时维护,只需每次跳完所有的后缀链接,直到跳到根,并沿路累加\(endpos\)集合大小即可。
但是这样的时间复杂度是\(O(|S|^2)\)的,我们考虑先把\(SAM\)构出来,然后\(dfs\)一遍\(parent\)树,把贡献都算上就可以了。
\(Code:\)
#include <bits/stdc++.h> using namespace std; const int N = 1e6+20; struct edge { int ver,next; } e[N*2]; struct SuffixAutomation { int maxlen[N*2],link[N*2],trans[N*2][26],cnt[N*2],tot,last; SuffixAutomation () { tot = last = 1; } inline void extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; cnt[cur] = 1; for (p=last;p&&!trans[p][c];p=link[p]) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); link[cl] = link[q]; for (;p&&trans[p][c]==q;p=link[p]) trans[p][c] = cl; link[cur] = link[q] = cl; } } last = cur; } }; SuffixAutomation T; char s[N]; int n,t,Head[N*2],ans[N]; inline void insert(int x,int y) { e[++t] = (edge){y,Head[x]} , Head[x] = t; } inline void dfs(int x) { for (int i=Head[x];i;i=e[i].next) { int y = e[i].ver; dfs( y ); T.cnt[x] += T.cnt[y]; } ans[T.maxlen[x]] = max( ans[T.maxlen[x]] , T.cnt[x] ); } int main(void) { scanf("%s",s+1); n = strlen( s + 1 ); for (int i=1;i<=n;i++) T.extend( s[i] - 'a' ); for (int i=2;i<=T.tot;i++) insert( T.link[i] , i ); dfs( 1 ); for (int i=n;i>=1;i--) ans[i] = max( ans[i] , ans[i+1] ); for (int i=1;i<=n;i++) printf("%d\n",ans[i]); return 0; }
统计所有本质不同子串的权值和 (HihoCoder1457)
考虑在\(DAWG\)上\(dp\),令\(f_i\)代表\(i\)状态所有子串的权值和,\(tot_i\)代表状态\(i\)的子串个数,\(topsort\)转移即可。
\[tot_i=\sum_{trans[x][id]=i}tot_x\ ,\ f_i=\sum_{trans[x][id]=i}f_x\times 10+id\times tot_x\]
由于本题对多个字符串同时询问,我们使用广义后缀自动机解决。
所谓广义后缀自动机,就是把多个字符串用一些间隔符并成一个字符串,然后在同一个后缀自动机中操作,当然,那些间隔符是不参与\(dp\)计算的。
\(Code:\)
#include <bits/stdc++.h> using namespace std; const int N = 2e6+20 , Mod = 1e9+7; struct SuffixAutomation { int maxlen[N*2],link[N*2],trans[N*2][11],tot,last; SuffixAutomation () { tot = last = 1; } inline void extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; for (p=last;p&&!trans[p][c];p=link[p]) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); link[cl] = link[q]; for (;p&&trans[p][c]==q;p=link[p]) trans[p][c] = cl; link[cur] = link[q] = cl; } } last = cur; } }; SuffixAutomation T; int m,len,deg[2*N]; long long tot[N*2],f[N*2],ans; char s[N]; inline void Topsort(void) { queue <int> q; q.push( 1 ) , tot[1] = 1; for (int i=1;i<=T.tot;i++) for (int j=0;j<=10;j++) ++deg[ T.trans[i][j] ]; while ( !q.empty() ) { int x = q.front(); q.pop(); for (int i=0;i<=10;i++) { int y = T.trans[x][i]; if ( y == 0 ) continue; if ( i != 10 ) { tot[y] = ( tot[y] + tot[x] ) % Mod; f[y] = ( f[y] + f[x] * 10 % Mod + 1LL * i * tot[x] % Mod ) % Mod; } if ( !--deg[y] ) q.push( y ); } } } int main(void) { scanf("%d",&m); for (int i=1;i<=m;i++) { scanf("%s",s+1); len = strlen( s + 1 ); for (int j=1;j<=len;j++) T.extend( s[j] - '0' ); if ( i != m ) T.extend( 10 ); } Topsort(); for (int i=1;i<=T.tot;i++) ans = ( ans + f[i] ) % Mod; printf("%lld\n",ans); return 0; }
普通LCP (ZROI902)
这道题要用后缀自动机先构建出后缀树,然后计算。
不妨假设我们已经有方法线性地用后缀自动机构建后缀树了,我们显然可以贪心地计算答案:在后缀树上,两个叶结点的\(LCA\)就是这两个后缀的最长公共前缀,那么我们就可以枚举每一个点作为\(LCA\),找最小的两个点来当\((i,j)\)来贡献答案即可。
为了保证字典序最小,我们在后缀树上按字典序倒叙枚举即可,这样最小字典序的答案会最后把答案覆盖。
现在问题就变成了如何构建后缀树,我们不妨回忆一下后缀链接和\(parent\)树的性质,还有后缀树的定义,其实可以发现倒序把字符串插入后缀自动机,得到的\(parent\)树就是后缀树。
还有一个问题就是我们需要知道后缀树上压缩边的首字符,方便按字典序查找,这个首字符是可以直接计算的,即\(e(link_i,i)_c=s[pos_i+maxlen_{link_i}]\)。
如何理解,节点\(link_i\)到\(i\)的压缩边的首字符就是字符串中第\(pos_i+maxlen_{link_i}\)个字符。我们再看\(pos_i+maxlen_{link_i}\)是什么:\(pos_i\)就是状态\(i\)被建立时的字符位置,而\(maxlen_{link_i}\)就代表了其后缀树上父亲节点自己的最大\(endpos\)集合位置,相加了就是后缀树压缩边上第一个字符,也就是后缀\(i\)和其父亲节点代表子串相异的第一个字符。
于是对所有节点计算答案贡献只需一遍\(dfs\)同时计算即可,时间复杂度\(O(n)\)。
\(Code:\)
#include <bits/stdc++.h> using namespace std; const int N = 1e6+20; struct SuffixAutomation { int maxlen[N*2],link[N*2],id[N*2],trans[N*2][4],tot,last; SuffixAutomation () { tot = last = 1; } inline void extend(int c,int pos) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; id[cur] = pos; for (p=last;p&&!trans[p][c];p=link[p]) trans[p][c] = cur; if ( p == 0 ) link[cur] = 1; else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); link[cl] = link[q] , id[cl] = id[q]; for (;p&&trans[p][c]==q;p=link[p]) trans[p][c] = cl; link[cur] = link[q] = cl; } } last = cur; } }; SuffixAutomation T; int n,son[N*2][4],a[N*2],b[N*2]; char s[N]; inline void insert(int x,int y,int c) { son[x][c] = y; } inline void build(void) { for (int i=2;i<=T.tot;i++) insert( T.link[i] , i , s[ T.id[i] + T.maxlen[T.link[i]] ] - 'a' ); } inline void dfs(int x) { int A,B; A = B = T.id[x]; for (int i=3;i>=0;i--) { int y = son[x][i]; if ( y == 0 ) continue; dfs( y ); if ( T.id[y] < A ) B = A , A = T.id[y]; else if ( T.id[y] < B ) B = T.id[y]; } a[T.maxlen[x]] = A , b[T.maxlen[x]] = B; for (int i=0;i<=3;i++) if ( son[x][i] ) T.id[x] = min( T.id[x] , T.id[son[x][i]] ); } inline void reset(void) { T.tot = T.last = 1; for (int i=1;i<=2*n+1;i++) { T.id[i] = T.link[i] = T.maxlen[i] = 0; memset( T.trans[i] , 0 , sizeof T.trans[i] ); memset( son[i] , 0 , sizeof son[i] ); } } int main(void) { int t; scanf("%d",&t); while ( t --> 0 ) { reset(); scanf("%s",s+1); n = strlen( s + 1 ); for (int i=n;i>=1;i--) T.extend( s[i] - 'a' , i ); build(); for (int i=1;i<=n;i++) a[i] = b[i] = n - i + 1; dfs( 1 ); for (int i=1;i<=n;i++) printf("%d %d\n",a[i],b[i]); } return 0; }