一、思路
- 动态规划
首先怎么确定状态,这里把 \(f[i][j\)] 表示为对于现在生成的密码已经(完成、结束的意思)到了第 \(i\) 个了,并且匹配到当前在子串中的位置是\(j\) 的密码个数。
一个状态机问题,先要明确有几种状态,对于每一个固定的\(i\) 和 \(j\) 来说,有\(m+1\)个状态。
每个状态根据\(s[i]\)的不同,出去的边也不同,我们需要逐个讨论可能的\(a\sim z\)进行研究。
如何判断某一种方案是合法解的呢?联系\(KMP\)的子串匹配方法,就是判断对于固定的 \(i\) 和 \(j\) 判断当前字符是不是和子串中 \(j+1\) 的字符匹配,匹配就\(j++\),不匹配 \(j\) 就回跳。
根据上面的分析,我们再来看这个状态的定义,想一想状态方程,因为每一个字母都对于固定的 \(i\) 和 \(j\) 都有固定的判断结果,那么我们只要对于每一种 \(i\) 和 \(j\)枚举一下\(26\)个字母 ,根据上面的判断方法判断一下是否可能 如果可能就可以状态转移了,其实就是讨论一下某个状态转化到哪个状态。
接下来,是我写题解的惯例,写一下代码每一步的含义和其中一些细节
1:\(KMP\)的预处理,初始化\(next\)数组,代码中用\(ne\)表示
2:循环:
2.1:第一层循环:枚举一下\(i\)的位置,也就是当前已完成密码的长度,不能到\(n\),最长是\(n-1\)(代码中是从\(0\)开始的)
2.2:第二层循环:枚举一下\(j\) 的位置,也就是在子串中的位置
2.3:第三层循环:枚举一下\(a \sim z\) 所有的字母 ,并且利用\(KMP\)判断是否当前密码有子串,如果没有更新\(f[i+1][j]\),为什么是\(i+1\),而不是\(i\) 呢?因为这里定义的状态是已经有的长度,不包括当前枚举的字母,我当时也迷惑了一会,现在写出来帮助一下不懂的小伙伴
3:最后把所有可能的 \(j\) 的位置加起来,就是答案,因为\(i\)最后肯定是 \(n\) ,所以枚举一下未知的 \(j\) 。
\(KMP\)中还有一个细节,就是要用\(u\)来对每一种状态更新,不要用\(j\),不要搞错了,\(j\)是枚举的状态,如果用 \(j\) 的话更新的状态就不对了,注意一下哈。
- 问题
为什么这样的状态表示是可行的呢?
因为\(S\)数组中的第\(n\)位有\(26\)个小写字母,匹配在\(T\)中的位置一定存在(因为不匹配,匹配到的位置是\(0\)),所以把所有\(f[n][0 \sim m-1]\)加起来即为总方案数。
二、原始版本
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
const int mod = 1e9 + 7;
int n; //n个长度的密码串
int m; //模板串的长度
int ne[N]; //kmp的ne数组
char p[N]; //模板串
int f[N][N]; //f[i][j]表示密码已经生成了i位,并且第i位匹配到模板串中位置为j时的方案数,这是方案数互相依赖相加的准确依据
int main() {
//构建的密码长度n
cin >> n >> (p + 1);//模板串p,模板串的下标是从1开始的
//计算模板串s的字符串长度
m = strlen(p + 1);
//kmp求ne数组,模板代码
for (int i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j;
}
//已经匹配了0位,且匹配的子串的位置是0时的方案数为1;(初始化)
f[0][0] = 1;
for (int i = 0; i < n; i++)//枚举密码串的每一位,这是一个DP打表的过程,所以从小到大遍历每一位
for (int j = 0; j < m; j++)//根据状态定义,需要枚举的第二维就是模板串的每一个位置
//j表示第i位密码匹配到的位置,因为不能包含子串,所以不能匹配到m这个位置
//认为前面i,j已经完成匹配的情况下,讨论密码串第i+1位的26种可能
for (char k = 'a'; k <= 'z'; k++) {
//在s[i+1]=k的时候,模板串需要跳到哪个位置?
int u = j;
while (u && k != p[u + 1]) u = ne[u];
if (k == p[u + 1]) u++;
//[i+1,u]这个状态是可以被[i,j]转化而来的
if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
}
int res = 0;
for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
//将所有的方案数加起来即为总方案数
printf("%d", res);
return 0;
}
优化一维复杂度降低到\(n^2\)
将建图部分抽取出来即可。。。!
时间复杂度:\(O(26*n^2)\)
三、预处理优化版本
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
const int mod = 1e9 + 7;
int n; //n个长度的密码串
int m; //模板串的长度
int ne[N]; //kmp的ne数组
char p[N]; //模板串
int f[N][N]; //f[i][j]表示密码已经生成了i位,并且第i位匹配到模板串中位置为j时的方案数,这是方案数互相依赖相加的准确依据
int g[N][26]; //
int main() {
//构建的密码长度n
cin >> n >> (p + 1);//模板串p,模板串的下标是从1开始的
//计算模板串s的字符串长度
m = strlen(p + 1);
//kmp求ne数组,模板代码
for (int i = 2, j = 0; i <= m; i++) {
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j++;
ne[i] = j;
}
// 预处理(可以优化一下,有重复的不用重新计算)
for (int j = 0; j < m; j++)
for (int k = 'a'; k <= 'z'; k++) {
int u = j;
while (u && p[u + 1] != k) u = ne[u];
if (p[u + 1] == k) u++;
//记录下这个数据
g[j][k - 'a'] = u;
}
// 状态计算
f[0][0] = 1;
for (int i = 0; i < n; i++)//枚举密码串的每一位,这是一个DP打表的过程,所以从小到大遍历每一位
for (int j = 0; j <= m; j++)//根据状态定义,需要枚举的第二维就是模板串的每一个位置
//j表示第i位密码匹配到的位置,因为不能包含子串,所以不能匹配到m这个位置
//认为前面i,j已经完成匹配的情况下,讨论密码串第i+1位的26种可能
for (char k = 'a'; k <= 'z'; k++) {
//模板串跳到哪个位置?
//模板串跳到哪个位置,与两个因素有关,1:j现在所在的位置,2:遇到的s[i+1]是什么,即k
int u = g[j][k - 'a'];
//状态转移
if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
}
int res = 0;
for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
//将所有的方案数加起来即为总方案数
printf("%d", res);
return 0;
}