后缀自动机

https://oi-wiki.org/string/sam/#_5 

oiwiki网上的

https://blog.csdn.net/thy_asdf/article/details/51569443

这个博客讲了很多题。

 广义后缀自动机的意思是在后缀自动机上添加上多个串,每次添加一个串的时候把last设为0.

 

 

!!后缀链接link(v)指向在不同等价类中的w的最长后缀。

后缀自动机的right数组,首先只有那些是前缀的节点的right的值等于1,然后通过之后的fail转移来是其他点的right值++。

比如说abcc,首先a,ab,abc,abcc的right值都等于1,然后通过fail指针转移,那么bcc这个点的right值将++。

直观上,字符串的 SAM 可以理解为给定字符串的 所有子串 的压缩形式。值得注意的事实是,SAM 将所有的这些信息以高度压缩的形式储存。对于一个长度为 n 的字符串,它的空间复杂度仅为 O(n) 。此外,构造 SAM 的时间复杂度仅为O(n)  。准确地说,一个 SAM 最多有 2n-1 个节点和 3n-4 条转移边。

第一张nex数组,第二张fail树。

 

//nex数组是当前能匹配到的最长子串,fail是到当前节点所能到的最长后缀的点(必须是存在的)
//
#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]; //后缀链接,fail节点的right集合比他自身的大,fail节点代表的串是他自身代表串的子串(后缀子串) 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的子串, 且没有重合的点。注意多组数据时的初始化。
后缀自动机上的每个状态都有一个right集合代表到达该状态的子串在原串中出现位置的右端点。
所以我们只需要知道每个状态最靠左和最靠右的端点分别在哪里,
如果两点直接的差不小于该点的len[i]就说明这个该位置代表的子串重复出现且没有重合,然后用len[i]更新答案即可 *
*/ 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; }

 

//#include<bits/stdc++.h>
#include<cstdio>
#include<cstring>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
const int maxn = 2e5 + 5;
const int maxc = 30;//如果太大可以用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]; //求拓扑序是用的数组。
ll fasum[maxn * 2];//表示当前节点与其fail节点以及fail节点的fail节点的所有子串之和。


/**
初始化
**/
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];
/**
HYSBZ - 4566
找A串的所有子串在B串中的出现次数。
对于B匹配到的每个点,得到的次数是当前节点在A中该串出现的次数*该节点管辖的子串,然后
加上他所有fail节点的出现次数*字符串个数。
因为如果当前节点能匹配到了,那么他的fail节点也能匹配到。
**/
void getallsame() {
    init();
    scanf("%s%s", s1, s2);
    int len1 = strlen(s1);
    int len2 = strlen(s2);
    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++) { int x = rak[i]; fasum[x] = fasum[fail[x] ] + epos[x] * (len[x] - len[fail[x] ]); } ll ans = 0; int p = 1, val = 0; for(int i = 0; i < len2; i++) { int x = s2[i] - 'a'; if(nex[p][x]) { p = nex[p][x]; val++; } else { while(p && !nex[p][x]) p = fail[p]; if(!p) { p = 1; val = 0; } else { val = len[p] + 1; p = nex[p][x]; } } ans += fasum[fail[p] ] + (val - len[fail[p] ]) * epos[p]; } printf("%lld\n", ans); } int main() { getallsame(); return 0; }

 

 

bzoj2882:用后缀自动机实现最小表示法。把串复制一遍,构建后缀自动机,每次选择最小的边转移即可

因为字符集很大,所以转移边用map来存即可

 

#include<map>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define it map<int,int>::iterator
const int maxn=1200010;
using namespace std;
int n,s[maxn];
 
void read(int &x){
    char ch;
    for (ch=getchar();!isdigit(ch);ch=getchar());
    for (x=0;isdigit(ch);ch=getchar()) x=x*10+ch-'0';
}
 
struct Tsam{
    map<int,int> ch[maxn];
    int dis[maxn],fa[maxn],root,last,tot;
    int newnode(int v){dis[++tot]=v;return tot;}
    void init(){last=root=++tot;}
    void add(int x){
        int p=last,np=newnode(dis[p]+1);
        last=np;
        for (;p&&!ch[p].count(x);p=fa[p]) ch[p][x]=np;//printf("char=%d %d\n",x,p);
        if (!p) fa[np]=root;
        else{
            int q=ch[p][x];
            if (dis[q]==dis[p]+1) fa[np]=q;
            else{
                int nq=newnode(dis[p]+1);
                ch[nq]=ch[q];
                fa[nq]=fa[q];
                fa[np]=fa[q]=nq;
                for (;ch[p][x]==q;p=fa[p]) ch[p][x]=nq;//printf("qp: char=%d %d %d\n",x,p,fa[p]);
            }
        }
    }
    void build(){
        init();
        for (int i=1;i<=n;i++) add(s[i]);
        for (int i=1;i<=n;i++) add(s[i]);
    }
    void getmin(){
        for (int i=1;i<=tot;i++){
            it iter=ch[i].begin();
            //printf("id=%d char=%d minson=%d\n",i,iter->first,iter->second);
        }
        for (int i=1,p=root;i<=n;i++){
            it iter=ch[p].begin();
            printf(i==n?"%d\n":"%d ",iter->first);
            p=iter->second;
            //printf("p=%d %d\n",p,ch[p].begin()->second);
        }
    }
}T;
 
