kmp算法模板

子串:必须连续
子序列:可以不连续

/**
 * 暴力做法
 * 外层循环标识s串的起点
 * 每次指定了s串的起点之后内层需要将p进行逐一对比
 * 如果能够完全匹配则输出此时s串的起点
 */
#include <iostream>
#include <string>

using namespace std;

int n, m;
char p[100010], s[1000010];

int main()
{
    cin >> n >> p;
    cin >> m >> s;
    
    for (int i = 0; i < n; ++ i)
    {
        int j = 0, k = i;
        while (j < n && p[j] == s[k])
        {
            ++ j;
            ++ k;
        }
        if (j == n) cout << i << ' ';
    }
    return 0;
}

/**
 * 暴力做法的问题是即使前面很多都已经匹配好了一旦后面出现一个不匹配的前面的白匹配了,还是会从第一个开始匹配
 * G T G T A C D
 * G T G T B E F
 * 当GTGT已经匹配完成后,在A和B的地方发生失配,按照暴力的做法,
 * G T G T A C D
 *   G T G T B E F
 * 会把模板串移动一位,但是可以发现这一次移动完全没有意义,举的这个例子只需要一次无用移动也可以找到正确的为止,但是在某些字符串中可能会有很多无用的移动
 * 下一次可能匹配成功的移动方法应该像下面这样,这样就可以保证在失配位之前是已经匹配好了的,这样就以尽可能少的移动次数最大可能的找到答案
 * G T G T A C D
 *     G T G T B E F
 * 
 * 在失配时,快速知道下次应该如何移动,我们需要的就是next数组
 * next[i]的含义是:数组下标从1开始,以1为起点,i为终点的序列中,最长的公共前后缀,比如
 * 1 2 3 4
 * G T G T
 * next[1] = 0 只有一个字母,不存在前缀和后缀的概念,所以是0
 * next[2] = 0 因为是GT没有公共前后缀,所以是0
 * next[3] = 1 最长公共前后缀是G
 * next[4] = 2 最长公共前后缀是GT
 * 巧妙的是,next数组的值恰好就是它的下一位发生失配时,应该移动的长度
 * 
 * 还是这个例子
 * G T G T A C D
 * G T G T B E F
 * 当在A和B发生失配时,T所对应的next是2
 * 所以移动方式是
 * G T G T A C D
 *     G T G T B E F
 * 恰好让原来T的位置变成next[4]即2的字母了
 * 巧妙的是数组都是从1开始的,所以next对应的长度正好就是对应的字母下标
 * 
 * 比较的时候下面那个字符串我们选择用j遍历,上面字符串选择用i遍历,每次比较的都是i和j+1,这是为了方便下面那个字符串索引next
 * 
 * 上面我们是假设next数组已经存在了,现在来想一下怎么计算这个next数组
 * 已知next的含义之后,不难发现求next的过程就是模板串自己和自己进行匹配的过程
 * 
 * 所有的设计单拿出来都显得很平常,但是放在一起来看却十分精妙,环环相扣,或许这就是算法的魅力所在
 * 
 * 下面为了叙述的方便,我们把模板串叫做短串,模式串叫做长串,模板串和模式串的关系是在模式串中寻找模板串
 */

#include <iostream>

using namespace std;

const int N = 1e5 + 10, M = 1e6 + 10;

int n, m;
int ne[N];
char p[N], s[M];

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;

    // 构造next数组, 短串自己和自己匹配, 说着简单,我对这个next的构造过程还是没那么明白,有点理解但绝对不扎实的感觉
    for (int i = 2, j = 0; i <= n; ++ i) // next[1] = 0是一定的,原因在49行给出了,所以直接从2开始
    {
        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 <= m; ++ i) // 无论短串有没有匹配成功,长串每次都要后移一位,如果匹配成功了,长串后移一位为了比较后面的很好理解,如果匹配失败了,
    {
        /** while 和 if的前后顺序不能颠倒
         * 在s[i] == p[j + 1]时,如果先进行if判断,那么j必然会变成j+1,然后会去比较s[i]和p[j+1],但是此时这个j+1已经不再是之前的j+1了,和之前的j相比已经是j+2了
         * 如果是这样的话,在下面这组数据中
         * s:G S
         * p:G S
         * G 和 G匹配成功后,j+1,然后while会让s的G和p的S再比较,发现不相等所以会让p向后移动,但是当s和p发生匹配后,下一次的比较应该是s的S和p的S,也就是i和j都应该+1再比较而非让s的G比较两次
         * 所以这就是不能先进行if再进行while的原因
         * 
         * 那为什么先让p后移再比较就是正确的呢?
         * 如果没有发生失配while不会进行直接判断短串的下一位,这是很好理解的
         * 如果发生了失配,先进行if再进行while会少判断一些东西
         * 比如说
         * G T G T A C D
         * G T G T B E F
         * 
         * 因为发生了失配,if不会执行,直接while
         * 
         * 第一次while之后会变成这样
         * G T G T A C D
         *     G T G T B E F
         * 
         * while因为还是失配的,所以还会while,会变成这样(j变成0)
         * G T G T A C D
         *         G T G T B E F
         * 
         * 按照原来先while再if的顺序,再while完成后会判断一次A 和 G
         * 但是现在if在while之前,所以A和G的比较就没有进行了,然后外层循环让s到了C
         * 在这个例子中不会发生问题,因为A和G没有匹配,即使缺少了这一次的比较,直接下次让C和G比较也是正确的,但程序明显是有问题的
         * 
         * 分析之后,我们确实可以得出在失配时必须先移动短串再进行比较,这个问题在记忆的时候就别这么复杂了,简单想即可
         */
        while (j && s[i] != p[j + 1]) j = ne[j]; // 判断是否失配,如是则将短串后移
        if (s[i] == p[j + 1]) ++ j;
        if (j == n) // j == 0时判断的是p[1],j == n-1时候判断的是p[n],所以j为n时说明全都匹配完成了
        {
            cout << i - n << ' ';
            j = ne[j]; // 这一次的匹配完成了,但是后面可能还有匹配,下一次的可能会匹配的最短移动还是遵循next数组
        }
    }

    return 0;
}
posted @ 2021-01-20 23:18  0x7F  阅读(95)  评论(0编辑  收藏  举报