字串KMP演算法
一、演算法介紹:
KMP演算法主要用於字串包含問題,如:
給定字串A,B,判斷B是否是A的子串
在這裡,我們把等待匹配的字串A稱為母串,用來匹配的串B稱為模式串
二、演算法流程:
(感性理解???)
如果按照一般思路,我們迴圈A串的各個元素,判斷是否包含B串,演算法時間複雜度過高,因為這個演算法忽略了之前我們已經對A,B串進行了比較的事實,所以在執行時進行了許多重複的判斷,導致了時間複雜度過高。
因此,我們可以考慮對其進行優化~~
不要噴,我的圖是搜的,我真心不會畫圖qwq
首先,來看一個例子。我們設A串(上面的串)的陣列下標為A【1~n】,B串(下面的串)的陣列下標B【1~m】
經過比較我們可以發現,當 i=4, j=4 時,字串中的元素不一樣了,也就是說我們現在找到的子串不合法了,所以為了節省時間,我們要跳過儘可能多的元素(因為經過比較我們發現A串中1~3個元素都與B串中第一個元素不一樣了,所以不需要再浪費時間進行比較了),但是在跳過元素時也應當滿足當有重複元素時,保留重複元素的特性,所以我們應該讓B[1]與A[5]衝齊。
我們再看另一個例子
原來的字串是這樣的:
A B C A B C D H I J K
A B C E
我們發現當元素A E不一樣了,但是B串中的第一個元素A又可以和A串中的第四個元素A重合,所以我們就把字串B移動到當前位置
再來一個例子!
我們發現此時元素C D 不一樣,但是A串中的第三個元素A與B串中的第一個元素A是匹配的啊,所以,我們開始移動B串:
下一個例子也是一樣滴:
這個例子不進行過多解釋,還請讀者自行思考(但是我相信 聰明的你們一定都會!!)
(好吧這個例子好像挺重要的所以我還是講一講把TAT)
匹配之後我們發現元素C B不一樣,但是我們不可以直接把B中的第一個元素A挪到第i+1個位置,因為那樣做的話我們會失去許多重複元素(在這個題裡是A[4~5]和B[1~2],都是A B)。
所以正確的做法是當存在重複元素時,讓模式串與母串重合
從上面幾個例子中我們可以找到一些規律:
當匹配失敗後,j要移動的下一個位置K(在母串中的對應位置K哈,不是模式串),有著這樣的關係:
最前面的K個字元和J之前的K個字元是一樣的!!!
用數學公式表達就是:
P[0 ~ k-1] == P[j-k ~ j-1]
這樣的話,如果 我們首先設定 j 要移動下一個位置為p[j],那麼我們就可以在母串中尋找模式串,程式碼如下:
j=0; for(int i=1;i<=n;i++){ while(j>0&&a[i]!=b[j+1]) j=p[j];//如果不相等,那麼我們就回到重合部分的 //最後一個元素,從最後一個元素開始尋找 if(a[i]==b[j+1]) j++; if(j==0){ cout<<i-m+1<<endl;//表示輸出當前相同字串的首字母下標 j=p[j];//因為可能存在多個包含關係,所以我們繼續搜尋 } }
這樣,我們就完成了在母串中對於模式串的查詢。
But but,我們是不是忘記了什麼重要的東西???
我們還沒有說明怎麼找到陣列P啊!!!
重頭戲來了(手動滑稽)
我們已經知道,P陣列是用來尋找A B字串重合元素的最後一個位置的陣列變數(或許也可以稱之為指標??)那麼其實....我們可以直接把P陣列定義為:求模式串中的子串的真重複字首和字尾(強烈建議停下來好好理解一下這句話)
我們來看一個例子:
我們通過A B串的對比可以發現,其實在不匹配元素之前,我們找到的A B子串是等價的,所以我們完全可以把找A B 串的重合部分(其實不太準確)轉化為求B 串的字首字尾(可愛)
我相信聰明的您們都已經明白了
所以,我們就相當於在一個字串中再次運用KMP演算法(其實整個KMP演算法就是一個巢狀問題)
我自己造一個例子吧 唉~~
1 2 3 4 5 6 7 8 9
B a b c a b d e a c
P 0 0 0 1 2 0 0 0 ?
就這個吧
P陣列代表的是我們可以返回的元素的下標,(也可以理解為如果下一個元素不匹配,我們可以從當前元素找到一條“退路”),所以我們從頭開始推P陣列
因為B[1]=a,因為它是最左邊的元素,所以不可能再有退路,所以它的返回值就是0(相當於返回到陣列B[0])
因為B[2]=b,與B[1]=a不相等,所以如果我們發現元素不相等,我們應當再次返回B[0];
B[3]=c同理?
終於到了B[4]=a!!!(精神一震!!) 我們突然發現B[4]=a與B[1]=a相等,所以我們可以從B[4]跳到B[1];
B[5]=B[4+1]正好與B[2]=B[2+1]相等,也就是當前子串的字首和字尾可以匹配,所以我們直接從B[5]返回B[2];(還記得我們的P陣列返回的是重合元素的末尾地址嗎?)
其它同理。各位大大可以自己多推幾遍(掩蓋住我其實並不想多寫的事實)
好了!大體思路就是這樣,程式碼實現如下
int j=0; for(int i=2;i<=m;i++)//因為我們的i表示當前元素是否與j+1匹配
//所以我們從i=2開始迴圈 { while(j>0&&b[i]!=b[j+1]) j=p[j]; if(b[j+1]===b[i]) j++; p[i]=j; }//預處理模式串中所有的情況
總的來說,KMP和上面的程式碼其實還是很好理解的??(哼,才不告訴你我學了6h+呢)
其實各位大大們只要多看幾遍題解+自己推幾遍就可以理解了~~~~吧?
完整程式碼如下:
題目:洛谷P3375 【模板】KMP字串匹配https://www.luogu.com.cn/problem/P3375
#include<iostream> #include<cstdio> #include<bits/stdc++.h> using namespace std; int p[1020000],n,m,j; char a[1020000],b[1020000]; int main() { cin>>a+1; cin>>b+1; n=strlen(a+1); m=strlen(b+1); // int j=0; for(int i=2; i<=m; i++) { while(j&&b[i]!=b[j+1]) j=p[j]; if(b[j+1]==b[i]) j++; p[i]=j; }//預處理 j=0; for(int i=1; i<=n; i++) { while(j>0&&a[i]!=b[j+1]) j=p[j]; if(a[i]==b[j+1]) j++; if(j==m) { // cout<<" *"; cout<<i-m+1<<endl; j=p[j];//再次跳回原來的地方,繼續判斷 } } for(int i=1; i<=m; i++) cout<<p[i]<<" "; return 0; }
——END——