1. 程式人生 > 其它 >學習筆記——KMP演算法

學習筆記——KMP演算法

以下均搬運自花姐姐的部落格

閒話:$KMP$作為一個經典的字串演算法,自然值得$OIer$學習,但往往因其實現難、不常考,導致$OIer$不想學(話說這不是人之常情嗎),今天,讓我們來解決這一毒瘤,改變我們遇字串就炸的現象吧!

一、何謂模式串匹配

模式串匹配,就是給定一個需要處理的文字串(理論上應該很長)和一個需要在文字串中搜索的模式串(理論上長度應該遠小於文字串),查詢在該文字串中,給出的模式串的出現有無、次數、位置等。

這就相當於$OJ$上判斷程式對錯的過程,$OJ$通過匹配每一個字元來判斷輸出結果的正誤,也算是一種模式串匹配。

二、KMP演算法的核心思想

首先,我們來了解一下$KMP$演算法的由來。度娘說

KMP演算法是一種改進的字串匹配演算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的。
因此人們稱它為克努特—莫里斯—普拉特操作(簡稱KMP演算法)。

然而,這些偉人我一個也不認識,但這並不妨礙它成為一個偉大的演算法。

首先,讓我們來分析一下我們樸素演算法被$T$飛的原因:我們用低效的列舉匹配文字串,導致在最壞情況下該演算法(好像也不能叫演算法)退化成O(文字串長度*匹配串長度),其實也比較好卡,如以下資料

文字串:aaaaaaaaaaa......aaaaaaaaab (其中有1e6個a)
模式串:aaaaaaaaaaa......aaaaaaaaab (其中有5e5個a)

那麼,我們要執行$2.5e6$次計算,會直接導致超時(TLE)

既然如此,當然輪到我們KMP演算法閃亮登場了!

KMP 的精髓在於,對於每次失配之後,我都不會從頭重新開始列舉,而是根據我已經得知的資料,從“某個特定的位置”開始匹配;而對於模式串的每一位,都有唯一的“特定變化位置”,這個在失配之後的特定變化位置可以幫助我們利用已有的資料不用從頭匹配,從而節約時間。

舉個栗子

文字串:abaabab
模式串:aba

正常的演算法(暴力)會在第三個字元之後重新開始匹配,但讓我們來看一看KMP演算法的執行過程

文字串:abaabab
模式串:   	aba

所以,KMP在每次失配之後就可以跳回之前的某一位,在從該位開始新的一輪匹配。

注意:
1.一般使用模式串來匹配失配陣列。

2.匹配位置的確定。

$str1$ 中,對於每一位 $str1(i) $,它的 $kmp$ 陣列應當是記錄一個位置 $j, j≤i$並且滿足 $str1(i)=str1(j) $並且在$j!=1 $時理應滿足$str1(1)$至 $ str1(j−1) $ 分別與 $str1(i−j+1)~str1(i−1) $ 的每一位相等。

對於2,我們可以用字首和字尾的思想來理解

模式串:abcdabc
字首:a,ab,abc,abcd,abcda,abcdab,abcdabc
字尾:c,bc,abc,dabc,cdabc,bcdabc,abcdabc

用$kmp$陣列記錄到它為止的模式串字首的真字首和真字尾最大相同的位置(注意,這個地方沒有寫錯,是真的有巢狀qwq)。

如上面例子中,字首和字尾的第三項相同,所以$kmp[7]=3$;

三、講了這麼多,直接上程式碼吧

1.$string$型別的$KMP$

int nxt[MAXN];
void getnext(string t){
	int j=0,k=-1;
	nxt[0]=-1;
	while(j<t.size()){
		if(k==-1||t[j]==t[k]) nxt[++j]=++k;
        //1.當k=-1時,肯定到頂了,不能再回溯,所以直接賦值
        //2.當t[j]==t[k]時,這個下標的nxt值就是上一個下標的值加1
		else k=nxt[k];//如果還沒有找到,就返回上一個可回溯的下標再找
	}
}

void KMP(string s,string t){
	int i=0,j=0;
	while(i<s.size()){
		if(j==-1||s[i]==t[j]){++i;++j;}
        //1.當j=-1時,說明到了邊界,把文字串的指標加1,再重新開始新一輪匹配
        //2.當s[i]==t[j]時,說明匹配到了,那就指標往後移,看下一位能否匹配
		else j=nxt[j];//如果沒有到邊界又沒有匹配到,就回溯看能否重新匹配
		if(j==t.size()){printf("%lld\n",i-t.size()+1);j=nxt[j];}
        //當j==t.size()時,說明已經完全匹配了,輸出答案,並回溯匹配其他位置上的合法答案
        //對輸出結果的解釋:i表示到下標為i的位置時兩串完全匹配,減去(t.size()-1)就是減去模式串的長度,結果就是匹配的起始位置
	}	
}

