码迷,mamicode.com
首页 > 其他好文 > 详细

后缀自动机

时间:2019-08-07 22:41:52      阅读:112      评论:0      收藏:0      [点我收藏+]

标签:通过   ext   没有   min   ret   第一个   details   增加   article   

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;
}

 

后缀自动机

标签:通过   ext   没有   min   ret   第一个   details   增加   article   

原文地址:https://www.cnblogs.com/downrainsun/p/11318009.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!