后缀自动机

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;

 

以上是关于后缀自动机的主要内容,如果未能解决你的问题,请参考以下文章

使用后缀自动机求后缀数组

bzoj 2251(后缀数组/后缀自动机)

Luogu3804模板后缀自动机(后缀自动机)

POJ2774 后缀自动机&后缀数组

广义后缀自动机模板

后缀自动机