int main(){
    //freopen("test.out","w",stdout);
    scanf("%d",&n);
    for (int i=1;i<=n;i++) read(s[i]);
    T.build(),T.getmin();
    return 0;
}

 

 

一道广义后缀自动机的题。

 

BZOJ3277: 串(广义后缀自动机)

  

Description

字符串是oi界常考的问题。现在给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中
至少k个字符串的子串(注意包括本身)。

Input

第一行两个整数n,k。
接下来n行每行一个字符串。
n,k,l<=100000

Output

输出一行n个整数,第i个整数表示第i个字符串的答案。

Sample Input

3 1
abc
a
ab

Sample Output

6 1 3

HINT

Source

 

广义后缀自动机?就是把一坨字符串建到一个后缀自动机上??

不过好难理解啊qwq。。

对于这题,首先我们要知道几个定理

1.节点i表示的本质不同的字符串可以由len[i]len[fa[i]]得到

2.一个串的子串 等价于 一个串所有前缀的所有后缀

这样子串就转换为求一个串的前缀的所有后缀的问题

前缀可以枚举,问题转换为求一个字符串的各个后缀在其他字符串中出现了多少次

这样我们可以把广义后缀自动机建出来,然后暴力沿着parent边跑,这样可以枚举出所有后缀

同时为了不重复枚举,我们需要记录下每个后缀是否已经被枚举过了

 

这样我们就可以知道一个状态出现的次数是否>=K,接下来我们只要统计出这个状态出现的次数就行了

根据定理1,这个很好统计

 

然后就做完啦

 

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int MAXN = 1e6 + 10;
string s[MAXN];
int N, K;
int fa[MAXN], len[MAXN], ch[MAXN][26], root = 1, last = 1, tot = 1, times[MAXN];
void insert(int x) {
    int now = ++tot, pre = last; last = now; len[now] = len[pre] + 1;
    for(; pre && !ch[pre][x]; pre = fa[pre]) ch[pre][x] = now;
    if(!pre) fa[now] = root;
    else {
        int q = ch[pre][x];
        if(len[q] == len[pre] + 1) fa[now] = q;
        else {
            int nows = ++tot; len[nows] = len[pre] + 1;
            memcpy(ch[nows], ch[q], sizeof(ch[q]));
            fa[nows] = fa[q]; fa[q] = fa[now] = nows;
            for(; pre && ch[pre][x] == q; pre = fa[pre]) ch[pre][x] = nows;
        } 
    }
}
int vis[MAXN], sum[MAXN];
void GetTimes() {//求出每一个状态在几个字符串出现过 
    for(int i = 1; i <= N; i++) {
        int now = root;
        for(int j = 0; j < s[i].length(); j++) {
            now = ch[now][s[i][j] - 'a'];//枚举每一个前缀 
            int t = now;
            while(t && vis[t] != i) vis[t] = i, times[t]++, t = fa[t];//枚举每一个后缀 
        }
    }
}
void dfs(int x) {
    if(x == root || vis[x]) return ;
    vis[x] = 1; 
    dfs(fa[x]); 
    sum[x] += sum[fa[x]];
}
int main() {
#ifdef WIN32
    freopen("a.in", "r", stdin);
#endif
    ios::sync_with_stdio(0);
    cin >> N >> K;
    for(int i = 1; i <= N; i++) cin >> s[i];
    for(int i = 1; i <= N; i++) {
        last = 1;
        for(int j = 0; j < s[i].length(); j++)
            insert(s[i][j] - 'a');
    }
    
    GetTimes();
    
    for(int i = 1; i <= tot; i++) sum[i] = (times[i] >= K) * (len[i] - len[fa[i]]);//i状态所表示的子串集合对答案的贡献
    memset(vis, 0, sizeof(vis));
    for(int i = 1; i <= tot; i++) dfs(i);
    for(int i = 1; i <= N; i++) {
        int ans = 0, now = root;
        for(int j = 0; j < s[i].length(); j++)
            now = ch[now][s[i][j] - 'a'], ans += sum[now];
        //枚举前缀,算出每一个前缀所包含的后缀对答案啊的贡献 
        printf("%d ", ans);
    }
    
    return 0;
}

 

posted @ 2019-08-07 21:36  downrainsun  阅读(274)  评论(0编辑  收藏  举报