思想
朴素的字符换匹配是 O ( n 2 ) O(n^2) O(n2)的,我们每次枚举模式串在文本串里的起点 i i i,然后逐个往后匹配,如果失配了,则去看下一个起点 i + 1 i+1 i+1
这为什么慢?因为每次失配,都要从头开始,如果我们能在失配之后不从头开始下一个位置的匹配,而是直接进入一个匹配到一半的状态,就能大幅加速。
K M P KMP KMP就是利用这一思想,首先,预处理出 p i ( i ) pi(i) pi(i),表示模式串的前缀 [ 0 , i ] [0,i] [0,i]的最长公共前后缀,这也被称为最长 b o r d e r border border, ( b o r d e r 指同时是前缀和后缀的子串 ) (border指同时是前缀和后缀的子串) (border指同时是前缀和后缀的子串)。
然后我们在匹配过程中,如果在文本串的 i i i位置开始匹配,匹配到模式串的 [ 0 , j ] [0,j] [0,j]失配了。此时我们当然可以从 i + 1 i+1 i+1开始,从模式串 0 0 0开始重新匹配,但如前所述,我们可以跳过一部分的匹配过程,直接从匹配到一半的状态开始,那最好情况是,在 [ i , i + j − 1 ] [i,i+j-1] [i,i+j−1]里找到一个最长后缀,是模式串的前缀,把这一段都匹配上,然后我们继续来到 s i s_i si这个位置继续匹配。
匹配到 [ 0 , j ] [0,j] [0,j]失配了,还意味着,文本串的 [ i , i + j − 1 ] [i,i+j-1] [i,i+j−1],就是模式串的 [ 0 , j − 1 ] [0,j-1] [0,j−1]前缀,所以我们要查 [ i , i + j − 1 ] [i,i+j-1] [i,i+j−1]的后缀,和模式串的前缀的最大匹配,就等价于查模式串的 [ 0 , j − 1 ] [0,j-1] [0,j−1]前缀和后缀的最大匹配,而这就是前面预处理的每个前缀的最长公共前后缀,直接去查 p i ( j − 1 ) pi(j-1) pi(j−1)。
如果匹配完了模式串,也就是模式串指针 [ 0 , j ] [0,j] [0,j]访问完整个模式串,就是找到了一个匹配。如果要找所有匹配,完成了一个匹配后也可以按照失配的逻辑来,跳 p i pi pi,这也叫跳 f a i l fail fail指针,意思是匹配失败后指向哪里继续匹配。
模板
s是模式串,t是文本串
c
vector<int> kmp(const string& s, const string& t) {
int m = t.size();
int n = s.size();
vector<int> vis;
vector<int> pi(n);
int cnt = 0;
for (int i = 1; i < n; i++) {
while (cnt && s[cnt] != s[i]) {
cnt = pi[cnt - 1];
}
if (s[i] == s[cnt]) {
cnt++;
}
pi[i] = cnt;
}
cnt = 0;
for (int i = 0; i < m; i++) {
while (cnt && s[cnt] != t[i]) {
cnt = pi[cnt - 1];
}
if (s[cnt] == t[i]) {
cnt++;
}
if (cnt == n) {
cnt = pi[cnt - 1];
vis.push_back(i - s.size() + 1);
}
}
return vis;
}
自动机视角
kmp前面不够直观的话,也可以从自动机的角度理解。
我们现在就是要以文本串为输入,构造一个接受包含模式串为子串的语言。
模式串正常的匹配就是一条链,当前在cnt位置,就是这条链上从左往右第cnt个节点,如果下一个字符匹配上了则移动到cnt+1状态。如果失配了,不回到初始状态重新匹配,而是记录跳到另一个位置cnt'继续匹配,也就是这条链上增加一些指向左边状态的指针。
这其实是一个比较简单的自动机
例题
循环数组,问能否匹配上模式串。结尾拼接一份,然后直接匹配
给一个模式串数组,问能否按顺序全匹配上,所有匹配互不重叠。贪心的想,肯定前面的模式串要尽早匹配上,这样给后面留的字符多,更可能让后面的也匹配上
每一个模式串找到最早匹配后,下一次匹配从这个匹配的结尾后开始
模式是给出每组相邻位置是增大,减小,还是不变。把原始数组根据这个规则改造成 0 , − 1 , 1 0,-1,1 0,−1,1,然后就可以和模式串直接匹配了
kmp优化dp 问模式串的重复,作为文本串的子串,最多重复多少次。
考虑 d p dp dp,转移 d p i = d p i − w o r d . s i z e + 1 dp_i=dp_{i-word.size}+1 dpi=dpi−word.size+1,前提是当前位置前面的子串等于模式串 w o r d word word,因为计数是以 w o r d word word为单位的,所以转移时也以一个模式串为单位。这需要检查每个长度 w o r d . s i z e word.size word.size的子数组是不是模式串。
暴力可过,更快的做法是,这就是问每个位置是不是一个匹配的结尾,直接 k m p kmp kmp记录匹配。
思维题
问一个串能否由它的一个子串的重复构成。如果能,那么 s = t t t t . . . t s=tttt...t s=tttt...t多个 t t t构成,那么我们把一个长度 t . s i z e t.size t.size的前缀挪到后面当后缀,这个串毫无变化,这等价于我们把这个串当成循环数组的话,除了 [ 1 , n ] [1,n] [1,n],还存在其它窗口也是 s s s本身。
那么就转化成了循环数组匹配,把s重复一遍变成ss,然后找s的匹配,并且这个匹配不能是s本身带来的,也就是不能是 [ 1 , n ] [ n + 1 , 2 n ] [1,n][n+1,2n] [1,n][n+1,2n]这俩窗口,想规避这两个匹配,可以把 s s ss ss的开头和结尾两个字符删掉,再跑匹配
找到一个最短串,三个模式串都是它的子串。最费空间的做法是三个模式串直接拼接在一起,想节省空间,可以让一个串接到后面时,它的前缀和前一个串的后缀有公共部分,这样这一块可以被两个串共享,节约这部分空间。
发现这就是kmp匹配过程,kmp的本质就是求一俩串的最长公共前后缀,如果这个长度等于一个串的话,就完成了一次匹配,但这个匹配是顺带的。核心还是在求最长公共前后缀。如果俩串相同,这就是在求 p i pi pi数组。跑一次kmp就能得到两个模式串的最长重叠部分。
问题是有三个串,不知道怎么排列最优,可以直接枚举所有排列,也不多。对于每个排列,就先把前两个拼接,再把第三个接在后面。
kmp+前缀和
给两个模式串,问对于第一个串的所有匹配位置,如果附近距离不超过k内有另一个串的匹配,称之为美丽下表,找到所有这样的下标。
对两个串分别找到所有匹配位置,枚举一个串的匹配位置,每次需要查周围半径k的区间内有没有另一个串的匹配,这是静态区间查,前缀和即可。
问在开头添加最少多少字符,可以把串变成回文的。
想添加尽量少的,就要尽可能多的利用目前已经是回文的前缀,也就是串可以看成s+t,s是个回文。现在要找到一个最长的s。
这当然可以马拉车,但现在用kmp做就要思考怎么转化。注意到,这个串反转是t'+s,后缀s由于是回文,反转后不变。
于是现在就是找一个s+t的最长的前缀,同时也是t'+s的最长后缀,这是最长公共前后缀问题,kmp正好可以解决。
问一个文本串a最少重复多少次,能让模式串成为他的子串?
可以看成文本串是一个无限延伸的循环数组,把文本串的指针取模即可。然后正常匹配。可能无解,陷入死循环,为了避免这一点,应该让匹配中,文本串的指针不超过a的第一次重复,因为如果后面的重复都和第一次重复完全相同了,如果第一次重复内都没匹配,后面也不可能。
实现上就是文本串指针i,模式串指针j,j也是模式串匹配上的长度,那i-j就是这次匹配在文本串中的开始位置,要求i-j<n即可
找到匹配位置了,让a的重复次数向上取整,使得a的重复后能覆盖住这次匹配。
三指针
模式串里有两个通配符,可以匹配零个或多个任意字符。找到文本串中的最短匹配。
通配符很难在kmp匹配中生效,所以干脆根据通配符把模式串断成三段,然后就是在文本串里找到三个模式串的匹配,并且三个匹配互不重叠,然后让最后一个匹配的结尾,到第一匹配的开头距离最短。
可以先kmp找到三个模式串的所有匹配位置,然后枚举中间(第二个)模式串的匹配,那左侧就是要找一个不重叠的,最晚的匹配,右侧要找一个不重叠的最早的匹配,都可以双指针解决,中间模式串滑动一下,另外两个模式串指针都滑动,也可以说是一个三指针。
三指针的边界实现上还是有点难度
c
vector<int> kmp(const string& s, const string& t) {
int m = t.size();
int n = s.size();
vector<int> vis;
if (s == "") {
vis.reserve(m + 1);
for (int i = 0; i < m + 1; i++) {
vis.push_back(i);
}
return vis;
}
vector<int> pi(n);
int cnt = 0;
for (int i = 1; i < n; i++) {
while (cnt && s[cnt] != s[i]) {
cnt = pi[cnt - 1];
}
if (s[i] == s[cnt]) {
cnt++;
}
pi[i] = cnt;
}
cnt = 0;
for (int i = 0; i < m; i++) {
while (cnt && s[cnt] != t[i]) {
cnt = pi[cnt - 1];
}
if (s[cnt] == t[i]) {
cnt++;
}
if (cnt == n) {
cnt = pi[cnt - 1];
vis.push_back(i - s.size() + 1);
}
}
return vis;
}
class Solution {
public:
int shortestMatchingSubstring(string s, string p) {
string t;
t.reserve(p.size());
vector<string> q;
for (char c : p) {
if (c == '*') {
q.push_back(t);
t = "";
} else {
t += c;
}
}
q.push_back(t);
vector<int> pos1 = kmp(q[0], s);
vector<int> pos2 = kmp(q[1], s);
vector<int> pos3 = kmp(q[2], s);
int ans = INT_MAX;
int len1 = q[0].size();
int len2 = q[1].size();
int len3 = q[2].size();
int i = 0, k = 0;
for (int j : pos2) {
while (k < pos3.size() && pos3[k] < j + len2) {
k++;
}
if (k == pos3.size()) {
break;
}
while (i < pos1.size() && pos1[i] + len1 - 1 < j) {
i++;
}
if (i > 0) {
ans = min(ans, pos3[k] + len3 - pos1[i - 1]);
}
}
return ans == INT_MAX ? -1 : ans;
}
};
kmp优化数位dp
求字典序大于s1,小于s2,并且不包含e作为子串的串个数
字典序这块就上下界数位dp就行,不包含e作为子串,需要在数位dp过程中做字符串匹配,如果匹配长度等于e了,也就是出现一个e子串,则把这个情况剪掉。
这需要把模式串匹配长度作为参数在数位dp里传递。多个参数可以压成一个int,用map记忆化。实现更简单,常数也没大太多
c
vector<int> get_pi(const string& s) {
int n = s.size();
vector<int> pi(n);
int cnt = 0;
for (int i = 1; i < n; i++) {
while (cnt && s[cnt] != s[i]) {
cnt = pi[cnt - 1];
}
if (s[i] == s[cnt]) {
cnt++;
}
pi[i] = cnt;
}
return pi;
}
const int mod = 1e9 + 7;
class Solution {
public:
int findGoodStrings(int n, string s1, string s2, string evil) {
int m = evil.size();
vector<int> pi = get_pi(evil);
unordered_map<int, int> mp;
auto&& dfs = [&](auto&& dfs, int i, bool lo, bool hi, int c) -> int {
if (i == n) {
return c < m;
}
if (c == m) {
return 0;
}
int cur = i + lo * (1 << 10) + hi * (1 << 20) + c * (1 << 25);
if (mp.count(cur)) {
return mp[cur];
}
int res = 0;
int s = lo ? s1[i] - 'a' : 0;
int t = hi ? s2[i] - 'a' : 25;
for (int d = s; d <= t; d++) {
int nc = c;
while (nc && evil[nc] - 'a' != d) {
nc = pi[nc - 1];
}
if (evil[nc] - 'a' == d) {
nc++;
}
res += dfs(dfs, i + 1, lo && (d == s), hi && (d == t), nc);
res %= mod;
}
mp[cur] = res;
return res;
};
return dfs(dfs, 0, 1, 1, 0);
}
};