题目大意:考虑只由’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>
using namespace std;
int next[110][4];
int dp[10010][110];
const char* AGCT = "AGCT";
// 预处理 可以利用KMP优化
// k种状态 其它(0) + k-1个str前缀(1~(k-1)) k的时候是禁止字符串
void cal_next( const int k, const string& str ) {
// 预处理
for ( int i = 0 ; i < k ; ++ i ) {
for ( int j = 0 ; j < 4 ; ++ j ) {
// 在str串的前缀后添加一个新字符 构成一个新串
string s = str.substr(0,i) + AGCT[j];
// 不断去除头部字符 直到成为str的一个前缀
while ( str.substr(0,s.size()) != s ) {
s = s.substr(1);
}
// next[i][j] 表示字符串的最后i个字符是str的前缀
// 当添加上字符j的时候组成新串是str的哪个前缀 状态转移
next[i][j] = s.size();
}
}
}
int solve(const int N, const int K) {
// dp[0][0] 0个字符可以组成的以状态其它结尾的字符串的种数 1 空串
dp[0][0] = 1;// dp[i][j] 表示字符串的前i个字符的状态j结尾的情况数
for ( int t = 1 ; t <= N ; ++ t ) {
for ( int i = 0 ; i < K ; ++ i ) {
for ( int j = 0 ; j < 4 ; ++ j ) {
// 状态i后添加字符j后 组成的新状态ti
int ti = next[i][j];
// 禁止字符串 需要跳过
if ( ti == K ) continue;
dp[t][ti] += dp[t-1][i];
}
}
}
int res = 0;
for ( int i = 0 ; i < K ; ++ i ) {
res += dp[N][i];
}
return res;
}
int main()
{
int n, k;
string str;
cin >> n >> k >> str;
cal_next(k, str);
cout << solve(n,k) << endl;
return 0;
}