【后缀自动机】
后缀自动机也是解决字符串问题的常用工具,犀利在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; }