浅谈KMP

来源:互联网 发布:java concurrent书籍 编辑:程序博客网 时间:2024/06/06 00:39

KMP

前言:

由于本人能力有限,很多步骤都是按照抽象思维理解,并没有像很多其它讲解KMP的算法,是借助了图加深理解的,请见谅.

基本知识:

KMP是由KnuthMorrisPratt三个人提出的,因此命名KMP,并不是以用途命名.
②最基本的KMP是一种字符串匹配的算法,也就是判断字符串B是否为字符串A的子串的一种算法.
KMP算法可以引申到非常多的地方,里面的思想在字符串里用途很广,思想很有用.

算法讲解:


问题:
给定两个字符串A,B.
A=abababaababacb
B=ababacb
B是否为A的子串?

朴素的想法:

枚举指针i表示A串的第i位,并枚举j依次判断与B串是否相等,但这样的时间复杂度最坏为O(nm)(容易想到如果每次都要匹配到接近B的最后一位,时间复杂度就大了).
n,m大一些时这种算法显然是不可取的.

那么KMP是如何做的呢?

我们用两个指针ij分别表示,A[ij+1..i]B[1..j]完全相等(KMP的最最基本性质,也是接下来解决一切问题的思路).

意思就是:
i是枚举的,随着i的增加j也会相应地变化,且j满足以i结尾时,以i往前的j个字符正好匹配B串的前j个字符(j一定是满足条件的最大值,因为每次增加i都会更新j).

那么现在假设需要检验A[i+1]B[j+1]的关系.

A[i+1]=B[j+1]时,ij各加一.

而当j=m时,B就是A的子串.

A[i+1]B[j+1]时,我们容易知道,已i为结尾的子串已经不可能满足完全匹配,那么,KMP的策略就是调整j的位置,以保证接下来能继续往下匹配.

现在我们看一看当 i=j=5时的情况.
 i =1 2 3 4 5 6 7 8 9
A=a b a b a b a a b a b
B=a b a b a c b 
 j =1 2 3 4 5 6 7 

这时i=5,j=5A[1..5]=B[1..5],而A[6]<>B[6],按照贪婪思想,我们应该要找到一个最大的k值,这个k值需要满足上面所说的KMP的基本性质——也就是需要使得B[1..k]B[jk+1..j]完全相等.

为什么要找到这样一个k值?
其实这是KMP的核心思想.

接下来我会详细分析一下为什么要找到这样的一个k值.

假设我们找到这样一个最大的k值,因为k<=j,且当前的A[ij+1..i]B[1..j]还是完全相等的,故A[ik+1..i]B[jk+1..j]也是完全相等的.

而我们知道k又满足B[1..k]B[jk+1..j]完全相等,显然,当我们把j=k时,就会满足A[ik+1..i]B[1..k]完全相等了

(这里不仅是核心,更是KMP的难点,搞懂这里,接下来一切步骤都会变得异常简单,另外请记住,一切带有“显然”部分,就是你需要思考的部分).

当找到这样一个K值后,下次我们就可以用B[k+1]判断是否与A[i+1]相等了.

显然B[k+1]也不一定等于A[i+1],所以我们的j是不断变化,一直缩小的,直到找到一个k使得当j=kB[j+1]=A[i+1]

那么可能有人问了?如何一直缩小,怎么缩小?我们继续往下细想.


好,
现在有一个问题,如何每次对于一个j,找到一个最大的k使得B[1..k]=B[jk+1..j]

因为每次求一个k值太慢了,我们可以转化一下思想,我们是否可一遍扫过B,求出对于B串的每一个位置的最大k值呢?

显然这是可以做到的.

因为只要你明白了KMP的核心思想,求这个值也并不难.

我们现在假设P[i]表示B[1..P[i]]=B[iP[i]+1..i].

因为现在我只有一个串,所以进行的,是这样的一个“自我匹配”的过程.

那么我也可以设一个值j,枚举一个值i,表示匹配到B串的第i个位置时,B[1..j]=B[ij+1..i],那么显然,每次的P[i]就等于j.

对于B[j+1]A[i+1]的关系,一样可以分情况讨论:

B[j+1]=A[i+1]时,赋值P[i]=j+1,并把ij各加1.

B[j+1]A[i+1]时,就把j变为P[j],以此继续判断B[j+1]A[i+1]的关系.
这里写图片描述

为什么可以直接把j赋值为p[j]?

如图,当B[j+1]<>B[i+1]时,但是我们有B[1..p[i]]是等于B[ip[i]+1..i]的,也就是串s1是等于串s2的.

且在串s1里也有B[1..p[j]]等于B[jp[j]+1..j],因为p[j]<=p[i],所以B[ip[j]+1..i]是与B[jp[j]+1..j]完全相等的.

