KMP 算法

KMP算法 + 边界处理

https://www.acwing.com/problem/content/description/4315/

本题的难点主要在于给定你区间的左右端点,如何求出来这个区间内成功匹配的个数

这里肯定要用前缀和,不然1e8的时间复杂度很悬

但是用前缀和的时候,我们要对左右端点稍微处理一下

在使用KMP算法匹配的时候,如果在位置原字符串的位置pos成功匹配(这里很关键),我们有两个选择:

  1. s[pos] ++,在匹配成功位置的右端点+1
  2. s[pos - m + 1] ++ (m表示匹配串的长度),在匹配成功位置的左端点+1

对于选择1,有这么一种情况ababcabcd ,假如ab 是我们要匹配的字符串,由于我们在匹配成功位置的右端点+1,那么,上串标下划线的地方s[pos]=1,如果我们选定红色区间的字符串(下标从1开始)s[8] - s[6] = 1 ,我们发现问题了!明明bc 不可能与ab匹配,但是我们得到的答案是1,这是由于我们匹配到了匹配串ab的最后一个元素b,但我们没有完全匹配到ab导致的,那么如何解决这个问题呢?

我们可以让左端点L 的位置变为L+m-1,什么意思呢?

对应上面的例子,我们发现,如果L=L+m-1 ,那么此时L和R重合,s[8]-s[7]=0

这时候,整个区间*[L,R]中的每一个点,就表示我们匹配到的一个长度为m的串的右端点,注意,是右端点!而我们在前面匹配成功的时候,也是在右端点+1*,这样,就可以保证不会匹配到一个后缀,导致出现错误答案。

同理,如果我们在左端点+1,那么给定区间的左端点就要减去*(m-1),区间中的每个点就表示匹配到的一个长度为m*的串的左端点。

同时注意,由于我们修改了左右端点,此时可能会出现L>R的情况,所以我们要判断一下

在左端点+1

cpp 复制代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m, q;
int ne[N], s[N];
char str[N], t[N];

int main()
{
    cin >> n >> m >> q;
    cin >> (str + 1) >> (t + 1);
    
    for(int i = 2, j = 0; i <= m; i ++ )
    {
        while(j && t[i] != t[j + 1])    j = ne[j];
        if(t[i] == t[j + 1])    j ++ ;
        ne[i] = j;
    }
    
    for(int i = 1, j = 0; i <= n; i ++ )
    {
        while(j && str[i] != t[j + 1])  j = ne[j];
        if(str[i] == t[j + 1])  j ++ ;
        if(j == m)
        {
            s[i - m + 1] ++ ;//在左端点+1
            j = ne[j];
        }
    }
    
    for(int i = 1; i <= n; i ++ )   //由于修改左端点的时候我们是在当前位置的前面+1,所以不能直接在匹配过程中求前缀和
        s[i] += s[i - 1];
      
    while(q -- )
    {
        int l, r;
        cin >> l >> r;
        r = r - m + 1;//右端点左移
        if(l > r)   cout << 0 << endl;
        else cout << s[r] - s[l - 1] << endl;
    }
    
    return 0;
}

在右端点+1

cpp 复制代码
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m, q;
int ne[N], s[N];
char str[N], t[N];

int main()
{
    cin >> n >> m >> q;
    cin >> (str + 1) >> (t + 1);
    
    for(int i = 2, j = 0; i <= m; i ++ )
    {
        while(j && t[i] != t[j + 1])    j = ne[j];
        if(t[i] == t[j + 1])    j ++ ;
        ne[i] = j;
    }
    
    for(int i = 1, j = 0; i <= n; i ++ )
    {
        s[i] = s[i - 1];
        while(j && str[i] != t[j + 1])  j = ne[j];
        if(str[i] == t[j + 1])  j ++ ;
        if(j == m)
        {
            s[i] ++ ;
            j = ne[j];
        }
    }
    
    // for(int i = 1; i <= n; i ++ )   cout << s[i] << " ";
    // cout << endl;
      
    while(q -- )
    {
        int l, r;
        cin >> l >> r;
        l += m - 1;
        if(l > r)   cout << 0 << endl;
        else cout << s[r] - s[l - 1] << endl;
    }
    
    return 0;
}

KMP算法和next数组的性质

目录

模板

例题:

