几天前我还会写普及组题,现在已经不会写了
题意
给你一个仅由字符 ( ext{H,T}) 构成的字符串 (S)。
有一个初始为空的字符串 (T),每次随机在 (T) 的末尾添加 ( ext{H}) 或 ( ext{T})。
问当 (S) 为 (T) 的后缀时,在末尾添加字符的期望次数(即 (T) 的期望长度)。
(|S|le 10^6)
题解
真的是个普及组题
不考虑构建 (T) 串,只考虑构建 (S) 串。
那么问题相当于转化为:初始时你在 (s) 串的第 (0) 位,当你在第 (i) 位时,有 (0.5) 的概率走到第 (i+1) 位,另外 (0.5) 的概率走到第 (fail[i+1]) 位。求走到第 (|S|) 位的期望步数。
(fail[i]) 表示将第 (i) 位字符反转后,(S) 串自己对自己做 KMP 时第 (i) 位的 (fail) 指针指向的位置。两次 KMP 预处理即可。
然后这就是个普及组的期望 (dp)……
方法 1
设 (f[i]) 表示第一次从第 (i) 位走到第 (i+1) 位的期望步数。
此时转移为 (f[i] = 0.5 imes 1 + 0.5 imes (1+f[fail[i+1]]+f[fail[i+1]+1]+...+f[i-1]+f[i]))
后面那一长串的意思就是 到达上一个匹配位置,再逐步走过来。
由于是递推,前缀和优化即可。
方法 2
另外一种方法:
设 (f[i]) 表示第 (i) 位走到第 (n) 位的期望步数。
转移为 (f[i] = 0.5 imes f[i+1] + 0.5 imes f[fail[i+1]] + 1)
移项得 (f[i+1] = 2 imes f[i] - f[fail[i+1]] - 2)
好像是带环的解方程,不能直接倒推 (f)。
但不难发现,这种倒推在起点处的性质很好,即 (f[0] = 0.5 imes f[1] + 0.5 imes f[0] + 1),即 (f[1]=f[0]-2)。
由于对于任意的 (ige 2),(f[i]) 最多只与 (2) 个满足 (jlt i) 的 (f[j]) 有关,所以从 (f[0]) 和 (f[1]) 出发可以递推出其它所有点的相关信息。
下面考虑什么是“相关信息”。
我们知道终点处 (f[|S|]=0)。如果能得到一个关于 (f[0]) 和 (f[|S|]) 的等式就好了,这样我们可以直接算出 (f[0])。
于是把每个位置用 (a imes f[0]+b) 表示出来。正着扫一遍 (S) 串递推出来所有位置的系数 (a) 和 (b),得到 (f[n]) 的两个系数后就可以得到关于 (f[0]) 和 (f[|S|]) 的等式了。
关于第二种做法,后来我想到一个问题:最后一步算 (f[0]) 要做除法(除以 (a[n])),不会得小数么?
然后把所有位置的 (a) 都输出出来,发现全是 (1)……
下意识地看了下转移式,突然明白了些什么……
所以这题可以不记系数 (a)……
本质就是解方程高斯消元
#include<bits/stdc++.h>
#define N 1000010
#define mod 1000000007
using namespace std;
inline int read(){
int x=0; bool f=1; char c=getchar();
for(;!isdigit(c);c=getchar()) if(c=='-') f=0;
for(; isdigit(c);c=getchar()) x=(x<<3)+(x<<1)+c-'0';
if(f) return x; return 0-x;
}
char s[N];
int n,fail[N],_fail[N],g[N];
void KMP(){
int j=0;
for(int i=2; i<=n; ++i){
while(j && s[i]!=s[j+1]) j=fail[j];
if(s[i]==s[j+1]) ++j;
fail[i]=j;
}
j=0;
for(int i=2; i<=n; ++i){
while(j && s[i]==s[j+1]) j=fail[j];
if(s[i]!=s[j+1]) ++j;
_fail[i]=j, j=fail[i];
}
}
int main(){
scanf("%s",s+1); n=strlen(s+1);
KMP();
g[1]=mod-2;
for(int i=2; i<=n; ++i) g[i] = ((g[i-1]*2%mod - g[_fail[i]] - 2) % mod + mod) % mod;
cout<<mod-g[n]<<endl;
return 0;
}