KMP算法和next数组的性质

目录

模板

例题:

1.匹配字符串:831. KMP字符串 - AcWing题库

next数组具有周期性

2.next周期的应用:141. 周期 - AcWing题库

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

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


 

模板

next[i]表示以i结尾的后缀中与其匹配的最大前缀的长度

求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];
        // 匹配成功后的逻辑
    }
}

 

例题:

1.匹配字符串:831. KMP字符串 - AcWing题库

AC代码

#include <iostream>
#include <cstring>

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;
    
}

 

next数组具有周期性

 

2.next周期的应用:141. 周期 - AcWing题库

 

 

这里假设在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代码

#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];  // t就对应上图[1,a]中的a,如果有周期的话,[1,t]就是最小的周期,周期长度为t(t-1+1)
                      // 这里判断 i>t 主要是判断 i==t 的情况,此时 i%t==0,i不可能大于t
                      // 因此下面也可以写为 if(t != t && i % t == 0) ...              
       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代码

#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开始,理解两个代码的不同之处

#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;
}

 


 

#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

 

posted @ 2022-05-05 08:42  光風霽月  阅读(43)  评论(0编辑  收藏  举报