You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

138 lines
5.2 KiB

2 years ago
##[$AcWing$ $1052$. 设计密码](https://www.acwing.com/problem/content/1054/)
### 一、题目描述
你现在需要设计一个密码 $S$$S$ 需要满足:
- $S$ 的长度是 $N$
- $S$ 只包含小写英文字母($a$~$z$)
- $S$ 不包含子串 $T$
例如:$abc$ 和 $abcde$ 是 $abcde$ 的子串,$abd$ 不是 $abcde$ 的子串。
**请问共有多少种不同的密码满足要求**
由于答案会非常大,请输出答案模 $10^9+7$ 的余数。
**输入格式**
第一行输入整数$N$,表示密码的长度。
第二行输入字符串$T$$T$中只包含小写字母。
**输出格式**
输出一个正整数,表示总方案数模 $10^9+7$ 后的结果。
**数据范围**
$1≤N≤50,1≤|T|≤N|T|$是$T$的长度。
**输入样例1**
```cpp {.line-numbers}
2
a
```
**输出样例1**
```cpp {.line-numbers}
625
```
**输入样例2**
```cpp {.line-numbers}
4
cbc
```
**输出样例2**
```cpp {.line-numbers}
456924
```
### 二、题目分析
① $len=strlen(p+1)$ ,即$len$是模式串的长度
```cpp {.line-numbers}
scanf("%s", a + 1); //输入abcdef共6个字符,放过下标为0的位置从下标为1开始
int len = strlen(a + 1); //含义从a下标偏移为1开始计算到末尾\0的长度
printf("%d", len); //输出答案6,理解让从1开始到末尾尾巴在哪里自己找计算返回长度
```
② 模式串是固定的,但$s$串是动态随便生成的,$s$串中的每个位置上都有$a \sim z$共$26$种可能
#### 闫氏$DP$分析法
**预求**
所有长度为$n$的生面的密码字符串中,不出现子串 $p$ 的方案数
**状态表示**
- **集合**
$f[i][j]$:密码已生成$i$位,并且,第$i$位匹配到子串$p$的位置是$j$的所有方案
- **属性**
$count$(方案数)
**状态转移**
$\large f[i][j]$:已经成功构建了一个长度为$i$的密码,当前密码串与模式串的匹配位置是$j$的情况
思考它的下一步变化:
- ① 下一步枚举尝试的字符,与$p$串的$p[j+1]$相等,并且,需要满足$p$串的$j+1<m$,也就是不能完整匹配成功。 $$\large f[i+1][j+1]+=f[i][j] \ (j+1<m)$$
$(i,j)$状态的方案数可以累加到$(i+1,j+1)$状态的方案数上去
- ②下一步枚举尝试的字符,与$p$串的$p[j+1]$不等,那么当前 **状态匹配** 会去往何方呢?根据$kmp$知识,就是 目标 **密码已经生成了$i+1$位,并且,第$i+1$位匹配到子串的位置是$x$时的方案数** !那这个$x$是什么东西呢?就是 **失配时的跳转位置**。
**$Q$:为啥这么做对呢?**
你想啊,现在要构造的密码串,是不是得避开模式串$p$,也就是不允许密码串中出现模式串$p$,换言之,见到模式串$p$就不允许选择,那你怎么能快速知道一个长串中是不是包含了某个子串$p$呢?当然是用$kmp$啊!
时刻保持警惕心,怕碰到红线,不允许出现$p$串,不就是随时需要记录准备好现在与$p$串匹配了多少吗,这个都不记录,你知道下面会不会出现$p$串?当然不能啦。
**初始值**
$f[0][0]=1$
**结果**
$\displaystyle res=f[n][0]+f[n][1]+f[n][2]+....+f[n][m-1]=\sum_{j=0}^{m-1}f[n][j]$
$s$串必须有$n$个长度,但$p$串不允许匹配到$m$,最多只能到$m-1$,到了$m$的话,就是完整匹配,也就是$s$串包含了$p$串。
### 四、$Code$
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 55;
const int MOD = 1e9 + 7;
int n, ne[N];
int f[N][N];
char p[N];
int res;
int main() {
scanf("%d %s", &n, p + 1); // 读入模式串,存入到p数组中下标从1开始
int m = strlen(p + 1); // 模式串的长度,读到\0结束会自动计算出p串的长度
// 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;
}
f[0][0] = 1; // 递推起点,文本串0个字符模板串0个字符此时方案数是1其它状态都是0种方案
// 开始填充dp表,为什么i,j都要从0开始呢这其实要先看一下状态转移方程f[i][j]出现在了方程中,而初始值是边界
// f[0][0]=1,所以填表的时候自然也就是从i=0,j=0开始才能使用上初始值
for (int i = 0; i < n; i++) //
for (int j = 0; j < m && j <= i; j++) // ,chj++,jm-1,f[n][m]
for (char ch = 'a'; ch <= 'z'; ch++) { // 准备填充的下一个字符ch
int u = j; // 要退到哪里去呢?
while (u && ch != p[u + 1]) u = ne[u]; // 不断回退,找到新起点
if (ch == p[u + 1]) u++; // 如果匹配下一个字符
f[i + 1][u] = (f[i + 1][u] + f[i][j]) % MOD;
}
// 枚举源串长度是n, 模式串匹配度不足m的累加
for (int i = 0; i < m; i++) res = (res + f[n][i]) % MOD;
cout << res << endl;
return 0;
}
```