/*
禁止字符串
考虑只由’A’,‘G’,‘C’,'T’四种字符组成的 DNA字符串。给定一个长度为 k的字符串 S。
请计算长度恰好为 n且不包含 S的字符串的个数。输出个数 mod 10009后的结果
*/
题目大意:考虑只由’A’, ‘G’, ‘C’, ‘T’四种字符组成的DNA字符串。给定一个长度为k的字符串s,请计算长度恰好为n且不包含s的字符串的个数,输出个数mod 10009后的结果。
题目来源是[挑战程序设计竞赛(第二版)]中华丽地处理字符串的第一题,单字符串情况。题目类型为字符串上的动态规划算法。
首先考虑最直观的算法就是:生成所有满足条件的字符串,但是字符串个数可能高达4^n,显然是行不通的。
接下来,与其在生成字符串后在判断它是否包含s,不如在搜索的过程中,每在末尾添加一个字符的时候,都确保其最后k个字符不包含s。可见最后k个字符之前的字符对以后的搜索并无影响。所以,我们可以以剩余字符的个数和最后k-1个字符为状态进行动态规划。
经过等价状态的归并后,其实只有k种状态。即已经生成的字符串的后缀和s的前缀的匹配长度作为状态。
如何理解这句话呢?当前有一个字符串abcd,而s串为cdab,那么匹配长度就是2,即匹配串的最后两个,和s串的最前面连个,可以发现,这样算下来其实有k个状态,k即s串的长度,因为s串有0~k-1个前缀,当出现s时需要剔除。
为了提高动态规划的效率,程序先使用预处理计算出了从某个状态添加某个字符后的状态转移表next[i][j],即当前状态为i,添加了字符j后的状态值(即匹配长度)。
动态规划策略:dp[i][j]是i个字符以状态j为结尾的情况数。
next[i][j]是当前状态为i,添加了字符j后的状态值。
状态转移方程:dp[t][next[i][j]] += dp[t-1][i];
对于t-1个字符以状态i结尾,添加了字符串j时,会生成t个字符以next[i][j]结尾的情况。所以新的状态数要加上dp[t-1][i]这个值。
#include<iostream>
#include<string.h>
#include<cstdio>
using namespace std;
const char *AGCT = "AGCT";
const int MOD = 10009;
const int MAX_K = 105;
const int MAX_N = 10005;
//输入
int N,K;
string S;
int next[MAX_K][4];//添加某个字符串后转移到的状态
int dp[MAX_N + 1][MAX_K];
void solve(){
//预处理
for(int i=0;i<K;i++){
for(int j=0;j<4;j++){
//在 S长度为 i的前缀后添加一个字符
string s = S.substr(0,i) + AGCT[j];
//反复删除第一个字符,直到成为 S的前缀
while(S.substr(0,s.length()) != s){
s = s.substr(1);
}
next[i][j] = s.length();
}
}
//dp[i][j]是i个字符以状态j为结尾的情况数。
//next[i][j]是当前状态为i,添加了字符j后的状态值。
//动态规划边界的初值
dp[0][0] = 1;
for(int i=1;i<K;i++)
dp[0][i] = 0;
//动态规划
for(int t=0;t<N;t++){
for(int i=0;i<K;i++)
dp[t+1][i] = 0;
for(int i=0;i<K;i++){
for(int j=0;j<4;j++){
int ti = next[i][j];
if(ti == K)
continue;//不允许出现 S
dp[t+1][ti] = (dp[t+1][ti] + dp[t][i]) % MOD;
}
}
}
int ans = 0;
for(int i=0;i<K;i++){
ans = (ans + dp[N][i]) % MOD;
}
printf("%d\n",ans);
}
int main(){
cin>>N>>K;
cin>>S;
solve();
return 0;
}