Codeforces 666E Forensic Examination

Codeforces 666E Forensic Examination

题目大意:有一串原字符 S S m 个匹配字符串,问 S[pl,pr] S [ p l , p r ] 这段字符在,编号在 [l,r] [ l , r ] 之间的字符串中 出现次数最多的是哪一个,出现了多少次(最大次数相同则输出编号较小的)

不完整理解+解法(后缀自动机+线段树合并)

理解在代码中

对于一大串字符的匹配,我们选择把 S S m 个字符串连起来(特殊符号连接)
然后我们就能发现,匹配上的字符串会在 right r i g h t 集合中出现
于是我们把 parent p a r e n t 树抽出来,因为 parent p a r e n t 对应的即是 right r i g h t 集合的树
然后把子串在 parent p a r e n t 树上的点都给标记对应的编号,用可持久化线段树记录

具体说一下可持久化线段树的结构:
    以字符的位置(构建后缀自动机时是以一个一个字符为点讨论的)为更新的标准
    然后以所属字符串编号为叶子节点的坐标建立线段树

然后用 parent p a r e n t 树的对应关系把它给合并起来(目的是方便统计)具体看代码解释

代码+注释(代码非本人写的,因为我还没完全理解)

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,k) for(i=j;i<=k;i++)
using namespace std;
const int M=24000005;
const int mxn=1200005;
char s[mxn];
int a,b,c,d,num,size,ans;
int cnt,m,T,p,q,np,nq,Q,tot,len;
int ls[M],rs[M],mx[M],id[M],B[mxn],C[mxn];
int color[mxn],pos[mxn],step[mxn],pre[mxn][24],son[mxn][28],root[mxn];

inline void update(int x)//更新操作
{
    if(mx[ls[x]]==mx[x] && id[ls[x]]<id[x]) id[x]=id[ls[x]];
    if(mx[ls[x]]>mx[x]) mx[x]=mx[ls[x]],id[x]=id[ls[x]];
    if(mx[rs[x]]>mx[x]) mx[x]=mx[rs[x]],id[x]=id[rs[x]];
}

inline void insert(int &x,int l,int r,int v)//线段树插入操作
{
    if(!x) x=(++size);
    if(l==r) {mx[x]++,id[x]=l;return;}
    int mid=l+r>>1;
    if(v<=mid) insert(ls[x],l,mid,v);
    else insert(rs[x],mid+1,r,v);
    update(x);
}//这里并不是主席树的操作(没有继承上一个节点),是为了合并做准备(合并的节点并不是连续的)

inline void find(int x,int l,int r,int L,int R)//查询操作
{
    if(!x) return;
    if(L<=l && r<=R)
    {
        if(ans<mx[x]) ans=mx[x],num=id[x];
        else if(ans==mx[x] && num>id[x]) num=id[x];
        return;
    }
    int mid=l+r>>1;
    if(R<=mid) find(ls[x],l,mid,L,R);
    else if(L>mid) find(rs[x],mid+1,r,L,R);
    else find(ls[x],l,mid,L,mid),find(rs[x],mid+1,r,mid+1,R);
}

inline int merge(int x,int y,int l,int r)//合并
{
    if(!x||!y) return x|y;//如果有一个节点不存在,就直接返回另一个几点
    int z=(++size),mid=l+r>>1;//新建节点
    if(l==r)
    {
        mx[z]=mx[x]+mx[y];//因为两个节点的对应字符串标号相同,所以直接将计数相加
        id[z]=l;
        return z;
    }
    //不一样则分别合并取最大
    ls[z]=merge(ls[x],ls[y],l,mid);
    rs[z]=merge(rs[x],rs[y],mid+1,r);
    update(z);
    return z;
}

inline void sam(int w)//建立后缀自动机,w为字符串标号,原字符串标号为0
{
    int i,j,c;
    fo(i,1,len+1)
    {
        p=np;
        if(i==len+1) c=27;
        else c=s[i]-'a'+1;
        step[np=(++tot)]=step[p]+1;
        if(!w && i<=len) pos[i]=np;//原字符串不加入线段树
        if(w && i<=len)  insert(root[np],1,cnt,w);//m个字符串加入线段树
        while(p && !son[p][c])
          son[p][c]=np,p=pre[p][0];//后面要在parent树上倍增,因此直接用pre[p][0]表示距离为1的父亲
        if(!p) {pre[np][0]=1;continue;}
        q=son[p][c];
        if(step[q]==step[p]+1)
          pre[np][0]=q;
        else
        {
            step[nq=(++tot)]=step[p]+1;
            memcpy(son[nq],son[q],sizeof son[q]);
            pre[nq][0]=pre[q][0];
            pre[q][0]=pre[np][0]=nq;
            while(p && son[p][c]==q)
              son[p][c]=nq,p=pre[p][0];
        }
    }
}

inline void init()
{
    int i,j;
    fo(i,1,tot) B[step[i]]++;
    fo(i,1,tot) B[i]+=B[i-1];
    fo(i,1,tot) C[B[step[i]]--]=i;
    for(i=tot;i>=1;i--)
    {
        int x=C[i],fa=pre[x][0];//在parent树上找合并的节点
        root[fa]=merge(root[fa],root[x],1,cnt);//合并节点->将当前的值累加到parent上
    }
    fo(j,1,22) fo(i,1,tot) pre[i][j]=pre[pre[i][j-1]][j-1];//parent树上倍增
}

int main()
{
    int i,j;
    scanf("%s",s+1);
    len=strlen(s+1);
    tot=np=1,sam(0);
    scanf("%d",&cnt);
    fo(i,1,cnt)
    {
        scanf("%s",s+1);
        len=strlen(s+1);
        sam(i);
    }
    init();
    scanf("%d",&Q);
    while(Q--)
    {
        scanf("%d%d%d%d",&c,&d,&a,&b);
        len=b-a+1,b=pos[b];
        for(j=22;j>=0;j--)
          if(step[pre[b][j]]>=len)
            b=pre[b][j];//通过倍增找到原字符串中的区间起点
        ans=0,num=0,find(root[b],1,cnt,c,d);//寻找当前节点的答案
        if(!ans) printf("%d %d\n",c,0);
        else printf("%d %d\n",num,ans);
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值