AcWing 831. KMP字符串

题目来源:AcWing 831. KMP字符串

一、题目描述

给定一个模式串 S S S,以及一个模板串 P P P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模板串 P P P 在模式串 S S S 中多次作为子串出现。

求出模板串 P P P 在模式串 S S S 中所有出现的位置的起始下标。

输入格式
第一行输入整数 N N N,表示字符串 P P P 的长度。

第二行输入字符串 P P P

第三行输入整数 M M M,表示字符串 S S S 的长度。

第四行输入字符串 S S S

输出格式
共一行,输出所有出现位置的起始下标(下标从 0 0 0 开始计数),整数之间用空格隔开。

数据范围
1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
1 ≤ M ≤ 1 0 6 1≤M≤10^6 1M106

输入样例:

3
aba
5
ababa

输出样例:

0 2

二、算法思路

KMP算法是字符串匹配算法,对暴力的那种一一比对的方法进行了优化,使时间复杂度大大降低。
基本定义:

  1. s[ ] :是模式串,即较长的字符串。
  2. p[ ] :是模板串,即较短的字符串。
  3. 非平凡前缀:指除了最后一个字符之外,字符串的全部头部字符组合,简称前缀。
  4. 非平凡后缀:指除了第一个字符之外,字符串的全部尾部字符组合,简称后缀。
  5. 部分匹配值:前缀和后缀的最长公共元素的长度
  6. next[ ] 部分匹配表:即 n e x t next next 数组,它存储的是每一个下标对应的“部分匹配值”,是KMP算法的核心

核心思想:在每次匹配 s s s 串失败时,不是把 p p p 串整体往后移动一位继续匹配,而是把 p p p 串整体往后移动至下一次可以和前面部分匹配的位置,这样就可以跳过大多数失败的步骤。而每次 p p p 串整体移动的步数就是通过查找 n e x t next next 数组确定的。

next数组的含义及手动模拟
然后来说明一下 n e x t next next 数组的含义:对 n e x t [ j ] next[ j ] next[j],是 p [ 1 , j ] p[ 1, j ] p[1,j] 串中前缀和后缀相同的最大长度(部分匹配值),即 p [ 1 , n e x t [ j ] ] = p [ j − n e x t [ j ] + 1 , j ] p[ 1, next[ j ] ] = p[ j - next[ j ] + 1, j ] p[1,next[j]]=p[jnext[j]+1,j]

例如:
在这里插入图片描述
手动模拟求 n e x t next next 数组,对 p = “ a b c a b ” p = “abcab” p=abcab

p p p a a a b b b c c c a a a b b b
下标 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5
n e x t [ ] next[ ] next[] 0 0 0 0 0 0 0 0 0 1 1 1 2 2 2

n e x t [ 1 ] next[ 1 ] next[1] :前缀 = 空集 ——————————— 后缀 = 空集 ———————————— n e x t [ 1 ] = 0 next[1] = 0 next[1]=0;

n e x t [ 2 ] next[ 2 ] next[2] :前缀 = { a } ——————————— 后缀 = { b } ————————————— n e x t [ 2 ] = 0 next[2] = 0 next[2]=0;

n e x t [ 3 ] next[ 3 ] next[3] :前缀 = { a , ab } ————————— 后缀 = { c , bc} ——————————— n e x t [ 3 ] = 0 next[3] = 0 next[3]=0;

n e x t [ 4 ] next[ 4 ] next[4] :前缀 = { a , ab , abc } ——————— 后缀 = { a , ca , bca } ———————— n e x t [ 4 ] = 1 next[4] = 1 next[4]=1;

n e x t [ 5 ] next[ 5 ] next[5] :前缀 = { a , ab , abc , abca } ———— 后缀 = { b , ab , cab , bcab} ————— n e x t [ 5 ] = 2 next[5] = 2 next[5]=2;

KMP算法匹配过程

​ KMP主要分两步:求 n e x t next next 数组、匹配字符串。个人觉得匹配操作容易懂一些,疑惑我一整天的是求next数组的思想。所以先把匹配字符串讲一下。

s s s 串和 p p p 串都是从下标 1 1 1开始的。 i i i 1 1 1开始, j j j 0 0 0开始,每次由 s [ i ] s[ i ] s[i] p [ j + 1 ] p[ j + 1 ] p[j+1]进行比较:
在这里插入图片描述
当匹配过程到上图所示时,s[a, b] = p[1, j] && s[i] != p[j + 1],此时要整体移动 p p p 字符串(不是移动 1 1 1格,而是直接移动到下次能够匹配的位置)
其中①串为[1, next[j]],③串为[j - next[j] + 1, j]。由匹配可知①串等于③串,③串等于②串。所以直接整体移动 p p p 串使①串到③串的位置即可。这个操作可由j = next[j]直接完成。 如此往复下去,当j == m时匹配成功。

匹配部分代码模板

for(int i = 1, j = 0; i <= n; i++)
{
    while(j && s[i] != p[j + 1]) j = ne[j];
    //如果j有对应p串的元素, 且s[i] != p[j+1], 则失配, 移动p串
    //用while是由于移动后可能仍然失配,所以要继续移动直到匹配或整个p串移到后面(j = 0)

    if(s[i] == p[j + 1]) j++;
    //当前元素匹配,j移向p串下一位
    if(j == m)
    {
        //匹配成功,进行相关操作
        j = next[j];  //继续匹配下一个子串
    }
}

:采用上述的匹配方法,即 s [ i ] s[i] s[i] p [ j + 1 ] p[j+1] p[j+1] 比较,而不推荐下标从 0 0 0开始的原因我认为是:若下标从 0 0 0开始的话, n e x t next next 数组的值都会相应 − 1 -1 1,这就会导致它的实际含义与其定义的意思不符(部分匹配值和 n e x t next next 数组值相差 1 1 1),思维上有点违和,容易出错。

KMP算法求next数组

​ next数组的求法是通过模板串自己与自己进行匹配操作得出来的(代码和匹配操作几乎一样)。
在这里插入图片描述
求next部分代码模板

for(int i = 2, j = 0; i <= m; i++)
{
    while(j && p[i] != p[j + 1]) j = next[j];

    if(p[i] == p[j + 1]) j++;

    next[i] = j;
}

代码和匹配操作的代码几乎一样,关键在于每次移动 i i i 前,将 i i i 前面已经匹配的长度记录到 n e x t next next 数组中。

三、代码

#include <iostream>

using namespace std;

const int N = 100010, M = 10010; //N为模式串长度,M匹配串长度

int n, m;
int ne[M]; //next[]数组,避免和头文件next冲突
char s[N], p[M];  //s为模式串, p为匹配串

int main()
{
    cin >> n >> s + 1 >> m >> p + 1;  //下标从1开始

    //求next[]数组
    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++;
        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)  //满足匹配条件,打印开头下标, 从0开始
        {
            //匹配完成后的具体操作
            //如:输出以0开始的匹配子串的首字母下标
            //printf("%d ", i - m); (若从1开始,加1)
            j = ne[j];            //再次继续匹配
        }
    }

    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

铁头娃撞碎南墙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值