后綴自動機也是解決字符串問題的常用工具,犀利在O(N)的空間復雜度下存在給定串的后綴以及子串,而且支持在線的操作。
POJ-1509 Glass Beads
題意:求一個字符串的最小表示的開始下標。
分析:其實有一個O(N)的算法專門來解決這個問題,並且實現非常簡單,不過后綴自動機同樣能夠解決這個問題。首先把這個串重復兩次,然后從前往后一一將字符加入到后綴自動機中,最后從根開始向下遍歷串的長度層即可。

#include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> using namespace std; const int N = 10005; char str[N]; struct SAM { struct Node { int ch[26]; int f, len; void init() { f = -1, len = 0; memset(ch, 0xff, sizeof (ch)); } }; Node sn[N<<1]; int idx, last; void init() { idx = last = 0; sn[idx++].init(); } int newnode() { sn[idx].init(); return idx++; } void add(int c) { int end = newnode(); int tmp = last; sn[end].len = sn[last].len + 1; for ( ; tmp != -1 && sn[tmp].ch[c] == -1; tmp = sn[tmp].f) { sn[tmp].ch[c] = end; } if (tmp == -1) sn[end].f = 0; // 所有的上一輪可接受點都沒有指向字符c的孩子節點 else { int nxt = sn[tmp].ch[c]; if (sn[tmp].len + 1 == sn[nxt].len) sn[end].f = nxt; // 如果可接受點有向c的轉移,且長度只加1,那么該孩子可以替代當前的end,並且end的雙親指向該孩子 else { int np = newnode(); sn[np] = sn[nxt]; sn[np].len = sn[tmp].len + 1; sn[end].f = sn[nxt].f = np; for (; tmp != -1 && sn[tmp].ch[c] == nxt; tmp = sn[tmp].f) { sn[tmp].ch[c] = np; } } } last = end; } }; SAM sam; int main() { int T; scanf("%d", &T); while (T--) { sam.init(); scanf("%s", str); int len = strlen(str); for (int i = 0; i < len*2; ++i) { sam.add(str[i%len]-'a'); } int p = 0; for (int i = 0; i < len; ++i) { for (int j = 0; j < 26; ++j) { if (sam.sn[p].ch[j] != -1) { p = sam.sn[p].ch[j]; break; } } } printf("%d\n", sam.sn[p].len-len+1); } return 0; }
SPOJ-1811 Longest Common Substring
題意:求兩個串的最長公共子串。
分析:先用第一個串構造出后綴自動機,然后逐個的匹配第二個串,如果當前節點失配,那么找 f 節點。

#include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> using namespace std; const int N = 250010; char s1[N], s2[N]; struct SAM { struct { int len, f, ch[26]; void init() { len = 0, f = -1; memset(ch, 0xff, sizeof (ch)); } } e[N<<1]; int idx, last; void init() { idx = last = 0; e[idx++].init(); } int newnode() { e[idx].init(); return idx++; } void add(int c) { int end = newnode(); int tmp = last; e[end].len = e[last].len + 1; for (; tmp != -1 && e[tmp].ch[c] == -1; tmp = e[tmp].f) { e[tmp].ch[c] = end; } if (tmp == -1) e[end].f = 0; else { int nxt = e[tmp].ch[c]; if (e[tmp].len + 1 == e[nxt].len) e[end].f = nxt; else { int np = newnode(); e[np] = e[nxt]; e[np].len = e[tmp].len + 1; e[nxt].f = e[end].f = np; for (; tmp != -1 && e[tmp].ch[c] == nxt; tmp = e[tmp].f) { e[tmp].ch[c] = np; } } } last = end; } }; SAM sam; int main() { while (scanf("%s %s", s1, s2) != EOF) { sam.init(); int len1 = strlen(s1); int len2 = strlen(s2); for (int i = 0; i < len1; ++i) { // 構造好第一個字符串的后綴自動機 sam.add(s1[i]-'a'); } int p = 0, ret = 0, clen = 0; for (int i = 0; i < len2; ++i) { int id = s2[i]-'a'; if (sam.e[p].ch[id] != -1) { clen++; p = sam.e[p].ch[id]; } else { for(; p != -1 && sam.e[p].ch[id] == -1; p = sam.e[p].f) ; if (p == -1) clen = 0, p = 0; else { clen = sam.e[p].len + 1; p = sam.e[p].ch[id]; } } ret = max(clen, ret); } printf("%d\n", ret); } return 0; }
SPOJ-8222 Substrings
題意:給定一個字符串,求出現次數最多的長度為 i 的子串的次數,i 的取值為 1 - len,len表示這個字符串的長度。
分析:首先使用這個字符串建立一個后綴自動機。此時每個節點的len屬性表示到該節點最長的子串長度,那么現將所有的節點按照len值排一個序(spoj的機器跑的實在是慢,所以選擇基數排序)。然后初始化中間那條最長鏈上的節點,每個節點代表一個后綴,這樣一開始就有n個數量為1的后綴。緊接着,我們將所有的節點按照長度從大到下來迭代更新,每次更新當然只能夠更新其f指針,f指針表示與其擁有相同的后綴但是最長長度小於當前后綴的最長長度。
這次也順便構造出一些字符串發現一個替換其他節點的點,可能被再次替換;一個被替換的點可能被替換多次。

