zoukankan      html  css  js  c++  java
  • KMP算法简单分析

    定义问题

    字符串匹配是这样一个问题: 对于两个包含且仅包含字母表∑中的字母的串P,T,计算出所有有效的**移进**s使得P[1..|P|] = T[s+1..s+|P|]。(|P|为P的长度)。
    或者说:求出在什么位置P被T完全包含。
    为了表达方便,定义m = |P|, n = |T|。P称为模式串,T称为匹配串

    朴素算法

    朴素算法是一种显然的方法。直接给出伪代码:

    Naive-Match (P, T)
        m = |P|, n = |T|
        for i = 1..n do
            if P[1..m] == T then
                print i" "

    朴素算法可以看成模式串紧贴匹配串滑动,尝试移进s = 1..n时能否匹配。大多数情况下,朴素算法已经可以解决问题。但是当数据极大(例如在很长的基因串中寻找一组基因)时,朴素算法的效率就显得差了。因此,科学家寻找到许多种优秀的匹配算法。这是一个常用算法时间对照表。

    算法 预处理 匹配
    朴素算法 0 O((n-m+1)m)
    Rabin-Karp Θ(m) O((n-m+1)m)
    有限自动机 Θ(m∑) Θ(n)
    Knuth-Morris-Pratt Θ(m) Θ(n)

    所有的字符串算法都很麻烦(毕竟蒟蒻)。其中KMP用处比较广。在《算法导论》里KMP的介绍是以有限自动机为基础的,然而我又看不懂,gedao了半天才大致明白KMP的思想。

    KMP算法

    Quote:来自 zrO matrix67 Orz

    假如,A=”abababaababacb”,B=”ababacb”,我们来看看KMP是怎么工作的。我们用两个指针i和j分别表示,A[i-j+ 1..i]与B[1..j]完全相等。也就是说,i是不断增加的,随着i的增加j相应地变化,且j满足以A[i]结尾的长度为j的字符串正好匹配B串的前 j个字符(j当然越大越好),现在需要检验A[i+1]和B[j+1]的关系。
    - 当A[i+1]=B[j+1]时,i和j各加一;什么时候j=m了,我们就说B是A的子串(B串已经整完了),并且可以根据这时的i值算出匹配的位置。
    - 当A[i+1]<>B[j+1],KMP的策略是调整j的位置(减小j值)使得A[i-j+1..i]与B[1..j]保持匹配且新的B[j+1]恰好与A[i+1]匹配(从而使得i和j能继续增加)。我们看一看当 i=j=5时的情况。
    详细内容参见 http://www.matrix67.com/blog/archives/115

    个人理解

    我是自己推导之后才看到上面大牛的解释,真的非常通俗。所以看不懂的同学可以去哪里膜拜一下。kmp算法实在比较恶心,虽然代码秘制煎蛋,不习惯推导的童鞋直接背下来就可以了。:(语言表达能力捉急):。
    ps:这里并没有使用图形辅助理解,个人认为这样更有利于理解kmp匹配原理。
    kmp基于一个函数π,π表示有最大的t < i使P[1..t] = P[m-t+1..m],则t = π。或者形式化地:

    π[i] = max{t | P[1..t] = P[m-t+1..m] 且 t < i}

    证明一个结论,对于任意T[k-i+1..k] = P[1..i],有:

    π[i] = max{t | P[1..t] = T[k-t+1..k] 且 t < i}
    用反证法,假设有π[i] < x < i使得P[1..x] = T[k-x+1..k]
     ∵ P[1..i] = T[k-i+1..k]
     ∴ T[k-x+1..k] = P[m-x+1..m]
     又 P[1..t] = T[k-x+1..k]
     ∴ P[1..x] = P[m-x+1..m]
     ∵ x > π[i], 根据定义,矛盾
    原命题得证。

    这个结论将说明kmp不会错过正确解。

    以及:

    如果有s使T[k-s+1..k] = P[1..s],
    那么有T[k-π[s]+1..k] = P[1..π[s]]。
    证明很简单,根据定义等量代换即可。

    这个结论将说明kmp不会找到错误解。

    这些结论并不足以证明kmp的正确性,但是基本可以看出主要思想了。事实上,通过π可以省略许多无用的比较(基于第二个结论)。kmp匹配算法代码如下:

    void kmp_match(int l) {
            // l是T的长度,pL是P的长度
            int q = 0;
            // 匹配的长度
            for (int i=1; i<=l; i++) {
                    while (q > 0 && P[q+1] != T[i])
                            q = pie[q];
                            // 无法匹配下一位,找到可以部分匹配的最大部分,或者没有可以匹配
                    if (P[q+1] == T[i])
                            q++;
                            // 下一位可以匹配
                    if (q == pL) {
                            // 找到
                            printf("Shift %d >>> ", i-pL);
                            q = pie[q];
                            // 找下一个匹配位置
                    }
            }
    }

    计算匹配函数π的方法:

    void kmp_init() {
            int k = 0;
            pie[1] = pie[0] = 0;
            // 第一位不可能找到匹配
            for (int i=2; i<=pL; i++) {
                    while (k > 0 && P[k+1] != P[i])
                            k = pie[k];
                    // 同上,自己匹配自己罢了
                    if (P[k+1] == P[i])
                            k++;
                    pie[i] = k;
                    // 记录最长匹配
            }
    }

    所谓自己匹配自己,就是π就是找到一对最大且相等的前缀和后缀,记录前缀出现位置。(基于定义)
    kmp大概就是这样了,多思考就可以想通。。

    kmp时间复杂度分析

    kmp的复杂度为Θ(n)-Θ(m),这里用摊还分析中的聚合分析法给出一个kmp_init复杂度分析例子。我们试图证明while循环的执行次数为O(n)。
    k的初值为0,而k的值增长有且只有一个途径:10行的k++。由于for循环一次k最多加一,n-1次循环之后k最多为n-1呢。由于π < i,因此while循环只会使k减少,且一次至少减少1。而k < n-1,所以while的循环次数为O(n)。不难得出kmp_init的复杂度为Θ(n)。用这种方法也可以得出kmp_match的复杂度为Θ(m)。

    linux下装逼代码

    装逼专用,仅售998,到linux上看看效果吧。

    #include <iostream>
    #include <cstdio>
    #include <cctype>
    using namespace std;
    char P[10005], T[10005];
    int pL;
    int pie[10005];
    int readfln(char *str) {
            char c;
            int i = 0;
            str[0] = '"';
            while (c = getchar()) {
                    if (c!= '
    ')
                            str[++i] = c;
                    else break;
            }
            return i;
    }
    void printfln(int shift,int l) {
            int beg = shift-5;
            if (shift <= 5)
                    beg = 0;
            else
                    printf("...");
            for (int i=beg+1; i<=shift; i++)
                    putchar(T[i]);
            printf("33[33m");
            printf("%s", P+1);
            printf("33[0m");
            int end = shift+pL+5;
            if (shift+pL+5 > l)
                    end = l;
            for (int i=shift+pL+1; i<=end; i++)
                    putchar(T[i]);
            if (shift+pL+5 < l)
                    printf("...");
            printf("
    ");
    }
    void kmp_init() {
            int k = 0;
            pie[1] = pie[0] = 0;
            for (int i=2; i<=pL; i++) {
                    while (k > 0 && P[k+1] != P[i])
                            k = pie[k];
                    if (P[k+1] == P[i])
                            k++;
                    pie[i] = k;
            }
    }
    void kmp_match(int l) {
            int q = 0;
            for (int i=1; i<=l; i++) {
                    while (q > 0 && P[q+1] != T[i])
                            q = pie[q];
                    if (P[q+1] == T[i])
                            q++;
                    if (q == pL) {
                            printf("Shift %d >>> ", i-pL);
                            printfln(i-pL,l);
                            q = pie[q];
                    }
            }
    }
    int main() {
            pL = readfln(P);
            kmp_init();
            int l;
            while (l = readfln(T))
                    kmp_match(l);
            return 0;
    }

    参考资料:《算法导论》

  • 相关阅读:
    dp学习笔记1
    hdu 4474
    hdu 1158(很好的一道dp题)
    dp学习笔记3
    dp学习笔记2
    hdu 4520+hdu 4522+hdu 4524(3月24号Tencent)
    hdu 1025(最长非递减子序列的n*log(n)求法)
    hdu 2063+hdu 1083(最大匹配数)
    hdu 1023
    《 Elementary Methods in Number Theory 》Exercise 1.3.12
  • 原文地址:https://www.cnblogs.com/ljt12138/p/6684390.html
Copyright © 2011-2022 走看看