CF506E 题解

CF506E 题解

$\text{Description}$

给定字符串 $S$ 和正整数 $n$,求有多少个回文字符串 $T$ 满足 $|T|=|S|+n$ 且 $S$ 是 $T$ 的一个子序列。

$|S|\le 200,n\le 10^9$

$\text{Solution}$

一眼看过去就很矩阵快速幂……所以直接 BM 加线性递推 $O(|S|^3+|S|^2\log n)$

由于 $T$ 是回文字符串,所以我们考虑在两边同时加字符,并且我们先考虑 $|T|$ 为偶数的情况。

设 $f(l,r,k)$ 表示已经填好了 $T$ 的左边及右边各 $k$ 位,且 $S$ $[l,r]$ 尚未匹配进 $T$ 的方案数,我们发现这样遗漏了 $S$ 已经完全匹配的情况,所以额外设 $g(k)$ 表示已经填好了 $T$ 的左边及右边各 $k$ 位且 $S$ 已经完全匹配的方案数。

这样的话容易得到转移方程,此处不赘述转移方程,但是会说推导过程。

如果此时 $S_l=S_r$,若 $r=l+1$ 或 $r=l$,它有两种选择,一种是直接匹配好(当 $r=l+1$ 且 $|T|$ 为偶数时是不可以在 $k=\lfloor\frac{|T|}{2}\rfloor$ 时进行的),一种是不匹配,填剩下的 $25$ 个字符;若 $r>l+1$,它仍有两种选择,一种是匹配掉 $l,r$,一种是不匹配,填剩下的 $25$ 个字符。

如果此时 $S_l\neq S_R$,我们仍有两种选择,一种是匹配掉其中一个(注意这里有两种转移),一种是不匹配,填剩下的 $24$ 个字符。

最后还有已经匹配完 $S$ 的情况,这个就是随便填 $26$ 种字符。

这样设计出来的转移方程复杂度极其之高。不过我们注意到 $k$ 那一维是开销最大的,而且它只会进行 $k+1\gets k$ 的转移,如果我们认为 $(l,r)$ 是一个点,然后按照转移关系连有向边(从 $A$ 转移到 $B$ 就是 $A\rightarrow B$),除了自环,其它的边由于都是大区间连向小区间,故形成了一个 DAG。而 $k$ 那一维的意义在此时就变成了从 $[1,|S|]$ 出发,到达 $g$ 时走了 $k$ 条边的方案数,这个可以用矩阵快速幂优化对吧,时间复杂度高达 $|S|^6\log (n+|S|)$,虽然优化很大,但不能过。

我们常常有一个优化手段,叫作 “适当合并等价信息”,或者说考虑某一类元素,它们的贡献是相同的,那么这一类元素的总贡献就是元素个数乘上单个元素的贡献。再来看一下我们的图有什么特点,它除了 DAG,还有自环……自环!而且每个点的自环数只有 $24,25,26$ 三种情况,并且只有终点有 $26$ 个自环。我们把一个点称作 “$x$点” 当且仅当这个点有 $x$ 个自环。更重要的是,一个 $24$ 点会让当前代表区间缩小 $1$,而一个 $25$ 点会让当前代表区间缩小 $2$(有一个特例是 $r=l$),我们的区间长度为 $|S|$,说明一条从起点($[1,n]$)出发到终点的路径上,若有 $x$ 个 $24$ 点,那么就有 $\lceil\frac{|S|-x}{2}\rceil$(上取整正是考虑到了那个特例)个 $25$ 点。不过最最重要,使得我们能合并所有 $24$ 点个数相同的合法路径的条件是:各种点的排列不影响答案。于是我们记 $cnt(i,l,r)$ 表示起点到 $[l,r]$ 的路径上有 $i$ 个 $24$ 点的数量(提一嘴,这个东西在偶数情况下的终止状态为 $r-l\le 1$),就可以 $O(|S|^3)$ DP出等价类元素个数。那么怎么计算某个等价类里单个元素的贡献呢?我们发现只需搞出来一条链,前面都是 $24$ 点,后面除了终点其它都是 $25$ 点,然后对这个链作矩阵快速幂即可。由于链有 $O(|S|)$ 个,长度为 $O(|S|)$,故复杂度为 $O(|S|^4\log (n+|S|))$,还需继续努力。

我们发现这些链其实共同点很多,能不能合并一下,一个矩阵快速幂计算完答案呢?当然可以,只需把 $24$ 点总数个点(上)和 $25$ 点总数个点(下)都列出来,然后形成两条链,链上边数都为 $1$,接着考虑跨链连边,若是第 $k$ 个 $24$ 点向下连边,那么就要让右边的 $25$ 点数量和 $24$ 点对应起来就可以了,注意这里边数不能是 $1$,而是这个等价类里元素的个数,这是为了方便计算。现在这个图上就只有 $O(|S|) $ 个点了,而我们只要做一次矩阵快速幂,复杂度为 $O(|S|^3\log (n+|S|))$。

