【AC自动机-fail树+离线+DFS序+树状数组】BZOJ2434(Noi2011)[阿狸的打字机]题解

题目概述

有一台打字机,可以:

  1. 在字符串末尾插入一个小写字母。
  2. 删除字符串末尾的一个小写字母。
  3. 输出当前字符串。

还有 m 个询问,每个询问 x,y 表示求第 x 个输出的字符串在第 y 个输出的字符串中的出现次数。

解题报告

观察打字的过程,我们发现这其实就是在构造一棵Trie:

  1. 在字符串末尾插入一个小写字母 在当前节点 now 扩展一个字符。
  2. 删除字符串末尾的一个小写字母 now 回退到父亲节点。

先按照打字顺序处理出Trie,然后构造好AC自动机和 fail 树,那么一个询问 x,y 就是查找字符串 x 对应的节点沿着 fail 树向下走能走到多少字符串 y 经过的节点,即查找 x 子树中有多少 y 经过的节点。

所以我们可以用DFS序+树状数组来快速统计 x 的子树,但问题是 y 经过的节点是哪些,如果直接从 fail 树上跳,复杂度肯定不对。由于这道题的构造方法比较特殊,我们会发现,如果想要构造到 y y 经过的节点在之前就肯定出现过了!

既然如此我们考虑离线,先把所有与 y 相关的 x 存下来,按照题目给出的打字顺序再处理一遍:

  1. 在字符串末尾插入一个小写字母 在当前节点 now 扩展一个字符,将新的 now 节点标记为 1
  2. 删除字符串末尾的一个小写字母 now 节点标记为 0 ,并回退到父亲节点。
  3. 输出当前字符串 处理 now 节点对应字符串 y 的所有 x ,答案为 x <script type="math/tex" id="MathJax-Element-34">x</script> 子树的权值和。

示例程序

代码有点乱……读者老爷凑合看看吧QAQ。

#include<cstdio>
using namespace std;
const int maxn=100000,maxm=100000,maxi=26;

int n,te,ans[maxm+5];
char s[maxn+5];

int E,lnk[maxn+5],qlnk[maxn+5],nxt[maxn+maxm+5],chd[maxn+maxm+5],w[maxn+maxm+5];
void Add(int x,int y) {chd[++E]=y;nxt[E]=lnk[x];lnk[x]=E;}
void Add(int x,int y,int z) {chd[++E]=y;w[E]=z;nxt[E]=qlnk[x];qlnk[x]=E;}
int si=1,son[maxn+5][maxi],top,stk[maxn+5],ID[maxn+5];
int que[maxn+5],fai[maxn+5];
void make_AC()
{
    top=0;
    for (int i=1;s[i];i++)
        if (s[i]=='B') top--; else
        if (s[i]=='P') ID[stk[top]]=++n; else
        {
            int &u=son[stk[top]][s[i]-'a'];if (!u) u=si++;
            stk[++top]=u;
        }
    int Head=0,Tail=0;
    for (int i=0;i<maxi;i++) if (son[0][i])
        que[++Tail]=son[0][i],fai[son[0][i]]=0;
    while (Head!=Tail)
    {
        int x=que[++Head];
        for (int i=0;i<maxi;i++)
        {
            if (!son[x][i]) {son[x][i]=son[fai[x]][i];continue;}
            int u=son[x][i];que[++Tail]=u;fai[u]=son[fai[x]][i];
        }
    }
    for (int i=1;i<si;i++) Add(fai[i],i);
}
int ti,Lt[maxn+5],Rt[maxn+5],L[maxn+5],R[maxn+5];
void Dfs(int x)
{
    Lt[x]=++ti;if (ID[x]) L[ID[x]]=ti;
    for (int j=lnk[x];j;j=nxt[j]) Dfs(chd[j]);
    Rt[x]=ti;if (ID[x]) R[ID[x]]=ti;
}
int c[maxn+5];
void Update(int x,int tem) {for (int p=x;p<=ti;p+=p&-p) c[p]+=tem;}
int Sum(int x) {int sum=0;for (int p=x;p;p-=p&-p) sum+=c[p];return sum;}
void Solve()
{
    top=0;
    for (int i=1;s[i];i++)
        if (s[i]=='B') Update(Lt[stk[top--]],-1); else
        if (s[i]=='P')
        {
            for (int j=qlnk[ID[stk[top]]];j;j=nxt[j])
                ans[w[j]]=Sum(R[chd[j]])-Sum(L[chd[j]]-1);
        } else
        {
            top++;stk[top]=son[stk[top-1]][s[i]-'a'];
            Update(Lt[stk[top]],1);
        }
}
int main()
{
    freopen("program.in","r",stdin);
    freopen("program.out","w",stdout);
    scanf("%s%d",s+1,&te);make_AC();
    for (int i=1;i<=te;i++)
    {
        int x,y;scanf("%d%d",&x,&y);
        Add(y,x,i);
    }
    Dfs(0);Solve();
    for (int i=1;i<=te;i++) printf("%d\n",ans[i]);
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值