• kmp即其扩展


    kmp

    前缀函数

    kmp中最重要的就是前缀函数nxt。

    定义$nxt[i]$是模式串最长的前缀等于其子串$s[1...i]$的真后缀。

    显然$nxt[1]=0$

    很容易想到一种暴力模拟的方法(见下面的程序):

    for (int i = 2; i <= len(s); i++) {
        int j = 0;
        if (s[j + 1] == s[i - j]) j++;
        nxt[i] = j;
    }

    很不幸这样的复杂度是$O(n^2)$的。

    考虑怎么优化,如果用hash+二分的方法可以优化到$O(nlog{n})$,已经可以通过大部分的题目。

    for (int i = 2; i <= len(s); i++) {
        int l = 0; r = i;
        while (l <= r) {
            int mid = (l + r) >> 1;
            if (hash[s[1...mid]] == hash[s[i - mid + 1...i]]) {
                nxt[i] = mid;
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
    }

    可是这是最优解吗?答案是否定的,我们还可以优化到$O(n)$。

    我们发现刚才的方法都没有好好利用已经求出的$nxt$,我们想想怎么利用这些已知信息。

    假设我们现在要求$nxt[i]$,记$j=nxt[i-1]$。则显然$s[1...j]=s[i-j...i-1]$,如果$s[j+1]=s[i]$的话,则$nxt[i]=j+1$。

    如果不是呢?能不能如果直接令$j=j-1$,则这和暴力差不多。

    上面的思路和之前犯了同样的错误——没有考虑已知信息。

    如果每次令$j=nxt[j]$,则会的得到正确的答案(证明易证,画个图就好了)。

    可以通过势能分析的方法证明其复杂度是正确的。

    for (int i = 2, j = 0; i <= len(s); i++) {
        while (j && s[i] != s[j + 1]) j = nxt[j];
        if (s[i] == s[j + 1]) j++;
        nxt[i] = j;
    }

    kmp

    kmp算法和求nxt大同小异,如果我们每次记录一个f数组$f_i$代表文本串以第i个字符结尾的后缀与模式串的前缀的最长公共长度。

    求法与求nxt基本一样,只用加一句如果当前长度为模式串大小也跳nxt。

    //s是模式串,t是文本串 
    for (int i = 1, j = 0; i <= len(t); i++) {
        while (j && (j == len(s) || t[i] != s[j + 1])) j = nxt[j];
        if (t[i] == s[j + 1]) j++;
        f[i] = j;
    }

    当然,前缀函数不止做字符串匹配这一种作用。

    求最小周期

    周期,即可以重复多次而得到原串的串。

    如abaaba的最小周期就是aba。

    aabbaabbba

    具体怎么求呢?

    如果$n-nxt[n]|n$,则为$n-nxt[n]$,否则为n。

    首先可以证明这样做是正确的。

    如果两个nxt有相交,且满足上述要求,易证这是对的。

    如果没有相交,则不可能满足上述要求。

    再证这是最小的,可以用反证法,如果还有更小的,则可以推出还有更大的nxt[n],矛盾。

    kmp自动机

    首先什么是DFA(确定有限状态自动机),其实就是对于每个状态,和字符集,都可以转移到另一个状态。

    状态在kmp这就是匹配长度。

    我们现在需要求出一个数组$ch[i][j]$表示状态i后加一个字符j会得到哪个状态。

    有初值$ch[0][s[1]]=1$。

    那怎们在线性时间内求出呢($O(sum n)$)。

    有一个直观的想法就是枚举每一位和下一位的字符,然后暴力跳nxt。

    这样固然可做,但还是犯了没有利用已知信息的错误。

    如果$j=s[i+1]$,那直接指向i+1。

    如果不,可以指向$ch[nxt[i]][j]$,这一步很好理解,因为$nxt[i]<i$,所以$ch[nxt[i]][j]$一定是之前已经求出来的,这样就可以在接近线性的时间复杂度内构建自动机了。

    例题:[HNOI2008]GT考试

    扩展kmp

    z函数

    我的字符串下标都是从1开始的

    现在要你求一个数组z,$z_i$代表字符串s的后缀$s[i...|s|]$的最长和s相同的前缀。

    求法类似manacher

    首先$z_1=|s|$不需要我们求。

    从$i=2$开始,同时维护两个指针$r,l$,分别指向历史最远匹配位置,和那次匹配的初始位置(是不是类似马拉车)。

    如果$i leq r$,根据已知信息可以得到$z[i] geq z[i-l+1]$,可以先令$z[i]=z[i-l+1]$,然后再暴力匹配。

    和马拉车一样,不能超过已知范围,所以还要和$r-i+1$取个min。

    之后就可以暴力匹配了。

    因为r是在不断往后移动的,所以复杂度是$O(|s|)$的(是不是和马拉车一模一样)。

    void Z(char *s, int *z) {
        int len = strlen(s + 1);
        for (int i = 2, l = 1, r = 1; i <= len; i++) {
            if (i <= r) z[i] = min(r - i + 1, z[i - l + 1]);
            while (i + z[i] <= len && s[i + z[i]] == s[z[i] + 1]) z[i]++;
            if (i + z[i] - 1 > r) {
                l = i;
                r = i + z[i] - 1;
            }
        }
    }

    匹配

    现在给你两个字符串s,t。让你求s的每一个后缀与t的最长公共前缀的长度。

    对于后缀i,我们把答案存到$p_i$中。

    这其实就是exkmp。

    就好像kmp和求nxt很像,exkmp和z函数也很像。

    只用把所有和文本串有关的z改成p(即取min中的z不动)。

    然后暴力匹配时把i+z[i]改成文本串即可。

    void exkmp(char *s, char *t, int *p, int *z) {
        int len = strlen(s + 1);
        for (int i = 1, l = 0, r = 0; i <= len; i++) {
            if (i <= r) p[i] = min(r - i + 1, z[i - l + 1]);
            while (i + p[i] <= len && s[i + p[i]] == t[p[i] + 1]) p[i]++;
            if (i + p[i] - 1 > r) {
                l = i;
                r = i + p[i] - 1;
            }
        }
    }

    现在你可以ACluogu的模板题了(https://www.luogu.com.cn/problem/P5410)。

    exkmp还有一种不优秀(我觉得)的写法,我会在附录A中给出。

    例题:[GDOI2014]Beyond

    如果第一个字符串的前缀$s1[1...i]$是环的话则必然有一个$1 leq j < i$,满足$s1[1...j]=s2[j+1...i],s1[j+1...i]=s2[1...j]$。

    所以要做两遍exkmp,定义p1为s1位文本串,s2为模式串的p,p2反过来定义。

    所以我们现在找到一个位置i,则$s2[1...p1[i]]=s1[i...i+p1[i]-1]$,现在只要$p2[p1[i]+1] geq i-1$,即可以够到i即可满足条件。

    将答案与$i+p1[i]-1$取max,最后输出答案。

    即$ans = max_{p2[p1[i]+1] geq i-1} i+p1[i]-1$

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 2000010;
    int n;
    char s1[N], s2[N];
    int z1[N], z2[N];
    int p1[N], p2[N];
    int ans;
    void Z(char *s, int *z) {
        int len = strlen(s + 1);
        for (int i = 2, l = 1, r = 1; i <= len; i++) {
            if (i <= r) z[i] = min(r - i + 1, z[i - l + 1]);
            while (i + z[i] <= len && s[i + z[i]] == s[z[i] + 1]) z[i]++;
            if (i + z[i] - 1 > r) {
                l = i;
                r = i + z[i] - 1;
            }
        }
    }
    void exkmp(char *s, char *t, int *p, int *z) {
        int len = strlen(s + 1);
        for (int i = 1, l = 0, r = 0; i <= len; i++) {
            if (i <= r) p[i] = min(r - i + 1, z[i - l + 1]);
            while (i + p[i] <= len && s[i + p[i]] == t[p[i] + 1]) p[i]++;
            if (i + p[i] - 1 > r) {
                l = i;
                r = i + p[i] - 1;
            }
        }
    }
    void cmax(int &x, int y) {
        if (y > x) x = y;
    }
    int main() {
        scanf("%d", &n);
        scanf("%s", s1 + 1);
        scanf("%s", s2 + 1);
        Z(s1, z1);
        Z(s2, z2);
        exkmp(s1, s2, p1, z2);
        exkmp(s2, s1, p2, z1);
        for (int i = 1; i <= n; i++)
            if (p2[p1[i] + 1] >= i - 1)
                cmax(ans, i + p1[i] - 1);
        printf("%d", ans);
        return 0;
    }

    完结撒花✿✿ヽ(°▽°)ノ✿。

    附录A exkmp的不优秀写法

    void get_nxt(char t[], int p) {
        nxt[p][1] = n;
        nxt[p][2] = 0;
        while (t[nxt[p][2] + 2] == t[nxt[p][2] + 1]) nxt[p][2]++;
        for (int i = 3, p0 = 2, r = p0 + nxt[p][p0] - 1; i <= n; i++) {
            if (i + nxt[p][i - p0 + 1] - 1 < r) nxt[p][i] = nxt[p][i - p0 + 1];
            else {
                nxt[p][i] = max(r - i + 1, 0);
                while (t[i + nxt[p][i]] == t[1 + nxt[p][i]]) nxt[p][i]++;
                p0 = i;
                r = p0 + nxt[p][p0] - 1;
            }
        }
    }
    void exkmp(char s[], char t[], int p) {
        extend[p][1] = 0;
        while (s[extend[p][1] + 1] == t[extend[p][1] + 1]) extend[p][1]++;
        for (int i = 2, p0 = 1, r = p0 + extend[p][p0] - 1; i <= n; i++) {
            if (i + nxt[p][i - p0 + 1] - 1 < r) extend[p][i] = nxt[p][i - p0 + 1];
            else {
                extend[p][i] = max(r - i + 1, 0);
                while (s[i + extend[p][i]] == t[1 + extend[p][i]]) extend[p][i]++;
                p0 = i;
                r = p0 + extend[p][p0] - 1;
            }
        }
    }

    这里nxt是z,extend是p。

     如果遇到其他有关kmp和exkmp的套路或有趣的思维我会补上

  • 相关阅读:
    tars go版本源码分析
    goim源码阅读
    vue weixin源码解读
    避免加锁的骚操作
    git一些常用操作
    eclipse 的习惯配置
    ai资源站
    转载 github上的一些安全资源收集
    转载 一个统计代码行数的python脚本
    C语言开发工具
  • 原文地址:https://www.cnblogs.com/zcr-blog/p/13051532.html
Copyright © 2020-2023  润新知