bzoj4166 月宫的符卡序列(manacher+链状双hash)

这里写图片描述

分析:
网上的题解少之又少,而且都是dalao码风,所以只能自力更生了

仔细分析一下,这道题就是求解本质不同的回文串以及出现位置
由于字符串比较长,我们需要用manacher算法

在manacher算法中,只有发生了扩展操作的时候才有可能产生新的(本质不同)回文串
但是RL数组只记录了最长回文串,然而其中会包含很多小的回文串

example
char : a b c d c b a   //RL标记出来的回文串
hw   : a b c d c b a
         b c d c b
           c d c
             d
      //所有回文串的中间位置都一样

难道我们需要把所有的回文串都抽出来处理贡献吗?
注意到所有回文串的中间位置都一样
我们考虑能不能把这条性质用上:
我们建立一棵回文树(瞎起的名。。。),长这样:
这里写图片描述
我们可以发现,对于那些互相包含的回文串,在树上是父子关系(短的是父亲)

从根到叶像是一种扩展关系,这恰好符合manacher扩展的过程

而在统计贡献的时候,儿子有的贡献父亲一定有(较长的出现了,较短的一定会包含在其中)
这样我们就可以从下到上统计贡献(复杂度O(n)

现在的问题就是怎么构建这样的一个树形结构:
在manacher的过程中,我们最开始会得到一个初始的RL值
这个RL值代表的字符串就是父结点
我们尝试扩展,如果扩展成功,就有可能在父结点下接一个叶子节点
为什么说是可能呢?因为有可能该结点下已经有了此子结点
这个时候我们就需要判断字符串是否本质不同

对于本质不同的字符串,我们可以采用一种相对简单的方法判断:hash
然而为了避免错误,我们可以采用较保险的双hash
一般hash都是用map完成(比较简单)
但是这道题用map会T掉
所以我们就只能采用链状hash


感觉分析过程就异常的恶心
涉及到了两种算法:manacher+hash(还是链状双hash)
接下来看一下程序实现

主体当然是manacher,但是在其中加了许多小操作
(总体上比较好理解,和分析中讲到的过程一样)

for (int i=1;i<=len;i++)
{
    int j=pos*2-i;
    if (i<mx)
        RL[i]=min(RL[j],mx-i);
    else RL[i]=1;

    if (RL[i]==1) last=0;
    else last=H.find(((i-RL[i])>>1)+1,(i+RL[i]-1)>>1);
    //找到父结点 
    while (s[i+RL[i]]==s[i-RL[i]]) 
    {
        RL[i]++;
        ll num=H.hash(((i-RL[i])>>1)+1,(i+RL[i]-1)>>1);
        //当前状态的hash值 
        if (H.findnum(num)==-1)     //如果是一个新的状态 
        {
            H.insert(num,++tt);     //添加新节点,编号是tt 
            f[tt]=0; fa[tt]=last;   //记录父结点 
            last=tt;                //向下走 
        }
        else last=H.findnum(num);   //找到该hash值在树上对应的结点编号 
    }

    if (last) f[last]^=(i>>1)-1;    //在叶子结点上记录贡献 

    if (i+RL[i]>mx){
        mx=i+RL[i];
        pos=i;
    }
}

我把所有的hash操作定义到了一个结构体中

首先我们先看一下HASH结构体中用到的数组的意义

const int x1=233,x2=2333;                //hash需要用到的两个标志值
const int mod1=19260817,mod2=998244353;  //双hash分别的模数,一个串有两个hash值
int st[mod1+10],val[N<<2],nxt[N<<2],tot;
//第一hash值相同的字符串都被存在了一条链上
//st[i] 第一个hash值i在链表中的起点 注意一下数组大小:第一个hash值的范围是不会超过mod1的
//val[i] 每个hash值对应的结点编号 
//nxt 链表中的下一条边
//tot 链表的大小

ll h1[N],h2[N],p1[N],p2[N],y[N<<2];
//h1 将原串按照x1和mod1进行hash,处理后对于所有子串可以O(1)得到hash值
//h2 将原串按照x2和mod2进行hash
//p1 x1^i,为得到子串的hash值所做的预处理
//p2 x2^i
//y 结点对应的第二个hash值

接下来我们看一下每一个函数的作用

初始化
void clear()
{
    memset(nxt,0,sizeof(nxt));
    memset(st,0,sizeof(st));
    tot=0;
}

void init()  //序列的hash值 
{
    p1[0]=p2[0]=1;
    h1[0]=h2[0]=0;
    for (int i=1;i<=len;i++)             //字符串从1开始
    {
        h1[i]=(h1[i-1]*x1+ch[i])%mod1;   //这里的预处理也可以从len到1
        h2[i]=(h2[i-1]*x2+ch[i])%mod2;
        p1[i]=p1[i-1]*x1%mod1;
        p2[i]=p2[i-1]*x2%mod2;
    }
}
得到一个子串的hash值

hash值是有公式的:
Hash[l,r]=h[r]-h[l-1]*x^(r-l+1)

ll hash(int s,int t)
{
    ll a=(h1[t]+mod1-h1[s-1]*p1[t-s+1]%mod1)%mod1;
    ll b=(h2[t]+mod2-h2[s-1]*p2[t-s+1]%mod2)%mod2;
    return (a<<32)|b;       //两个hash值连在一起 
    //因为我们得到的是两个hash值,但是传回的只能是一个值
    //由于两个模数都没有超过int,所以我们可以直接每2^32存一个数
}
插入新结点
void insert(ll num,int t)  //num数hash值  t结点编号
{
    ll num1=num>>32;       //我们要通过一个ll范围的总hash值还原出原来的两个hash值
    ll num2=num-(num1<<32); 
    y[++tot]=num2;         //因为我们以第一个hash值链表的标志,第二个hash值就存在了y中
    nxt[tot]=st[num1];     //tot是链表的大小 nxt指向下一个结点在链表中的位置
    val[tot]=t;            //在树中的结点编号
    st[num1]=tot;
}
查找hash值在树中对应的编号
int findnum(ll num)
{
    ll num1=num>>32;        //第一个hash值 
    ll num2=num-(num1<<32); //第二个hash值 
    for (int i=st[num1];i;i=nxt[i])
        if (y[i]==num2)
            return val[i];
    return -1;              //不存在这个hash值
}
查找hash值以及在树中的编号
int find(int s,int t)
{
    ll num=hash(s,t);
    return findnum(num);
}

在最后统计答案的时候,按道理说我们应该遍历一下这棵树
但是因为我们在添加结点的时候是严格按照从根到叶的顺序进行的
所以我们在统计答案的时候只要逆序循环一遍,每次维护父结点的答案即可

int ans=0;
    for (int i=tt;i>=1;i--)    //tt个结点(本质不同的回文串) 
    {
        ans=max(ans,f[i]);
        f[fa[i]]^=f[i];
    }
    printf("%d\n",ans);

tip

一开始读错题。。。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define ll long long

using namespace std;

const int N=1000010;
const int x1=233,x2=2333;
const int mod1=19260817,mod2=998244353;
char ch[N],s[N<<1];
int RL[N<<1],n,f[N],fa[N],cnt;
struct node{
    int x,y,nxt;
};
node way[N<<1];
int len;

struct HASH{
    int st[mod1+10],val[N<<2],nxt[N<<2],tot;
    ll h1[N],h2[N],p1[N],p2[N],y[N<<2];
    //p:x^i   h:hash值 

    void clear()
    {
        memset(nxt,0,sizeof(nxt));
        memset(st,0,sizeof(st));
        tot=0;
    }

    void init()  //序列的hash值 
    {
        p1[0]=p2[0]=1;
        h1[0]=h2[0]=0;
        for (int i=1;i<=len;i++)
        {
            h1[i]=(h1[i-1]*x1+ch[i])%mod1;
            h2[i]=(h2[i-1]*x2+ch[i])%mod2;
            p1[i]=p1[i-1]*x1%mod1;
            p2[i]=p2[i-1]*x2%mod2;
        }
    }

    ll hash(int s,int t)
    {
        ll a=(h1[t]+mod1-h1[s-1]*p1[t-s+1]%mod1)%mod1;
        ll b=(h2[t]+mod2-h2[s-1]*p2[t-s+1]%mod2)%mod2;
        return (a<<32)|b;       //两个hash值连在一起 
    }

    int findnum(ll num)
    {
        ll num1=num>>32;        //第一个hash值 
        ll num2=num-(num1<<32); //第二个hash值 
        for (int i=st[num1];i;i=nxt[i])
            if (y[i]==num2)
                return val[i];
        return -1;
    }

    int find(int s,int t)
    {
        ll num=hash(s,t);
        return findnum(num);
    }

    void insert(ll num,int t)
    {
        ll num1=num>>32;
        ll num2=num-(num1<<32); 
        y[++tot]=num2;          //tot是不同的hash数 
        nxt[tot]=st[num1];
        val[tot]=t;
        st[num1]=tot;
    }
};
HASH H;

int init()
{
    s[0]='@';
    for (int i=1;i<=len*2;i+=2)
    {
        s[i]='#';
        s[i+1]=ch[(i+1)/2];
    }
    s[len*2+1]='#';
    s[len*2+2]='$';
    return len*2+1;
}

void Manacher()
{
    int len=init();
    int mx=0,pos=0,tt=0;
    ll last;
    H.clear();  H.init();
    for (int i=1;i<=len;i++)
    {
        int j=pos*2-i;
        if (i<mx)
           RL[i]=min(RL[j],mx-i);
        else RL[i]=1;

        if (RL[i]==1) last=0;
        else last=H.find(((i-RL[i])>>1)+1,(i+RL[i]-1)>>1);
        //找到父结点 
        while (s[i+RL[i]]==s[i-RL[i]]) 
        {
            RL[i]++;
            ll num=H.hash(((i-RL[i])>>1)+1,(i+RL[i]-1)>>1);
            //当前状态的hash值 
            if (H.findnum(num)==-1)     //如果是一个新的状态 
            {
                H.insert(num,++tt);     //添加新节点,编号是tt 
                f[tt]=0; fa[tt]=last;   //记录父结点 
                last=tt;                //向下走 
            }
            else last=H.findnum(num);   //找到该hash值在树上对应的结点编号 
        }

        if (last) f[last]^=(i>>1)-1;    //在叶子结点上记录贡献 

        if (i+RL[i]>mx){
            mx=i+RL[i];
            pos=i;
        }
    }

    int ans=0;
    for (int i=tt;i>=1;i--)    //tt个本质不同的回文串 
    {
        ans=max(ans,f[i]);
        f[fa[i]]^=f[i];
    }
    printf("%d\n",ans);
}

int main()
{
    scanf("%d",&n);
    while (n--)
    {
        memset(s,0,sizeof(s)); 
        memset(ch,0,sizeof(ch));
        scanf("%s",ch+1);
        len=strlen(ch+1);
        Manacher();
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值