- 参考《算法笔记》
目录
字符串 hash
字符串 hash
- 对只有大写字母的字符串,可以将字符串当作二十六进制的数, 然后将其转换为十进制。其中
str[i]
表示字符串的 i i i 号位,index
函数将 A ~ Z 转换为 0 ~ 25,H[i]
表示字符串的前 i i i 个字符的 hash 值
H [ i ] = H [ i − 1 ] × 26 + i n d e x ( s t r [ i ] ) H[i] = H[i-1] \times 26+ index(str[i]) H[i]=H[i−1]×26+index(str[i])在这个转换方式中, 虽然字符串与整数是一一对应的, 但由于没有进行适当处理, 因此当字符串长度较长时, 产生的整数会非常大, 没办法用一般的数据类型保存 - 为了应对这种情况, 只能舍弃一些 “唯一性 ”,将产生的结果对一个整数 mod 取模
H [ i ] = ( H [ i − 1 ] × 26 + i n d e x ( s t r [ i ] ) ) % m o d H[i] = (H[i- 1] \times 26 + index(str[i]))\ \%\ mod H[i]=(H[i−1]×26+index(str[i])) % mod但可能有多个字符串的 hash 值相同, 导致冲突 - 不过幸运的是, 在实践中发现, 在
int
数据范围内, 如果把进制数设置为一个 1 0 7 10^7 107 级别的素数p
(例如 10000019), 同时把mod
设置为一 个 1 0 9 10^9 109 级别的素数 (例如 1000000007), 那么冲突的概率将会变得非常小,很难产生冲突
H [ i ] = ( H [ i − 1 ] × p + i n d e x ( s t r [ i ] ) ) % m o d H[i] = (H[i-1]\times p+ index(str[i]))\ \%\ mod H[i]=(H[i−1]×p+index(str[i])) % mod- 如果确实碰到了极其针对进制数
p
= 10000019、模数mod
= 1000000007 的数据, 只需要调整p
和mod
就可以使其不冲突,或者使用效果更强的双 hash 法,用两个 hash 函数生成的整数组合表示一个字符串,例如可以使用孪生素数mod1
= 1000000007 和mod2
= 1000000009 作为模数,进制数p
保持 10000019 不变,然后用pair
组合H1[i]
与H2[i]
来代表一个字符串,就可以基本保证不冲突
H 1 [ i ] = ( H 1 [ i − 1 ] × p + i n d e x ( s t r [ i ] ) ) % m o d 1 H 2 [ i ] = ( H 2 [ i − 1 ] × p + i n d e x ( s t r [ i ] ) ) % m o d 2 H_1[i] = (H_1[i-1]\times p+ index(str[i]))\ \%\ mod_1 \\H_2[i] = (H_2[i-1]\times p+ index(str[i]))\ \%\ mod_2 H1[i]=(H1[i−1]×p+index(str[i])) % mod1H2[i]=(H2[i−1]×p+index(str[i])) % mod2
- 如果确实碰到了极其针对进制数
- 问题:给出
N
N
N 个只有小写字母的字符串, 求其中不同的字符串的个数 (也可以用
set
或者map
直接一步实现, 但是速度比字符串 hash 会慢一点)
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
const int MOD = 1000000007; // 1e9+7
const int P = 10000019; // 1e7+19
vector<int> ans;
// 字符串 hash
long long hashFunc(string str)
{
long long H = 0; // 使用 long long 避免溢出
for(int i = 0; i < str.length(); i++) {
H = (H * P + str[i] -'a') % MOD;
}
return H;
}
int main() {
string str;
while(getline(cin, str), str != "#") { // 输入 str 直到 # 时停止
long long id = hashFunc(str);
ans.push_back(id);
}
sort(ans.begin(), ans.end());
int count = 0;
for (int i = 0; i < ans.size(); i++) {
if(i == 0 || ans[i] != ans[i - 1]) {
count++;
}
}
cout << count << endl;
return 0;
}
子串 hash
即,求解
H[i...j]
H [ i ⋯ j ] = index ( str [ i ] ) × p j − i + i n d e x ( str [ i + 1 ] ) × p j − i − 1 + index ( str [ j ] ) × p 0 \mathrm{H}[\mathrm{i} \cdots \mathrm{j}]=\operatorname{index}(\operatorname{str}[\mathrm{i}]) \times \mathrm{p}^{\mathrm{j}-\mathrm{i}}+\mathrm{index}(\operatorname{str}[\mathrm{i}+1]) \times \mathrm{p}^{\mathrm{j}-\mathrm{i}-1}+\text { index }(\operatorname{str}[\mathrm{j}]) \times \mathrm{p}^{0} H[i⋯j]=index(str[i])×pj−i+index(str[i+1])×pj−i−1+ index (str[j])×p0
- 然后尝试通过
H[j]
的散列函数来推导出H[i...j]
:
H [ j ] = H [ j − 1 ] × p + i n d e x ( s t r [ j ] ) = ( H [ j − 2 ] × p + index ( str [ j − 1 ] ) ) × p + index ( str [ j ] ) = H [ j − 2 ] × p 2 + index ( str [ j − 1 ] ) × p + i n d e x ( str [ j ] ) = … = H [ i − 1 ] × p j − i + 1 + index ( str [ i ] ) × p j − i + … + i n d e x ( str [ j ] ) × p 0 = H [ i − 1 ] × p j − i + 1 + H [ i … j ] \begin{aligned} \mathrm{H}[\mathrm{j}] &=\mathrm{H}[\mathrm{j}-1] \times \mathrm{p}+\mathrm{index}(\mathrm{str}[\mathrm{j}]) \\ &=(\mathrm{H}[\mathrm{j}-2] \times \mathrm{p}+\operatorname{index}(\operatorname{str}[\mathrm{j}-1])) \times \mathrm{p}+\operatorname{index}(\operatorname{str}[\mathrm{j}]) \\ &=\mathrm{H}[\mathrm{j}-2] \times \mathrm{p}^{2}+\text { index }(\operatorname{str}[\mathrm{j}-1]) \times \mathrm{p}+\mathrm{index}(\operatorname{str}[\mathrm{j}]) \\ &=\ldots \\ &=\mathrm{H}[\mathrm{i}-1] \times \mathrm{p}^{\mathrm{j}-\mathrm{i}+1}+\operatorname{index}(\operatorname{str}[\mathrm{i}]) \times \mathrm{p}^{\mathrm{j}-\mathrm{i}}+\ldots+\mathrm{index}(\operatorname{str}[\mathrm{j}]) \times \mathrm{p}^{0} \\ &=\mathrm{H}[\mathrm{i}-1] \times \mathrm{p}^{\mathrm{j}-\mathrm{i}+1}+\mathrm{H}[\mathrm{i} \ldots \mathrm{j}] \end{aligned} H[j]=H[j−1]×p+index(str[j])=(H[j−2]×p+index(str[j−1]))×p+index(str[j])=H[j−2]×p2+ index (str[j−1])×p+index(str[j])=…=H[i−1]×pj−i+1+index(str[i])×pj−i+…+index(str[j])×p0=H[i−1]×pj−i+1+H[i…j]因此
H [ i ⋯ j ] = H [ j ] − H [ i − 1 ] × p j − i + 1 \mathrm{H}[\mathrm{i} \cdots \mathrm{j}]=\mathrm{H}[\mathrm{j}]-\mathrm{H}[\mathrm{i}-1] \times \mathrm{p}^{\mathrm{j}-\mathrm{i}+1} H[i⋯j]=H[j]−H[i−1]×pj−i+1加上原先的取模操作就可以得到
H [ i ⋯ j ] = ( H [ j ] − H [ i − 1 ] × p j − i + 1 ) % m o d \mathrm{H}[\mathrm{i} \cdots \mathrm{j}]=(\mathrm{H}[\mathrm{j}]-\mathrm{H}[\mathrm{i}-1] \times \mathrm{p}^{\mathrm{j}-\mathrm{i}+1})\ \%\ mod H[i⋯j]=(H[j]−H[i−1]×pj−i+1) % mod由于括号内部可能小于 0, 因此为了使结果非负,需要先对结果取模,然后加上 m o d mod mod 后再次取模,以得到正确的结果
H [ i ⋯ j ] = ( ( H [ j ] − H [ i − 1 ] × p j − i + 1 ) % m o d + m o d ) % m o d \mathrm{H}[\mathrm{i} \cdots \mathrm{j}]=((\mathrm{H}[\mathrm{j}]-\mathrm{H}[\mathrm{i}-1] \times \mathrm{p}^{\mathrm{j}-\mathrm{i}+1})\ \%\ mod+mod)\ \%\ mod H[i⋯j]=((H[j]−H[i−1]×pj−i+1) % mod+mod) % mod
- 问题:输入两个长度均不超过 1000 的字符串,求它们的最长公共子串的长度 (子串必须连续)
- 可以先分别对两个字符串的每个子串求出 hash 值 (同时记录对应的长度),然后找出两堆子串对应的 hash 值中相等的那些,便可以找到最大长度,时间复杂度为 O ( n 2 + m 2 ) O(n^2+m^2) O(n2+m2), 其中 n n n 和 m m m 分别为两个字符串的长度
#include <iostream>
#include <cstdio>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1000000007;
const LL P = 10000019;
const LL MAXN = 1010; // MAXN 为字符串最长长度
// powP[i] 存放 P^i % MOD, H1 和 H2 分别存放 str1 和 str2 的 hash 值
LL powP[MAXN], H1[MAXN] = {0}, H2[MAXN] = {0};
// pr1 存放 str1 的所有 <子串 hash 值,子串长度>, pr2 同理
vector<pair<int, int>> pr1, pr2;
// init 函数初始化 powP 函数
void init(int len) {
powP[0] = 1;
for(int i = 1; i <= len; i++) {
powP[i] = (powP[i - 1] * P) % MOD;
}
}
// calH 函数计算字符串 str 的 hash 值
void calH(LL H[], string &str) {
H[0] = str[0]; // H[0] 单独处理
for(int i = 1; i < str.length(); i++) {
H[i] = (H[i - 1] * P + str[i]) % MOD;
}
}
// calSingleSubH 计算 H[i...j]
int calSingleSubH(LL H[], int i, int j) {
if(i == 0)
return H[j];
return ((H[j] - H[i - 1] * powP(j - i + 1)) % MOD + MOD) % MOD;
}
// calSubH 计算所有子串的 hash 值, 并将 <子串 hash 值, 子串长度> 存入 pr
void calSubH(LL H[], int len, vector<pair<int, int>>&pr) {
for(int i = 0; i < len; i++) {
for(int j = i; j < len; j++) {
int hashValue = calSingleSubH(H, i, j);
pr.push_back(make_pair(hashValue, j - i + 1));
}
}
}
// 计算 pr1 和 pr2 中相同的 hash 值,维护最大长度
int getMax() {
int ans = 0;
for(int i = 0; i < pr1.size(); i++) {
for(int j = 0; j < pr2.size(); j++)
if(pr1[i].first == pr2[j].first)
ans = max(ans, pr1[i].second);
}
}
}
return ans;
}
int main() {
string str1, str2;
getline(cin, str1);
getline(cin, str2);
init(max(str1.length(), str2.length())); // 初始化 powP 数组
calH(H1, strl); // 分别计算 str1 和 str2 的 hash 值
calH(H2, str2);
calSubH(H1, str1.length(), pr1);
calSubH(H2, str2.length(), pr2);
printf("ans = %d\n", getMax());
return 0;
}
最长回文子串
- 这里将用字符串 hash + 二分的思路去解决它,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
- 对一个给定的字符串
str
, 可以先求出其字符串 hash 数组H1
, 然后再将str
反转,求出反转字符串rstr
的 hash 数组H2
, 接着分回文串的奇偶情况进行讨论- (1) 回文串的长度是奇数:枚举回文中心点
i
i
i, 二分子串的半径
k
k
k, 找到最大的使子串
[
i
−
k
,
i
+
k
]
[i-k, i + k]
[i−k,i+k] 是回文串的
k
k
k
- 其中判断子串
[
i
−
k
,
i
+
k
]
[i-k,i + k]
[i−k,i+k] 是回文串等价于判断
str
的两个子串 [ i − k , i ] [i-k, i] [i−k,i] 与 [ i , i + k ] [i, i+ k] [i,i+k] 是否是相反的串。而这等价于判断str
的 [ i − k , i ] [i-k, i] [i−k,i] 子串与反转字符串rstr
的 [ l e n − 1 − ( i + k ) , l e n − 1 − i ] [len- 1 - (i + k),len-1- i] [len−1−(i+k),len−1−i] 子串是否相同, 因此只需要判断 H 1 [ i − k ⋅ ⋅ ⋅ i ] H_1[i - k···i] H1[i−k⋅⋅⋅i] 与 H 2 [ l e n − 1 − ( i + k ) . . . l e n − 1 − i ] H_2[len- 1 - (i + k)...len - 1 - i] H2[len−1−(i+k)...len−1−i] 是否相等即可
- 其中判断子串
[
i
−
k
,
i
+
k
]
[i-k,i + k]
[i−k,i+k] 是回文串等价于判断
- (2) 回文串的长度是偶数:枚举回文空隙点, 令
i
i
i 表示空隙左边第一个元素的下标,二分子串的半径
k
k
k, 找到最大的使子串
[
i
−
k
+
1
,
i
+
k
]
[i - k + 1, i + k]
[i−k+1,i+k] 是回文串的
k
k
k
- 其中判断子串
[
i
−
k
+
1
,
i
+
k
]
[i-k + 1, i + k]
[i−k+1,i+k] 是回文串等价于判断
str
的 [ i − k + 1 , i ] [i-k+ 1, i] [i−k+1,i] 子串与反转字符串rstr
的 [ l e n − 1 − ( i + k ) , l e n − 1 − ( i + 1 ) ] [len- 1 - (i + k), len -1 - (i + 1)] [len−1−(i+k),len−1−(i+1)] 子串是否相同,因此只需要判断 H 1 [ i − k + 1... i ] H_1[i-k+ 1...i] H1[i−k+1...i] 与 H 2 [ l e n − 1 − ( i + k ) . . . l e n − 1 − ( i + 1 ) ] H_2[len - 1 - (i + k)...len -1 - (i + 1)] H2[len−1−(i+k)...len−1−(i+1)] 是否相等即可。
- 其中判断子串
[
i
−
k
+
1
,
i
+
k
]
[i-k + 1, i + k]
[i−k+1,i+k] 是回文串等价于判断
- (1) 回文串的长度是奇数:枚举回文中心点
i
i
i, 二分子串的半径
k
k
k, 找到最大的使子串
[
i
−
k
,
i
+
k
]
[i-k, i + k]
[i−k,i+k] 是回文串的
k
k
k
#include <iostream>
#include <cstdio>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1000000007; // MOD 为计算 hash 值时的模数
const LL P = 10000019; // P 为计算 hash 值时的进制数
const LL MAXN = 200010; // MAXN 为字符串最长长度
// powP[i] 存放 P^i % MOD, H1 和 H2 分别存放 str 和 rstr 的 hash 值
LL powP[MAXN], H1[MAXN], H2[MAXN];
// init 函数初始化 powP 函数
void init() {
powP[0] = 1;
for(int i = 1; i < MAXN; i++) {
powP[i] = (powP[i - 1] * P) % MOD;
}
}
// calH 函数计算字符串 str 的 hash 值
void calH(LL H[], string &str) {
H[0] = str[0];
for(int i = 1; i < str.length(); i++) {
H[i] = (H[i - 1] * P + str[i]) % MOD;
}
}
// calSingleSubH 计算 H[i···j]
int calSingleSubH(LL H[], int i, int j) {
if (i == 0)
return H[j];
return ((H[j] - H[i - 1] * powP[j - i + 1]) % MOD + MOD) % MOD;
}
// 对称点为 i, 字符串长 len, 在 [l, r] 里二分回文半径
// 寻找最后一个满足条件"hashL == hashR"的回文半径
// 等价于寻找第一个满足条件"hashL != hashR"的回文半径,然后减 1 即可
// isEven 当求奇回文时为 0, 当求偶回文时为 1
int binarySearch(int l, int r, int len, int i, int isEven) {
while (l < r) { // 当出现 l == r 时结束 (因为范围是 [l, r])
int mid = (l + r) / 2;
// 左半子串 hash 值 H1[H1L…H1R], 右半子串 hash 值 H2[H2L…H2R]
int H1L = i - mid + isEven, H1R = i;
int H2L = len - 1 - (i + mid), H2R = len - 1 - (i + isEven);
int hashL = calSingleSubH(H1, H1L, H1R);
int hashR = calSingleSubH(H2, H2L, H2R);
if (hashL != hashR)
r = mid; // hash 值不等, 说明回文半径<=mid
else
l = mid + 1; // hash值相等, 说明回文半径>mid
}
return l - 1; // 返回最大回文半径
}
int main() {
init(); // 初始化 powP
string str;
getline(cin, str);
calH(H1, str); // 计算 str 的 hash 数组
reverse(str.begin(), str.end()) ; // 将字符串反转
calH(H2, str); // 计算 rstr 的 hash 数组
int ans = 0;
// 奇回文
for(int i = 0; i < str.length(); i++) {
// 二分上界为分界点 i 的左右长度的较小值加 1
int maxLen = min(i, (int)str.length() - 1 - i) + 1;
int k = binarySearch(0, maxLen, str.length(), i, 0);
ans = max(ans, k * 2 + 1);
}
// 偶回文
for (int i = 0; i < str.length(); i++) {
// 二分上界为分界点 i 的左右长度的较小值加 1 (注意左长为 i+1)
int maxLen = min(i + 1, (int)str.length() - 1 - i) + 1;
int k = binarySearch(0, maxLen, str.length(), i, 1);
ans = max(ans, k * 2);
}
printf("%d\n", ans);
return 0;
}
PAT (Advanced level) 1040 Longest Symmetric String
- 典型的最长回文子串问题
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
using LL = long long;
const int MAXN = 1002;
const LL P = 1e7 + 19, Mod = 1e9 + 7;
char str[MAXN];
LL powP[MAXN], H1[MAXN], H2[MAXN];
void init(int len)
{
powP[0] = 1;
for (int i = 1; i <= len; ++i)
{
powP[i] = (powP[i - 1] * P) % Mod;
}
}
void calcH(LL H[], int len)
{
H[0] = str[0];
for (int i = 1; i != len; ++i)
{
H[i] = (H[i - 1] * P + str[i]) % Mod;
}
}
LL calcSubStrH(LL H[], int i, int j)
{
if (i == 0)
{
return H[j];
}
else {
return ((H[j] - H[i - 1] * powP[j - i + 1]) % Mod + Mod) % Mod;
}
}
int binarySearch(int left, int right, int i, int len, int isEven)
{
while (left < right)
{
int mid = (left + right) / 2;
int H1L = i - mid + isEven, H1R = i;
int H2L = (len - 1 - (i + mid)), H2R = (len - 1 - (i + isEven));
int h1 = calcSubStrH(H1, H1L, H1R);
int h2 = calcSubStrH(H2, H2L, H2R);
if (h1 == h2)
{
left = mid + 1;
}
else {
right = mid;
}
}
return left - 1;
}
int main(void)
{
fgets(str, MAXN, stdin);
int len = strlen(str) - 1;
init(len);
calcH(H1, len);
reverse(str, str + len);
calcH(H2, len);
int ans = -1;
// 奇回文
for (int i = 0; i != len; ++i)
{
int r = binarySearch(0, min(i, len - 1 - i) + 1, i, len, 0);
ans = max(ans, 2 * r + 1);
}
// 偶回文
for (int i = 0; i < len - 1; ++i)
{
int r = binarySearch(0, min(i + 1, len - 1 - i) + 1, i, len, 1);
ans = max(ans, 2 * r);
}
printf("%d", ans);
return 0;
}
KMP 算法
字符串的匹配问题
- 如果给出两个字符串
text
和pattern
, 需要判断字符串pattern
是否是字符串text
的子串。 一般把字符串text
称为文本串,而把字符串pattern
称为模式串- 例如,给定文本串
text="caniwaitforyourheart"
, 那么模式串pattem="wait"
是它的子串
- 例如,给定文本串
- 如果暴力求解,时间复杂度为 O ( m n ) O(mn) O(mn),其中 n n n 和 m m m 分别是文本串与模式串的长度。 显然, 当 n n n 和 m m m 都达到 1 0 5 10^5 105 级别的时候完全无法承受
- 而 KMP 算法, 时间复杂度为 O ( n + m ) O(n +m) O(n+m)
KMP 算法是由 Knuth、 Morris、 Pratt 这 3 位科学家共同发现的,这也是其名字的由来
next
数组
next
数组
- 假设有一个字符串
s
s
s (下标从 0 开始), 那么它以
i
i
i 号位作为结尾的子串就是
s
[
0...
i
]
s[0...i]
s[0...i]。对该子串来说, 长度为
k
+
1
k+1
k+1 的前缀和后缀分别是
s
[
0...
k
]
s[0...k]
s[0...k] 与
s
[
i
−
k
.
.
.
i
]
s[i-k...i]
s[i−k...i]。 现在定义一个
int
型数组next
, 其中next[i]
表示使子串 s [ 0... i ] s[0...i] s[0...i] 的前缀 s [ 0... k ] s[0...k] s[0...k] 等于后缀 s [ i − k . . . i ] s[i-k...i] s[i−k...i] 的最大的 k k k (注意前缀跟后缀可以部分重叠, 但不能是 s [ 0... i ] s[0...i] s[0...i] 本身);如果找不到相等的前后缀, 那么就令next[i] = -1
。显然,next[i]
就是所求最长相等前后缀中前缀最后位的下标- 例如,对字符串
s
=
"
a
b
a
b
a
a
b
"
s = "ababaab"
s="ababaab" 作为举例,
next
数组的计算过程如下所示,可以结合图 12-1 进行理解。 图中对每个next[i]
的计算都给了两种阅读方式, 其中上框直接用下画线画出了子串 s [ 0... i ] s[0...i] s[0...i] 的最长相等前后缀, 而下框将子串 s [ 0... i ] s[0...i] s[0...i] 写在两行, 让第一行提供后缀, 第二行提供前缀, 然后将相等的最长前后缀框起来:- e.g.
i
=
0
i = 0
i=0 时: 子串
s
[
0...
i
]
s[0...i]
s[0...i] 为 “
a
a
a”, 由于找不到相等的前后缀 (前后缀均不能是子串
s
[
0...
i
]
s[0...i]
s[0...i] 本身), 因此令
next[0] = -1
; 其余情况同理
- e.g.
i
=
0
i = 0
i=0 时: 子串
s
[
0...
i
]
s[0...i]
s[0...i] 为 “
a
a
a”, 由于找不到相等的前后缀 (前后缀均不能是子串
s
[
0...
i
]
s[0...i]
s[0...i] 本身), 因此令
- 例如,对字符串
s
=
"
a
b
a
b
a
a
b
"
s = "ababaab"
s="ababaab" 作为举例,
递推求解 next
数组 (假设已经求出了 next[0]
~ next[i-1]
, 现在要用它们来推出 next[i]
)
- 以求字符串 “
a
b
a
b
a
b
c
abababc
abababc” 的
next
数组为例,假设已经有了next[0] = -1
、next[1]= -1
、next[2] = 0
、next[3] = 1
, 现在来求解next[4]
- 如图 12-2 所示,当已经得到
next[3] = 1
时,最长相等前后缀为 “ a b ab ab”, 之后在计算next[4]
时,由于s[4] = s[next[3] + 1]
, 因此可以把最长相等前后缀 “ a b ab ab” 扩展为 “ a b a aba aba”, 因此next[4] = next[3] + 1 = 2
, 并令 j j j 指向next[4]
- 如图 12-2 所示,当已经得到
- 接着在此基础上求解
next[5]
。如图 12-3 所示,当已经得到next[4] = 2
时,由于s[5] != s[next[4] + 1]
, 因此不能扩展最长相等前后缀。 这个时候不妨缩短前后缀长度!此时希望找到一个最大的 j j j, 使得s[5] = s[j + 1]
, 并且s[0...j]
(图中的波浪线 ∼ \sim ∼) 是s[0…2]
= “ a b a aba aba” 的后缀(而显然s[0...j]
是s[0…2]
的前缀)。- 可见,如果暂时不考虑
s[5] = s[j + 1]
的条件,s[0...j]
实际上就是s[0…2]
的最长相等前后缀。因此,只要令j = next[2]
, 然后再判断s[5] = s[j + 1]
是否成立:如果成立,说明s[0...j+1]
是s[0...5]
的最长相等前后缀,令next[5] = j + 1
即可;如果不成立,就不断让j = next[i]
, 直到 j j j 回到了 − 1 -1 −1, 或是途中s[5]= s[j + 1]
成立
- 可见,如果暂时不考虑
- 由上面的例子可以发现,每次求出
next[i]
时,总是让 j j j 指向next[i]
, 以方便继续求解next[i + 1]
之用。由于next[0] = -1
一定成立,因此初始情况下可以令 j j j 指向 − 1 -1 −1
// getNext 求解长度为 len 的字符串 s 的 next 数组
void getNext(char s[], int len)
{
int j = -1;
next[0] = -1; // 初始化 next 数组,令 j= next[0] = -1
for (int i = 1; i < len; ++i) { // 递推求解
while (j != -1 && s[i] != s[j + 1]) {
j = next[j];
}
if (s[i] == s[j + 1]) {
++j;
}
next[i] = j;
}
}
KMP 算法
- KMP 算法与之前的求
next
数组的思想十分相似。此处给定一个文本串text
和一个模式串pattern
(长度为 m m m), 然后判断模式串pattern
是否是文本串text
的子串
KMP 算法
- 以
text = "abababaabc"
、pattern = "ababaab"
为例。如图 12-6 所示 ,令 i i i 指向text
的当前欲比较位,令 j j j 指向pattern
中当前已被匹配的最后位,我们发现text[i]= pattern[j + 1]
成立,也就是pattern[j + 1]
也被成功匹配,此时 i , j i,j i,j 均加 1 以继续比较,直到 j j j 到达 m − 1 m-1 m−1 说明pattern
是text
的子串
- 接着继续匹配,如图 12-7 所示。此时
i
i
i 指向
text[5]
、 j j j 指向pattern[4]
, 表明pattern[0...4]
已经全部匹配成功。但此处text[5] != pattem[4 + 1]
, 匹配失败。此时我们应寻求回退到一个离当前的 j j j 最近的 j ′ j' j′ 使得text[i] = pattem[j' + 1]
能够成立, 并且pattern[0...j']
仍然与text
的相应位置处于匹配状态, 即pattern[0·· ·j']
是pattern[0·· ·j]
的后缀 (显然也为前缀)。 这很容易令人想到之前求next
数组时碰到的类似问题。 也就是说,只需要不断令j = next[j]
, 直到 j j j 回退到 − 1 -1 −1 或是text[i] = pattern[j + 1]
成立,然后继续匹配即可 (可以看到, j j j 即为pattern
当前已被匹配的最后位)
// KMP 算法,判断 pattern 是否是 text 的子串
bool KMP(char text[], char pattern[]) {
int n = strlen(text), m = strlen(pattern); // 字符串长度
getNext(pattern, m); // 计算 pattern 的 next 数组
int j = -1; // 初始化 j 为 -1,表示当前还没有任意一位被匹配
for(int i = 0; i < n; i++) { // 试图匹配 text[i]
while(j != -1 && text[i] != pattern[j + 1]) {
j = next[j]; // 不断回退
}
if (text[i] == pattern[j + 1]) {
j++;
}
if (j == m - 1) { // pattern 完全匹配,说明 pattern 是 text 的子串
return true;
}
}
return false; // 执行完 text 还没匹配成功,说明 pattern 不是 text 的子串
}
统计文本串 text
中模式串 pattern
出现的次数
// KMP 算法,统计 pattern 在 text 中出现的次数
int KMP(char text[], char pattern[]) {
int n = strlen(text), m = strlen(pattern);
getNext(pattern, m); // 计算 pattern 的 next 数组
int ans = 0, j = -1; // ans 表示成功匹配次数
for(int i = 0; i < n; i++) { // 试图匹配 text[i]
while (j != -1 && text[i] != pattern[j + 1]) {
j = next[j];
}
if(text[i] == pattern[j + 1]) {
j++;
}
if(j == m - 1) { // pattern 完全匹配
ans++; // 成功匹配次数加1
j = next[j]; // 让 j 回退到 next[j] 继续匹配
}
}
return ans; // 返回成功匹配次数
}
时间复杂度
- 首先,整个
for
循环中 i i i 是不断加 1 的,所以在整个过程中 i i i 的变化次数是 O ( n ) O(n) O(n) 级别。 接下来考虑 j j j 的变化,我们注意到 j j j 只会在一行中增加,并且每次只加 1,这样在整个过程中 j j j 最多只会增加 n n n 次;而其他地方的 j j j 都是不断减小的, 由于 j j j 最小不会小于 − 1 -1 −1, 因此在整个过程中 j j j 最多只能减少 n n n 次(否则 j j j 就会小于 − 1 -1 −1 了),也就是说while
循环对整个过程来说最多只会执行 n n n 次, 因此 j j j 在整个过程中的变化次数是 O ( n ) O(n) O(n) 级别的。 由于 i i i 和 j j j 在整个过程中的变化次数都是 O ( n ) O(n) O(n), 因此for
循环部分的整体时间复杂度就是 O ( n ) O(n) O(n)。考虑到计算next
数组需要 O ( m ) O(m) O(m) 的时间复杂度 (用同样的分析方法可以得到), 因此
T ( n ) = O ( n + m ) T(n)= O(n+m) T(n)=O(n+m)
优化
- 来看下面这种情况: 用模式串 “
a
b
a
b
a
b
ababab
ababab” 去匹配文本串 “
a
b
a
b
a
c
a
b
ababacab
ababacab”, 其中试图匹配字符 ‘
c
c
c’ 的过程如图 12-9 所示。在这个例子中,一开始
i
=
5
i= 5
i=5、
j
=
4
j= 4
j=4, 因此
text[i]= 'c'
、pattern[j + 1] = 'b'
, 它们不匹配;接着 j j j 回退到next[4] = 2
, 发现pattern[j + 1]
还是 ‘ b b b’, 还是不匹配;于是 j j j 回退到next[2] = 0
, 此时又有pattem[j + 1]
是 ‘ b b b’ 毫无疑问肯定还是不匹配;最后 j j j 回退到next[0] = -1
, 此时终于出现一个pattem[j + 1]
不是 ‘ b b b’ 的了,可以和text[i]
比较了。显然,在第一次text[i]
与 ‘ b b b’ 发生失配之后,接下来一连串的 ‘ b b b’ 是必然失配的,它们与text[i]
的比较毫无意义,要是能想办法直接跳过这些 ‘ b b b’, 就能提高一定效率
- 可以想到, 如果能修改
next[j]
存放的内容, 让它可以跳过无意义回退的部分,一步回退到恰当的位置,即让pattern[j + 1] != pattern[next[j] + 1]
能够直接成立, 这样当 j + 1 j+ 1 j+1 位失配时就只需要一次回退了。这个过程只需要在求解next
数组过程的基础上稍作修改即可得到 - 优化后的
next
数组被称为nextval
数组, 它丢失了next
数组的最长相等前后缀的含义,却让失配时的处理达到了最优,因此nextval[i]
的含义应该理解为当模式串pattern
的 i + 1 i + 1 i+1 位发生失配时, i i i 应当回退到的最佳位置
// getNextval 求解长度为 len 的字符串 s 的 nextval 数组
void getNextval(char s[], int len) {
int j = -1;
nextval[0] = -1;
for (int i = 1; i < len; i++) { // 求解 nextval[1] ~ nextval[len - 1]
while(j != -1 && s[i] != s[j + 1]) {
j = nextval[j];
}
if (s[i] == s[j + 1])
{
j++; // 令 j 指向原 next[i] 的位置
}
// 与 getNext 函数相比只有下面不同
if (j == -1 || s[i + 1] != s[j + 1]) { // j == -1 不需要回退
nextval[i] = j;
} else {
nextval[i] = nextval[j]; // s[i + 1] == s[next[i] + 1] 时,需继续退回
// 注意:nextval[j] 是已求出的最优退回位置,直接继承即可
}
}
}
可能会有人疑惑,为什么在
s[i + 1] != s[j + 1]
的判断前不需要加个i < len
的判断。事实上从nextval
的含义上来说, 如果 i i i 已经是模式串pattern
的最后一位, 那么 i + 1 i + 1 i+1 位失配的说法从匹配的角度来讲是没有意义的(由于s[len] = '\0'
, 且 j j j 一定小于 i i i, 因此一定会失配),也就是说,nextval[len - 1]
本身其实可有可无,它在 KMP 算法的匹配过程中不会被用到
值得注意的是, 由
nextval
数组的含义,getNextval
算法和 KMP 算法中的while
都可以替换成if
, 因为每次最多只会执行一次
从有限状态自动机的角度看待 KMP 算法
- 对 KMP 算法来说, 实际上相当于对模式串
pattern
构造一个有限状态状态自动机, 然后把文本串text
的字符从头到尾一个个送入这个自动机, 如果自动机可以从初始状态开始达到终止状态, 那么说明pattern
是text
的子串
注意到,图中所有回退的箭头其实就是
next
数组代表的位置 (其中 − 1 -1 −1 和 0 0 0 可以统合并为起始状态)
补充: 如果把这个自动机推广为树形, 就会产生字典树 (也叫前缀树), 此时就可以解决多维字符串匹配问题, 即多个文本串匹配多个模式串的匹配问题。 通常把解决多维字符串匹配问题的算法称为 AC 自动机,事实上 KMP 算法只是 AC 自动机的特殊情形