#include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> using namespace std; const int N = 250005; struct Node { int f, ml, ch[26]; void init() { ml = 0, f = -1; memset(ch, 0xff, sizeof (ch)); } }; struct SAM { Node e[N<<1]; int idx, last; int newnd() { e[idx].init(); return idx++; } void init() { idx = 0; last = newnd(); } void add(int c) { int end = newnd(); int tmp = last; e[end].ml = e[last].ml + 1; for (; tmp != -1 && e[tmp].ch[c] == -1; tmp = e[tmp].f) { e[tmp].ch[c] = end; } if (tmp == -1) e[end].f = 0; else { int nxt = e[tmp].ch[c]; if (e[tmp].ml + 1 == e[nxt].ml) e[end].f = nxt; else { int nd = newnd(); e[nd] = e[nxt]; e[nd].ml = e[tmp].ml + 1; e[nxt].f = e[end].f = nd; for (; tmp != -1 && e[tmp].ch[c] == nxt; tmp = e[tmp].f) { e[tmp].ch[c] = nd; } } } last = end; } }; char str[N]; SAM sam; int pk[N<<1]; // pk[i]表示排名為i的節點編號 int ws[N]; // ws[i]表示值為i的數出現了多少次 int ans[N]; // ans[i]表示所有長度為i的子串出現的最多次數 int c[N<<1]; // c[i]表示i號節點表示的最長子串能夠被包含多少次 int main() { while (scanf("%s", str) != EOF) { sam.init(); memset(c, 0, sizeof (c)); memset(ans, 0, sizeof (ans)); int len = strlen(str); for (int i = 0; i < len; ++i) sam.add(str[i] - 'a'); // 構造好后綴自動機 // 按照每個節點所能夠承載的最長子串進行排序 for (int i = 0; i <= len; ++i) ws[i] = 0; for (int i = 1; i < sam.idx; ++i) ++ws[sam.e[i].ml]; for (int i = 1; i <= len; ++i) ws[i] += ws[i-1]; for (int i = sam.idx-1; i > 0; --i) pk[ws[sam.e[i].ml]--] = i; // 初始化原始的后綴 for (int i = 0, p = 0; i < len; ++i) { ++c[p = sam.e[p].ch[str[i]-'a']]; } for (int i = sam.idx-1; i > 0; --i) { // 長度長的后綴能夠更新與其擁有相同后綴的較短的后綴 ans[sam.e[pk[i]].ml] = max(ans[sam.e[pk[i]].ml], c[pk[i]]); if (sam.e[pk[i]].f > 0) c[sam.e[pk[i]].f] += c[pk[i]]; } for (int i = 1; i <= len; ++i) printf("%d\n", ans[i]); } return 0; }
SPOJ-1812 Longest Common Substring II
題意:求多個串的最長公共子串。
分析:首先將第一個串建立一個后綴自動機,然后以該串為參照,分別對應每個串求出后綴自動機中的每個節點所能夠匹配的最長子串長度,需要對每個節點按照len值排一個序,用以在包含子串之間進行更新。每更新完一輪值,其實就可以相應的縮小這個len值了,因為是所有串的公共子串,因此是由最小值來決定的。

