【已整理】后缀自动机 【基础】 总
看了好多遍资料,换了一份模板,重新整理。
模板
const int MAXN = 100005; const int SIGMA_SIZE = 26; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后topo_sort *然后get_right **/
1.uva719
题意:给出一个字符串,问你这个字符串的最小循环表示法的起始位置,位置编号从1开始
假设这个串为S,长度为len,则我们构建一个串SS,然后加入后缀自动机,然后每次贪心走字典序最小的字母,走len步即可。
由于每次都是贪心走的字典序最小的,所以只要走len步,所找到的串一定就是最小循环表示,关键是,我们这样贪心地走,是否一定能走够len步。答案是肯定的,我们把串S复制了一份,则我们第一步走出的那个字符的位置,一定是第一个S中的某个位置,后面再贪心地走,可能会对这个起始位置有所微调,但是一旦定下来,一定是第一个S串中的位置,这个位置定下来后,我们走的就是从这个位置开始,长度为len的子串,所以我们这么贪心地走一定是可以走够len步的,也就是我们可以通过这种贪心的方法找到一个串的最小循环表示。
关键是,如何得知找到的这个串的结束位置(或者起始位置),结束位置就是step[now],now是最后走到的节点,我这里不是很懂
其实只要证明,我最后走到的点,一定是主链上的点,那么就能说明这样做是对的了
自己举个例子,手动建一个后缀自动机,会发现加第二遍S的字符时,不会再新建节点了,所以这就说明了我最后走到的点一定是主链上的点
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 20005; const int SIGMA_SIZE = 26; int idx(char c) { return c - 'a'; } struct SAM { int sz, last; int trans[MAXN*2+9][SIGMA_SIZE], pre[MAXN*2+9], step[MAXN*2+9];//step的含义是当前结点所代表的子串集合中长度最长的子串的长度 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init () { sz = 0, last = 1; newNode(0); } void add_char(char ch) { newNode(step[last]+1); int c = idx(ch); int p = last, np = sz; while (p && !trans[p][c]) { trans[p][c] = np; p = pre[p]; } if (p) { int q = trans[p][c]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][c] == q) { trans[p][c] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } int solve(int len){ int cnt=0; int now=1; while(cnt<len){ for(int i=0;i<SIGMA_SIZE;i++){ int v=trans[now][i]; if(v!=0){ now=v; cnt++; break; } } } return step[now]; } }sam; char str[MAXN]; int main () { int t; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<2;i++){ for(int j=0;j<len;j++) sam.add_char(str[j]); } printf("%d\n",sam.solve(len)-len+1); } return 0; }
2.SPOJ LCS
题意:给出两个字符串,问你这两个字符串的最长公共子串的长度是多少。
对第一个串建立后缀自动机,第二个串在第一个串的后缀自动机上走,每次转移成功,则cnt++,否则走suffix link,直到走到一个节点x,有对应的转移,那么cnt=maxlen[x]+1,同时转移到节点x的后继节点,如果回到根节点也没有对应的转移,则cnt=0
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 250005; const int SIGMA_SIZE = 26; int idx(char c) { return c - 'a'; } struct SAM { int sz, last; int trans[MAXN*2+9][SIGMA_SIZE], pre[MAXN*2+9], step[MAXN*2+9];//step的含义是当前结点所代表的子串集合中长度最长的子串的长度 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init () { sz = 0, last = 1; newNode(0); } void add_char(char ch) { newNode(step[last]+1); int c = idx(ch); int p = last, np = sz; while (p && !trans[p][c]) { trans[p][c] = np; p = pre[p]; } if (p) { int q = trans[p][c]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][c] == q) { trans[p][c] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void solve(char str[],int len){ int cnt=0,now=1,res=0; for(int i=0;i<len;i++){ int c=idx(str[i]); int v=trans[now][c]; if(v!=0){ now=v; cnt++; res=max(res,cnt); } else{ int u=now; while(u&&trans[u][c]==0) u=pre[u]; if(u==0&&trans[u][c]==0){ cnt=0; now=1; } else{ cnt=step[u]+1; res=max(res,cnt); now=trans[u][c]; } } } printf("%d\n",res); } }sam; char str[MAXN]; int main () { sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.add_char(str[i]); scanf("%s",str); sam.solve(str,strlen(str)); return 0; }
题意:给出一个字符串,问你这个字符串的每一种长度的子串中出现次数最多的子串的出现次数
建立后缀自动机,然后对于每一个状态去更新答案,状态s表示的最长的子串是step[s],出现次数是right[s],用right[s]去更新ans[step[s]],最后输出之前要对ans从后往前扫一遍,ans[i]=max(ans[i],ans[i+1])
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 250005; const int SIGMA_SIZE = 26; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int g[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(g[sz], 0, sizeof(g[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !g[p][v]) { g[p][v] = np; p = pre[p]; } if (p) { int q = g[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) g[nq][j] = g[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && g[p][v] == q) { g[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void get_tuopu(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = g[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } int ans[MAXN]; void solve(int n){ memset(ans,0,sizeof(ans)); for(int i=2;i<=sz;i++){ int v=step[i]; ans[v]=max(ans[v],right[i]); } for(int i=n-1;i>=1;i--) ans[i]=max(ans[i],ans[i+1]); for(int i=1;i<=n;i++) printf("%d\n",ans[i]); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后get_right **/ int K; char s[MAXN]; int main () { scanf("%s", s); sam.init(); int n = strlen(s); for (int i = 0; i < n; i++) sam.insert(s[i]); sam.get_tuopu(); sam.get_right(s); sam.solve(n); return 0; }
题意:求若干个串的最长公共子串。
用其中的一个串建立后缀自动机,然后对所有其他的串,对每个串求出每个状态的最长公共子串存在cur数组中,然后用cur数组去更新rec数组,取相对较小者。
非常重要的注意点:当u匹配时,其suffixlink指向的节点v一定也匹配,但最长匹配长度限制为step[v]。所以我们在用cur数组更新rec数组之前,需要保证所有的u对应的v已经更新为u和它本身的最大匹配长度。这里如果不拓扑排序后再更新就会有问题,我们用计数排序求出拓扑排序,然后在拓扑序下更新即可,具体见代码。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 100005; const int SIGMA_SIZE = 26; int cur[MAXN*2],rec[MAXN*2]; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } void solve(char str[],int len){ int cnt=0,now=1; memset(cur,0,sizeof(cur)); for(int i=0;i<len;i++){ int c=idx(str[i]); int v=trans[now][c]; if(v!=0){ now=v; cnt++; cur[now]=max(cur[now],cnt); } else{ int u=now; while(u!=0&&trans[u][c]==0) u=pre[u]; if(u==0&&trans[u][c]==0){ now=1; cnt=0; } else{ cnt=step[u]+1; now=trans[u][c]; cur[now]=max(cur[now],cnt); } } } for(int j=sz;j>=2;j--){ int u=pos[j]; rec[u]=min(rec[u],cur[u]); int v=pre[u]; int tt=min(cur[u],step[v]); cur[v]=max(cur[v],tt); } } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后get_right **/ char str[MAXN]; int main (){ sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.insert(str[i]); sam.topo_sort(); fill(rec,rec+MAXN*2,MAXN); while(~scanf("%s",str)) sam.solve(str,strlen(str)); int res=0; int sz=sam.sz; for(int i=2;i<=sz;i++) res=max(res,rec[i]); if(res==MAXN) res=len; printf("%d\n",res); return 0; }
5.hihocoder 1445
题意:求一个串中一共有多少个不同的子串
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long int ll; const int MAXN = 1000005; const int SIGMA_SIZE = 26; int idx(char c) { return c - 'a'; } struct SAM { int sz, last; int trans[MAXN*2+9][SIGMA_SIZE], pre[MAXN*2+9], step[MAXN*2+9];//step的含义是当前结点所代表的子串集合中长度最长的子串的长度 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init () { sz = 0, last = 1; newNode(0); } void add_char(char ch) { newNode(step[last]+1); int c = idx(ch); int p = last, np = sz; while (p && !trans[p][c]) { trans[p][c] = np; p = pre[p]; } if (p) { int q = trans[p][c]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][c] == q) { trans[p][c] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void solve(){ ll res=0; for(int i=2;i<=sz;i++) res+=step[i]-step[pre[i]]; printf("%lld\n",res); } }sam; char str[MAXN]; int main () { sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.add_char(str[i]); sam.solve(); return 0; }
6.hdu6194 string string string
题意:求一个串中一共有多少个出现了k次的不同的子串
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 100005; const int SIGMA_SIZE = 26; int idx(char c) { return c -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int c = idx(str[i]); p = trans[p][c]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } void solve(int k){ ll res=0; for(int i=2;i<=sz;i++){ if(right[i]==k) res+=step[i]-step[pre[i]]; } printf("%lld\n",res); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后topo_sort *然后get_right **/ char str[MAXN]; int main (){ int t,k; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ sam.init(); scanf("%d",&k); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.insert(str[i]); sam.topo_sort(); sam.get_right(str); sam.solve(k); } return 0; }
7.hihocoder1457
题意:求一个数字串中所有子串代表的数字的和
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 1000005; const ll mod = 1e9+7; const int SIGMA_SIZE = 11; int cur[MAXN*2],rec[MAXN*2]; int idx(char ch) { return ch -'0'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } ll sum[MAXN<<1],validcnt[MAXN<<1]; void solve(){ topo_sort(); memset(sum,0,sizeof(sum)); memset(validcnt,0,sizeof(validcnt)); validcnt[1]=1; for(int i=1;i<=sz;i++){ int u=pos[i]; for(int j=0;j<SIGMA_SIZE-1;j++){ int v=trans[u][j]; if(v==0) continue; validcnt[v]+=validcnt[u];validcnt[v]%=mod; sum[v]+=((sum[u]*10)%mod+(validcnt[u]*j)%mod)%mod;sum[v]%=mod; } } ll res=0; for(int i=2;i<=sz;i++){ res+=sum[i]; res%=mod; } printf("%lld\n",res); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后get_right **/ char str[MAXN]; int main (){ sam.init(); int n; scanf("%d",&n); for(int i=0;i<n;i++){ scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.insert(str[i]); if(i!=n-1) sam.insert('9'+1); } sam.solve(); return 0; }
8.hdu4436
题意:求一个数字串中所有子串代表的数字的和(前导零的个数不同的相同数字串视为相同,不重复计算)
和上题相比,只需要在初始节点不走0这个转移即可。
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <queue> typedef long long int ll; using namespace std; const int MAXN = 100005; const int SIGMA_SIZE = 11; const int mod = 2012; int cur[MAXN*2],rec[MAXN*2]; int idx(char ch) { return ch -'0'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } int sum[MAXN<<1],validcnt[MAXN<<1]; void solve(){ memset(sum,0,sizeof(sum)); memset(validcnt,0,sizeof(validcnt)); topo_sort(); validcnt[1]=1; for(int i=1;i<=sz;i++){ int u=pos[i]; int ss=0,tt=SIGMA_SIZE-1; if(u==1) ss=1; for(int j=ss;j<tt;j++){ int v=trans[u][j]; if(v!=0){ validcnt[v]+=validcnt[u];validcnt[v]%=mod; sum[v]+=((sum[u]*10)%mod+(j*validcnt[u])%mod)%mod;sum[v]%=mod; } } } int res=0; for(int i=2;i<=sz;i++){ res+=sum[i]; res%=mod; } printf("%d\n",res); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后topo_sort *然后get_right **/ char str[MAXN]; int main (){ int n; while(~scanf("%d",&n)){ sam.init(); for(int i=0;i<n;i++){ scanf("%s",str); int len=strlen(str); for(int j=0;j<len;j++) sam.insert(str[j]); if(i!=n-1) sam.insert('9'+1); } sam.solve(); } return 0; }
题意:求一个数字串的第k小子串
建立后缀自动机,预处理一个counter数组,表示每个节点和其后继节点中有多少个前缀相同的串。
那么对于第k小子串,我们贪心的在后缀自动机中走,每次获取一个后继节点,如果后继节点的counter值小于k,说明这个节点及其后继节点代表的子串的字典序都是小于我要找的这个子串的,所以k减去counter值,然后用下一个字符去找后继节点继续这个过程。如果后继节点的counter值>=k,说明我要找的串就在这个后继节点和其后继节点中,所以k--,表示减去当前节点的这个串,输出当前路径的字符,然后走到这个后继节点,重复上述过程,直到k等于0。
看题目的要求,如果第k小的子串不一定存在,即k比较大的情况,需要先统计出一共有多少个子串,再进行判断,本题不需要。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 90005; const int SIGMA_SIZE = 26; int cur[MAXN*2],rec[MAXN*2]; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } int counter[MAXN<<1]; void prework(){//处理出后继节点中有多少个串是以当前节点的某个串为前缀的 topo_sort(); for(int i=1;i<=sz;i++) counter[i]=1; for(int i=sz;i>=1;i--){ int u=pos[i]; for(int j=0;j<SIGMA_SIZE;j++){ int v=trans[u][j]; if(v!=0) counter[u]+=counter[v]; } } } void solve(int k){ int now=1; while(k){ for(int i=0;i<SIGMA_SIZE;i++){ int v=trans[now][i]; if(v!=0){ if(counter[v]<k) k-=counter[v]; else{ putchar('a'+i); k--; now=v; break; } } } } putchar('\n'); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后topo_sort *然后get_right **/ char str[MAXN]; int main (){ sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.insert(str[i]); sam.prework(); int p,q; scanf("%d",&q); for(int i=0;i<q;i++){ scanf("%d",&p); sam.solve(p); } return 0; }
10.poj3415
题意:给出两个字符串S、T,问你有多少个三元组(i,j,k),使得S的i位置开始和T的j位置开始的长度为k的子串匹配。(以三元组的形式给出,说明重复匹配需要重复计,在后缀自动机中相当于要用到right数组)
既然是匹配的问题,那么我们以S建立后缀自动机,然后T在S的后缀自动机上跑,和2类似的方法。
如果在状态x,子串的长度为y,且y>=k,则我们不仅要统计这个子串的贡献,还要统计它的所有前驱节点的贡献,所以我们需要预处理出一个点及其所有前驱节点长度大于等于k的子串的贡献。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 100005; const int SIGMA_SIZE = 52; int idx(char ch) { if(ch>='A'&&ch<='Z') return ch-'A'+26; return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } ll counter[MAXN<<1]; void pre_work(int k){ for(int i=0;i<=sz;i++) counter[i]=0; for(int i=1;i<=sz;i++){ int u=pos[i]; counter[u]+=counter[pre[u]]; int tt=step[pre[u]]+1; if(step[u]>=k) counter[u]+=1LL*right[u]*(step[u]-max(tt,k)+1); } } void get_count(char str[],int k){ ll res=0; int len=strlen(str); int now=1,cur=0; for(int i=0;i<len;i++){ int c=idx(str[i]); int v=trans[now][c]; if(v!=0){ now=v; cur++; } else{ int u=now; while(u!=0&&trans[u][c]==0) u=pre[u]; if(u==0&&trans[u][c]==0){ now=1; cur=0; } else{ cur=step[u]+1; now=trans[u][c]; } } if(cur>=k){ res+=counter[pre[now]]; int tt=step[pre[now]]+1; res+=1LL*right[now]*(cur-max(tt,k)+1); } } printf("%lld\n",res); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后topo_sort *然后get_right **/ char str[MAXN]; int main (){ int k; while(scanf("%d",&k)){ if(k==0) break; sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.insert(str[i]); sam.topo_sort(); sam.get_right(str); sam.pre_work(k); scanf("%s",str); sam.get_count(str,k); } return 0; }
11.hdu4622
题意:2000长度的字符串,10000个区间询问,每次输出这个区间内有多少个不同的子串。
最暴力的后缀自动机在时间复杂度上也不会T,排序优化一下1000ms。
用暴力方法果然T了。。。还是得排序优化的。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 2005; const int SIGMA_SIZE = 26; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } int solve(){ int res=0; for(int i=2;i<=sz;i++) res+=step[i]-step[pre[i]]; return res; } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后topo_sort *然后get_right **/ char str[MAXN]; struct Node{ int l,r,pos; }node[10005]; int ans[10005]; bool cmp(Node a,Node b){ if(a.l!=b.l) return a.l<b.l; return a.r<b.r; } int main (){ int t,q; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ scanf("%s",str); scanf("%d",&q); for(int i=0;i<q;i++){ scanf("%d%d",&node[i].l,&node[i].r); node[i].l--;node[i].r--; node[i].pos=i; } sort(node,node+q,cmp); int pre=-1,cur; for(int i=0;i<q;i++){ int x=node[i].l,y=node[i].r; if(x!=pre){ sam.init(); pre=x; cur=x-1; } for(int i=cur+1;i<=y;i++) sam.insert(str[i]); cur=y; ans[node[i].pos]=sam.solve(); } for(int i=0;i<q;i++) printf("%d\n",ans[i]); } return 0; }
12.codeforces 427D
题意:求两个字符串的最短公共子串,要求这个子串在两个串都只出现一次。
以第一个串建立后缀自动机,每个节点的子串的出现次数为right数组,然后第二个串在第一个串的后缀自动机上跑一遍,用num数组记录经过节点的次数,最后按照字典序从后往前扫一遍,最后统计答案的时候要求right值和num值都为1。
一个状态表示的明明是多个子串,第二个串在第一个串的后缀自动机上走,走到某个节点x,则当前的最长匹配长度k必然位于minlen[x]和maxlen[x]之间,这时候我直接num[x]++,为什么可以这样呢?因为同一个状态中表示的子串的出现位置是相同的,而且一个长的串匹配了,则其缩短至minlen[x]的串都是可以匹配的,也就是说,我在某个位置,一旦匹配其中的某个串,那别的串也都是匹配的,所以这时候我们可以对于每个状态都可以整体考虑,而且最后统计答案的时候,用的一定是minlen数组。
如果走到了状态x,那么其前驱节点表示的子串也都是匹配的,也要统计进去,这是我们采取的方法类似于获取right数组的函数,利用拓扑序列,从后往前更新。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 5005; const int SIGMA_SIZE = 26; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void topo_sort(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } int counter[MAXN<<1]; void solve(char str[],int len){ int u; for(int i=0;i<=sz;i++) counter[i]=0; int now=1; for(int i=0;i<len;i++){ int c=idx(str[i]); int v=trans[now][c]; if(v==0){ u=now; while(u!=1&&trans[u][c]==0) u=pre[u]; if(u==1&&trans[u][c]==0) now=1; else now=trans[u][c]; } else now=v; counter[now]++; } for(int i=sz;i>=1;i--){ int u=pos[i]; counter[pre[u]]+=counter[u]; } int res=MAXN; for(int i=2;i<=sz;i++){ if(right[i]==1&&counter[i]==1) res=min(res,step[pre[i]]+1); } if(res==MAXN) res=-1; printf("%d\n",res); } }sam; char str[MAXN]; int main (){ sam.init(); scanf("%s",str); int len=strlen(str); for(int i=0;i<len;i++) sam.insert(str[i]); sam.topo_sort(); sam.get_right(str); scanf("%s",str); sam.solve(str,strlen(str)); return 0; }
13.Mr. Panda and Strips Gym - 101194C
题意:给出n个串,让你输出第一个串的子串中,字典序最小的,且不出现在其他任意一个串中的子串。
对其他n-1个串建立后缀自动机,第一个串在上面跑,每次失配的时候,用当前节点代表的子串的最小长度+1去更新答案。对于u==1且失配的情况需要特判。
经过下一题的启发,发现本题也可以用相同的方法做。
有一个小难点,利用这个方法只能求出最小的长度,但是无法求出位置,所以我们需要解决一个问题,已知状态x和长度k,输出这个子串,我们只需要处理出一个rec数组,表示每个状态的endpos集合中的最小值(其实任何一个都行),这样我们就可以从这个值往前推k-1位,找到这个串在原串的起始位置了。
这个rec数组的求法非常简单,用原串在自己的后缀自动机上跑一遍就行了,注意最后要用拓扑序扫一遍!
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> #include <cmath> #include <queue> typedef long long int ll; using namespace std; const int MAXN = 250005+50005; const int SIGMA_SIZE = 27; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void solve(char s[]){ int len=strlen(s); int now=1,cur=0,res=MAXN,rec; for(int i=0;i<len;i++){ int c=idx(s[i]); int v=trans[now][c]; if(v!=0){ now=v; cur++; } else{//失配 int u=now; while(u!=0&&trans[u][c]==0){ if(u==1) cur=1; else cur=step[pre[u]]+2; if(cur<res){ res=cur; rec=i+1-cur; } else if(cur==res){ bool flag=true; for(int j=rec,k=i+1-cur;j<rec+res&&flag;j++,k++){ if(s[j]>s[k]) flag=false; else if(s[j]<s[k]) break; } if(!flag) rec=i+1-cur; } u=pre[u]; } if(u==0){ cur=0; now=1; } else{ cur=step[u]+1; now=trans[u][c]; } } } if(res==MAXN) printf("Impossible\n"); else{ for(int i=rec;i<rec+res;i++) putchar(s[i]); printf("\n"); } } }sam; char str[MAXN],s[MAXN]; int main(){ int t,n; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ sam.init(); scanf("%d",&n); scanf("%s",str); for(int i=1;i<n;i++){ scanf("%s",s); int len=strlen(s); for(int i=0;i<len;i++) sam.insert(s[i]); if(i!=1) sam.insert('z'+1); } printf("Case #%d: ",cas); sam.solve(str); } return 0; }
14.hdu4416
题意:给出一个串和n个串,问你第一个串的所有子串中,有多少个没有出现在其他的任何一个子串中。
对第一个串建立后缀自动机,剩下的n个串每个串都要在第一个串的后缀自动机上跑一遍,每次更新counter数组,counter数组表示的是一个状态中出现的最长的子串。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; typedef long long ll; const int MAXN = 250005; const int SIGMA_SIZE = 26; int idx(char ch) { return ch -'a'; } struct SAM { int sz, last; int trans[MAXN<<1][SIGMA_SIZE], pre[MAXN<<1], step[MAXN<<1]; int pos[MAXN<<1], right[MAXN<<1], cnt[MAXN<<1];//right表示当前状态所表示的字符串的出现次数 //pos和cnt用作拓扑排序 void newNode(int s) { step[++sz] = s; pre[sz] = 0; memset(trans[sz], 0, sizeof(trans[sz])); } void init() { sz = 0, last = 1; newNode(0); } void insert(char ch){ newNode(step[last] + 1); int v = idx(ch), p = last, np = sz; while (p && !trans[p][v]) { trans[p][v] = np; p = pre[p]; } if (p) { int q = trans[p][v]; if (step[q] == step[p] + 1) pre[np] = q; else { newNode(step[p] + 1); int nq = sz; for (int j = 0; j < SIGMA_SIZE; j++) trans[nq][j] = trans[q][j]; pre[nq] = pre[q]; pre[np] = pre[q] = nq; while (p && trans[p][v] == q) { trans[p][v] = nq; p = pre[p]; } } } else pre[np] = 1; last = np; } void get_tuopu(){ for (int i = 0; i <= sz; i++) cnt[i] = 0; for (int i = 1; i <= sz; i++) cnt[step[i]]++; for (int i = 1; i <= sz; i++) cnt[i] += cnt[i-1]; for (int i = 1; i <= sz; i++) pos[cnt[step[i]]--] = i; } void get_right(char* str){ int p = 1, n = strlen(str); for (int i = 0; i <= sz; i++) right[i] = 0; for (int i = 0; i < n; i++) { int v = idx(str[i]); p = trans[p][v]; right[p]++; } for (int i = sz; i; i--) { int u = pos[i]; right[pre[u]] += right[u]; } } int counter[MAXN<<1]; void prework(){ for(int i=1;i<=sz;i++) counter[i]=step[pre[i]]; } void add(char s[],int len){ int now=1,cur=0; for(int i=0;i<len;i++){ int c=idx(s[i]); int v=trans[now][c]; if(v!=0){ now=v; cur++; counter[now]=max(counter[now],cur); } else{ int u=now; while(u&&!trans[u][c]) u=pre[u]; if(u==0){ now=1; cur=0; } else{ cur=step[u]+1; now=trans[u][c]; counter[now]=max(counter[now],cur); } } } } void solve(){ for(int i=sz;i>=1;i--){ int u=pos[i]; if(counter[u]>step[pre[u]]) counter[pre[u]]=step[pre[u]]; } ll res=0; for(int i=2;i<=sz;i++) res+=step[i]-counter[i]; printf("%lld\n",res); } }sam; /** *先调用sam.init(); *然后一个字母一个字母的insert *然后get_right **/ int K; char s[MAXN]; int main(){ int t,n; scanf("%d",&t); for(int cas=1;cas<=t;cas++){ sam.init(); scanf("%d",&n); scanf("%s",s); int len=strlen(s); for(int i=0;i<len;i++) sam.insert(s[i]); sam.prework(); for(int i=0;i<n;i++){ scanf("%s",s); sam.add(s,strlen(s)); } sam.get_tuopu(); printf("Case %d: ",cas); sam.solve(); } return 0; }

浙公网安备 33010602011771号