zoukankan      html  css  js  c++  java
  • 后缀自动机

    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 ", 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 ", 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 ",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 "); } ll res = 0; for(int i = 1; i <= sz; i++) res = max(ans[i], res); printf("%lld ", 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 ", 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 ", 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 ", 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 ", 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
    ",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
    ",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
    ",i,iter->first,iter->second);
            }
            for (int i=1,p=root;i<=n;i++){
                it iter=ch[p].begin();
                printf(i==n?"%d
    ":"%d ",iter->first);
                p=iter->second;
                //printf("p=%d %d
    ",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;
    }
  • 相关阅读:
    centos7安装kubenetes
    用户密码字典
    curl使用
    docker部署rabbitmq集群
    记一次使用docker搭建fastdfs服务的过程
    filebeat删除多余标签
    Python format格式化输出
    python3 统计NGINX pv uv 最多IP访问
    linux修改网卡名为eth0
    模式查找
  • 原文地址:https://www.cnblogs.com/downrainsun/p/11318009.html
Copyright © 2011-2022 走看看