2.$char$陣列型別的$KMP$

#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#define il inline
#define ll long long
#define gc getchar
#define int long long
#define R register
using namespace std;
//---------------------初始函式-------------------------------
il int read(){
	R int x=0;R bool f=0;R char ch=gc();
	while(!isdigit(ch)) {f|=ch=='-';ch=gc();}
	while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
	return f?-x:x;
}

il int max(int a,int b) {return a>b?a:b;}

il int min(int a,int b) {return a<b?a:b;}


//---------------------初始函式-------------------------------

const int MAXN=1e6+10;
char s1[MAXN],s2[MAXN];
int kmp[MAXN];

signed main(){
	scanf("%s%s",s1+1,s2+1);
    //細節:s1+1表示從下標為一的位置開始讀入,方便之後的操作
	int lens1=strlen(s1+1),lens2=strlen(s2+1);
    //因為char不像string一樣有很多自帶函式,所以要用<cstring>庫中的函式求長度
	kmp[0]=kmp[1]=0;//初始化
	for(R int j=1,k=0;j<lens2;++j){
		while(k&&s2[j+1]!=s2[k+1]) k=kmp[k];
        //當k>0且s2[j+1]!=s2[k+1]時,說明既沒有到邊界又沒有匹配到,就回溯看能否重新匹配
		if(s2[j+1]==s2[k+1]) ++k;
        //由上面的while迴圈可知,現在的k一定是匹配的,所以我們只需要判斷這一位能否比上一位多匹配一個字元
		kmp[j+1]=k;//賦值這一位最多能匹配的字元
	}
	for(R int j=0,k=0;j<lens1;++j){
		while(k&&s1[j+1]!=s2[k+1]) k=kmp[k];
        //當k>0且s2[j+1]!=s2[k+1]時,說明既沒有到邊界又沒有匹配到,就回溯看能否重新匹配
		if(s1[j+1]==s2[k+1]) ++k;
        //由上面的while迴圈可知,現在的k一定是匹配的,所以我們只需要判斷這一位能否比上一位多匹配一個字元
		if(k==lens2){printf("%lld\n",j+1-lens2+1);k=kmp[k];}
        //當k==lens2時,說明已經完全匹配了,輸出答案,並回溯匹配其他位置上的合法答案
        //對輸出結果的解釋:j表示到下標為j+1的位置時兩串完全匹配,減去(lens2-1)就是減去模式串的長度,結果就是匹配的起始位置        
	}
	for(R int i=1;i<=lens2;++i) printf("%lld ",kmp[i]);
	return 0;
}

好了,$KMP$的基本內容到此結束,在加一個時間複雜度分析就完美了。以下引用$rqy$的話:

每次位置指標$i++$時,失配指標$j$至多增加一次,所以$j$至多增加$len$次,從而至多減少$len$次,所以就是$\Theta$($len_N$+$len_M$)=$\Theta$(N+M)。

其實我們也可以發現,$ KMP $演算法之所以快,不僅僅由於它的失配處理方案,更重要的是利用字首字尾的特性,從不會反反覆覆地找,我們可以看到程式碼裡對於匹配只有一重迴圈,也就是說 $KMP$ 演算法具有一種“最優歷史處理”的性質,而這種性質也是基於$ KMP $的核心思想的。

另外一篇講的比較好的部落格

OI-wiki上別人推薦的部落格

完結撒花了

沒想到吧,我又回來了!

作為一名合格的$OIer$,我們當然要講練結合,打出一套合擊拳,才能更好的鞏固我們對$KMP$演算法的理解。

1.來一道裸的$KMP$的題(題解)

2.這才是$KMP$的板子(題解)

3.對$kmp$陣列新定義(當提升思維的題做)(題解)(我的程式碼)

差一點自主做出的紫題,還是要多注意碼程式碼時的細節

4.$KMP$+線性$DP$(講的特別詳細的題解)(我的程式碼)

5.終極難題:$KMP$+$DP$+矩陣乘法

啊,這題是真的寫不動,先咕著吧。

原來KMP還可以求迴圈子串,學到了學到了,再扔幾道例題吧!

1.求最短迴圈子串(學習$KMP$求迴圈子串的不錯的部落格+題解)(我的程式碼)

2.求迴圈子串數量(講的不錯的題解)(我的程式碼)

3.求最長迴圈子串長度和(題解)(我的程式碼)

(終於上了一道要腦子的題,要用類似並查集路徑壓縮的優化)