#include <cstdio> #include <cstring> #include <cstdlib> #include <algorithm> using namespace std; const int N = 100005; struct Node { int len, f; int ch[26]; void init() { len = 0, f = -1; memset(ch, 0xff, sizeof (ch)); } }; struct SAM { Node e[N<<1]; int idx, last; int newnd() { e[idx].init(); return idx++; } void init() { idx = 0; last = newnd(); } void add(int c) { int end = newnd(); int tmp = last; e[end].len = e[last].len + 1; for (; tmp != -1 && e[tmp].ch[c] == -1; tmp = e[tmp].f) { e[tmp].ch[c] = end; } if (tmp == -1) e[end].f = 0; else { int nxt = e[tmp].ch[c]; if (e[tmp].len + 1 == e[nxt].len) e[end].f = nxt; else { int nd = newnd(); e[nd] = e[nxt]; e[nd].len = e[tmp].len + 1; e[nxt].f = e[end].f = nd; for (; tmp != -1 && e[tmp].ch[c] == nxt; tmp = e[tmp].f) { e[tmp].ch[c] = nd; } } } last = end; } }; char str[N]; SAM sam; int ws[N]; int pk[N<<1]; int dp[N<<1]; // dp[i]表示第i號節點能夠匹配最長的子串長度 int main() { sam.init(); scanf("%s", str); int slen = strlen(str); for (int i = 0; i < slen; ++i) sam.add(str[i] - 'a'); // 按照能夠匹配的子串長度按照從大到小排序 for (int i = 0; i <= slen; ++i) ws[i] = 0; for (int i = 1; i < sam.idx; ++i) ws[sam.e[i].len]++; for (int i = 1; i <= slen; ++i) ws[i] += ws[i-1]; for (int i = sam.idx-1; i > 0; --i) pk[ws[sam.e[i].len]--] = i; Node *ele = sam.e; while (scanf("%s", str) != EOF) { slen = strlen(str); int len = 0; for (int i = 0, p = 0; i < slen; ++i) { int c = str[i] - 'a'; if (ele[p].ch[c] != -1) { ++len; p = ele[p].ch[c]; dp[p] = max(len, dp[p]); } else { while (p != -1 && ele[p].ch[c] == -1) p = ele[p].f; if (p != -1) { len = ele[p].len + 1; p = ele[p].ch[c]; dp[p] = max(len, dp[p]); } else len = p = 0; } } for (int i = sam.idx-1; i > 0; --i) { int v = pk[i]; ele[v].len = min(ele[v].len, dp[v]); // 縮小能夠匹配的最長子串長度 dp[ele[v].f] = max(dp[ele[v].f], dp[v]); // 擴充包含子串的匹配長度 dp[v] = 0; } } int ans = 0; for (int i = 1; i < sam.idx; ++i) ans = max(ans, ele[i].len); printf("%d\n", ans); return 0; }
SPOJ-7258 Lexicographical Substring Search
題意:給定一個字符串,取出所有的子串按照字典序排序並去重后,求第K大的子串。
分析:依據字符串構建一個自動機,設dp[i]表示以根到第i個節點為前綴的子串共有多少個。初始化每個節點的dp[]值為1,然后根據按照len值排序的節點順序進行更新,最后從根向下進行一次搜索即可。代碼TLE了,不知道如何更改。

