题目
给一个只由数字构成的字符串s(|s|<=2e6)
求s的所有本质不同回文串之和%1e9+7,
每一个回文串的贡献为其十进制下的值
思路来源
字符串hash_短hash-CSDN博客(Hash相关)
计蒜客 2018南京网络赛 I Skr(马拉车+哈希)_i. skr-CSDN博客
ACM-ICPC 2018 南京赛区网络预赛 I. Skr (马拉车+字符串hash/回文自动机)_acm-icpc 2018 南京赛区网络预赛 skr-CSDN博客
Skr(马拉车+哈希)_a number is skr, if and only if it's unchanged aft-CSDN博客
题解
考虑到每个回文串都会在Manacher的过程中被遍历到,
那么我们每得到一个回文串,就判断该回文串是否已经被加入Hash里了即可
已经加入了就不计入其贡献,否则将其加入Hash并统计其贡献
P(这里取2e6+7)进制下mod2^64与10进制下mod(1e9+7)的搭配,冲突性很小,能卡过
子串Hash值小技巧:利用前缀和,[1,r]与[1,l-1]左对齐,求[l,r]的Hash值
子串Sum值小技巧同上,都是之前没有好好留意的写法
然后Hash用的就是建图那里的链式前向星类似的写法,开三个数组,开散列链表Hash
后续:
学了回文树(又名回文自动机),发现这玩意比后缀自动机简单多了……
所以就是第一次插入本质不同回文串,也就是新创建节点的时候,加上那个数的贡献即可……
心得
魔改Manacher,之前一直写Manacher的板子题
魔改Hash,之前一直只总结Hash的板子
Hash还是比较正常,利用了一个前缀和对其作差求子串的Hash值
剩下的就是p进制下的开散列,与链式前向星一致
Manacher相关下标还是比较重要的啊,好好debug
代码1(Manacher+Hash)
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
const int maxn=2e6+10;
const int p=2e6+7;//p进制下 Hash链式前向星 开散列
const int mod=1e9+7;
char w[maxn],t[maxn<<1];
int R[maxn<<1],tot;
//R[i]: 以i为中心的包含i和井号的最长回文半径 减1为回文串长
int head[maxn<<1],Next[maxn<<1],cnt;//链式
ull link[maxn<<1];//该点是否存在映射值
ull base[maxn<<1],Hash[maxn<<1];//p进制下 mod(2^64) Hash值的前缀和
ll base10[maxn<<1],sum[maxn<<1];//10进制下 mod(1e9+7) 数字值的前缀和
ll res;
bool add(ull x)
{
ull u=x%p;//P为hash的模数
for(int i=head[u];i;i=Next[i])
if(link[i]==x)return 1;
Next[++cnt]=head[u];
head[u]=cnt;
link[cnt]=x;
return 0;
}
ll solve(int l,int r)
{
ull x=Hash[r]-Hash[l-1]*base[r-l+1];//[l,r]的Hash值 利用前缀和 头对齐之后作差得Hash值
if(add(x))return 0;
ll res=(sum[r]-sum[l-1]*base10[(r-l+2)/2]%mod+mod)%mod;//答案只在mod(1e9+7)下进行
//(r-l+1)/2是串长的一半向下取整 [(r-l+1)+1]/2是串长的一半向上取整
//对于一个1#2#1的串,其数字的数量应为串长一半向上取整
return res;
}
int init1(char w[],char t[])//原串:w 新串:t
{
tot=0;
memset(t,0,sizeof t);
memset(R,0,sizeof R);
int len=strlen(w);
t[0]='@';
for(int i=0;i<len;++i)
{
t[++tot]='#';
t[++tot]=w[i];
}
t[++tot]='#';
t[++tot]='*';
return 2*len+1;
//新串长度 为#3#2#3#的长度 不包括@和*
//新串下标为 [1,2*len+1]
}
void init2(char t[],int len)
{
cnt=0;
memset(head,0,sizeof head);
memset(Next,0,sizeof Next);
memset(link,0,sizeof link);
base[0]=base10[0]=1;
sum[0]=Hash[0]=0;
for(int i=1;i<=len;++i)
{
base[i]=base[i-1]*p;//p进制下 mod(2^64)
base10[i]=base10[i-1]*10%mod;//10进制下 mod(1e9+7)
Hash[i]=Hash[i-1]*p+t[i];
if(t[i]>='0'&&t[i]<='9')sum[i]=(sum[i-1]*10+t[i]-'0')%mod;
else sum[i]=sum[i-1];
}
}
void manacher(char t[],int len)
{
int r=0,pos=0;
//int ans=0;
res=0;
for(int i=1;i<=len;++i)
{
if(t[i]!='#')(res+=solve(i,i))%=mod;//[i,i]的贡献
if(r>i)R[i]=min(r-i,R[2*pos-i]);
else R[i]=1;
while(t[i-R[i]]==t[i+R[i]])
{
if(t[i+R[i]]!='#')(res+=solve(i-R[i],i+R[i]))%=mod;//[i-R[i],i+R[i]]的贡献
R[i]++;
}
if(R[i]+i>r)
{
pos=i;//最右位置对应的回文中心
r=R[i]+i;//最右位置
}
//ans=max(ans,R[i]);
}
//return ans-1;
}
int main()
{
while(~scanf("%s",w))
{
int a=init1(w,t);
init2(t,a);
manacher(t,a);
printf("%lld\n",res);
}
return 0;
}
代码2(回文树)
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 2e6+5 ;
const int N = 10 ;
const int mod=1e9+7;
char s[MAXN];
int len;
ll ten[MAXN],ans;//ten[i]表示1后面i个0
struct Palindromic_Tree {
int next[MAXN][N] ;//next指针,next指针和字典树类似,指向的串为当前串两端加上同一个字符构成
int fail[MAXN] ;//fail指针,失配后跳转到fail指针指向的节点
int cnt[MAXN] ;//cnt[i]表示编号为i的节点表示的回文字符串在整个字符串中出现了多少次。
//cnt[i]建树时求出的不是完全的,最后count()函数跑一遍以后才是正确的
int num[MAXN] ;//num[i]表示节点i表示的回文字符串中,有多少个本质不同的字符串(包括本身)
ll v[MAXN];//每个点代表的实际十进制的值
int len[MAXN] ;//len[i]表示节点i表示的回文串的长度
int S[MAXN] ;//存放添加的字符
int last ;//指向上一个字符所在的节点,方便下一次add
int n ;//字符数组指针
int p ;//节点指针
int newnode ( int l ) {//新建节点
for ( int i = 0 ; i < N ; ++ i ) next[p][i] = 0 ;
cnt[p] = 0 ;
num[p] = 0 ;
len[p] = l ;
return p ++ ;
}
void init () {//初始化
p = 0 ;
newnode ( 0 ) ;
newnode ( -1 ) ;
last = 0 ;
n = 0 ;
S[n] = -1 ;//开头放一个字符集中没有的字符,减少特判
fail[0] = 1 ;
}
int get_fail ( int x ) {//和KMP一样,失配后找一个尽量最长的
while ( S[n - len[x] - 1] != S[n] ) x = fail[x] ;
return x ;
}
void add ( int c ) {
c -= '0';
S[++ n] = c ;
int cur = get_fail ( last ) ;//通过上一个回文串找这个回文串的匹配位置
if ( !next[cur][c] ) {//如果这个回文串没有出现过,说明出现了一个新的本质不同的回文串
int now = newnode ( len[cur] + 2 ) ;//新建节点
if(len[now]==1)v[now]=c;
else if(len[now]==2)v[now]=ten[1]*c+c;
else v[now]=((c*ten[len[cur]]+v[cur])*ten[1]+c)%mod;//前后各补一个c
ans=(ans+v[now])%mod;
fail[now] = next[get_fail ( fail[cur] )][c] ;//和AC自动机一样建立fail指针,以便失配后跳转
next[cur][c] = now ;
num[now] = num[fail[now]] + 1 ;
}
last = next[cur][c] ;
cnt[last] ++ ;
}
void count()//如果不count向上汇总 每个节点代表的回文串 当且仅当完全相同时才会被记次数
{
for ( int i = p - 1 ; i >= 0 ; -- i )
cnt[fail[i]] += cnt[i] ;
//fail[i]累加i的cnt,因为如果fail[v]=u,则u一定是套在v内的子回文串!
//所以 u自己出现了一次 在v中出现了一次 归回去能统计出u出现的次数
}
}a;
int main()
{
ten[0]=1;
for(int i=1;i<MAXN;++i)
ten[i]=(ten[i-1]*10)%mod;
a.init();
scanf("%s",s);
len=strlen(s);
for(int i=0;i<len;++i)
a.add(s[i]);
printf("%d\n",ans);
return 0;
}