【CF963D】Frequency of String (AC自动机)

【CF963D】Frequency of String (AC自动机)

​ 前段时间重新学习了AC自动机(以前听同学讲,只写了个假模板,就再也没接触过了),练了几道题目,算是掌握了AC自动机的基础了。并且写了一个自己能用的模板。

​ 昨天刷了这道题,打算写一个题解。


【题目描述】

​ 给出一个字符串 s s s ( ∣ s ∣ ≤ 1 0 5 ) (|s|\leq 10^5) (s105),有 n n n ( n ≤ 1 0 5 ) (n\leq10^5) (n105)个询问,第 i i i个询问包含一个整数 k i k_i ki ( 1 ≤ k i ≤ ∣ s ∣ ) (1 \leq k_i\leq |s|) (1kis)和一个字符串 m i m_i mi ( ∑ m i ≤ 1 0 5 ) (\sum m_i \leq 10^5) (mi105)。对于每个询问,要求找到一个字符串 t t t,满足 t t t s s s的字串,且在字符串 t t t中,串 m i m_i mi的全匹配恰好出现了 k i k_i ki次。你只需要对每个询问输出对应字符串 t t t的最小长度就可以了。如果没有满足要求的字符串 t t t,请输出 − 1 -1 1。(保证 m i m_i mi互不相同)

​ 时限 1.5s 空限 500Mb


【AC自动机】

​ 先来拆分一下AC自动机,整理一些特性:字典树,fail值,fail树

字典树:

​ AC自动机是建立在一棵字典树上的。字典树上的每个节点都是一个状态,代表着一个前缀(即从根节点一直到达该节点的路径组成的字符串,这一定是字典树中某些单词的前缀)。

fail值:

​ 每个节点都具有一个fail值。fail值具有重要意义。我们已经知道了,每个节点代表一个前缀,也就是一个字符串。而节点的fail值是另一个节点的编号,也就是说,指向了另一个前缀,也就是另一个字符串。不妨令当前节点代表的字符串为 s s s,其fail指向节点代表的字符串为 t t t,根据AC自动机的性质,这两个字符串满足:

​ (1) t t t s s s的后缀。

​ (2) ∣ t ∣ < ∣ s ∣ |t|<|s| t<s

​ (3)在该自动机中所有满足前两个性质字符串中, ∣ t ∣ |t| t是最大的。

匹配过程:

​ 匹配时,从根节点开始,根据输入的字符串的字符顺序,在AC自动机中匹配,遇到无法匹配时,则将当前状态移动到fail值所指状态上,继续尝试匹配,重复这个过程直到匹配成功(在我的模板中,根节点是一个万配点,所以一定会匹配成功)。

fail树:

​ 由于根的fail值是空,其他每个点必有一个fail值,因此把fail值视为一条边,AC自动机上的点可以根据fail值构造出一棵树,称为fail树。fail树意义重大,如果没有考虑到它的特性,相当于是学了假的AC自动机。

​ fail树上祖先和子孙的关系:祖先节点代表的串,是子孙节点代表的串的一个真后缀,AC自动机的使用大多基于这个特性。


参考解题思路:

​ 如果我们能够知道每个串 m i m_i mi s s s中完成全匹配的结束位置,通过链式结构将这些位置储存起来,就能够通过一遍扫描求出 t t t的最短长度。

比如, m i m_i mi完成全匹配的位置为:2,4,7,8,9,10,且 k i = 3 k_i=3 ki=3

那么我们扫描过程中发现“7,8,9”这三个连续位置的长度最小,由此得到 t t t的最短长度 ∣ t ∣ m i n = 9 − 7 + ∣ m i ∣ |t|_{min}=9-7+|m_i| tmin=97+mi

​ 那么我们想到一个朴素直接的方法,用AC自动机跑出这些全匹配的位置。

​ 首先,根据所有的串 m i m_i mi建立AC自动机,然后拿串 s s s去匹配,匹配过程中会经历 ∣ s ∣ |s| s个状态。当某个状态匹配成功时,其在fail树上的所有祖先节点也将匹配成功。但注意,并不是所有祖先节点都对应于某个串 m i m_i mi,有的节点只代表某个前缀。我们可以遍历当前状态以及所有祖先状态,如果遍历到的这个状态对应于某个串 m i m_i mi,我们就跑出了这个串的一个全匹配的位置,于是记录下来。

​ 这个算法能够不遗漏的找出每个 m i m_i mi的全匹配位置,正确性有保证,但是如何保证效率呢?

​ 注意,fail树上只有部分节点是对应于某个串 m i m_i mi的,我们称这些节点是有效的,其余节点是无效的,如果我们在fail树上提前dfs一遍,就能预处理出每个点最近的一个有效祖先,从而跳过那些无效祖先,节省时间。

​ 实际上加上这个优化,就可以通过了。但是我们需要明白为什么。

​ 该算法时间复杂性 O ( n n ) \rm O(n\sqrt n) O(nn ),下面是分析:

结论: fail树上,任何一个节点的有效祖先数量最大为 O ( ∑ m i ) \rm O( \sqrt{\sum m_i}) O(mi )

证明:假设某个节点的有效祖先数量为 k k k个,那么,这些祖先节点代表着 k k k个字符串,而根据fail树的性质,每个字符串的长度都不相等,因此这些有效串的长度之和的下界为 k ( k + 1 ) 2 \frac{k(k+1)}{2} 2k(k+1)。根据题意,所有有效串的和 ∑ m i ≥ k ( k + 1 ) 2 \sum m_i\geq \frac{k(k+1)}{2} mi2k(k+1),因此 k k k的数量最大为 O ( ∑ m i ) \rm O(\sqrt {\sum m_i}) O(mi )

