1. 程式人生 > >淺談單模式串字串匹配演算法(KMP)

淺談單模式串字串匹配演算法(KMP)

字串演算法很有趣,尤其是KMP和AC自動機~~

大綱

F4bVNq.jpg

1.問題定義

字串匹配是電腦科學中最古老、研究最廣泛的問題之一。一個字串是一個定義在有限字母表∑上的字元序列。例如,ATCTAGAGA是字母表∑ = {A,C,G,T}上的一個字串。字串匹配問題就是在一個大的字串T中搜索某個字串P的所有出現位置。其中,T稱為文字,P稱為模式,T和P都定義在同一個字母表∑上。

還是比較好理解的,這段就過了吧

2.實現

1.樸素

俗話說,暴力出奇跡。這種匹配問題給人的第一直覺就是用暴力,一個個比嘛,不匹配就換下一個唄

char T[maxn],P[
maxn] void check() { int lenT=stren(T); int lenP=strlen(P); int i = 0; // 主串的位置 int j = 0; // 模式串的位置 while (i < t.length && j < p.length) { if (t[i] == p[j]) { i++;j++;} else { i = i - j + 1; j = 0; } } if (
j == p.length) return i - j; else return -1; } ...

但是,這份程式碼與接下來要分析的KMP比起來,差了太多,因為它的時間複雜度太大

2.KMP

1.思想

假設現在有一主串T,一模式串P

F4L9mQ.png

當匹配到P的最後一個字元時發現失配了,那麼上面那段暴力程式碼就會將P整體後移1位然後再重複

F4LSOg.png

但不難發現,P中其實有我們可以利用的東西
P的第1,2位和失配的最後一位字元的前兩位字元完全一樣
既然我們已經把1~7位都匹配了一遍況且第1,2位和6,7位相等,這也就意味著T的6,7位和P的1,2位是等價的,於是就可以有下面的操作

F4LCwj.png

可以用幾個式子來概括當T[i]與P[j]失配時,可以直接將P向後移動k個位置而不用依次比較的原因(來自https://www.cnblogs.com/yjiyjige/p/3263858.html)

T [ i ] ! = P [ j ] 當T[i] != P[j]時

T [ i j i 1 ] = = P [ 0 j 1 ] 有T[i-j 到 i-1] == P[0 到 j-1]

P [ 0 k 1 ] = = P [ j k j 1 ] 由P[0 到k-1] == P[j-k 到j-1]

T [ i k i 1 ] = = P [ 0 k 1 ] 必然:T[i-k 到i-1] == P[0 到k-1]

這就是它的思想,不做無用功~~

流程(以luogu3375為例)

nxt陣列

上文已經提到過,當失配時,我們不需要像暴力程式碼一樣一個個跳,而是可以通過“智慧的手段”來減少工作量。而這個“智慧的手段”是什麼?其實就是大名鼎鼎的next陣列(為了避免關鍵字重名,一般使用nxt)
什麼是nxt陣列?
nxt[i]就是指模式串中以第i-1項結尾的字串的最長公共真前後綴的長度,其性質是 P [ 1 &gt; n x t [ i ] ] = = P [ s t r l e n ( p ) n x t [ i ] ] &gt; i 1 ] P[1-&gt;nxt[i]]==P[strlen(p)-nxt[i]]-&gt;i-1]
nxt陣列的作用
細心一點就可以看出來,上式中的nxt[i]就是上文裡講到的k(移動的位數)

因此當失配時,直接讓失配時的指標跳nxt陣列就好了

假設我們此時已經有了nxt陣列,那麼KMP的大致框架就出來了

#define clean(arry,num); memset(arry,num,sizeof(arry));
#define loop(i,start,end) for(int i=start;i<=end;i++)
#define anti_loop(i,start,end) for(int i=start;i<=end;i--)
#define printarry(arry,size) for(int i=1;i<=size;i++)printf("%d ",arry[i]);
void KMP()
{
	int len1=strlen(s1);
	int len2=strlen(s2);
	int i=0;int j=0;
	while(i<len1)
	{
		if(j==len2-1&&s1[i]==s2[j])printf("%d\n",i-len2+2);
		if(j==-1||s1[i]==s2[j]){i++;j++;}
		else j=nxt[j];
	}
	printarry(nxt,len2);
}

如何求得nxt陣列
現在我們離KMP就差一個nxt陣列啦
先給出求nxt的程式碼,看看能看懂不

void getnxt()
{
	nxt[0]=-1;//初值
	int len=strlen(s2);//s2是模式串,就是P
	int j=-1;
	int i=0;
	while(i<len)
	{
		if(j==-1||s2[i]==s2[j]){nxt[++i]=++j;}
		else j=nxt[j];
	}
	return;
}

在這份程式碼中,我們用了i,j兩個指標來維護nxt陣列,外層的while是nxt的位數(從第0位列舉到最後一位),i就是外層的指標
然後的if…else是精華,它以j為指標,是為了維護i的nxt值,這其中包含了3種情況:
1.當j實際指向第1個字元時
2.當i,j所指的字元相同時
3.當i,j指向的字元不同時

第一種情況和第二種情況都比較好解決,直接就把i,j向後移一位,然後用j的值來更新nxt[i](只要i,j所指的字元相同,那麼其前面的字元也都相等),其在程式碼中就體現在

if(j==-1||s2[i]==s2[j]){nxt[++i]=++j;}

第三種情況實際上是一個遞迴,只是這個遞迴沒有用函式呼叫的方式
當第三種情況出現時,可以肯定j以前的nxt都求出來了,而此時的s2[i]!=s2[j],也就意味著s2的nxt[j]項可能與s2[i]相同,於是將j賦值nxt[j],繼續迴圈,此時,如果j仍然不滿足要求,那就又繼續賦值,直到找到符合要求的或是達到邊界(j=-1)

這期間,j的值是從於i相差1一直減小,最小到-1的

這體現在

else j=nxt[j];

這就是KMP的大致框架,下面附上程式碼

#include<bits/stdc++.h>
using namespace std;
#define clean(arry,num); memset(arry,num,sizeof(arry));
#define loop(i,start,end) for(int i=start;i<=end;i++)
#define anti_loop(i,start,end) for(int i=start;i<=end;i--)
#define printarry(arry,size) for(int i=1;i<=size;i++)printf("%d ",arry[i]);
const int maxn=1000000+10;
char s2[maxn],s1[maxn];
int nxt[maxn];
void calcnxt()
{
	int len=strlen(s2);
	int j=-1;
	int i=0;
	while(i<len)
	{
		if(/*j<=1*/j==-1||s2[i]==s2[j]){nxt[++i]=++j;}
		else j=nxt[j];
	}
	return;
}
void KMP()
{
	int len1=strlen(s1);
	int len2=strlen(s2);
	int i=0;int j=0;
	while(i<len1)
	{
		if(j==len2-1&&s1[i]==s2[j])
		{
			printf("%d\n",i-len2+2);//
		}
		if(j==-1||s1[i]==s2[j]){i++;j++;}
		else j=nxt[j];
	}
	printarry(nxt,len2);
}
int main()
{
   freopen("datain.txt","r",stdin);
   scanf("%s%s",s1,s2);
   clean(nxt,0);
   nxt[0]=-1;
   calcnxt();
   KMP();
   return 0;
}
/********************************************************************
   ID:Andrew_82
   LANG:C++
   PROG:KMP
********************************************************************/

感覺自己對這個演算法的理解又加深了
本文的部分思路來自以下幾個大佬的部落格
孤~影
北京小王子
若有不周之處,請多多指教