一般使用多项式\(\mathrm{Hash}\)赋权的方法,将字符串映射到一个正整数。html
\[f(s)=\sum_{i=1}^{|s|}|s_i|\times P^i\ (\bmod\ p)\]c++
能够支持\(O(1)\)末端插入字符,\(O(1)\)提取一段字串的\(\mathrm{Hash}\)值。算法
每次查询的冲突率大概在\(\frac{1}{p}\)左右,若是查询次数较多,能够采用双模数\(\mathrm{Hash}\)。数组
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 20 , Mod = 998244353 , P = 131; inline int inc(int a,int b) { return a + b >= Mod ? a + b - Mod : a + b; } inline int mul(int a,int b) { return 1LL * a * b % Mod; } inline int dec(int a,int b) { return a - b < 0 ? a - b + Mod : a - b; } inline void Inc(int &a,int b) { a = inc( a , b ); } inline void Mul(int &a,int b) { a = mul( a , b ); } inline void Dec(int &a,int b) { a = dec( a , b ); } int Pow[N],val[N],n,m; char s[N]; inline int GetHash(int l,int r) { return dec( val[r] , mul( val[l-1] , Pow[r-l+1] ) ); } int main(void) { scanf( "%s" , s+1 ); n = strlen( s + 1 ); Pow[0] = 1; for (int i = 1; i <= n; i++) Pow[i] = mul( Pow[i-1] , P ) , val[i] = inc( s[i] - 'a' , mul( val[i-1] , P ) ); scanf( "%d" , &m ); for (int i = 1; i <= m; i++) { int l1,l2,r1,r2; scanf( "%d%d%d%d" , &l1 , &r1 , &l2 , &r2 ); GetHash(l1,r1) == GetHash(l2,r2) ? puts("Yes") : puts("No"); } return 0; }
肯定性有限状态自动机,识别且仅识别字符串集合\(S\)中的全部字符串。函数
支持\(O(|s|)\)插入字符串,\(O(|s|)\)检索字符串。优化
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 20; struct Trie { int e[N][26],end[N],tot; Trie(void) { tot = 1; } inline void Insert(char *s) { int n = strlen( s + 1 ) , p = 1; for (int i = 1; i <= n; i++) { int c = s[i] - 'a'; if ( !e[p][c] ) e[p][c] = ++tot; p = e[p][c]; } end[p] = true; } inline bool Query(char *s) { int n = strlen( s + 1 ) , p = 1; for (int i = 1; i <= n; i++) { int c = s[i] - 'a'; if ( !e[p][c] ) return false; p = e[p][c]; } return end[p]; } };
定义一个字符串的\(\mathrm{Border}\)为其公共先后缀。ui
定义字符串的前缀函数\[\pi(p)=\max_{s(1,t)=s(p-t+1,p)}\{t\}\]spa
含义即为字符串\(s\)的前缀\(s_p\)最长\(\mathrm{Border}\)的长度。遍历字符串,每次从上一个位置的最长\(\mathrm{Border}\)处开始向后匹配,若是匹配失败则再跳\(\mathrm{Border}\),直至匹配成功便可求出一个字符串的全部前缀函数。指针
定义势能函数\(\Phi(p)\)为前缀字符串\(s_p\)的最长\(\mathrm{Border}\)长度,根据\(\mathrm{Knuth-Morris-Pratt}\)算法,有\(\Phi(p)\leq\Phi(p-1)+1\),若暴力跳\(\mathrm{Border}\),则势能下降,可知总时间复杂度为\(O(n)\)。code
若求出了一个字符串的前缀函数,则能够实现单模式串的字符串匹配,失配就从最长的\(\mathrm{Border}\)处开始从新匹配便可,时间复杂度为\(O(n+m)\),分析方法相似。
#include <bits/stdc++.h> using namespace std; const int N = 1000020; int n,m,fail[N]; char s[N],t[N]; int main(void) { scanf( "%s\n%s" , s+1 , t+1 ); n = strlen( s + 1 ) , m = strlen( t + 1 ); for (int i = 2 , j = 0; i <= m; i++) { while ( j && t[j+1] != t[i] ) j = fail[j]; j += ( t[j+1] == t[i] ) , fail[i] = j; } for (int i = 1 , j = 0; i <= n; i++) { while ( j && ( t[j+1] != s[i] || j == m ) ) j = fail[j]; j += ( t[j+1] == s[i] ); if ( j == m ) printf( "%d\n" , i - m + 1 ); } for (int i = 1; i <= m; i++) printf( "%d%c" , fail[i] , " \n"[ i == m ] ); return 0; }
对于一个字符串\(s\),定义其\(\mathrm{KMP}\)自动机知足:
\(1.\) 状态数为\(n+1\)。
\(2.\) 识别全部前缀。
\(3.\) 转移函数\(\delta(p,c)\)为状态\(p\)所对应前缀接上字符\(c\)后最长\(\mathrm{Border}\)位置前缀对应的状态。
构造方法与\(\mathrm{Knuth-Morris-Pratt}\)算法相似,时间复杂度为\(O(n\Sigma)\)。
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 20; struct KMPAutomaton { int trans[N][26],n; inline void Build(char *s) { n = strlen( s + 1 ) , trans[0][s[1]-'a'] = 1; for (int i = 1 , j = 0; i <= n; i++) { for (int k = 0; k < 26; k++) trans[i][k] = trans[j][k]; trans[i][s[i]-'a'] = i + 1; j = trans[j][ s[i] - 'a' ]; } } };
肯定性有限状态自动机,识别全部后缀在指定字符串集合\(S\)中的字符串。
首先,咱们初始化\(\mathrm{Aho-Corasick}\)自动机为指定字符串集合\(S\)的\(\mathrm{Trie}\)树,而后按照\(\mathrm{bfs}\)序构造转移函数\(\delta\)。
咱们定义每个状态有一个\(\mathrm{fail}\)指针,\(\mathrm{fail}(x)=y\)当且仅当状态\(y\)表明的字符串是状态\(x\)表明字符串的后缀,且\(y\)表明字符串的长度最长。
咱们只需\(\mathrm{bfs}\)原\(\mathrm{Trie}\)树,当节点\(x\)在\(\mathrm{Trie}\)上存在字符为\(c\)的转移边时,咱们令\(\delta(x,c)=\mathrm{Trie}(x,c)\),并更新其\(\mathrm{fail}\)指针为\(\delta(\mathrm{fail}(x),c)\),反之,则能够令\(\delta(x,c)=\delta(\mathrm{fail}(x),c)\),易知其正确性。
\(\mathrm{Aho-Corasick}\)自动机能够实现多模式串的文本匹配,构造和匹配的时间复杂度均为线性(值得注意的是,计算贡献若是选择暴跳\(\mathrm{fail}\),则时间复杂度没法保证)。
\(\mathrm{Knuth-Morris-Pratt}\)自动机就是只有一个串\(\mathrm{Aho-Corasick}\)自动机。
#include <bits/stdc++.h> using namespace std; const int N = 2e5 + 20; struct AhoCorasickautomaton { int trans[N][26],fail[N],end[N],q[N],tot,head,tail; inline void insert(char *s,int id) { int len = strlen( s + 1 ) , now = 0; for (int i = 1; i <= len; i++) { int c = s[i] - 'a'; if ( !trans[now][c] ) trans[now][c] = ++tot; now = trans[now][c]; } end[id] = now; } inline void build(void) { head = 1 , tail = 0; for (int i = 0; i < 26; i++) if ( trans[0][i] ) q[++tail] = trans[0][i]; while ( head <= tail ) { int x = q[head++]; for (int i = 0; i < 26; i++) if ( !trans[x][i] ) trans[x][i] = trans[fail[x]][i]; else { fail[trans[x][i]] = trans[fail[x]][i]; q[++tail] = trans[x][i]; } } } };
肯定性有限状态自动机,识别且仅识别一个序列的全部子序列。
根据定义,能够构造一个\(|s|+1\)个状态的自动机,而后倒序连边便可,每个状态均可以做为终止状态,时间复杂度\(O(n\Sigma)\)。
#include <bits/stdc++.h> using namespace std; const int N = 1e6; struct SequenceAutomaton { int trans[N][26],next[26]; inline void Build(char *s) { int n = strlen( s + 1 ); memset( next , 0 , sizeof next ); for (int i = n; i >= 1; i--) { next[ s[i] - 'a' ] = i; for (int j = 0; j < 26; j++) trans[i-1][j] = next[j]; } } };
求出一个字符串\(s\)全部循环表示中字典序最小的一个。
能够用两个指针\(i,j\)扫描,表示比较\(i,j\)两个位置开头的循环同构串,并暴力依次向下比较,直到发现长度\(k\),使得\(s_{i+k}>s_{j+k}\),那么咱们能够直接令\(i=i+k+1\),由于对于任意的\(p\in[0,k]\),同构串\(s_{i+p}\)都比同构串\(s_{j+p}\)劣,因此不用再比较。
易知其时间复杂度为\(O(n)\)。
#include <bits/stdc++.h> using namespace std; const int N = 3e5 + 20; int n,s[N<<1]; int main(void) { scanf( "%d" , &n ); for (int i = 1; i <= n; i++) scanf( "%d" , &s[i] ) , s[i+n] = s[i]; int i = 1 , j = 2 , k; while ( i <= n && j <= n ) { for (k = 0; k < n && s[i+k] == s[j+k]; k++); if ( k == n ) break; if ( s[i+k] > s[j+k] ) ( i += k + 1 ) += ( i == j ); if ( s[i+k] < s[j+k] ) ( j += k + 1 ) += ( i == j ); } i = min( i , j ) , j = i + n - 1; for (int p = i; p <= j; p++) printf( "%d " , s[p] ); return puts("") , 0; }
肯定性有限状态自动机,识别且仅识别一个字符串的全部后缀。
采用增量法构造,详见『后缀自动机入门 SuffixAutomaton』。
使用静态数组存转移边,时空复杂度\(O(n\Sigma)\),用链表能够将时间复杂度优化到\(O(n)\)。用平衡树存转移边,时间复杂度\(O(n\log \Sigma)\),空间复杂度\(O(n)\)。
struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; // trans为转移函数,link为后缀连接,maxlen为状态内的最长后缀长度 // tot为总结点数,last为终止状态编号 SuffixAutomaton () { last = tot = 1; } // 初始化:1号节点为S inline void Extend(int c) { int cur = ++tot , p; maxlen[cur] = maxlen[last] + 1; // 建立节点cur for ( p = last; p && !trans[p][c]; p = link[p] ) // 遍历后缀连接路径 trans[p][c] = cur; // 没有字符c转移边的连接转移边 if ( p == 0 ) link[cur] = 1; // 状况1 else { int q = trans[p][c]; if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; // 状况2 else { int cl = ++tot; maxlen[cl] = maxlen[p] + 1; // 状况3 memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } last = cur; } };
肯定性有限状态自动机,识别且仅识别字符串集合\(S\)中全部字符串的全部后缀。
构造方法与狭义后缀自动机相似,只需在转移边产生冲突时分裂节点便可。
时空复杂度均与后缀自动机相同。
值得一提的是,广义后缀自动机若是采用线段树合并来维护\(\mathrm{endpos}\)集合,则需\(\mathrm{dfs}\)遍历\(\mathrm{Parent}\)树来合并,不能够按照基数排序的拓扑序来合并。
struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot; SuffixAutomaton () { tot = 1; } inline int Extend(int c,int pre) { if ( trans[pre][c] == 0 ) { int cur = ++tot , p; maxlen[cur] = maxlen[pre] + 1; for ( p = pre; 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] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , link[q] = link[cur] = cl; } } return cur; } else { int q = trans[pre][c]; if ( maxlen[q] == maxlen[pre] + 1 ) return q; else { int cl = ++tot; maxlen[cl] = maxlen[pre] + 1; memcpy( trans[cl] , trans[q] , sizeof trans[q] ); while ( pre && trans[pre][c] == q ) trans[pre][c] = cl , pre = link[pre]; return link[cl] = link[q] , link[q] = cl; } } } };
将一个字符串\(s\)的全部后缀插入到一个\(\mathrm{Trie}\)树中,咱们称这棵\(\mathrm{Trie}\)树全部叶子节点的虚树为这个字符串的后缀树。
根据\(\mathrm{endpos}\)等价类的定义及性质,容易得知原串倒序插入后缀自动机后的\(\mathrm{Parent}\)树就是该串的后缀树,因此能够用后缀自动机的构造方法求后缀树。
时间复杂度和后缀自动机的时间复杂度相同,能够\(O(n)\)顺带求后缀数组。
#include <bits/stdc++.h> using namespace std; const int N = 2e5+20; struct SuffixAutomaton { int trans[N][26],link[N],maxlen[N],tot,last; int id[N],flag[N],trie[N][26],sa[N],rk[N],hei[N],cnt; // id 表明这个状态是几号后缀 , flag 表明这个状态是否对应了一个真实存在的后缀 SuffixAutomaton () { tot = last = 1; } inline void Extend(int c,int pos) { int cur = ++tot , p; id[cur] = pos , flag[cur] = true; 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] ); while ( p && trans[p][c] == q ) trans[p][c] = cl , p = link[p]; link[cl] = link[q] , id[cl] = id[q] , link[q] = link[cur] = cl; } } last = cur; } inline void insert(int x,int y,char c) { trie[x][c-'a'] = y; } inline void Build(char *s,int n) { for (int i = n; i >= 1; i--) Extend( s[i]-'a' , i ); for (int i = 2; i <= tot; i++) insert( link[i] , i , s[ id[i] + maxlen[link[i]] ] ); } inline void Dfs(int x) { if ( flag[x] ) sa[ rk[id[x]] = ++cnt ] = id[x]; for (int i = 0 , y; i < 26; i++) if ( y = trie[x][i] ) Dfs(y); } inline void Calcheight(char *s,int n) { for (int i = 1 , k = 0 , j; i <= n; i++) { if (k) --k; j = sa[ rk[i]-1 ]; while ( s[ i+k ] == s[ j+k ] ) ++k; hei[ rk[i] ] = k; } } }; SuffixAutomaton T; char s[N]; int main(void) { scanf( "%s" , s+1 ); int n = strlen( s+1 ); T.Build( s , n ) , T.Dfs(1); T.Calcheight( s , n ); for (int i = 1; i <= n; i++) printf( "%d%c" , T.sa[i] , " \n"[ i == n ] ); for (int i = 2; i <= n; i++) printf( "%d%c" , T.hei[i] , " \n"[ i == n ] ); return 0; }
肯定性有限状态自动机,识别且仅识别一个字符串\(s\)的全部回文字串的右半部分。
因为回文串分奇偶,因此回文自动机有两个初始状态,分别表明奇回文串和偶回文串。
可使用数学概括法证实,字符串\(s\)最多只有\(|s|\)个本质不一样的回文字串,因此回文自动机的一个状态就表明一个回文字串。而回文自动机的一条转移边就表明在原串的两边各加一个字符,这样转移后的字符串仍然是回文串,同时也解释了为何回文自动机只识别回文串的右半部分。
回文自动机一样采用增量法构造。对于每个状态,咱们额外记录其最长回文后缀所对应的状态,称为\(\mathrm{link}\)函数。当咱们在字符串末尾插入一个字符时,咱们从原串最后的状态开始跳\(\mathrm{link}\),直至能够构成回文串,并肯定新的状态。
对于新的状态,仍然能够继续跳\(\mathrm{link}\),找到其最长回文后缀。
能够把回文自动机看做两棵树,也称为回文树。对于\(\mathrm{link}\)指针,也构成了一棵树,能够称之为回文后缀树。定义势能函数\(\Phi(p)\)表示状态\(p\)在回文后缀树中的深度,根据构造算法,易知\(\Phi(p)\leq\Phi(\mathrm{link}(p))+1\),而跳\(\mathrm{link}\)则势函数减少。又由于回文自动机的状态数是\(O(n)\)的,回文后缀树的最大深度也就是\(n\),能够得知构造算法的时间复杂度不超过\(O(n)\)。
其空间复杂度为\(O(n\Sigma)\),使用邻接表存边,时间复杂度升至\(O(n\Sigma)\),空间复杂度降至\(O(n)\)。若是使用\(\mathrm{Hash}\)表存边,时空复杂度均降至\(O(n)\)。
因为一个回文串的最长回文后缀必然是它的一个\(\mathrm{Border}\),因此回文树\(\mathrm{dp}\)可能用到\(\mathrm{Border\ Series}\)的等差性质。回文自动机中就会额外记录两个参量\(\mathrm{dif}\)和\(\mathrm{slink}\),\(\mathrm{dif}(x)=\mathrm{len}(x)-\mathrm{len}(\mathrm{link}(x))\),\(\mathrm{slink}(x)\)记录了回文后缀树上\(x\)最深的一个祖先,知足\(\mathrm{dif}(\mathrm{slink}(x))\not=\mathrm{dif}(x)\),这些均可以在构造过程当中顺带维护。
#include <bits/stdc++.h> using namespace std; const int N = 1e6 + 20 , Mod = 1e9 + 7; struct PalindromesAutomaton { int n,tot,last,link[N],slink[N],trans[N][26],len[N],dif[N],s[N]; PalindromesAutomaton(void) { len[ last = 0 ] = 0 , link[0] = 1; len[1] = -1 , tot = 1 , s[0] = -1; } inline void Extend(int c) { int p = last; s[++n] = c; while ( s[n] != s[ n - len[p] - 1 ] ) p = link[p]; if ( trans[p][c] == 0 ) { int cur = ++tot , q = link[p]; len[cur] = len[p] + 2; while ( s[n] != s[ n - len[q] - 1 ] ) q = link[q]; link[cur] = trans[q][c] , trans[p][c] = cur; dif[cur] = len[cur] - len[ link[cur] ]; if ( dif[cur] != dif[ link[cur] ] ) slink[cur] = link[cur]; else slink[cur] = slink[ link[cur] ]; } last = trans[p][c]; } };