1.匹配字符串:831. KMP字符串 - AcWing题库\](#1.匹配字符串:831. KMP字符串 - AcWing题库) [next数组具有周期性](#next数组具有周期性) \[2.next周期的应用:141. 周期 - AcWing题库\](#2.next周期的应用:141. 周期 - AcWing题库) \[3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库\](#3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库) \[4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库\](#4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库) *** ** * ** *** ### 模板 **next\[i\]表示以i结尾的后缀中与其匹配的最大前缀的长度** ```cpp 求Next数组: // s[]是模式串,p[]是模板串, n是s的长度,m是p的长度 for (int i = 2, j = 0; i <= m; i ++ ) { while (j && p[i] != p[j + 1]) j = ne[j]; //注意是while循环,j=2开始 if (p[i] == p[j + 1]) j ++ ;//这里的j可以表示为以i为终点的串中的以i为终点的后缀中与模板穿最大匹配长度,即匹配时一定包含了i这个位置的字符 ne[i] = j; } // 匹配 for (int i = 1, j = 0; i <= n; i ++ ) { while (j && s[i] != p[j + 1]) j = ne[j]; if (s[i] == p[j + 1]) j ++ ; if (j == m) { j = ne[j]; // 匹配成功后的逻辑 } } ``` ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png) ### 例题: #### 1.匹配字符串:[831. KMP字符串 - AcWing题库](https://www.acwing.com/problem/content/833/) **AC代码** ```cpp #include #include using namespace std; const int N = 1000007, M = 100007; //next在某些头文件中是关键词 int ne[M]; //next数组表示在当前位置不匹配,需要移动的距离,就是最大前缀的长度 char p[M], s[N]; int m, n; int main() { cin >> m >> (p + 1) >> n >> (s + 1);//+的优先级比>>高,不加括号会报错 //求next数组 for(int i = 2, j = 0; i <= m; i ++ ) //i从2开始,因为next[1]初始化为0 { while(p[i] != p[j + 1] && j) j = ne[j]; //因为我们每次比较的是查找串S的第i位和模板串P的第j+1位,所以我们要回退next[j]的距离,不是next[j+1],即j+1位置之前的最长后缀距离 if(p[i] == p[j + 1]) j ++ ; //如果匹配成功,j进一位,保存当前位置的next值,否则只能保存0,因为while中肯定是循环到j=0结束的 ne[i] = j; } //匹配 for(int i = 1, j = 0; i <= n; i ++ ) { while(s[i] != p[j + 1] && j) j = ne[j];//如果不匹配,j回退,直到j退无可退,即j=0到达原点的时候 /* 为什么要设置成i匹配j的下一位,如果设置成i匹配j,那么当i与j的第一位就不匹配时,即s[i]!=p[1],我们是要回退next[0]的距离的, 但next[0]是规定成0的,那么就会死循环,因为j回退0等于不回退,除非我们更改next[0]的值,但这显然是不可能的 所以我们让i匹配j的下一位,并设置j=0的位置为j的起点,这样当i与j的第一位就不匹配时,我们不让他回退了,因为这时显然退无可退 所以j只能进位,毕竟退无可退 */ if(s[i] == p[j + 1]) j ++ ; //如果匹配,j进一位 if(j == m) //整个模板串都匹配成功了 { cout << i - m << " "; } } cout << endl; return 0; } ``` ![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png) ### next数组具有周期性 ### **2.next周期的应用:** [141. 周期 - AcWing题库](https://www.acwing.com/problem/content/143/) ![img](https://i-blog.csdnimg.cn/blog_migrate/834655f945dd7282b2c49475024aaae5.png)![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png)编辑 这里假设在n处不匹配,模板串回退到next\[n\]即b的位置,那么b就是最大前缀的终点,假设a为最大后缀的起点 就有区间长度\[1, b\]=\[a, n\],因为\[a, b\]是他们的公共部分,所以区间长度\[1, a\]=\[b, n

假设下面的区间就是模板串移动后的位置,沿垂直方向做下一个移动后的模板串a在原位置的a',有原区间[a, a' ]=[1, a]

又因为[1, a]=[b, n],所以说[a, a' ] = [a' , b]

所以区间a[1, a] = [a, a' ] = [a' , b] = [b, n],即[1, n]可以被均分为四个子区间,区间长度为n-next[i]

一个重要的点是不要被这个图误导了,误认为next周期一定为4,其实当next[n]与a重合时,周期为3,当next[n]与1重合时,周期为1......

AC代码

cpp 复制代码
#include <iostream>
#include <cstring>

using namespace std;

const int N = 1000010;

char s[N];
int ne[N], n, T;

void get_next()
{
    ne[1] = 0;
    for(int i = 2, j = 0; i <= n; i ++ )
    {
        while(s[i] != s[j + 1] && j)  j = ne[j];
        if(s[i] == s[j + 1])  j ++ ;
        ne[i] = j;
    }
}

int main()
{
    while(cin >> n, n)
    {
        cout << "Test case #" << ++T << endl;
        cin >> (s + 1);
        
        get_next();
        
        for(int i = 1; i <= n; i ++ )
        {
            int t = i - ne[i];
            if(i > t && i % t == 0) cout << i << " " << i/t << endl;//
        }
        cout << endl;
    }
    
    
    return 0;
}

