【填坑】KMP

KMP用于求b串在a串的出现位置(字符串匹配)。我们通过一道题来讲讲KMP的算法过程。
【HDU 1711】给定两个数字串a、b。求b在a第一次出现的位置。若没出现输出-1。
一般的暴力算法就是暴力匹配。若有一个位置匹配失败,则回溯后重新匹配。时间复杂度高达 O ( n m ) O(nm)
可以发现,这个过程中有大量的冗余运算。比方说,匹配失败后(蓝色点),图中的红色段都是已经匹配好的了。
在这里插入图片描述
那么我们想要做的,就是将b向右移动若干个单位,使得a中的蓝点能够匹配上就行了。
我们把移动后的串记作b’(如图)
在这里插入图片描述
显然,b’的红色段(即b红色段的开头一小段)等于a红色段的末尾一小段(即b红色段的末尾一小段),也就是说。b’的红色段是b的一个前缀后缀公共子串。为了避免遗漏,b’的红色段就应该是b前i-1个元素的最长前缀后缀公共子串。

举一个例子。若现在有一个串:
abababa
其前缀有:
a
ab
aba
abab
ababa
ababab
后缀有:
a
ba
aba
baba
ababa
ababab
前缀后缀公共子串有:
a
aba
ababa
(原串本身不能作为前缀后缀公共子串)

于是,我们用nxt[i]表示若在b的i处匹配失败,应该再从哪里开始匹配。这个数组可以用递推搞定。特殊地,nxt[0]=-1
首先,由上面的分析可知,nxt[i]就相当于b前i-1个元素的最长前缀后缀公共子串的长度。
考虑已求出前i-1个nxt[i]。那么就判断第i-1个字符是否能加入到i-2的最长前缀后缀公共子串中。如果能,就有nxt[i]=nxt[i-1]+1。否则,令j=nxt[i-1],重复上述过程。
讲得不是很清楚,但看看代码应该就能明白:

		nxt[0] = -1;
        for(i = 1; i < m; i++)
        {
            j = nxt[i - 1];
            while(j != -1 && b[i - 1] != b[j])
                j = nxt[j];
            nxt[i] = j + 1;
        }

然后用nxt[i]计算匹配位置:

for(i = j = 0; i <= n; i++, j++)
       {
           if(j == m)
           {
               printf("%d\n", i - j + 1);
               break;
           }
           if(i == n)
           {
               puts("-1");
               break;
           }
           while(j != -1 && a[i] != b[j])
               j = nxt[j];
       }

题目的完整代码:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int mn = 1000005, mm = 10005;
int a[mn], b[mm], nxt[mm];
int main()
{
   int t, n, m, i, j;
   scanf("%d", &t);
   while(t--)
   {
       scanf("%d%d", &n, &m);
       for(i = 0; i < n; i++)
           scanf("%d", &a[i]);
       for(i = 0; i < m; i++)
           scanf("%d", &b[i]);
       if(n < m)
       {
           puts("-1");
           continue;
       }
       nxt[0] = -1;
       for(i = 1; i < m; i++)
       {
           j = nxt[i - 1];
           while(j != -1 && b[i - 1] != b[j])
               j = nxt[j];
           nxt[i] = j + 1;
       }
       for(i = j = 0; i <= n; i++, j++)
       {
           if(j == m)
           {
               printf("%d\n", i - j + 1);
               break;
           }
           if(i == n)
           {
               puts("-1");
               break;
           }
           while(j != -1 && a[i] != b[j])
               j = nxt[j];
       }
   }
}

附:时间复杂度分析:
计算匹配位置部分显然为 O ( n ) O(n)
预处理nxt[i]时,根据其构造方法可知,每一个位置最多会被当做“跳板”1次。这样,内层处理的平摊复杂度为 O ( 1 ) O(1) 。于是预处理部分为 O ( m ) O(m) ,总时间复杂度为 O ( n + m ) O(n+m)

猜你喜欢

转载自blog.csdn.net/C20181503csy/article/details/85841331