一般使用多項式\(\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]; } };