引入
在计算机中,人类的语言、文字等往往利用字符串进行保存,而字符串的匹配也对人们的生活产生着举足轻重的作用,例如个人信息的匹配、搜索引擎的使用等,为了在庞大的数据中找到我们所需的数据,字符串匹配的效率就决定了解决问题的难易程度。
实现
对于字符串的匹配,最简单且最容易实现的方法就是将模式串与文本串一一比对,观察每一位上的字符是否相等,如果不等则将模式串后移一位,再逐一进行比对,假定模式串的长度为
m
m
m,文本串的长度为
n
n
n,则需要进行
(
n
−
m
)
×
m
\left ( n-m\right)×m
(n−m)×m次比较,而一般情况下
n
>
>
m
n>>m
n>>m,所以朴素的枚举算法的时间复杂度为
O
(
n
×
m
)
O(n×m)
O(n×m),在模式串和文本串足够大时,该算法的效率将变得低下。
为了解决这个问题,首先要观察是什么原因导致了算法的效率大打折扣。原因在于在每次比对过程中,一旦两个字符串某个位置不匹配,就要重新回溯,往后移动一个位置再从头进行比较。在有些情况下,这种回溯是没有意义的,例如对于文本串abababab和模式串ababc,一般的做法如下:
不难发现,其中第二次比对是没有任何意义的,为了避免这种不必要的回溯,首先要引入前缀和后缀的概念:
对于字符串 s 0 s 1 s 2 . . . s n − 1 s n s_0s_1s_2...s_{n-1}s_n s0s1s2...sn−1sn,从 s 0 s_0 s0开始的任意子串(即不包含 s n s_n sn)称为该字符串的前缀,类比,以 s n s_n sn结尾的任意子串(即不包含 s 0 s_0 s0)称为该字符串的后缀,通过对模式串进行一个预处理,从而避免不必要的回溯,最终使得算法的复杂度降低为 O ( m + n ) O(m+n) O(m+n)。
而KMP算法的核心步骤在于求解 nxt 数组,nxt 数组用于存储模式串中的前缀和后缀相同时的最大长度,其中 i 表示对于模式串其前缀
s
0
s
1
.
.
.
s
i
−
1
s_0s_1...s_{i-1}
s0s1...si−1的长度,nxt[i] 表示该前缀所表示的字符串中前缀和后缀的最大公共长度。
实现代码如下:
void kmp() //s是从1开始的,便于理解
{
nxt[1] = 0;
for (int i = 2, k = 0; i <= len; i++)
{
while (k && s[k + 1] != s[i])
{
k = nxt[k];
}
if (s[k + 1] == s[i])
{
k++;
}
nxt[i] = k;
}
}
这段代码用最难以理解的地方就是循环中的while循环,举个例子,当 i = 20 时,模式串的前缀为 ababab…ababab (省略号中不重复,长度为20),此时代码执行完该循环后 k = 6,nxt[20] = 6。进入下一次循环,此时要比对 s[k + 1] 和 s[i],此时 k + 1 的值为 7 ,即比对上一次循环结束后的最长相同前缀的下一位与新加入的字符。
当相同时循环是不执行的,所以只是在上一次循环的基础上最大长度加 1 了而已,也不难理解。重点在于当 s[k + 1] != s[i] 时,循环体到底时如何进行重新匹配前缀和后缀的,考虑这样一个情况,由于当前最后一个字符不等,所以在考虑长度时前缀和后缀的范围应如下图所示:
那么如何来确定这个序列的长度呢,更一般的,假设第 i 次循环所确定的长度为
l
l
l,那么一定有
s
1
s
2
.
.
.
s
l
=
=
s
i
−
l
+
1
.
.
.
s
i
−
1
s
i
s_1s_2...s_l == s_{i-l+1}...s_{i-1}s_i
s1s2...sl==si−l+1...si−1si,那么就有
s
1
s
2
.
.
.
s
l
−
1
=
=
s
i
−
l
+
1
.
.
.
s
i
−
2
s
i
−
1
s_1s_2...s_{l-1} == s_{i-l+1}...s_{i-2}s_{i-1}
s1s2...sl−1==si−l+1...si−2si−1,根据这个等式可以发现,这就是前缀与后缀相等的情况,如图所示:
可以发现,新的前缀是原前缀的前缀,新的后缀是原后缀的后缀,而原前缀和原后缀是相等的,那么问题就转化为原前缀的前缀和后缀的最大长度是多少,而这个长度之前已存储在 nxt[l - 1] 中,也就是 nxt[k] 中,进行循环找到最大长度,如果不存在,那么会一直循环下去直到 k = nxt[1],即 k = 0,循环终止,对应也就是首字符和尾字符是否相等,如果不等,那这段序列也就没有相同的前缀和后缀。
例题 CF 535D-Tavas and Malekas
题目大意是给定一个字符串,然后给出
m
\;m\;
m个位置,将该字符串插入,问能否可以形成一个合法的长度为
n
\;n\;
n的字符串。分析题目不难得出思路:对于每次插入,只需要判断插入时是否会叠加,如果叠加了重合部分二者是否相等,即对应 KMP 算法的前缀与后缀问题。统计插入结束后还有多少个空位,答案就是
2
6
c
n
t
26^{cnt}
26cnt,(注意的点都写在注释里了)。
Solution:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const int maxn = 1e6 + 5;
inline ll read()
{
ll t = 0;
char ch = getchar();
while (ch < '0' || ch>'9')
{
ch = getchar();
}
while (ch >= '0' && ch <= '9')
{
t = (t << 3) + (t << 1) + (ch ^ 48);
ch = getchar();
}
return t;
}
inline void write(ll t)
{
if (t > 9)
write(t / 10);
putchar(t % 10 + 48);
}
ll n, m, y, num, ans;
ll len, nxt[maxn];
char p[maxn];
inline void kmp()
{
nxt[1] = 0;
for (int i = 2, j = 0; i <= len; i++)
{
while (j && p[j + 1] != p[i])
{
j = nxt[j];
}
if (p[j + 1] == p[i])
{
j++;
}
nxt[i] = j;
}
}
inline void quickPower()
{
ans = 1;
ll t = 26;
while (num)
{
if (num & 1)
{
ans = (ans * t) % mod;
}
t = (t * t) % mod;
num >>= 1;
}
ans %= mod;
}
int main()
{
n = read();
m = read();
cin >> p + 1;
len = strlen(p + 1);
kmp();
if (!m) //如果没有一个插入位置,那么所有n个位置都有26种可能
{
num = n;
}
else
{
ll endPos = 0; //记录每次插入结束后占据的最后一个位置
while (m--)
{
y = read();
if (endPos)
{
if (y > endPos) //新插入位置与上一次插入不重合
{
num += y - endPos - 1; //增加二者之间的空格
endPos = y + len - 1; //更新末尾位置
}
else
{
ll l = endPos - y + 1;
ll h = nxt[len];
bool flag = false;
while (h) //坑点,插入位置可能重合但不是最大长度,所以要循环判断每次可能
{
if (h < l) //重合部分比最大长度还长,一定不符合要求
break;
if (h == l) //二者重合合法,可以插入
{
flag = true;
break;
}
h = nxt[h];
}
if (flag) //重合位置符合要求,更新末尾位置
{
endPos = y + len - 1;
}
else
{
puts("0");
return 0;
}
}
}
else
{
//对于第一次插入,无需判断重合,但是插入位置不一定是1号位,所以前面有y - 1个空位
num = y - 1;
endPos = y + len - 1;
}
}
if (endPos > n) //当最后一次插入时不足以全部插入,为不符合要求的情况
{
puts("0");
return 0;
}
if (endPos < n) //最后一次插入后末尾位置之后还有空位,需要补上
{
num += n - endPos;
}
}
quickPower();
write(ans);
putchar(10);
return 0;
}
完结撒花!!!