机试: 字符串专题

  • 参考《算法笔记》

字符串 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[i1]×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[i1]×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[i1]×p+index(str[i])) % mod
    • 如果确实碰到了极其针对进制数 p = 10000019、模数 mod = 1000000007 的数据, 只需要调整 pmod 就可以使其不冲突,或者使用效果更强的双 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[i1]×p+index(str[i])) % mod1H2[i]=(H2[i1]×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[ij]=index(str[i])×pji+index(str[i+1])×pji1+ 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[j1]×p+index(str[j])=(H[j2]×p+index(str[j1]))×p+index(str[j])=H[j2]×p2+ index (str[j1])×p+index(str[j])==H[i1]×pji+1+index(str[i])×pji++index(str[j])×p0=H[i1]×pji+1+H[ij]因此
    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[ij]=H[j]H[i1]×pji+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[ij]=(H[j]H[i1]×pji+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[ij]=((H[j]H[i1]×pji+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] [ik,i+k] 是回文串的 k k k
      • 其中判断子串 [ i − k , i + k ] [i-k,i + k] [ik,i+k] 是回文串等价于判断 str 的两个子串 [ i − k , i ] [i-k, i] [ik,i] [ i , i + k ] [i, i+ k] [i,i+k] 是否是相反的串。而这等价于判断 str [ i − k , i ] [i-k, i] [ik,i] 子串与反转字符串 rstr [ l e n − 1 − ( i + k ) , l e n − 1 − i ] [len- 1 - (i + k),len-1- i] [len1(i+k),len1i] 子串是否相同, 因此只需要判断 H 1 [ i − k ⋅ ⋅ ⋅ i ] H_1[i - k···i] H1[iki] H 2 [ l e n − 1 − ( i + k ) . . . l e n − 1 − i ] H_2[len- 1 - (i + k)...len - 1 - i] H2[len1(i+k)...len1i] 是否相等即可
    • (2) 回文串的长度是数:枚举回文空隙点, 令 i i i 表示空隙左边第一个元素的下标,二分子串的半径 k k k, 找到最大的使子串 [ i − k + 1 , i + k ] [i - k + 1, i + k] [ik+1,i+k] 是回文串的 k k k
      • 其中判断子串 [ i − k + 1 , i + k ] [i-k + 1, i + k] [ik+1,i+k] 是回文串等价于判断 str [ i − k + 1 , i ] [i-k+ 1, i] [ik+1,i] 子串与反转字符串 rstr [ l e n − 1 − ( i + k ) , l e n − 1 − ( i + 1 ) ] [len- 1 - (i + k), len -1 - (i + 1)] [len1(i+k),len1(i+1)] 子串是否相同,因此只需要判断 H 1 [ i − k + 1... i ] H_1[i-k+ 1...i] H1[ik+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[len1(i+k)...len1(i+1)] 是否相等即可。
#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 算法

字符串的匹配问题

  • 如果给出两个字符串 textpattern, 需要判断字符串 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[ik...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[ik...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; 其余情况同理
        在这里插入图片描述

递推求解 next 数组 (假设已经求出了 next[0] ~ next[i-1], 现在要用它们来推出 next[i])

  • 以求字符串 “ a b a b a b c abababc abababc” 的 next 数组为例,假设已经有了 next[0] = -1next[1]= -1next[2] = 0next[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]
      在这里插入图片描述
  • 接着在此基础上求解 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 m1 说明 patterntext 的子串
    在这里插入图片描述
  • 接着继续匹配,如图 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 的字符从头到尾一个个送入这个自动机, 如果自动机可以从初始状态开始达到终止状态, 那么说明 patterntext 的子串
    在这里插入图片描述

注意到,图中所有回退的箭头其实就是 next 数组代表的位置 (其中 − 1 -1 1 0 0 0 可以统合并为起始状态)

补充: 如果把这个自动机推广为树形, 就会产生字典树 (也叫前缀树), 此时就可以解决多维字符串匹配问题, 即多个文本串匹配多个模式串的匹配问题。 通常把解决多维字符串匹配问题的算法称为 AC 自动机,事实上 KMP 算法只是 AC 自动机的特殊情形

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值