ACM-ICPC 2018 焦作赛区网络预赛 H. String and Times(后缀自动机)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_37025443/article/details/82937816

题目链接

题意:

求原串S中出现次数在[A,B]之间的子串的个数

解析:

后缀自动机的经典应用。

我是通过结合了求不同子串个数的d[]和不同子串出现次数的sz[]来做的。

在拓扑序遍历后缀自动机的时候,我们求不同子串个数是默认d[]初始为1 的,因为默认一个状态点本身也是一条可行路径

但是这里我们是有限定次数的,所以对于一个状态点v表示的子串出现次数sz[v]不在[A,B]时,那么他本身就不是一条可行路径

所以不需要加1,然后这样递推上去就行了(注意这里d[i]表示的是从状态i出发,不同的子串的数目,即不同的路径数)

其实还有一种更简单的做法,直接将len相减来求,res+=sam.e[v].len-sam.e[sam.e[v].f].len;

扫描二维码关注公众号,回复: 4129258 查看本文章

因为对于每一个状态点v表示的字符串是唯一的,不可能在其他的状态点出现,所以不会重复,那么一个状态点

表示的字符串是在[A,B]里面的话,我们直接把这些字符串加到答案里面就可以了,因为这些字符串不可能在其他地方出现了。

其实上面那个解法也用到了这个性质,不将不满足的状态点的d[]+1,就是因为他不可能在其他地方出现了,所以一定是不行的。

#include <cstdlib>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef unsigned int uint;
const int N = 1e5+10;

struct Node {
    int f, len, ch[26];
    void init() {
        len = 0, f = -1;
        memset(ch, 0xff, sizeof (ch));
    }
};

ll sz[N<<1];  //sz[i]表示状态i的right(i)的大小,即状态i表示的字符串出现的次数

struct SAM {
    Node e[N<<1];
    int idx, last;
    void init() {
        idx = 0;
        last = newnd();
    }
    int newnd() {
        e[idx].init();
        return idx++;
    }
    void add(int c) {
        int end = newnd();
        int p = last;
        e[end].len = e[p].len + 1;
        for (; p != -1 && e[p].ch[c] == -1; p = e[p].f) {
            e[p].ch[c] = end;
        }
        if (p == -1) e[end].f = 0;
        else {
            int nxt = e[p].ch[c];
            if (e[p].len + 1 == e[nxt].len) e[end].f = nxt;
            else {
                int nd = newnd();
                e[nd] = e[nxt];
                e[nd].len = e[p].len + 1;
                e[nxt].f = e[end].f = nd;
                for (; p != -1 && e[p].ch[c] == nxt; p = e[p].f) {
                    e[p].ch[c] = nd;
                }
            }
        }
        sz[end]=1;
        last = end;
    }
};
SAM sam;
char str[N];
int ws[N];
int tp[N<<1];
ll d[N<<1]; //d[i]表示从状态i出发,不同的子串的数目,即不同的路径数


int main()
{

    int A,B;
    while(scanf("%s%d%d",str,&A,&B)!=EOF)
    {
        sam.init();
        int len=strlen(str);
        for(int i=0;str[i];i++) sam.add(str[i]-'A');  //插入
        //对sam的节点按照len,从小到大排序重新标号,即给定节点的拓扑序
        for(int i=0;i<=len;i++) ws[i]=0;
        for(int i=1;i<sam.idx;i++) ws[sam.e[i].len]++;
        for(int i=1;i<=len;i++) ws[i]+=ws[i-1];
        for(int i=sam.idx-1;i>0;i--) tp[ws[sam.e[i].len]--]=i;

        //按照拓扑序进行遍历
        //ll res=0;
        for(int i=sam.idx-1;i;i--)
        {
            int v=tp[i];
            sz[sam.e[v].f]+=sz[v];
            if(sz[v]>=A&&sz[v]<=B) d[v]++;
            //if(sz[v]>=A&&sz[v]<=B) res+=sam.e[v].len-sam.e[sam.e[v].f].len;
            //我看其他也有直接将len相减来求的,这个方法书是可以A的,直接输出res就可以了
            for(int j=0;j<26;j++)
                if(sam.e[v].ch[j]!=-1) d[v]+=d[sam.e[v].ch[j]];
        }
        ll ans=0;
        for(int i=0;i<26;i++)
        {
            if(sam.e[0].ch[i]!=-1) ans+=d[sam.e[0].ch[i]];
        }
        for(int i=0;i<sam.idx;i++) sz[i]=0,d[i]=0;

        printf("%lld\n",ans);


        getchar();
    }
}

猜你喜欢

转载自blog.csdn.net/qq_37025443/article/details/82937816