3 minute read

一·算法简介

1.简介:一种高效字符串匹配算法,通过预处理模式串生成next数组(部分匹配表),在匹配失败时跳过无效比较,将时间复杂度优化至O(n+m)(n为主串长度,m为模式串长度)

2.next数组

  • 定义​:next[i]表示模式串P[0..i]中,​最长相等真前缀和真后缀的长度​(不包括子串本身)
  • 作用​:当匹配失败时,根据next值移动模式串指针,避免主串回溯
  • 构建逻辑
    • P[0]:无前缀/后缀,next[0]=0
    • P[1..8]:若P[i] == P[j],则j++;否则j = next[j-1]回退

二·经典例题

1.

P3375 【模板】KMP

题目描述

给出两个字符串 $s_1$ 和 $s_2$,若 $s_1$ 的区间 $[l, r]$ 子串与 $s_2$ 完全相同,则称 $s_2$ 在 $s_1$ 中出现了,其出现位置为 $l$。
现在请你求出 $s_2$ 在 $s_1$ 中所有出现的位置。

定义一个字符串 $s$ 的 border 为 $s$ 的一个非 $s$ 本身的子串 $t$,满足 $t$ 既是 $s$ 的前缀,又是 $s$ 的后缀。
对于 $s_2$,你还需要求出对于其每个前缀 $s’$ 的最长 border $t’$ 的长度。

输入格式

第一行为一个字符串,即为 $s_1$。
第二行为一个字符串,即为 $s_2$。

输出格式

首先输出若干行,每行一个整数,按从小到大的顺序输出 $s_2$ 在 $s_1$ 中出现的位置。
最后一行输出 $|s_2|$ 个整数,第 $i$ 个整数表示 $s_2$ 的长度为 $i$ 的前缀的最长 border 长度。

输入输出样例 #1

输入 #1

ABABABC
ABA

输出 #1

1
3
0 0 1

说明/提示

样例 1 解释

对于 $s_2$ 长度为 $3$ 的前缀 ABA,字符串 A 既是其后缀也是其前缀,且是最长的,因此最长 border 长度为 $1$。

数据规模与约定

本题采用多测试点捆绑测试,共有 3 个子任务

  • Subtask 0(30 points):$ s_1 \leq 15$,$ s_2 \leq 5$。
  • Subtask 1(40 points):$ s_1 \leq 10^4$,$ s_2 \leq 10^2$。
  • Subtask 2(30 points):无特殊约定。
  • Subtask 3(0 points):Hack。
对于全部的测试点,保证 $1 \leq s_1 , s_2 \leq 10^6$,$s_1, s_2$ 中均只含大写英文字母。

解:

#include<iostream>
#include<vector>

using namespace std;

vector<int> buildnext(const string& p) {
	vector<int> next(p.length(), 0);  // 初始化 next 数组,长度与 p 相同,初始值全为 0
	int len = 0;                      // 当前最长公共前后缀长度(即 border 长度)
	int i = 1;                        // 从模式串的第 2 个字符开始遍历(下标从 1 开始)

	while (i < p.length()) {          // 遍历模式串 p
		if (p[i] == p[len]) {         // 当前字符匹配成功(扩展 border)
			len++;                    // border 长度 +1
			next[i] = len;            // 记录当前位置的 border 长度
			i++;                      // 继续处理下一个字符
		}
		else {                      // 当前字符匹配失败
			if (len != 0) {           // 若已有部分匹配,则回溯到前一个 border 位置
				len = next[len - 1];  // 关键:利用已计算的 next 值回溯(避免重复匹配)
			}
			else {                  // 无匹配部分(border 长度为 0)
				next[i] = 0;          // 当前无 border,记录为 0
				i++;                  // 继续处理下一个字符
			}
		}
	}
	return next;  // 返回构建好的 next 数组
}

