目录

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

kmp

1.算法分析

1.1 符号介绍

  1. 周期
    0 < p < |s|, s[i] = s[i + p],满足这个条件,p称为s的周期
  2. border
    0 < r < |s|, pre(s, r) == suf(s, r),满足这个条件,称pre(s, r)是s的border
  3. 周期和border
  • 3和6都是abcabcab的周期
  • abcab和ab都是abcabcab的border
  • pre(s, k)是s的border <=> |s| - k是s的周期

1.2 算法思想

    双指针算法优化时间复杂度,同时利用ne数组记录已经匹配成功的位置以优化时间复杂度

1.3 算法原理

    ne数组含义,ne[i] == j表示以i位置结尾的区间和以j位置结尾的区间完全相等,如下图两段红色的区间。
kmp 算法 第1张
    同时,可以证明ne数组匹配到的L的长度,一定是最大的,也就是说,ne[i] == j时,最长只能是L2 == L1,不可能出现两段长度大于L1的L3和L4,且满足L3 == L4
    P串的0 ~ j位和S串的b1 ~ i-1位匹配成功,但P串的j+1位和S串的i位不相等,那么本来应该把P串的第0位开始和S串的b2位开始逐个匹配。但是由于L1 = L3(使用ne数组记录),L2 = L1, 所以L2 = L3,那么表明P串的0~L3位已经和S串的i-1-L2~i-1位匹配成功,那么无需逐个匹配,只需要把P串L3后面一位和S的第i位开始匹配即可,通过这样的方式优化时间复杂度。
kmp 算法 第2张
1.ne数组的生成
    对模式串进行匹配,一个指针i在前从2开始,一个指针j在后从1开始,两个指针分别向前扫描,进行模式匹配,如果匹配成功,记录ne[i] = j;匹配不成功,j向后退,j = ne[j]
2.模式匹配
    一个指针i指向原串S,一个指针j指向模式串P,逐个匹配,匹配成功,则匹配下一个;匹配不成功,j往回退,j = ne[j];

1.3 时间复杂度

O(m + n)

2. 基本性质

2.1 border的传递性

弱传递性: 串s是串t的border,串t是串r的border,那么s是r的border

  • aba是ababa的border
  • ababa是abababa的border
  • 则有传递性aba是abababa的border

更强的传递性: 串s是串r的border,串t是r的border,单|t| < |s|,则串t是串s的border

  • aba是abababa的border
  • ababa是abababa的border
  • 则aba也是ababa的border

mb(s) 表示s的最长border,则:

  • mb(s),mb(mb(s)),...,构成了s的所有border
  • s的所有border环环相扣,被1条链串起来

2.2 失配树

    将ne[i]视为i点的父节点,那么通过ne数组可以把0~N点连成一棵树,满足性质:

  1. 点i的所有祖先都是前缀pre(s, i)的border
  2. 没有祖先关系两个点i,j没有border关系
    计算ne数组可以看作:从j=fa[i-1]开始不断往上走,找第一个满足s[j+1]=s[i]的点,把i点的父亲设为j+1

kmp 算法 第3张

2.3 循环节

    定理:假设S的长度为len,则S存在最小循环节,循环节的长度L=len-next[len],子串为S[0…len-next[len]-1]。
(1)如果len可以被len - next[len]整除,则表明字符串S可以完全由循环节循环组成,循环周期T=len/L。
(2)如果不能,说明还需要再添加几个字母才能补全。需要补的个数是循环个数L-len%L=L-(len-L)%L=L-next[len]%L,L=len-next[len]。

sample:

  1. s0s1s2s3s4s5 ,next[6]=3
    即s0s1s2=s3s4s5
    很明显可知:循环子串为s0s1s2,L=len-next[6]=3,且能被len整除。

  2. s0s1s2s3s4s5s6 ,next[7]=4
    此时len-next[7]=3,即L=3
    由s0s1s2s3=s3s4s5s6
    可知s0s1=s3s4,s2s3=s5s6
    从而可知s0s1s2=s3s4s5,s0=s3=s6
    即如果再添加3-4%3=2个字母(s1s2,加在原字符串的最后),那么得到的字符串就可以由s0s1s2循环3次组成

2. 典型例题

2.1 字符串匹配

2.1.1 一维字符串匹配

acwing831KMP字符串

给定一个模式串S,以及一个模板串P, 模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。

#include <bits/stdc++.h>

using namespace std;

int const N = 1e6 + 10;
int n, m;
char p[N], s[N];
int ne[N];

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    // 生成ne数组
    for (int i = 2, j = 0; i <= n; ++i)
    {
        while (j && p[j + 1] != p[i]) j = ne[j];  // 对模式串进行匹配,如果没有匹配成功,j变成ne[j]的位置
        if (p[j + 1] == p[i]) j++;  // 匹配成功,j到后一位
        ne[i] = j;  // 记录匹配成功的i位和j位的对应关系
    }
    // 利用ne数组进行模式匹配
    for (int i = 1, j = 0; i <= m; ++i)
    {
        while (j && p[j + 1] != s[i]) j = ne[j];  // 没匹配成功, j变成ne[j]的位置
        if (p[j + 1] == s[i]) j++;  // 匹配成功,j往后一位
        if (j == n)   // 如果完全匹配完毕
        {
            cout << i - n << " ";
            j = ne[j];  // 寻找p在s中的下一次出现的位置,利用ne数组优化时间复杂度
        }
    }
    return 0;
}