#include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> using namespace std; typedef unsigned int uint; const int N = 90005; struct Node { int f, len, ch[26]; void init() { len = 0, f = -1; memset(ch, 0xff, sizeof (ch)); } }; struct SAM { Node e[N<<1]; int idx, last; void init() { idx = 0; last = newnd(); } int newnd() { e[idx].init(); return idx++; } void add(int c) { int end = newnd(); int p = last; e[end].len = e[p].len + 1; for (; p != -1 && e[p].ch[c] == -1; p = e[p].f) { e[p].ch[c] = end; } if (p == -1) e[end].f = 0; else { int nxt = e[p].ch[c]; if (e[p].len + 1 == e[nxt].len) e[end].f = nxt; else { int nd = newnd(); e[nd] = e[nxt]; e[nd].len = e[p].len + 1; e[nxt].f = e[end].f = nd; for (; p != -1 && e[p].ch[c] == nxt; p = e[p].f) { e[p].ch[c] = nd; } } } last = end; } }; SAM sam; Node *ele; char str[N]; int ws[N]; int pk[N<<1]; uint dp[N<<1]; int path[N]; void solve() { int Q; uint K; scanf("%d", &Q); while (Q--) { scanf("%u", &K); int idx = 0, p = 0; while (K) { for (int i = 0; i < 26; ++i) { if (dp[ele[p].ch[i]] < K) { K -= dp[ele[p].ch[i]]; } else { path[idx++] = i; K--; p = ele[p].ch[i]; break; } } } for (int i = 0; i < idx; ++i) { printf("%c", path[i] + 'a'); } puts(""); } } int main() { // freopen("1.in", "r", stdin); sam.init(); ele = sam.e; scanf("%s", str); int slen = strlen(str); for (int i = 0; i < slen; ++i) sam.add(str[i] - 'a'); for (int i = 0; i <= slen; ++i) ws[i] = 0; for (int i = 1; i < sam.idx; ++i) ws[ele[i].len]++; for (int i = 1; i <= slen; ++i) ws[i] += ws[i-1]; for (int i = sam.idx-1; i > 0; --i) pk[ws[ele[i].len]--] = i; for (int i = 1; i < sam.idx; ++i) dp[i] = 1; for (int i = sam.idx-1; i > 0; --i) { const int &v = pk[i]; for (int j = 0; j < 26; ++j) { if (ele[v].ch[j] != -1) dp[v] += dp[ele[v].ch[j]]; } } solve(); return 0; }
HDU-4622 Reincarnation
題意:給定一個字符串,長度最長為2000,有至多10000組詢問,每個詢問給定一個區間,求出該區間內共有多少個不同的子串。
分析:一開始是直接對每一個詢問構建一個后綴自動機,超時了。正解是對詢問進行排序,擁有相同L值的區間可以合並,由於最多只有2000個字符,因此重建的次數就最多就是2000次。對於給定的后綴自動機,子串的個數是遍歷每一個節點,ans += ele[i].len - ele[ele[i].f].len,意思為枚舉每個子串的最后一個元素,新增的子串個數就是到該點最長后綴減去與其父親節點的重復后綴部分。后綴數組則是在遍歷height數組的時候確定每個子串的開始位置,然后減去相同的前綴部分。

