CF506E Mr. Kitayuta's Gift
一、题目
二、解法
使我深受洗礼的一道题,是既有思维难度又有代码难度不可多得的好题!
先考虑偶回文串吧!首先考虑如何计数,题目都告诉你只关心最终状态,我们直接对最终状态计数。考虑枚举法确定原来字符在最终序列的位置,那么可以简单计数来确定方案。
用 \(dp\) 优化这个过程,设 \(f_{i,l,r}\) 为考虑最终序列的前 \(i\) 个和后 \(i\) 个字符,原串还剩下区间 \([l,r]\) 没有确定位置的方案数,设 \(g_i\) 表示考虑最终序列的前 \(i\) 个和后 \(i\) 个字符,原串已经用完的方案数。
为了不算重我们需要把某种方案只使用某种方式表示,就算它可以通过多种"方式"表示出来,我们也规定一种方式来储存它:
若 \(s_l=s_r,r-l\leq 1\),则可以直接到终状态,\(f\) 向 \(g\) 转移,还可以不同地填字符对(\(25\) 种):
若 \(s_l=s_r,r-l\geq 2\),则可以把两边取出来配对,也可以不同地填字符对(\(25\) 种):
若 \(s_l\not=s_r\),则可以取出一边再拿个任意字符来配对,也可以不同地填字符对(\(24\) 种):
最后是 \(g\) 的转移,可以有 \(26\) 种任意的填法:\(g_{i+1}\leftarrow 26\cdot g_i\)
上面的转移可以直接套矩阵乘法,时间复杂度 \(O(m^6\log n)\)
请容许我盗波图,继续优化我们可以画出转移的图像,下图的红点表示左右端点消减 \(1\) 的转移点,绿点表示左右端点消减 \(2\) 的转移点,答案就是这个图上起点到终点的路径方案数:
因为原串大小固定,如果一条路径上有 \(k\) 个红点,那么就必须有 \(\lfloor\frac{m-k}{2}\rfloor\) 个绿点。而路径的方案数是和自环紧密相关的,也就是和经过的红点和绿点个数相关的,而和权值只有 \(1\) 的边没有太大关系,所以说本质不同的路径条数只有 \(O(m)\) 条。
所以可以统计每种路径的出现次数,对它单独矩乘,简单乘法原理就可以算出答案,时间复杂度 \(O(m^4\log m)\),统计路径的那一步可以用记忆化搜索,设 \(h_{i,l,r}\) 为 \(i\) 个绿点,考虑区间 \([l,r]\) 的方案数。
进一步优化可以考虑整体 \(dp\),因为它们的转移规则是相同的,所以我们建在一张图上:
其中从后往前第 \(i\) 个红点\(/\)绿点就表示路径上还需要走 \(i\) 个红点\(/\)绿点,连接红点和绿点之间的边就设置成这种路径的条数即可,注意没有红点需要初始化在绿点上,用矩阵加速跑这张图时间复杂度 \(O(m^3\log n)\)
如果最后是奇回文串,那么可以把不合法的情况减去,也就是最后一步通过长度为 \(2\) 的绿点转移到终点的情况,那么我们去掉终点的自环,再只保留到这些绿点的路径,同样跑一遍矩乘就可以出答案。
实现小细节:由于我们建出的图是上三角(只会由编号小的转移到编号大的),所以做乘法的时候也可以只乘上三角,据说常数直接小到 \(\frac{1}{6}\)
后记:好像可以猜本题有线性递推,先用 \(\tt BM\) 解出来,验证之后直接上常系数齐次线性递推:传送门
三、总结
本题我所缺失的就是那步关键的去重处理,方案如何表示很重要,如果只关心最终方案,那么过程中有多种方法可以强制通过某一种方法达到最终方案。
其他设计到的技巧:设计图论模型直观表示问题;等价类的划分;转移方式相同使用整体 \(dp\)
本题的模型其实也可以称为有限状态自动机,字符串问题可以朝这个方向思考一下。
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int M = 505;
const int MOD = 1e4+7;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,k,g[M][M],f[M],h[M][M][M];char s[M];
int ceil(int x)
{
return (x>>1)+(x&1);
}
int dfs(int i,int l,int r)
{
if(i<0 || l>r) return 0;
if(h[i][l][r]!=-1) return h[i][l][r];
int &o=h[i][l][r];o=0;
if(l==1 && r==m) return o=!i;
if(l>1 && r<m && s[l-1]==s[r+1])
o=(o+dfs(i,l-1,r+1))%MOD;
if(l>1 && s[l-1]!=s[r])
o=(o+dfs(i-1,l-1,r))%MOD;
if(r<m && s[l]!=s[r+1])
o=(o+dfs(i-1,l,r+1))%MOD;
return o;
}
void qkpow(int b)
{
while(b>0)
{
if(b&1)
{
int a[M]={};
for(int i=1;i<=k;i++)
for(int j=1;j<=k;j++)
a[i]=(a[i]+f[j]*g[j][i])%MOD;
swap(a,f);
}
int a[M][M]={};
for(int i=1;i<=k;i++)
for(int j=i;j<=k;j++)
for(int l=j;l<=k;l++)
a[i][l]=(a[i][l]+g[i][j]*g[j][l])%MOD;
swap(a,g);
b>>=1;
}
}
signed main()
{
scanf("%s",s+1),m=strlen(s+1);n=read();
memset(h,-1,sizeof h);k=m+ceil(m);
//[1,m) red ; [m,k) green ; k end
for(int i=0;i<m;i++)
{
int c=0;
for(int j=1;j<=m;j++)
{
c=(c+dfs(i,j,j))%MOD;
if(j<m && s[j]==s[j+1])
c=(c+dfs(i,j,j+1))%MOD;
}
if(!i)
{
f[m]=c;g[k][k]=26;
for(int j=m;j<k;j++)
g[j][j+1]=1,g[j][j]=25;
}
else
{
g[i][k-ceil(m-i)]=c;g[i][i]=24;
if(i>1) g[i-1][i]=1;
else f[i]=1;
}
}
int F[M]={},G[M][M]={};
memcpy(F,f,sizeof F);
memcpy(G,g,sizeof G);
qkpow(ceil(n+m));
if((n+m)%2==0)//even length
{
printf("%d\n",f[k]);
return 0;
}
int ans=f[k];
memcpy(f,F,sizeof f);
memcpy(g,G,sizeof g);
//build the new graph,containing illegal roads
for(int i=0;i<m;i++)
{
int c=0;
for(int j=1;j<m;j++) if(s[j]==s[j+1])
c=(c+dfs(i,j,j+1))%MOD;
if(i) g[i][k-ceil(m-i)]=c;
else f[m]=c,g[k][k]=0;
}
qkpow(ceil(n+m));
printf("%d\n",(ans-f[k]+MOD)%MOD);
}