2.1.2 二维字符串匹配

POJ 2185 Milking Grid
在N*M字符矩阵中找出一个最小子矩阵,使其多次复制所得矩阵包含原矩阵。N<=1e4, M<=75

/*
本题要求最小子矩阵,把每行看成一个字符,求个ne数组,然后把每列看成一个字符,求ne数组
要使得子矩阵最小,那么就是要是的周期最小,即border最大,那么直接把m-ne[m]即可
答案即为(n-ne[n])*(m-ne[m])
*/
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

int const N = 1e5 + 10, M = 80;
char s[N][M];
int n, m, ne[N];

// 每行当成字符
bool cmp1(int i, int j) {
    for (int k = 1; k <= m; ++k) 
        if (s[i][k] != s[j][k]) return false;
    return true;
}

// 每列当成字符
bool cmp2(int i, int j) {
    for (int k = 1; k <= n; ++k)
        if (s[k][i] != s[k][j]) return false;
    return true;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) cin >> s[i] + 1;

    // 行做kmp
    for (int i = 2, j = 0; i <= n; ++i) {
        while (j && !cmp1(i, j + 1)) j = ne[j];
        if (cmp1(i, j + 1)) j++;
        ne[i] = j;
    }
    int ans = n - ne[n];

    // 列做kmp
    memset(ne, 0, sizeof ne);
    for (int i = 2, j = 0; i <= m; ++i) {
        while (j && !cmp2(i, j + 1)) j = ne[j];
        if (cmp2(i, j + 1)) j++;
        ne[i] = j;
    }
    ans = ans * (m - ne[m]);
    printf("%d", ans);
    return 0;
}

2.2 失配树问题(周期、循环节问题)

2.2.1 失配树版题

P5829 【模板】失配树
m组询问,每组询问给定p,q,求s的p前缀和q前缀的最长公共border的长度。

/*求p和q地最长border即为求p和q点的lca,如果lca=p||lca=q,那么lca=fa[lca]*/
#include <bits/stdc++.h>

using namespace std;