B[1..p[j]]=B[jp[j]+1..j],所以B[ip[j]+1..i]B[1..p[j]]是完全相等的,那么当我们把j=p[j]后,就可以拿B[j+1]A[i+1]进行判断了.

其实这个步骤是与上述匹配时的过程是一样的,只不过这个是进行“自我匹配”.

对于KMP,只要你能沉下心来去想,就会变得很容易.


代码部分:

var        p:array[0..100] of longint;        s1,s2:string;        n,i,j,tot:longint;begin        readln(s1);        readln(s2);        p[1]:=0;        j:=0;        for i:=2 to length(s2) do        begin                while (s2[j+1]<>s2[i]) and (j>0) do j:=p[j];                if s2[j+1]=s2[i] then inc(j);                p[i]:=j;        end;                                //自我匹配        j:=0;        for i:=1 to length(s1) do        begin                while (s2[j+1]<>s1[i]) and (j>0) do j:=p[j];                if s2[j+1]=s1[i] then inc(j);                if j=length(s2) then                begin                        inc(tot);            //统计个数                        j:=p[j];             //这里一样运用到了KMP的核心思想,这么操作可以方便继续往下找其他可能被匹配的串                end;        end;                                 //KMP匹配        if tot>0 then        begin                writeln('YES');                writeln(tot);        end        else        writeln('NO');end.


让我们看一道简单的例题:
https://jzoj.net/senior/#main/show/1536

题意:

给定一个字符串,求其中同时满足前缀串、后缀串相等的所有字符串.

abcabc,则abc为一个同时满足前缀后缀相等的字符串.

题解:

很显然,我们既然要找同时满足前缀后缀相等,那么我对于一个字符串,我求出其顺序对应的p数组,那么对于整个字符串,满足相等的肯定有p[n]这个长度的字符串,第二个相等的,肯定是p[p[n]]......,以此类推.

代码:

var        p,ans:array[0..400000] of longint;        i,j,tot:longint;        s:ansistring;procedure dfs(x:longint);begin        if p[x]>0 then        begin                inc(tot);                ans[tot]:=p[x];                dfs(p[x]);        end;end;begin        readln(s);        j:=0;        for i:=2 to length(s) do        begin                while (s[i]<>s[j+1]) and (j>0) do j:=p[j];                if s[j+1]=s[i] then inc(j);                p[i]:=j;        end;        dfs(length(s));        for i:=tot downto 1 do                write(ans[i],' ');        write(length(s),' ');end.




再看一道利用kmp思想加以解决dp问题的题目:
https://jzoj.net/senior/#main/show/4886

题意:

求任意构造的小写字符串且不包含给定串的串的个数.


我们可以设f[i][j]表示当前构造的字符串到了第i位,与给定字符串匹配到第j位的方案数.

显然,当给定串无重复字母时,我们可以这么考虑转移状态:

当下一位选给定串的第j+1位时,显然可以用f[i][j]更新f[i+1][j+1]
当下一位选给定串的第1位时,显然可以更新f[i+1][1]
当下一位选给定串的其它位时,因为无重复字母,故不匹配,故更新到f[i+1][0]

但是,如果有重复字母呢?
如:A=ccababab   B=ababac ,当构造的串A最后一位选b时,虽然不能继续更新,但还是可以更新f[8,4]的,故我们可以有如下思路:
a[i][c]表示在任意构造的串的第i+1位选字符c在给定串里所能往前匹配的最远位置.
那么显然,这里的a[7,b]=4.

那么如果有这么一个匹配数组,下次我就可以用f[i][j]去更新f[i+1][a[j,c]]了.

所以,问题转化为如何求a[i][c]

显然,一种暴力的求法:

ss:=s;fillchar(a,sizeof(a),0);for i:=0 to length(s)-1 do        for ch:='a' to 'z' do        begin                s:=ss;                s[i+1]:=ch;                flag:=false;                for j:=1 to i+1 do                        if copy(s,j,i+2-j)=copy(ss,1,i+2-j) then                        begin                                flag:=true;                                break;                        end;                if flag then a[i,ch]:=i+2-j;       end;

事实上,我们可以借鉴KMP的思想,因为我们每次只更新构造串里的第i+1字符,那么我可以先对构造串求一遍自我匹配,于是同自我匹配一样,不过把匹配的字符变成枚举的a..z了.

j:=0;fillchar(next,sizeof(next),0);fillchar(a,sizeof(a),0);for i:=2 to length(s) dobegin        while (j>0) and (s[j+1]<>s[i]) do j:=next[j];        if s[j+1]=s[i] then inc(j);        next[i]:=j;end;for i:=0 to length(s) do        for ch:='a' to 'z' do        begin                j:=i;                while (j>0) and (s[j+1]<>ch) do j:=next[j];                if s[j+1]=ch then inc(j);                a[i,ch]:=j;        end;
0 0