1. 程式人生 > >ACM-ICPC 2018 焦作賽區網路預賽 H. String and Times(字尾自動機)

ACM-ICPC 2018 焦作賽區網路預賽 H. String and Times(字尾自動機)

題目連結

題意:

求原串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;

因為對於每一個狀態點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();
    }
}