KMP原理+例题

思想

朴素的字符换匹配是 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'继续匹配,也就是这条链上增加一些指向左边状态的指针。

这其实是一个比较简单的自动机

例题

796. 旋转字符串

循环数组,问能否匹配上模式串。结尾拼接一份,然后直接匹配

1764. 通过连接另一个数组的子数组得到一个数组

给一个模式串数组,问能否按顺序全匹配上,所有匹配互不重叠。贪心的想,肯定前面的模式串要尽早匹配上,这样给后面留的字符多,更可能让后面的也匹配上

每一个模式串找到最早匹配后,下一次匹配从这个匹配的结尾后开始

3036. 匹配模式数组的子数组数目 II

模式是给出每组相邻位置是增大,减小,还是不变。把原始数组根据这个规则改造成 0 , − 1 , 1 0,-1,1 0,−1,1,然后就可以和模式串直接匹配了

1668. 最大重复子字符串

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记录匹配。

459. 重复的子字符串

思维题

问一个串能否由它的一个子串的重复构成。如果能,那么 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的开头和结尾两个字符删掉,再跑匹配

2800. 包含三个字符串的最短字符串

找到一个最短串,三个模式串都是它的子串。最费空间的做法是三个模式串直接拼接在一起,想节省空间,可以让一个串接到后面时,它的前缀和前一个串的后缀有公共部分,这样这一块可以被两个串共享,节约这部分空间。

发现这就是kmp匹配过程,kmp的本质就是求一俩串的最长公共前后缀,如果这个长度等于一个串的话,就完成了一次匹配,但这个匹配是顺带的。核心还是在求最长公共前后缀。如果俩串相同,这就是在求 p i pi pi数组。跑一次kmp就能得到两个模式串的最长重叠部分。

问题是有三个串,不知道怎么排列最优,可以直接枚举所有排列,也不多。对于每个排列,就先把前两个拼接,再把第三个接在后面。

3008. 找出数组中的美丽下标 II

kmp+前缀和

给两个模式串,问对于第一个串的所有匹配位置,如果附近距离不超过k内有另一个串的匹配,称之为美丽下表,找到所有这样的下标。

对两个串分别找到所有匹配位置,枚举一个串的匹配位置,每次需要查周围半径k的区间内有没有另一个串的匹配,这是静态区间查,前缀和即可。

214. 最短回文串

问在开头添加最少多少字符,可以把串变成回文的。

想添加尽量少的,就要尽可能多的利用目前已经是回文的前缀,也就是串可以看成s+t,s是个回文。现在要找到一个最长的s。

这当然可以马拉车,但现在用kmp做就要思考怎么转化。注意到,这个串反转是t'+s,后缀s由于是回文,反转后不变。

于是现在就是找一个s+t的最长的前缀,同时也是t'+s的最长后缀,这是最长公共前后缀问题,kmp正好可以解决。

686. 重复叠加字符串匹配

问一个文本串a最少重复多少次,能让模式串成为他的子串?

可以看成文本串是一个无限延伸的循环数组,把文本串的指针取模即可。然后正常匹配。可能无解,陷入死循环,为了避免这一点,应该让匹配中,文本串的指针不超过a的第一次重复,因为如果后面的重复都和第一次重复完全相同了,如果第一次重复内都没匹配,后面也不可能。

实现上就是文本串指针i,模式串指针j,j也是模式串匹配上的长度,那i-j就是这次匹配在文本串中的开始位置,要求i-j<n即可

找到匹配位置了,让a的重复次数向上取整,使得a的重复后能覆盖住这次匹配。

3455. 最短匹配子字符串

三指针

模式串里有两个通配符,可以匹配零个或多个任意字符。找到文本串中的最短匹配。

通配符很难在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;
    }
};

1397. 找到所有好字符串

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);
    }
};
相关推荐
追随者永远是胜利者11 小时前
(LeetCode-Hot100)20. 有效的括号
java·算法·leetcode·职场和发展·go
瓦特what?11 小时前
快 速 排 序
数据结构·算法·排序算法
niuniudengdeng12 小时前
基于时序上下文编码的端到端无文本依赖语音分词模型
人工智能·数学·算法·概率论
hetao173383712 小时前
2026-02-13~16 hetao1733837 的刷题记录
c++·算法
你的冰西瓜14 小时前
2026春晚魔术揭秘——变魔法为物理
算法
忘梓.14 小时前
解锁动态规划的奥秘:从零到精通的创新思维解析(10)
c++·算法·动态规划·代理模式
foolish..14 小时前
动态规划笔记
笔记·算法·动态规划
消失的dk14 小时前
算法---动态规划
算法·动态规划
羑悻的小杀马特14 小时前
【动态规划篇】欣赏概率论与镜像法融合下,别出心裁探索解答括号序列问题
c++·算法·蓝桥杯·动态规划·镜像·洛谷·空隙法