2020/10/11 Acwing-KMP算法

题目

给定一个模式串S,以及一个模板串P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模板串P在模式串S中多次作为子串出现。
求出模板串P在模式串S中所有出现的位置的起始下标。
输入格式
第一行输入整数N,表示字符串P的长度。
第二行输入字符串P。
第三行输入整数M,表示字符串S的长度。
第四行输入字符串S。
输出格式
共一行,输出所有出现位置的起始下标(下标从0开始计数),整数之间用空格隔开。
数据范围
1≤N≤105
1≤M≤106
输入样例:
3
aba
5
ababa
输出样例:
0 2

题目分析

给出一个较长的字符串和短些的模板串,目的是在字符串中找到模板串,并返回匹配到的串中的首索引,如果有多个匹配,那么要返回多个数值.
字符串的索引为i,模板串的索引为j
朴素算法是i遍历,在每个i中,比较i指针和j指针的值是否相同,相同则j指针前移,i继续遍历(也就是i和j同时前行),不同则i继续遍历,j退回起点.
朴素做法的问题在于,每次j都是退回起点,从而浪费了匹配到的信息,如果能够少退回几格,那么效率就能得到很大提升.

算法分析

  1. next数组
    明确数组的含义: 数组针对的是模板串,数组的索引表示的是模板串的某个位置的索引,数组的值表示的是模板串以某位置(i)为边界,前面的字符串的最长匹配的前后缀的长度.
    eg: ababab|
    在|前,最长的前后缀长度是4(abab),表示的含义是i前的字符串前面n个字符和后面n个字符完全相同.
    Q:那么为什么要做这个工作呢?找到模板串任意位置的最长前后缀有什么意义呢?
    A:充分利用之前匹配成功的信息,减少需要对比的次数.
  2. 匹配过程
    让i依次遍历,知道遍历到字符串的最后一个,表示字符串所有的字符都考虑完全.
    首先要知道j指针的含义:表示j之前所有的字符都匹配了
    在每次遍历中,观察i指针与j指针(代码的含义是j指针的下一个)所指的字符是否相同,如果相同,j指针前移,i本次遍历结束(i指针前移),最好的情况是有很多都匹配,这说明i和j都前移了很多字符,也就意味着此刻j前的所有字符都是匹配的;如果不同,我们让j前移变为next[j],也就是说,我们的期望降低了,我们只认为是前缀匹配了,然后再次进行匹配,如果下一个字符匹配成功,那么继续前移;如果下一个字符匹配失败,说明我们还需要继续降低期望,变成"前缀的前缀"来进行匹配.依次类推.

算法实现

  1. next数组是如何实现的?
    我们给出两个指针i=0,j=2,j表示的就是边界,也即是我们要看的就是j前的最长前后缀,而i表示的是进行匹配的指针.
    模板串下标从1开始
    j之所以选择2是因为从2开始才有两个字符进行匹配,才有意义,i选择0是因为我们比较的是i+1和j的值,如果相同了i才进行移动.
    现在,我们制定一个规则,只有当i+1和j的值相同时,两指针才进行移动,表示匹配成功;否则,i前移至next[i],继续进行匹配,这样,i一直指向的是匹配后缀的字符串,并且保证能增加时增加(最长),不能增加(不匹配)就退而求其次,继续匹配.

2.整体排序是如何实现的?
字符串的索引为i,模板串的索引为j
当i和j匹配时,j++,i到下一个循环(i++);
当i个j不匹配时,j退到next[j],继续进行匹配(while语句)
当j到达模板串的末尾,表示全部匹配,此时进行要求操作(输出,如果要匹配所有则强制让j进行回退到next[j]即可)

源代码

import java.io.*;
class Main{
    public static void main(String[] args)throws Exception{
        BufferedReader br=new BufferedReader(new InputStreamReader(System.in));
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(System.out));
        int n=Integer.parseInt(br.readLine());
        String p1=br.readLine();
        int[] ne=new int[100010];
        char[] p=new char[100010];
        char[] s=new char[1000010];
        for(int i=1;i<=n;i++){
            p[i]=p1.charAt(i-1);
            }
        int m=Integer.parseInt(br.readLine());
        String s1=br.readLine();
        for(int i=1;i<=m;i++){
            s[i]=s1.charAt(i-1);
        }
        
        
        //建立ne数组,存储对于模式数组,所有i对应 匹配后缀的 最长前缀的 结尾.
        
        for(int i=2,j=0;i<=n;i++){
            //下一个J没匹配上
            while(j>0&&p[i]!=p[j+1])
            //说明J太长了,那么我们还要找满足i附近的后缀,而恰J能满足,所以J的自己也是有戏的,因此J回退
            //换句话说,一开始的J能匹配i向前很多个字符,但是再往下1个就不能满足了(不能满足后缀前缀匹配,而且能变的其实只有后缀前缀的长度,i其实是不能动的)
            //因此当前的J是不满足条件的,要向前搜索,缩小前后缀的长度,以J为后缀端点,
            j=ne[j];
            //这句的含义是如果J有值才让j++,否则J是不应该加的
            if(p[i]==p[j+1]) j++;
            ne[i]=j;
        }
        
        //进行匹配
        //字符串下角标从1开始,i表示字符串,J表示模板串
        for(int i=1,j=0;i<=m;i++){ 
            //同样在没有匹配时不应该进行回退操作(因为没有回退可退了)
            while(j>0&&s[i]!=p[j+1]) j=ne[j];
            if(s[i]==p[j+1]) j++;
            if(j==n){
                bw.write((i-n)+" ");
                j=ne[j];
                
            }
        }
        bw.flush();
        br.close();
        bw.close();
        
    }
}

说明

由于测试序列比较多,为了防止TLE,不能使用Scanner,而要用BF/BW,而且,对于bw的flush操作,不要每次循环都flush,这样会多很多次无用的操作,在全写入缓冲区后统一进行flush即可
//这里感谢帮忙的小伙伴找到问题
之前说进行一次操作flush一次主要是保存数据,防止数据丢失,算法题是不需要考虑这些的.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值