vector<int> kmpsearch(const string& t, const string& p) {
	if (p.empty()) return {};            // 边界处理:模式串为空时直接返回
	vector<int> position;               // 存储所有匹配的起始位置
	vector<int> next = buildnext(p);     // 获取模式串 p 的 next 数组
	int i = 0, j = 0;                    // i 遍历文本串 t,j 遍历模式串 p

	while (i < t.size()) {               // 遍历文本串
		if (j < p.size() && t[i] == p[j]) {  // 当前字符匹配成功
			i++;
			j++;
		}
		if (j == p.size()) {             // 完整匹配到模式串
			position.push_back(i - j);   // 记录匹配位置的起始索引(0-based)
			if (j > 0) j = next[j - 1];  // 回溯 j 以支持重叠匹配(如 "AAAA" 中找 "AAA")
			else j = 0;                  // 边界:模式串长度为 1 时避免越界
		}
		else if (i < t.size() && t[i] != p[j]) {  // 当前字符匹配失败
			if (j > 0) j = next[j - 1];  // 回溯 j 到前一个 border 位置
			else i++;                    // 模式串已回溯到起点,移动文本串指针
		}
	}
	return position;  // 返回所有匹配位置
}

int main() {
	string t, p;
	cin >> t >> p;
	vector<int> position = kmpsearch(t, p);
	vector<int> next = buildnext(p);
	for (int& i : position) {
		cout << i + 1 << endl;
	}
	if (!next.empty()) {
		cout << next[0];
		for (int i = 1; i < next.size(); i++) {
			cout << " " << next[i];
		}
	}
	return 0;
}

【评】此题为最基础的模板题(然而还是比较抽象),结合视频食用更佳😋

2.

0408 前缀中的周期

题目描述

一个字符串的前缀是从第一个字符开始的连续若干个字符,例如”abaab”共有5个前缀,分别是a, ab, aba, abaa, abaab。

我们希望知道一个N位字符串S的前缀是否具有循环节。换言之,对于每一个从头开始的长度为 i (i 大于1)的前缀,是否由重复出现的子串A组成,即 AAA…A (A重复出现K次,K 大于 1)。如果存在,请找出最短的循环节对应的K值(也就是这个前缀串的所有可能重复节中,最大的K值)。

输入格式

输入包括多组测试数据。每组测试数据包括两行。 第一行包括字符串S的长度N(2 <= N <= 1 000 000)。 第二行包括字符串S。 输入数据以只包括一个0的行作为结尾。

输出格式

对于每组测试数据,第一行输出 “Test case #“ 和测试数据的编号。 接下来的每一行,输出前缀长度i和重复测数K,中间用一个空格隔开。前缀长度需要升序排列。 在每组测试数据的最后输出一个空行。

输入输出样例 #1

输入 #1

3
aaa
12
aabaabaabaab
0

输出 #1

Test case #1
2 2
3 3

Test case #2
2 2
6 2
9 3
12 4

解:

#include<iostream>
#include<vector>

using namespace std;

vector<int> buildnext(string s) {
	if (s.empty()) return {};
	vector<int> next(s.length(), 0);
	int len = 0;
	int i = 1;
	while (i < s.length()) {
		if (s[i] == s[len]) {
			len++;
			next[i] = len;
			i++;
		}
		else {
			if (len != 0) {
				len = next[len - 1];
			}
			else {
				next[i] = 0;
				i++;
			}
		}
	}
	return next;
}

int main() {
	int len;
	int num = 1;
	string s;
	while (cin >> len) {
		if (len == 0)
			break;
		cin >> s;
		cout << "Test case #" << num << endl;
		num++;
		vector<int> next = buildnext(s);
		for (int i = 1; i < s.length(); i++) {
			if (next[i] != 0) {
				int cyclelen = i + 1 - next[i];
				if ((i + 1) % cyclelen == 0) {
					int I = i + 1;
					int K = I / cyclelen;
					cout << I << " " << K << endl;
				}
			}
		}
		cout << endl;
	}
	return 0;
}

【评】循环节长度公式的原理:最小循环节长度 = 子串总长度 - 最长公共前后缀长度(确认循环节存在:验证子串长度能被循环节长度整除(确保完整重复))想清楚这一点,看似复杂的题目便迎刃而解💕

Tags:

Categories:

Updated:

Comments