const int N = 1e6 + 10, M = N * 2;
int f[N][22], d[N];  // f[i][j]表示从i开始,往上走2^j步到达的点,d为深度,dist为距离
int e[M], ne[M], h[N], idx;
int T, n, m, t;  // t为数的深度
queue<int> q;
char s[N];
int nex[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// 预处理:得到每个点的深度,距离,f数组
void bfs()
{
    q.push(0);
    d[0] = 1;
    while (q.size())
    {
        int x = q.front();
        q.pop();
        for (int i = h[x]; i != -1; i = ne[i])
        {
            int y = e[i];
            if (d[y]) continue;
            d[y] = d[x] + 1;  // 更新深度
            
            // 进行dp更新
            f[y][0] = x;
            for (int j = 1; j <= t; ++j)
            {
                f[y][j] = f[f[y][j - 1]][j - 1];  // 分两段处理
            }
            q.push(y);
        }
    }
}

// 查找x和y的最近公共祖先
int lca(int x, int y)
{
    if (d[x] > d[y]) swap(x, y);  // 保证x的深度浅一点
    for (int i = t; i >= 0; --i)
        if (d[f[y][i]] >= d[x]) y = f[y][i];  // 让x和y到同一个深度
    if (x == y) return x;
    for (int i = t; i >= 0; --i)  // 让x和y之差一步就能相遇
    {
        if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    }
    return f[x][0];
}

int main()
{
    cin >> s + 1;
    n = strlen(s + 1);

    // 求ne数组
    for (int i = 2, j = 0; i <= n; ++i)
    {
        while (j && s[j + 1] != s[i]) j = nex[j];  // 对模式串进行匹配,如果没有匹配成功,j变成ne[j]的位置
        if (s[j + 1] == s[i]) j++;  // 匹配成功,j到后一位
        nex[i] = j;  // 记录匹配成功的i位和j位的对应关系
    }

    memset(h, -1, sizeof h);
    t = (int)(log(n) / log(2)) + 1; // 得到树的深度
    
    // 读入一棵树
    for (int i = 1; i <= n; ++i) {
        add(i, nex[i]), add(nex[i], i);
    }
    
    cin >> m;
    bfs();
    // 回答询问
    for (int i = 1; i <= m; ++i)
    {
        int a, b;
        scanf("%d %d", &a, &b);
        int lcaa = lca(a, b);
        if (lcaa == a || lcaa == b) lcaa = f[lcaa][0];
        printf("%d\n", lcaa);
    }
    return 0;
}

2.2.2 求最小border

luogu P3435 [POI2006]OKR-Periods of Words
给定一个字符A,对于A的每个前缀pre(A, i),求最长字符串Bi,使得len(Bi) < i, 且pre(A, i)是若干Bi连接成的字符串的前缀,求出每个Bi的长度和
len(A) <= 1e6

/*
本题要求的是最短border,那么就无法使用ne数组了
我们考虑在失配树上去思考问题,最短border就是离i点最远且最接近0但不等于0的那个点
因此我们可以使用路径压缩的思路,每次找到i对应的那个最短border后,修改ne数组,使得i直接指向它的最短border
树可以不需要建出来
*/
#include <bits/stdc++.h>

using namespace std;

int const N = 1e6 + 10;
int ne[N], n;
char s[N];

int main()
{
    cin >> n >> s + 1;
    // 生成ne数组
    for (int i = 2, j = 0; i <= n; ++i)
    {
        while (j && s[j + 1] != s[i]) j = ne[j];  // 对模式串进行匹配,如果没有匹配成功,j变成ne[j]的位置
        if (s[j + 1] == s[i]) j++;  // 匹配成功,j到后一位
        ne[i] = j;  // 记录匹配成功的i位和j位的对应关系
    }

    long long res = 0;
    for(int i = 1, j; i <= n; i++) {
        j=i;
        while(ne[j]) j = ne[j];
        if (ne[i]!=0) ne[i] = j;//记忆化
        res += (long long)i - j;
    }
    printf("%lld\n", res);

    return 0;
}

2.2.3 求border数目

    在求ne数组的同时,我们可以一起把border数组求出来

P2375[NOI 2014]动物园
给定长度为N的字符串s,对每一个前缀pre(s, i),求出长度不超过一半的border数量.N<=1e6

/*本题要求求出每个前缀pre(s, i),长度小于i/2得border得数目,记dep[x]长度为x时的border数目,那么我们可以在
求ne数组时就把dep数组求出来(dep数组其实就是每个点的深度)。那么我们可以通过倍增的方法从i开始往上跳,找到第一个深度小于i/2的位置,然后把对应的cnt[i]乘到答案即可*/
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 1e6 + 10, MOD = 1e9 + 7;
int ne[N], f[20][N], n, t, dep[N];
char s[N];

int main()
{
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);

    cin >> t;
    while (t--)
    {
        scanf("%s", s + 1);
        n = strlen(s + 1);

        dep[1] = 1;

        // kmp求ne数组,求dep数组
        for (int i = 2, j = 0; i <= n; ++i) {
            while (j && s[i] != s[j + 1]) j = ne[j];
            if (s[i] == s[j + 1]) j++;
            ne[i] = j;
            f[0][i] = ne[i];
            if (!ne[i]) dep[i] = 1;
            else dep[i] = dep[ne[i]] + 1;
        }

        // 初始化倍增数组
        for (int j = 1; j < 20; ++j) 
            for (int i = 1; i <= n; ++i) 
                f[j][i] = f[j - 1][f[j - 1][i]];
        
        // 从i点往上跳,跳到第一个<=i/2的位置
        LL ans = 1;
        for (int i = 2; i <= n; ++i) {
            int tt = i;
            for (int j = 19; j >= 0; --j) {
                if (f[j][tt] * 2 > i) tt = f[j][tt];
            }

            ans = ans * dep[tt] % MOD;
        }
        printf("%lld\n", ans);
    }
    
    return 0;
}
/*本题还可以做两边kmp,第一遍求出每个点的深度dep数组,然后第二次做kmp时,用ne数组暴力往上跳,
直到跳到<=i/2的位置,这样做是线性的(记x[i]为i/2的border的位置,那么x[i]<=x[i-1]+1,因此j是递减的,可以保证这个算法是O(n)的)*/
#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
int const N = 1e6 + 10, MOD = 1e9 + 7;
int ne[N], n, t, dep[N];
char s[N];

int main()
{
    freopen("in.txt", "r", stdin);
    freopen("out.txt", "w", stdout);

    cin >> t;
    while (t--)
    {
        scanf("%s", s + 1);
        n = strlen(s + 1);

        dep[1] = 1;

        // kmp求ne数组,求dep数组
        for (int i = 2, j = 0; i <= n; ++i) {
            while (j && s[i] != s[j + 1]) j = ne[j];
            if (s[i] == s[j + 1]) j++;
            ne[i] = j;
            if (!ne[i]) dep[i] = 1;
            else dep[i] = dep[ne[i]] + 1;
        }

        LL ans = 1;

        // 记x[i]为i/2的border的位置,那么x[i]<=x[i-1]+1
        for (int i = 2, j = 0; i <= n; ++i) {
            while (j && s[i] != s[j + 1]) j = ne[j];
            if (s[i] == s[j + 1]) j++;
            while ((j << 1) > i) j = ne[j];  // 因此j是递减的,可以保证这个算法是O(n)的
            ans = ans * (dep[j] + 1) % MOD;
        }
        printf("%lld\n", ans);
    }
    
    return 0;
}
扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