#include <cstdlib> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N = 2005; char str[N]; struct Node { int f, len, ch[26]; void init() { len = 0, f = -1; memset(ch, 0xff, sizeof (ch)); } }; int pk[N<<1]; int ws[N]; int dp[N<<1]; struct SAM { Node e[N<<1]; int idx, last; void init() { idx = 0; last = newnd(); } int newnd() { e[idx].init(); return idx++; } void add(int c) { int end = newnd(); int p = last; e[end].len = e[p].len + 1; for (; p != -1 && e[p].ch[c] == -1; p = e[p].f) { e[p].ch[c] = end; } if (p == -1) e[end].f = 0; else { int nxt = e[p].ch[c]; if (e[p].len + 1 == e[nxt].len) e[end].f = nxt; else { int nd = newnd(); e[nd] = e[nxt]; e[nd].len = e[p].len + 1; e[nxt].f = e[end].f = nd; for (; p != -1 && e[p].ch[c] == nxt; p = e[p].f) { e[p].ch[c] = nd; } } } last = end; } }; SAM sam; int cal() { int ret = 0; Node *ele = sam.e; for (int i = 1; i < sam.idx; ++i) { ret += ele[i].len - ele[ele[i].f].len; } return ret; } struct Query { int l, r, No; bool operator < (const Query & t) const { if (l != t.l) return l < t.l; else return r < t.r; } }; Query seq[10005]; int ans[10005]; void solve() { int Q; scanf("%d", &Q); for (int i = 1; i <= Q; ++i) { scanf("%d %d", &seq[i].l, &seq[i].r); seq[i].No = i; } sort(seq+1, seq+1+Q); seq[0].l = -1; for (int i = 1; i <= Q; ++i) { if (seq[i].l != seq[i-1].l) { sam.init(); for (int j = seq[i].l; j <= seq[i].r; ++j) { sam.add(str[j] - 'a'); } ans[seq[i].No] = cal(); } else { for (int j = seq[i-1].r + 1; j <= seq[i].r; ++j) { sam.add(str[j] - 'a'); } ans[seq[i].No] = cal(); } } for (int i = 1; i <= Q; ++i) { printf("%d\n", ans[i]); } } int main() { int T; scanf("%d", &T); while (T--) { scanf("%s", str + 1); solve(); } return 0; }
HDU-4641 K-string
題意:給定一個原始的字符串,有m次操作,每次操作可以向該字符串末尾添加一個字符或者詢問在字符串中出現了至少K次的子串一共有多少個?
分析:首先使用后綴自動機的這種寫法在極端情況下(K非常大,字符串全為同一個字符)是會TLE的,不過該題能夠使用該算法水過。在后綴自動機的節點中添加新的節點信息:num,表示以該節點結束的子串出現的次數。那么沒插入一個元素就遍歷 f 指針更新與當前后綴有共同后綴的更短的子串,如果次數已經等於K,說明已經被統計過了,立即退出遍歷過程,因為更前面的也一定被統計過了;如果加一之后剛好等於K則說明這個子串可以被統計了,子串的個數為 e[tmp].len - e[e[tmp].f].len 統計后也立即退出,否則一直往前更新。

#include <cstdlib> #include <cstring> #include <cstdio> #include <algorithm> using namespace std; typedef long long LL; const int N = 50005; const int M = 200005; int n, K, m; char str[N]; LL ans; struct Node { int len, f, num, ch[26]; void init() { len = num = 0; f = -1; memset(ch, 0xff, sizeof (ch)); } }; struct SAM { Node e[N+M<<1]; int idx, last; int newnd() { e[idx].init(); return idx++; } void init() { idx = 0; last = newnd(); } void add(int c) { // printf("__%c__\n", c + 'a'); int end = newnd(); int tmp = last; e[end].len = e[last].len + 1; for (; tmp != -1 && e[tmp].ch[c] == -1; tmp = e[tmp].f) { e[tmp].ch[c] = end; } if (tmp == -1) e[end].f = 0; else { int nxt = e[tmp].ch[c]; if (e[tmp].len+1 == e[nxt].len) e[end].f = nxt; else { int nd = newnd(); e[nd] = e[nxt]; e[nd].len = e[tmp].len + 1; e[nxt].f = e[end].f = nd; for (; tmp != -1 && e[tmp].ch[c] == nxt; tmp = e[tmp].f) { e[tmp].ch[c] = nd; } } } for (tmp = end; tmp != 0; tmp = e[tmp].f) { if (e[tmp].num == K) break; e[tmp].num++; if (e[tmp].num == K) { ans += e[tmp].len - e[e[tmp].f].len; break; } } last = end; } }; SAM sam; int main() { while (scanf("%d %d %d", &n, &m, &K) != EOF) { sam.init(); ans = 0; scanf("%s", str); int slen = strlen(str), op; for (int i = 0; i < slen; ++i) sam.add(str[i] - 'a'); while (m--) { scanf("%d", &op); if (op == 1) { scanf("%s", str); sam.add(str[0] - 'a'); } else { printf("%I64d\n", ans); } } } return 0; }