后缀自动机
Posted downrainsun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了后缀自动机相关的知识,希望对你有一定的参考价值。
https://oi-wiki.org/string/sam/#_5
oiwiki网上的
https://blog.csdn.net/thy_asdf/article/details/51569443
这个博客讲了很多题。
//#include<bits/stdc++.h> #include<cstdio> #include<cstring> #include<queue> #include<vector> #include<algorithm> using namespace std; const int maxn = 2e4 + 5; const int maxc = 180;//如果太大可以用map const int mod = 1e9 + 7; const int inf = 0x3f3f3f3f; typedef long long ll; int len[maxn * 2]; //最长子串的长度(该节点字串数量=len[x]-len[fail[x]]) int fail[maxn * 2]; //后缀链接(最短串前部减少一个字符所到达的状态) int cnt[maxn * 2]; //被后缀连接的数量,方法一求拓扑排序时需要。 int nex[maxn * 2][maxc]; //状态转移(尾部加一个字符的下一个状态)(图),如果不只是字母,而是很大的话可以用map. int sz; //节点编号 int last; //最后节点 ll epos[maxn * 2]; // enpos数(该状态子串出现数量) ll sum[maxn * 2], rak[maxn * 2]; //求拓扑序是用的数组。 int val[maxn], mi[maxn * 2], ma[maxn * 2]; /** 初始化 **/ void init() //初始化 last = sz = 1; //1表示root起始点 空集 fail[1] = len[1] = 0; /** SAM建图 **/ void Extend(int c) //插入字符,为字符ascll码值 int cur = ++sz; //创建一个新节点cur; len[cur] = len[last] + 1; // 长度等于最后一个节点+1 mi[cur] = ma[cur] = len[cur]; epos[cur] = 1; //接受节点子串除后缀连接还需加一 int p; //第一个有C转移的节点; for (p = last; p && !nex[p][c]; p = fail[p]) nex[p][c] = cur;//沿着后缀连接 将所有没有字符c转移的节点直接指向新节点 if (!p) fail[cur] = 1; cnt[1]++; //全部都没有c的转移 直接将新节点后缀连接到起点 else int q = nex[p][c]; //p通过c转移到的节点 if (len[p] + 1 == len[q]) //pq是连续的 fail[cur] = q; cnt[q]++; //将新节点后缀连接指向q即可,q节点的被后缀连接数+1 else int nq = ++sz; //不连续 需要复制一份q节点 len[nq] = len[p] + 1; //令nq与p连续 fail[nq] = fail[q]; //因后面fail[q]改变此处不加cnt memcpy(nex[nq], nex[q], sizeof(nex[q])); //复制q的信息给nq for (; p&&nex[p][c] == q; p = fail[p]) nex[p][c] = nq; //沿着后缀连接 将所有通过c转移为q的改为nq fail[q] = fail[cur] = nq; //将cur和q后缀连接改为nq cnt[nq] += 2; // nq增加两个后缀连接 last = cur; //更新最后处理的节点 char s1[maxn], s2[maxn]; /** 求一个串每个长度的所有子串中,出现最多的次数spoj8222 **/ /** 方法一:bfs的拓扑排序,不是主要方法 **/ //求npos数,即该节点子串出现次数 void GetNpos(char ch[], int len1) for(int i = 0; i < len1; i++) Extend(ch[i] - ‘a‘); queue<int>q; for (int i = 1; i <= sz; i++) if (!cnt[i]) q.push(i); //将所有没被后缀连接指向的节点入队 while (!q.empty()) int x = q.front(); q.pop(); epos[fail[x]] += epos[x]; //子串数量等于所有后缀连接指向该节点的子串数量和+是否为接受节点 if (--cnt[fail[x]] == 0)q.push(fail[x]); //当所有后缀连接指向该节点的处理完毕后再入队 //求出所有长度为k的子串中出现次数最多的子串出现次数 void GetSubMax() ll a[maxn]; scanf("%s", s1);//方法一长度为i的子串出现最大次数 int len1 = strlen(s1); GetNpos(s1, len1); for (int i = 1; i <= sz; i++) a[len[i]] = max(a[len[i]], epos[i]); //长度≤k的子串中出现次数最多的子串出现次数的最小值 for (int i = len1 - 1; i >= 1; i--) a[i] = max(a[i], a[i + 1]); //求一遍后缀最大值就是答案 for (int i = 1; i <= len1; i++) printf("%lld\n", a[i]); /** 方法二数组的逆拓扑序 **/ void getmaxlen() ll num[maxn * 2];//方法二长度为i的子串出现最大次数。 init(); scanf("%s", s1); int len1 = strlen(s1); for(int i = 0; i < len1; i++) Extend(s1[i] - ‘a‘); //下面是计数排序的思想。相当于找一个数前面有多少个数,从而知道这个数排在哪。 for(int i = 1; i <= sz; i++) sum[len[i] ]++; for(int i = 1; i <= len1; i++) sum[i] += sum[i - 1]; for(int i = 1; i <= sz; i++) rak[sum[len[i] ]-- ] = i; for(int i = sz; i >= 1; i--) int x = rak[i]; epos[fail[x] ] += epos[x]; for(int i = 1; i <= sz; i++) num[len[i] ] = max(num[len[i] ], epos[i]); for(int i = len1 - 1; i >= 1; i--) num[i] = max(num[i], num[i + 1]); for(int i = 1; i <= len1; i++) printf("%lld\n", num[i]); /** 求不相同字串数量 **/ void GetSubNum() ll ans = 0; for (int i = 2; i <= sz; i++) ans += len[i] - len[fail[i]]; //一状态子串数量等于len[i]-len[fail[i]] printf("%lld\n",ans); /** 求多个字符串的最长公共子串spoj1812 **/ void getnlcs() ll maxnlcs[maxn * 2]; //求多个子串最长公共子串时,每个串来匹配时能匹的最长长度。 ll ans[maxn * 2]; //求多个子串的最长公共子串时的结果数组。 init(); scanf("%s", s1); int len1 = strlen(s1); for(int i = 0; i < len1; i++) Extend(s1[i] - ‘a‘); for(int i = 1; i <= sz; i++) ans[i] = len[i]; //下面是计数排序的思想。相当于找一个数前面有多少个数,从而知道这个数排在哪。 for(int i = 1; i <= sz; i++) sum[len[i] ]++; for(int i = 1; i <= len1; i++) sum[i] += sum[i - 1]; for(int i = 1; i <= sz; i++) rak[sum[len[i] ]-- ] = i; while(~scanf("%s", s2)) memset(maxnlcs, 0, sizeof(maxnlcs)); int len2 = strlen(s2); int p = 1; ll tmp = 0; for(int i = 0; i < len2; i++) int x = s2[i] - ‘a‘; if(nex[p][x]) tmp++; p = nex[p][x]; else while(p && !nex[p][x]) p = fail[p]; if(!p) tmp = 0; p = 1; else tmp = len[p] + 1; p = nex[p][x]; maxnlcs[p] = max(maxnlcs[p], tmp); //首先如果一个点能够匹配到的话,那么他的fail指针的点也一定可以匹配到,因为fail指针的 //的点是原来节点的子串,所以下面的节点要先更新,然后更新其fail指针的。这个需要逆拓扑。 for(int i = sz; i >= 1; i--) ll x = rak[i]; ans[x] = min(ans[x], maxnlcs[x]); if(maxnlcs[x] && fail[x]) maxnlcs[fail[x] ] = len[fail[x] ]; // printf("scsc\n"); ll res = 0; for(int i = 1; i <= sz; i++) res = max(ans[i], res); printf("%lld\n", res); /** 求两个字符串的最长公共子串。spoj1811 直接根据后缀自动机的状态转移图来遍历,如果存在这个字符就继续往下走,不存在则开始跳fail, 直到找到存在那个字符的,此时只有这个fail点与最开始的点后缀相同。 **/ void getlcs() init(); scanf("%s%s", s1, s2); int len1 = strlen(s1), len2 = strlen(s2); for(int i = 0; i < len1; i++) Extend(s1[i] - ‘a‘); int ans = 0, tmp = 0, p = 1; for(int i = 0; i < len2; i++) int x = s2[i] - ‘a‘; if(nex[p][x]) tmp++; p = nex[p][x]; else while(p && !nex[p][x]) p = fail[p]; if(!p) tmp = 0; p = 1; else tmp = len[p] + 1; p = nex[p][x]; ans = max(ans, tmp); printf("%d\n", ans); /** bzoj3998 求一个字符串中第K大的串,op=0代表相同的串在不同位置只算一次,op=1代表可以算多次。 所以先求出所有子串的可能出现次数。epos数组 然后求出某个点以这个点开头的字符串数量。num数组。 然后在dfs去找。 **/ ll num[maxn * 2]; void dfsk(int rt, int rk) if(rk <= epos[rt]) return; rk -= epos[rt]; for(int i = 0; i < 26; i++) int v = nex[rt][i]; if(v) if(rk <= num[v]) putchar(‘a‘ + i); dfsk(v, rk); return; else rk -= num[v]; void getk() scanf("%s", s1); int op, k; scanf("%d%d", &op, &k); init(); int len1 = strlen(s1); for(int i = 0; i < len1; i++) Extend(s1[i] - ‘a‘); //下面是计数排序的思想。相当于找一个数前面有多少个数,从而知道这个数排在哪。 for(int i = 1; i <= sz; i++) sum[len[i] ]++; for(int i = 1; i <= len1; i++) sum[i] += sum[i - 1]; for(int i = 1; i <= sz; i++) rak[sum[len[i] ]-- ] = i; for(int i = sz; i >= 1; i--) int x = rak[i]; if(op == 1) epos[fail[x] ] += epos[x]; else epos[x] = 1; epos[1] = 0; for(int i = sz; i >= 1; i--) int x = rak[i]; num[x] = epos[x]; for(int j = 0; j < 26; j++) int v = nex[x][j]; if(v) num[x] += num[v]; // for(int i = 1; i <= sz; i++) printf("%lld\n", epos[i]); if(num[1] < k) puts("-1"); else dfsk(1, k); puts(""); /** 求变化趋势相同的子串且长度大于等于5.poj1743 因为是变化趋势,所以要先差分一下,那么就相当于差分数组建后缀自动机,然后找长度大于等于4的子串, 且没有重合的点。注意多组数据时的初始化。 **/ int tmp[maxn * 2]; int cmp(int x, int y) return len[x] > len[y]; void getSameTend() int n; while(~scanf("%d", &n)) if(n == 0) break; init(); memset(mi, inf, sizeof(mi)); memset(ma, 0, sizeof(ma)); memset(nex, 0, sizeof(nex)); memset(fail, 0, sizeof(fail)); memset(sum, 0, sizeof(sum)); for(int i = 1; i <= n; i++) scanf("%d", &val[i]); for(int i = 1; i < n; i++) val[i] = val[i + 1] - val[i], Extend(val[i] + 88); for(int i = 1; i <= sz; i++) sum[len[i] ]++; for(int i = 1; i < n; i++) sum[i] += sum[i - 1]; for(int i = 1; i <= sz; i++) rak[sum[len[i] ]-- ] = i; for(int i = sz; i > 0; i--) int x = rak[i]; ma[fail[x] ] = max(ma[fail[x] ], ma[x]); mi[fail[x] ] = min(mi[fail[x] ], mi[x]); int ans = 0; for(int i = sz; i >= 1; i--) ans = max(ans, min(ma[i] - mi[i], len[i])); ans++; if(ans < 5) ans = 0; printf("%d\n", ans); int main() getlcs(); getnlcs(); getmaxlen(); getk(); getSameTend(); return 0;
以上是关于后缀自动机的主要内容,如果未能解决你的问题,请参考以下文章