分析:
网上的题解少之又少,而且都是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;
}