3.对字符串匹配过程的理解:159. 奶牛矩阵 - AcWing题库

思路参考:AcWing 160. 匹配统计 - AcWing

求Next数组:

// s[]是模式串,p[]是模板串, n是s的长度,m是p的长度

for (int i = 2, j = 0; i <= m; i ++ )

{

while (j && p[i] != p[j + 1]) j = ne[j];

if (p[i] == p[j + 1]) j ++ ; //深刻理解:这里的j可以表示为以i为终点的串中的 以i为终点的后缀中与模板穿最大匹配长度,即匹配时一定包含了i这个位置的字符和前面匹配成功的j-1个字符

ne[i] = j;

}

AC代码

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 200010;

char s[N], t[N];
int ne[N];
int f[N];   //因为我们无法求出确定的前缀长度,只能求出最小的前缀长度,所以f[i]表示一个范围,f[i]表示所有后缀中匹配长度大于等于i的后缀数

int main()
{
    int n, m, q;
    cin >> n >> m >> q;
    scanf("%s%s", s + 1, t + 1);
    
    //初始化next数组
    for(int i = 2, j = 0; i <= m; i ++ )
    {
        while(t[i] != t[j + 1] && j)   j = ne[j];
        if(t[i] == t[j + 1])    j ++ ;
        ne[i] = j;  
    }
    
    //匹配
    for(int i = 1, j = 0; i <= n; i ++ )
    {
        while(s[i] != t[j + 1] && j)   j = ne[j];
        if(s[i] == t[j + 1])    j ++ ;
        f[j] ++;
    }
    
    for(int i = m; i ; i -- )   f[ne[i]] += f[i];
    
    
    while(q --)
    {
        int t;
        cin >> t;
        cout << (f[t] - f[t + 1]) << endl;    //f数组是一个范围
    }
    
    
    
    return 0;
}

4.将KMP算法匹配每次单个字符拓展到每次匹配一个串 + next数组周期性质 + 双重KMP(或者暴力+KMP):159. 奶牛矩阵 - AcWing题库

思路

  1. 如果我们要求得一个最小的覆盖子矩阵,设他的长为width,宽为height,那么height*width的积最小
  2. 因为列的范围为75,是一个很小的数,所以我们可以暴力求解矩阵的长width,再由width求解height,但要注意的一点是,某一行的一个长度width具有周期性,但到了下一行不一定还是满足,所以求width时要遍历所有行
  3. 因为我们显然可以求出多个满足周期性质的width,但我们不可能每一个都尝试求该width条件下的height,所以我们只会找一个最优的width,那么哪一个width满足最优条件,即矩阵积最小的情况呢
  4. 我们从定义考虑,已知矩阵面积为height*width,width假设已知(我们已经找到了那个最优的解),而height=n-next[n](由next数组的周其性质可得,我们要找的是整列的周期,所以是n-next[n]),因为n已经固定,那么我们只要让next[n]尽可能的大,那么height就会尽可能地小,矩阵面积就会尽可能的小。
  5. 我们通过next数组的定义可以知道,next[i]是在i位置最大的与后缀相同的前缀的长度,所以说next[i]越大,说明这个i位置之前的字符组成的串匹配度越高,例如:aaaaa的匹配程度最高,abcde匹配程度最低,当然也会受长度的影响,aaaaaaa的匹配度高于aaa。
  6. 那么问题就转化成了求如何才能使字符串的匹配程度更高,但是到这里别忘了,我们这里匹配的是一个width长度的串,不再是一个单个的字符了,如果时刻记得这一点,这个问题就很清晰了,当然是width最小的时候,匹配程度最高了,为什么呢?
  7. 假设,width=4时每次匹配的width串都是满足的,那么width=5时呢?这就不一定了,但width=3时肯定时满足的,因为width=4都满足,width=2肯定也满足,因为不满足就代表着匹配程度低,next[i]值小,n-next[i]大,矩阵乘积大,所以假象就成立

AC代码

初始化矩阵的行标都从1开始,前代码列标从0开始,后代码从1开始,理解两个代码的不同之处

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <string.h>

using namespace std;

const int N = 10010, M = 80;

