指针扫描型字符串算法
【最小表示法】
循环表示:从一个位置开始向后遍历,到末尾再倒回去最前面。一个字符串(数组)一共有
最小表示法就是最小的循环表示。
例如,3 1 4 9 1 的最小表示法是 1 3 1 4 9.
如果我们用打擂台比大小的方式,因为字符串之间比较需要时间,总共是
把字符串列出来,搞两个指针
两个指针同时往后挪,直到当
比较一下这两个字符串,发现
但是只有
不,我们看看
我们发现,不仅是
换言之,这一次我们不仅淘汰了
所以
像这样一直从
注意点:
-
如果两个指针转了一圈都完全相等,随便挑一个作为不优的。
-
如果
,随便挑一个 。 -
可以用取模代替转回去的运算。
【KMP】
朴素:
想法:当匹配失败时,不要一个一个跳,直接跳到合适的位置。
定义
如果不存在真前缀真后缀,或者不存在这个长度使得前后缀相等,令
例子:
j
A: a b a c a b a k
B: a b a c a b a w
i
此时失配,
j
A : a b a c a b a k
B:OOOOO a b a c a b a w
i
这时又失配了,
j
A : a b a c a b a k
B:OOOOOO O a b a c a b a w
i
还是失配,此时
j
A : a b a c a b a k
B:OOOOOOO O a b a c a b a w
i
还是失配,而
j
A : a b a c a b a k .........
B:OOOOOOOOOO a b a c a b a w
i
那如何求
a a b c a a b c d
0 1 2 3 4 5 6 7 8
判断
这里有一个要求:
这其实很好理解,
又
#include <bits/stdc++.h>
using namespace std;
// 计算得到w的nxt数组,nxt[i]表示w的前i个字符中,最长相同前后缀长度
vector<int> getNxt(string &w) {
// nxt[0]无意义,nxt[1]为0,从nxt[2]开始推
vector<int> nxt = vector<int>(w.size() + 1, 0);
for (int i = 1; i < w.size(); i++) { // 利用nxt[i]推nxt[i + 1]
// 前cur个字符和 w[i]之前的cur个字符相同
int cur = nxt[i];
// 故第cur+1个(w[cur])若和 w[i]相同,则组成i+1前缀的匹配成功
while (cur > 0 && w[cur] != w[i]) //不成功则换更短的cur尝试
cur = nxt[cur];
if (w[cur] == w[i]) // 若成功,则设置 nxt[i + 1]的值,若最后也没成功,nxt[i + 1]保持初值0即可
nxt[i + 1] = cur + 1;
}
return nxt;
}
// 找到所有位置i,使得a[i]开始是一个w
void kmp(string &a, string &w) {
vector<int> nxt = getNxt(w);
// 匹配a[i]和w[j],若失败a[i]与w[nxt[j]]继续匹配
for (int i = 0, j = 0; i <= a.size(); i++) {
if (j == w.size()) { // 若试图匹配w[w.size()],则通过过一位置判断已经成功
cout << i - w.size() + 1 << endl; // i为成功的后一个位置,则i - w.size()为起始位置,本题下标从1开始故再+1
j = nxt[j]; // 成功后也要移动并试着匹配下一个
}
if (i == a.size()) // 为在末尾匹配w过一位置,需要考虑a过一位置
break;
while (j > 0 && a[i] != w[j]) // 若 w[j]匹配失败但还有相同前后缀,则跳跃
j = nxt[j];
if (a[i] == w[j]) // 若能成功,则下次匹配下一个,否则保持0下次从头匹配
j++;
}
for (int i = 1; i <= w.size(); i++)
cout << nxt[i] << " ";
cout << endl;
}
string s1, s2;
int main()
{
cin >> s1 >> s2;
kmp(s1, s2);
return 0;
}
【题目们】
考察对
我们先求出
接下来的问题就是,对于固定的前后缀,如何快速判断中间是否存在一个相等的子串?
用 KMP 就太慢了。
我们可以发现:不如假设枚举的固定前后缀为
那 其实挺显然的)
问题就转化为:一个给定的
这时,我们再给出一个结论:如果一个字符串的一个
所以我们根本没必要每迭代一次
我们只要在求
// 计算得到w的nxt数组,nxt[i]表示w的前i个字符中,最长相同前后缀长度
void getNxt(string &w) {
// nxt[0]无意义,nxt[1]为0,从nxt[2]开始推
vector<int> nxt = vector<int>(w.size() + 1, 0);
for (int i = 1; i < w.size(); i++) { // 利用nxt[i]推nxt[i + 1]
// 前cur个字符和 w[i]之前的cur个字符相同
int cur = nxt[i];
// 故第cur+1个(w[cur])若和 w[i]相同,则组成i+1前缀的匹配成功
while (cur > 0 && w[cur] != w[i]) //不成功则换更短的cur尝试
cur = nxt[cur];
if (w[cur] == w[i]) // 若成功,则设置 nxt[i + 1]的值,若最后也没成功,nxt[i + 1]保持初值0即可
nxt[i + 1] = cur + 1;
}
// 先求非结束位置的最长公共前后缀长度k
// 这说明有非前后缀的k长度子串匹配前缀,实际上,通过这个子串直接构造出1~k-1长度的匹配
int k = 0;
for (int i = 2; i < w.size(); i++)
k = max(k, nxt[i]);
// ans为整个w的 <= k长度的最长公共前后缀长度,用cur枚举所有长度
int ans = 0, cur = nxt[w.size()];
while (cur > 0) {
if (cur <= k) {
ans = cur;
break;
}
cur = nxt[cur];
}
if (ans == 0)
cout << "Just a legend" << endl;
else
cout << w.substr(0, ans) << endl;
}
如果有 ,那一定有 长度的相等前后缀。
如果枚举前缀,用 KMP,是
两个性质:
-
如果
是一个字符串不停循环,然后截下来一部分得来的,那么 是 的最小循环周期。(手玩一个长一点的字符串就知道了) -
我们可以通过
找出所有循环周期。
注:POJ2406(Power strings)也用到相关性质。
是最小循环周期。
还是要 KMP 匹配,但是要处理两端拼接的情况。
KMP 的核心是主串的指针不后退,所以我们希望在匹配(删除)了一个模式串的时候,不要倒回去,直接接着匹配。
这个时候就需要解决一个问题:当我们删除了
我们在 KMP 时(和匹配/删除字符串同步进行),额外对每个位置记录一个匹配位置:while (j > 0 && a[i] != w[j]) j = nxt[j];
之前的
为什么是第一次?因为这样才能在删除之后续上最长的提前匹配好的长度。
总结一下:记录一个第一次匹配位置,这样可以使每次匹配成功删除一个子串后,模式串不从头匹配。
代码细节:
我们要用栈存着目前处理过的所有位置,以方便我们在匹配成功后,找到字符串开头的在原本字符串的位置,因为记录的匹配位置
用栈的原因:这不是直接减去模式串长度就能找到开头的,因为可能我们匹配成功的字符串中间本来被不知道多少个之前删除掉了的模式串隔开,只是在删除之后拼起来的。
而且所有匹配结束后,栈剩下的字符就是要输出的字符串,刚好。
记录失配位置,模式串不从头匹配。
把
最小循环节的长度就是
不加优化:枚举所有前缀
优化:其实很简单,类似并查集的路径压缩,搜索的记忆化。如果我们要找
朴素:
优化:枚举每个前缀
因为要把
因此,只要 1
。
不停迭代,直到
记录一个
但是一步一步枚举
先求出
(想覆盖
什么时候
首先,如果
否则:我们可以保证后
这可以感性理解一下,我们只有在后
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!