KMP
原理及用法
KMP主要用于解决字符串匹配类问题。典型的例题是给定一个长度为N的字符串
S
S
S和一个长度为M的模式串
P
P
P,求
P
P
P在
S
S
S中出现了几次。
若我们朴素的枚举
S
S
S的每个位置来匹配
P
P
P的话,时间复杂度是
O
(
N
M
)
O(NM)
O(NM)的,这显然是不能接受的。
所以我们可以使用KMP算法来解决此题,它可以在
O
(
N
+
M
)
O(N+M)
O(N+M)内得出答案。
朴素的做法中我们每次匹配失败时,会将
P
P
P从头开始匹配,这也是浪费大量时间的主要原因。而KMP算法则使用预先记录的
n
e
x
t
next
next数组保证了匹配失败时我们回退
P
P
P到与
S
S
S的最长后缀相匹配的最长前缀处的位置,这保证了我们遍历
S
S
S时
P
P
P串最多回退
N
+
M
N+M
N+M次。
n
e
x
t
next
next存储的是模式串
P
P
P每个位置最近的回退位置,实际上
n
e
x
t
next
next数组的求法与S串无关,因为与
S
S
S最长后缀相匹配时其实就是与
P
P
P的最长后缀相匹配。
inline void kmp() {
n = strlen(s + 1);
m = strlen(p + 1);
for (int i = 2, j = 0; i <= m; i ++ ) {
while (j && p[i] != p[j + 1]) j = nt[j];
if (p[i] == p[j + 1]) j ++;
nt[i] = j;
}
for (int i = 1, j = 0; i <= n; i ++ ) {
while (j && s[i] != p[j + 1]) j = nt[j];
if (s[i] == p[j + 1]) j ++;
f[i] = j;
}
}
我们可以发现实际上求
n
e
x
t
next
next数组的过程和求答案数组
f
f
f的过程是一样的,这无疑增加了代码的冗余度
所以我们可以将
P
P
P和
S
S
S首尾相接,使用后半段的
n
e
x
t
next
next数组来代替
f
f
f 数组,注意相接时在中间添加一个特殊字符,防止后段的后缀计算同时计算了原来
P
P
P串的部分
inline void kmp() {
n = strlen(s + 1);
m = strlen(p + 1);
p[m + 1] = '@';
for (int i = 1, j = m + 2; i <= n; i ++, j ++ ) p[j] = s[i];
for (int i = 2, j = 0; i <= n + m + 1; i ++ ) {
while (j && p[i] != p[j + 1]) j = nt[j];
if (p[i] == p[j + 1]) j ++;
nt[i] = j;
}
}
题目
字符串匹配例题
思路
KMP模板题
代码
inline void kmp() {
n = strlen(s + 1);
m = strlen(p + 1);
p[m + 1] = '#';
for (int i = 1, j = m + 2; i <= n; i ++, j ++ ) p[j] = s[i];
int len = n + m;
int j = 0;
nt[1] = 0;
for (int i = 2; i <= n + m + 1; i ++ ) {
while (j && p[i] != p[j + 1]) j = nt[j];
if (p[i] == p[j + 1]) j ++;
nt[i] = j;
}
int cnt = 0;
for (int i = m + 2; i <= n + m + 1; i ++ )
cnt += (nt[i] == m);
if (!cnt) puts("-1\n-1");
else {
printf("%d\n", cnt);
for (int i = m + 2; i <= n + m + 1; i ++ )
if (nt[i] == m) printf("%d ", i - 2 * m);
printf("\n");
}
}
Secret word
思路
一个结论: 字符串的最小循环节等于 n − n e x t [ n ] n - next[n] n−next[n]
代码
inline void kmp() {
m = strlen(p + 1);
nt[1] = 0;
for (int i = 2, j = 0; i <= m; i ++ ) {
while (j && p[i] != p[j + 1]) j = nt[j];
if (p[i] == p[j + 1]) j ++;
nt[i] = j;
}
printf("%d\n", m - nt[m]);
}
Secret word
思路
本题我们可以将s反转后相接在s后,next记录的后缀值就能对应当前的前缀值
代码
inline void kmp() {
m = strlen(p + 1);
nt[1] = 0;
p[m + 1] = '@';
for (int i = m, j = m + 2; i >= 1; i --, j ++) p[j] = p[i];
for (int i = 2, j = 0; i <= 2 * m + 1; i ++ ) {
while (j && p[i] != p[j + 1]) j = nt[j];
if (p[i] == p[j + 1]) j ++;
nt[i] = j;
}
int mx = 0;
for (int i = m + 2; i <= 2 * m + 1; i ++ ) mx = max(mx, nt[i]);
for (int i = mx; i >= 1; i -- ) printf("%c", p[i]);
puts("");
}
EXKMP(扩展KMP)
原理及用法
与
K
M
P
KMP
KMP主要计算字符串某一位置的向前的最大后缀相匹配的最大前缀的功能相反,扩展
K
M
P
KMP
KMP则为了解决字符串某一位置到字符串尾的最大后缀与最大前缀相匹配的问题。
而且扩展
K
M
P
KMP
KMP的思路相比
K
M
P
KMP
KMP来说也更好理解,我们只要从前往后维护一个
[
L
,
R
]
[L,R]
[L,R]的区间,区间内部的点可以直接通过目前后缀所对应的前缀位置已经得到的答案来初步计算,如果已得出的后缀长度没有超出所维护的区间的长度,则前后缀的答案是相等的,否则就向后暴力枚举更新位置。
而区间外的位置同样只需暴力枚举答案即可,因为整个字符串向后枚举的最多N次,所以扩展KMP的时间复杂度是
O
(
N
)
O(N)
O(N)的。
int L = 1, R = 0;
for (int i = 2; i <= n; i ++ ) {
if (i <= R) {
int k = i - L + 1;
z[i] = min(z[k], R - i + 1);
}
while (i + z[i] <= n + m + 1 && p[1 + z[i]] == p[i + z[i]]) ++ z[i];
if (i + z[i] - 1> R) L = i, R = i + z[i] - 1;
}
其中 z [ ] z[] z[]数组的实际意义是与KMP的 n e x t next next数组相对应的
题目
字符串匹配例题
思路
本题同样可以使用 EXKMP来得出答案,注意前提是必须将两个字符串合并来计算 z [ ] z[] z[]数组
代码
inline void exkmp() {
n = strlen(s + 1);
m = strlen(p + 1);
p[m + 1] = '@';
for (int i = m + 2, j = 1; j <= n; i ++, j ++ ) p[i] = s[j];
int L = 1, R = 0;
for (int i = 2; i <= n + m + 1; i ++ ) {
if (i <= R) {
int k = i - L + 1;
z[i] = min(z[k], R - i + 1);
}
while (i + z[i] <= n + m + 1 && p[1 + z[i]] == p[i + z[i]]) ++ z[i];
if (i + z[i] - 1> R) L = i, R = i + z[i] - 1;
}
int cnt = 0;
for (int i = m + 2; i <= n + m + 1; i ++ ) cnt += (z[i] == m);
if (!cnt) puts("-1\n-1");
else {
printf("%d\n", cnt);
for (int i = m + 2; i <= n + m + 1; i ++ )
if (z[i] == m) printf("%d ", i - m - 1);
puts("");
}
}
Password
思路
根据
z
[
]
z[]
z[]数组的实际意义,当
z
[
i
]
z[i]
z[i]
n
−
i
+
1
n - i + 1
n−i+1时,则这个位置的后缀刚好是字符串的后缀及前缀,其满足了题目中的前后缀要求,而剩余的中间位置出现的条件我们可以转化为从当前
i
i
i位置向前能否找到一个位置的
z
[
j
]
z[j]
z[j] 大于
i
−
n
+
1
i-n+1
i−n+1,因为匹配了更长的前缀显然可以匹配更短的前缀,我们可以截取长度为
i
−
n
+
1
i-n+1
i−n+1的前缀来匹配i位置的后缀。
所以我们只要从前往后遍历字符串找一个最大的后缀长度同时满足以上两个条件即可。
代码
inline void exkmp() {
m = strlen(p + 1);
int L = 1, R = 0;
for (int i = 2; i <= m; i ++ ) {
if (i <= R) {
int k = i - L + 1;
z[i] = min(z[k], R - i + 1);
}
while (i + z[i] <= m && p[1 + z[i]] == p[i + z[i]]) ++ z[i];
if (i + z[i] - 1> R) L = i, R = i + z[i] - 1;
}
int ans = 0, x = 0;
// for (int i = 1; i <= m; i ++ ) cout << i + z[i] - 1 << ' ';
// cout << endl;
for (int i = 1; i <= m; i ++ ) {
if (i + z[i] - 1 == m)
if (x >= z[i]) {
ans = z[i];
break;
}
x = max(x, z[i]);
}
if (!ans) puts("Just a legend\n");
else {
for (int i = 1; i <= ans; i ++ ) printf("%c", p[i]);
puts("");
}
}