​ 所以说,在 ∣ s ∣ |s| s个状态中,每个状态的有效祖先数目最大为 O ( ∑ m i ) \rm O(\sqrt {\sum m_i}) O(mi ),因此,所有目标串的全匹配的位置总数目最大为 O ( ∣ s ∣ ∑ m i ) \rm O(|s|\sqrt {\sum m_i}) O(smi ),因为 ∣ s ∣ , ∑ m i |s|,\sum m_i s,mi n n n同数量级,所以全匹配位置总数目最大为 O ( n n ) \rm O(n\sqrt n) O(nn )

​ 最后扫描链式结构复杂性也是 O ( n n ) \rm O(n\sqrt n) O(nn ),总复杂性 O ( n n ) \rm O(n\sqrt n) O(nn )

​ 这道题还有一个很坑的地方,因为 n ≤ 1 0 5 n\leq 10^5 n105,所以 n n n\sqrt n nn 可达 3 × 1 0 7 3\times 10^7 3×107,如果你使用链表来扫描,可能会被卡常数。

​ 为什么呢?遍历链式结构有什么慢的呢?手写链表还会被卡常吗?然而亲身试法,的确TLE了。

​ 链表结构的遍历常数来自于寻址,在遍历的过程中,以不确定的顺序去访问大量的地址,这是非常缓慢的。我们需要使用vector。vector中遍历的地址是连续的,在访问同样数量的地址的情况下,会比链表快很多。

​ 但是vector插入慢啊!(是的没错,但是能过这道题……可能和数据特征有关?)

​ 看来这道题的瓶颈应该在于最后的遍历。

​ 最后这个点很有意思,但是我了解的并不多,还有待深入挖掘。


【参考代码】
#define George_Plover
#include <list>
#include <queue>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <iomanip>
#include <algorithm>
#define MAXM 100101
#define MAXN 100101
#define MAXL (100101*430)
#define LL long long
#define RG register
using namespace std;
vector<int>vec[MAXN];
struct ACM{
    
    
    const static int None = 0;
    int fail[MAXM];
    int siz,root;
    queue<int>q;
    struct node{
    
    
        int son[26];
        int cnt;
        void init(){
    
    
            cnt=0;memset(son,0,sizeof(son));
        }
    }tr[MAXM];
    struct Fail_Tree{
    
    
        int tot,pre[MAXM],to[MAXM],lin[MAXM],top[MAXM];
        bool vis[MAXM];
        void init()
        {
    
    
            tot=0;
            memset(pre,0,sizeof(pre));
            memset(vis,0,sizeof(vis));
        }
        void add(int x,int y)
        {
    
    
            tot++;lin[tot]=pre[x];pre[x]=tot;to[tot]=y;
        }
        void dfs(int x,int fa)
        {
    
    
            if(vis[fa])
                top[x]=fa;
            else
                top[x]=top[fa];
            for(int i=pre[x];i;i=lin[i])
            {
    
    
                int v=to[i];
                dfs(v,x);
            }
        }
    }tree;
    void init()
    {
    
    
        for(int i=1;i<=siz;i++)
            tr[i].init();
        tree.init();
        siz=1;root=1;fail[root]=None;tree.top[None]=None;
        for(int i=0;i<26;i++)
            tr[None].son[i]=root;
    }
    ACM(){
    
    init();}
    void add(char *s)
    {
    
    
        static int CNT=0;
        int tmp=root;
        for(int i=0;s[i];i++)
        {
    
    
            if(!tr[tmp].son[s[i]-'a'])
                tr[tmp].son[s[i]-'a']=++siz;
            tmp=tr[tmp].son[s[i]-'a'];
        }
        tr[tmp].cnt=++CNT;
        tree.vis[tmp]=1;
    }
    void get_fail()
    {
    
    
        q.push(root);
        while(!q.empty())
        {
    
    
            int u=q.front();q.pop();
            for(int i=0;i<26;i++)
            {
    
    
                int v=tr[u].son[i];
                if(!v)
                    continue;
                int tmp=fail[u];
                while(tmp!=None&&!tr[tmp].son[i])
                    tmp=fail[tmp];
                fail[v]=tr[tmp].son[i];
                tree.add(tr[tmp].son[i], v);
                q.push(v);
            }
        }
        tree.dfs(root,None);
    }
    void work(char *s)
    {
    
    
        int tmp=root;
        for(int i=0;s[i];i++)
        {
    
    
            while(tmp!=None&&!tr[tmp].son[s[i]-'a'])
                tmp=fail[tmp];
            tmp=tr[tmp].son[s[i]-'a'];
            int p=tmp;
            
            while(p!=None)
            {
    
    
                if(tree.vis[p]){
    
    
                    vec[tr[p].cnt].push_back(i);
                }
                p=tree.top[p];
            }
             
        }
    }
}acm;
int n,a[MAXN],len[MAXN];
char T[MAXM],S[MAXM];

int main()
{
    
    
    scanf("%s",T);
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
    
    
        scanf("%d%s",&a[i],S);
        acm.add(S);
        len[i]=(int)strlen(S);
    }
    acm.get_fail();
    acm.work(T);
    for(int i=1;i<=n;i++)
    {
    
    
        int ans=MAXL;
        if(vec[i].size()<a[i])
            printf("-1\n");
        else
        {
    
    
            for(int j=0;j+a[i]-1<vec[i].size();j++)
                ans=min(ans,vec[i][j+a[i]-1]-vec[i][j]+len[i]);
            printf("%d\n",ans);
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/George_Plover/article/details/107141631
今日推荐