题目地址:
https://www.acwing.com/problem/content/1054/
你现在需要设计一个密码 S S S, S S S需要满足: S S S的长度是 N N N; S S S只包含小写英文字母; S S S不包含子串 T T T;例如: a b c abc abc和 a b c d e abcde abcde是 a b c d e abcde abcde的子串, a b d abd abd不是 a b c d e abcde abcde的子串。请问共有多少种不同的密码满足要求?由于答案会非常大,请输出答案模 1 0 9 + 7 10^9+7 109+7的余数。
输入格式:
第一行输入整数
N
N
N,表示密码的长度。第二行输入字符串
T
T
T,
T
T
T中只包含小写字母。
输出格式:
输出一个正整数,表示总方案数模
1
0
9
+
7
10^9+7
109+7后的结果。
数据范围:
1
≤
N
≤
50
1≤N≤50
1≤N≤50
1
≤
∣
T
∣
≤
N
1≤|T|≤N
1≤∣T∣≤N,
∣
T
∣
|T|
∣T∣是
T
T
T的长度。
字符串匹配的过程可以用状态机来模拟。实际上KMP算法就是在模拟一种状态机,该状态机是依据模式串生成,并且可以接受以模式串为子串的任意字符串。即,我们先由模式串生成KMP里的next数组,接着用这个数组构造出一个有限状态自动机,使得这个DFA(Deterministic Finite Automaton,确定有限状态自动机的英文缩写)只接受以该模式串为子串的字符串。我们以字符串"aba"
为模式串举例,具体来说如下:
1、先求出s = "aba"
的next数组。参考https://blog.csdn.net/qq_46105170/article/details/113805346。这里的next数组可以是未优化版的也可以是优化版的,因为本质上来说,它们构造出的DFA识别的语言是一样的。
2、构造一个DFA,它有三个状态,分别是
0
,
1
,
2
,
3
0,1,2,3
0,1,2,3,
0
0
0是初始状态,
3
3
3是接受状态(该DFA只有一个接受状态)。先构造边
δ
(
0
,
a
)
=
1
,
δ
(
1
,
b
)
=
2
,
δ
(
2
,
a
)
=
3
\delta(0,a)=1,\delta(1,b)=2,\delta(2,a)=3
δ(0,a)=1,δ(1,b)=2,δ(2,a)=3,这三个转移边是显然的,对应的情况是恰好存在子串"aba"
。接下来考虑失配边,这个字符串的next数组是
n
e
=
[
−
1
,
0
,
0
]
n_e=[-1,0,0]
ne=[−1,0,0],这个数组规定了如果在
s
[
i
]
s[i]
s[i]存在失配,应该如何跳转。跳转规则如下:比如在
s
[
1
]
s[1]
s[1]和
s
[
2
]
s[2]
s[2]处失配的时候(对应的是当前状态在
1
1
1和
2
2
2的时候,比如当前位于状态
1
1
1,然后读入了'a'
字符,这样就发生了失配),比如当前读到的字符是'c'
,那么就回到状态
0
0
0,然后继续在状态
0
0
0处匹配'c'
(也就是看一下
s
[
0
]
s[0]
s[0]是否等于'c'
),仍然不匹配,那么就跳到
n
e
[
0
]
=
−
1
n_e[0]=-1
ne[0]=−1,这个
−
1
-1
−1并不是一个真实状态,它是一个虚拟的状态,假想
s
[
−
1
]
s[-1]
s[−1]是一个通配符,可以匹配任意字符,那么此时就匹配上了,沿着这条虚拟的边走到状态
0
0
0(所以我们可以特判一下状态
−
1
-1
−1就行了,不需要真在程序里写这个状态);再比如,在状态
1
1
1的时候读到了字符'b'
,那么此时是匹配的,直接沿着匹配边走到下一个状态,也就是状态
2
2
2。
3、总结一下该DFA的跳转规则。当当前位于状态
i
i
i,并且读入了字符
α
\alpha
α的时候,进行如下循环:只要当前处于的状态
j
j
j不是
−
1
-1
−1,并且
s
[
j
]
s[j]
s[j]和
α
\alpha
α不匹配,那么就跳转到
n
e
[
j
]
n_e[j]
ne[j]去,直到
j
=
−
1
j=-1
j=−1或者
s
[
j
]
=
α
s[j]=\alpha
s[j]=α为止,此时沿着匹配边走一步到状态
j
+
1
j+1
j+1去。总结来说就是
δ
(
i
,
α
)
=
j
+
1
\delta(i,\alpha)=j+1
δ(i,α)=j+1。
由于上述DFA接受某个字符串,当且仅当其以构造该DFA的模式串为子串。这样一来,要求不含 S S S为子串的字符串数量,相当于就是在问,从DFA的状态 0 0 0出发,跳转 N N N次,并且中途和终点没有跳到接受状态 l S l_S lS的路径个数。这可以用动态规划来做。设 f [ k ] [ p ] f[k][p] f[k][p]是跳 k k k步跳到状态 p p p的路径条数,那么可以按照跳到状态 p p p之前在哪儿来分类,则有: f [ k ] [ p ] = ∑ q → p f [ k − 1 ] [ q ] f[k][p]=\sum_{q\to p} f[k-1][q] f[k][p]=q→p∑f[k−1][q]初始条件 f [ 0 ] [ 0 ] = 1 f[0][0]=1 f[0][0]=1(因为初始状态就是状态 0 0 0)。最终答案就是: ∑ p = 0 l S − 1 f [ N ] [ p ] \sum_{p=0}^{l_S-1} f[N][p] p=0∑lS−1f[N][p]即跳 N N N步没有跳到状态 l S l_S lS的路径条数。
由于不方便知道某个状态之前是哪个状态,我们可以用当前状态来更新未来状态,即可以用 f [ k − 1 ] [ q ] f[k-1][q] f[k−1][q]来累加到 f [ k ] [ p ] f[k][p] f[k][p]上去。代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 55, mod = 1e9 + 7;
int n, m;
char s[N];
int f[N][N];
int ne[N];
// 求next数组
void build_ne() {
ne[0] = -1;
for (int i = 0, j = -1; i < m - 1;)
if (j < 0 || s[j] == s[i]) {
i++;
j++;
ne[i] = s[i] != s[j] ? j : ne[j];
} else j = ne[j];
}
int main() {
scanf("%d%s", &n, s);
m = strlen(s);
build_ne();
f[0][0] = 1;
// 枚举步数
for (int i = 1; i <= n; i++)
// 枚举当前在哪个状态
for (int j = 0; j < m; j++)
// 枚举当前在状态j的时候获得的输入字符
for (char ch = 'a'; ch <= 'z'; ch++) {
// 开始计算从状态j开始,读入ch后会跳到哪个状态
int u = j;
while (u != -1 && ch != s[u]) u = ne[u];
u++;
// 状态s.size()是接受态,走到其的路径条数不用计算,否则累加一下路径条数
if (u < m) f[i][u] = (f[i][u] + f[i - 1][j]) % mod;
}
int res = 0;
for (int i = 0; i < m; i++) res = (res + f[n][i]) % mod;
printf("%d\n", res);
}
时间复杂度 O ( N l S 2 ) O(Nl_S^2) O(NlS2),空间 O ( N l S ) O(Nl_S) O(NlS)。
下面给出字符串下标从 1 1 1开始的版本。此版本中,设字符串长度 m m m,那么状态是 0 , 1 , . . . , m 0,1,...,m 0,1,...,m这些,其中 m m m是匹配状态,本题中不考虑。每个状态 u u u在读入字符串 c c c的时候,当 c ≠ s [ u + 1 ] c\ne s[u+1] c=s[u+1]的时候发生跳转,一直跳转到 n e k [ u ] ne^k[u] nek[u]直到 u = 0 ∨ c = s [ u + 1 ] u=0\lor c= s[u+1] u=0∨c=s[u+1]成立,如果 c = s [ u + 1 ] c=s[u+1] c=s[u+1]则 u u u向后跳一格。代码如下:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 55, mod = 1e9 + 7;
int n, m;
char s[N];
int f[N][N], ne[N];
void build_ne() {
for (int i = 2, j = 0; i <= m; i++) {
while (j && s[i] != s[j + 1]) j = ne[j];
if (s[i] == s[j + 1]) j++;
ne[i] = i < m && s[i + 1] != s[j + 1] ? j : ne[j];
}
}
int main() {
cin >> n >> s + 1;
m = strlen(s + 1);
build_ne();
f[0][0] = 1;
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
for (char ch = 'a'; ch <= 'z'; ch++) {
int u = j;
while (u && s[u + 1] != ch) u = ne[u];
if (s[u + 1] == ch) u++;
if (u < m) f[i + 1][u] = (f[i + 1][u] + f[i][j]) % mod;
}
int res = 0;
for (int j = 0; j < m; j++) res = (res + f[n][j]) % mod;
printf("%d\n", res);
}
时空复杂度一样。