int n, m;
char str[N][M];
bool st[M];
int ne[N];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ )
    {
        cin >> str[i];
        for (int j = 1; j <= m; j ++ )
        {
            bool is_match = true;
            for (int k = j; k < m; k += j)
            {
                for (int u = 0; u < j && k + u < m; u ++ )
                    if (str[i][u] != str[i][k + u])
                    {
                        is_match = false;
                        break;
                    }
                if (!is_match) break;
            }
            if (!is_match) st[j] = true;
        }
    }

    int width;
    for (int i = 1; i <= m; i ++ )
        if (!st[i])
        {
            width = i;
            break;
        }

    // cout << "widht" << width << endl;
    
    for (int i = 1; i <= n; i ++ ) str[i][width] = 0;

    for (int j = 0, i = 2; i <= n; i ++ )
    {
        while (j && strcmp(str[j + 1], str[i])) j = ne[j];
        if (!strcmp(str[j + 1], str[i])) j ++ ;
        ne[i] = j;
    }

    int height = n - ne[n];
    
    // cout << "height: " << height << endl;
    
    cout << width * height << endl;

    return 0;
}

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10007;

char str[N][100];
int ne[N];
int n, m;
bool check[N];

int main()
{
    cin >> n >> m;  //n行m列
    
    memset(check, true, sizeof check);  //初始化所有width长度为true,即是可行的
    
    for(int i = 1; i <= n; i ++ )   //n从1开始
    {
        scanf("%s", str[i] + 1);    //m从1开始
        
        for(int j = 1; j <= m; j ++ )   //对每一行枚举width长度
        {
            if(check[j]) //为false就不用检查了
            {
                for(int k = j + 1; k <= m; k += j )   //将j后面的所有区间长度为j的区间和区间[1, j]比较
                {
                    for(int u = 1; u <= j && u + k - 1 <= m; u ++ )   //和[1, j]作比较,u+j<=m防止越界比较
                    {
                        if(str[i][u] != str[i][k + u - 1])  //每次比较第u位和第k+u-1位即第一个区间的第u为和当前区间的第u位
                        {
                            check[j] = false;
                            break;
                        }
                    } 
                    if(!check[j])   break;
                }
            }
        }
    }
    
    
    
    int width;
    for(int i = 1; i <= m; i ++ )
    {
        if(check[i])    //找到最小满足条件的width就结束
        {
            width = i;
            break;
        }
    }
    
    // cout << width << endl;
    
    
    for(int i = 1; i <= n; i ++ )   str[i][width + 1] = 0;  //相当于截取一个字符串
    
    
    //KMP匹配列height
    //先写出模板,然后修改,因为我们比较的不是单个的字符了,而是一个长度为width串
    // for(int i = 2, j = 0; i <= n; i ++ )
    // {
    //     while(str[i] != str[j + 1] && j)   j = en[j];
    //     if(str[i] == str[j + 1])    j ++ ;
    //     ne[i] = j;
    // }
    
    for (int j = 0, i = 2; i <= n; i ++ )
    {
        while (j && strcmp(str[j + 1] + 1, str[i] + 1)) j = ne[j];
        if (!strcmp(str[j + 1] + 1, str[i] + 1) ) j ++ ;
        ne[i] = j;
    } 

    int height  = n - ne[n];
    
    // cout << height << endl;
    
    cout << width * height << endl;
    
    return 0;
}

一些小知识

  1. sizeof是一个运算符,不是一个函数,所以后面的对象可以不用加括号,直接 sizeof a 即可
  2. 如果想要将一个char[n][n]的字符数组从开头截取一部分,只需要在截取的末尾处让a[n][p] = 0,那么第n行就截取了一个[0,p]的串,并且如果遍历字符数组,p之后的位置就无法遍历了,因为'0'表示一个字符串的结尾 ('0'的ascall码就是0)
  3. 虽然KMP具有周期性,但也不能乱用,只有当循环节完全循环完时,即不缺也不少,才是一个真正的周期
  4. 用strcmp函数将KMP匹配字符转化为匹配串,当两个字符串相等时返回0,否则返回1或者-1

详细Next数组性质参考:困扰已久的KMP - AcWing

KMP算法视频讲解:找不到页面 - AcWing

题目

https://www.acwing.com/activity/content/problem/content/869/

https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/

https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/

相关推荐
Emberone3 小时前
从C到C++:一脚踹开面向对象的大门
开发语言·c++
DDzqss3 小时前
3.25打卡day45
c++·算法
JMchen1234 小时前
Android NDK开发从入门到实战:解锁应用性能的终极武器
android·开发语言·c++·python·c#·android studio·ndk开发
程序猿编码5 小时前
隐匿注入型ELF加壳器:原理、设计与实现深度解析(C/C++ 代码实现)
c语言·网络·c++·elf·代码注入
m0_734998015 小时前
Day 26
数据结构·c++·算法
Summer_Uncle7 小时前
【QT学习】Qt界面布局的生命周期和加载时机
c++·qt
小CC吃豆子7 小时前
C++ 继承
开发语言·c++
tankeven7 小时前
HJ151 模意义下最大子序列和(Easy Version)
c++·算法
fengenrong7 小时前
20260325
开发语言·c++