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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##[$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++) // 匹配长度,需要注意的是完成一个字符ch的匹配则j++,所以j的上限是m-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;
}
```