偶数讨论完了,还剩奇数的情况。当 $|T|$ 为奇数时,大体一样,但是回忆上面的转移,有提到过一个向终点的转移只能在 $|T|$ 为偶数时进行,我们先按照偶数的情况计算,然后减去这一情况的数量即可。怎么设计才能做到最后一条边是由那个转移变化来的呢?首先要去掉终点的自环,然后要让 $24$ 点向 $25$ 点连的边数只有形如 $cnt(i,l,l+1)$ 的和,因为我们只需去掉这种情况的转移,然后再做一次矩阵快速幂即可。

常数的优化呢只需注意 “小点连向大点”,所以矩阵都是上三角矩阵,操作时可以不去考虑下三角部分。

$\text{Code}$

代码参考自 @xht37。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
#include<bits/stdc++.h>
#define REG register
#define MAXL 305
using namespace std;
const int Mod=10007;
inline void read(int& x){
static char c;
while(!isdigit(c=getchar()));x=c^48;
while(isdigit(c=getchar()))x=(x*10)+(c^48);
}

inline int Add(int a,int b){return (a+b)%Mod;}
inline int Del(int a,int b){return (a-b+Mod)%Mod;}

char str[MAXL];
int s,m,n;

int G[MAXL][MAXL],Ans[MAXL],Cnt[MAXL][MAXL][MAXL];
int TmpG[MAXL][MAXL],TmpAns[MAXL];

inline void VMulM(){
int R[MAXL];memset(R,0,sizeof R);
for(REG int j=1;j<=m;++j)
for(REG int i=1;i<=m;++i)
R[i]=Add(R[i],Ans[j]*G[j][i]%Mod);
for(REG int i=1;i<=m;++i) Ans[i]=R[i];
}

inline void MMulM(){
int R[MAXL][MAXL];memset(R,0,sizeof R);
for(REG int i=1;i<=m;++i)
for(REG int k=i;k<=m;++k)
for(REG int j=k;j<=m;++j)
R[i][j]=Add(R[i][j],G[i][k]*G[k][j]%Mod);
for(REG int i=1;i<=m;++i)
for(REG int j=1;j<=m;++j)
G[i][j]=R[i][j];
}

int GetCnt(int Stp,int l,int r){
if(Stp<0) return 0;
if(~Cnt[Stp][l][r]) return Cnt[Stp][l][r];
Cnt[Stp][l][r]=0;
if(l==1&&r==s) return Cnt[Stp][l][r]=!Stp;
if(l^1&&str[l-1]^str[r]) Cnt[Stp][l][r]=Add(Cnt[Stp][l][r],GetCnt(Stp-1,l-1,r));
if(r^s&&str[l]^str[r+1]) Cnt[Stp][l][r]=Add(Cnt[Stp][l][r],GetCnt(Stp-1,l,r+1));
if(l^1&&r^s&&str[l-1]==str[r+1]) Cnt[Stp][l][r]=Add(Cnt[Stp][l][r],GetCnt(Stp,l-1,r+1));
return Cnt[Stp][l][r];
}

inline void Pow(int b){
while(b){
if(b&1) VMulM();
MMulM(),b>>=1;
}
}

inline void Work(){
memset(Cnt,-1,sizeof Cnt);
scanf("%s",str+1);
s=strlen(str+1),m=s+(s+1)/2;
read(n);
for(REG int i=0;i<s;++i){ // 此处枚举的是路径上 n24 的个数
int C=0;
for(REG int j=1;j<=s;++j){
C=Add(C,GetCnt(i,j,j));
if(j^s&&str[j]==str[j+1]) C=Add(C,GetCnt(i,j,j+1));
}
if(i){
G[i][m-(s-i+1)/2]=C,G[i][i]=24;
if(i^1) G[i-1][i]=1;
else Ans[i]=1;
}
else{
Ans[s]=C,G[m][m]=26;
for(REG int j=s;j<m;++j) G[j][j+1]=1,G[j][j]=25;
}
// printf(" DEBUG , I : %d , C : %d \n",i,C);
}
if((n+s)&1){
for(REG int i=1;i<=m;++i) TmpAns[i]=Ans[i];
for(REG int i=1;i<=m;++i)
for(REG int j=1;j<=m;++j)
TmpG[i][j]=G[i][j];
}
Pow((n+s+1)/2);
if((n+s)&1){
int Sum=Ans[m];
for(REG int i=1;i<=m;++i) Ans[i]=TmpAns[i];
for(REG int i=1;i<=m;++i)
for(REG int j=1;j<=m;++j)
G[i][j]=TmpG[i][j];
for(REG int i=0;i<s;++i){
int C=0;
for(REG int j=1;j<=s;++j)
if(j^s&&str[j]==str[j+1]) C=Add(C,GetCnt(i,j,j+1));
if(i) G[i][m-(s-i+1)/2]=C;
else Ans[s]=C,G[m][m]=0;
}
Pow((n+s+1)/2);
printf("%d\n",Del(Sum,Ans[m]));
}
else printf("%d\n",Ans[m]);
getchar();
}

int main(){Work();}
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×