P3375 【模板】KMP字符串匹配
題目描述
如題,給出兩個字符串s1和s2,其中s2為s1的子串,求出s2在s1中所有出現的位置。
為了減少騙分的情況,接下來還要輸出子串的前綴數組next。
(如果你不知道這是什麽意思也不要問,去百度搜[kmp算法]學習一下就知道了。)
輸入輸出格式
輸入格式:
第一行為一個字符串,即為s1
第二行為一個字符串,即為s2
輸出格式:
若幹行,每行包含一個整數,表示s2在s1中出現的位置
接下來1行,包括length(s2)個整數,表示前綴數組next[i]的值。
輸入輸出樣例
輸入樣例#1:
ABABABC
ABA
輸出樣例#1: 1 3 0 0 1
說明
時空限制:1000ms,128M
數據規模
設s1長度為N,s2長度為M
對於30%的數據:N<=15,M<=5
對於70%的數據:N<=10000,M<=100
對於100%的數據:N<=1000000,M<=1000000
樣例說明:
所以兩個匹配位置為1和3,輸出1、3
解析:
本來還想用暴力算法去做這道題,結果看到這個玄學的防騙分方式後,果斷選擇花兩個小時學習KMP,順便orz%%%了SRY大佬~~~
KMP基本思想:
比如,在簡單的一次匹配失敗後,我們會想將模式串盡量的右移和主串進行匹配。右移的距離在KMP算法中是如此計算的:在已經匹配的模式串子串中,找出最長的相同的前綴和後綴,然後移動使它們重疊。(摘自百度百科)
之後我們就可以開心的開始敲代碼辣~~~首先我們要考慮的是如何找到最長的相同前綴後綴,最樸素的算法是O(n2)(n為模式串長度(等待匹配的串A為文本串,進行匹配的串B為模式串))
我們來想一想如何優化:首先,在算P[i]之前(p[i]代表模式串前i位字符最長相同前綴後綴),我們已知的是p[0...i-1];我們可以用遞推的方法去求解。
和一般的遞推方法不同的是,這個遞推並沒有固定的遞推式,網上有許多人說是“自己匹配自己”,而我並不這麽認為。。。蒟蒻和大佬的思維果然不一樣,再次orz%%%
舉一個簡單的栗子~:abaa,p[1]=0,p[2]=0,p[3]=1,下面我們重點看一下如何算p[4];由於前邊第一個a和第三個a已經匹配成功,如果2個字符和當前字符可以匹配的話,那麽直接加上1就好了;
重點在於不能匹配的情況,這也是本蒟蒻學習KMP最大的障礙。匹配失敗,證明當前的前綴後綴位數太多了,匹配不上,所以我們要減小前綴的數目,所以我們要在這個前綴裏再找前後綴,看到p[1]=0,所以我們直接看第1個和當前字符能不能匹配即可。發現匹配成功,於是乎p[4]=1;推廣到一般情況:設正在計算第i號字符對應的p[i]值,先去找前i-1個字符的最長相同前後綴(p[i-1]),設p[i-1]=j,然後如果b[j+1]==b[i],則p[i]=j+1;否則順著失配邊走,走到可以匹配或者確定無法匹配位置,對應著下面這段代碼:
for(int i=2;i<=m;i++) { while(j>0&&b[i]!=b[j+1])j=p[j];//好好體會這個while循環,失配的精髓。 if(b[i]==b[j+1])j++; p[i]=j; }
預處理完p數組後,我們就可以開始正式匹配了,結合百度百科的基本思想可以體會到,KMP之所以會省時間,是因為它實際上是一個動態選取最優值的過程,它很好地利用了前綴的性質,沒有無用功。
當不可以匹配的時候,樸素算法是右移一位,而KMP能移多少移多少,也就是所謂的自我匹配。
最後上AC代碼:
#include<iostream> #include<cstring> using namespace std; char a[1000010],b[1000010]; int n,m,p[1000010],j; int main() { cin>>a+1; cin>>b+1; n=strlen(a+1); m=strlen(b+1); for(int i=2;i<=m;i++) { while(j>0&&b[i]!=b[j+1])j=p[j]; if(b[i]==b[j+1])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<<i-m+1<<endl; j=p[j];//繼續匹配 } } for(int i=1;i<=m;i++) { cout<<p[i]<<" "; } return 0; }
P3375 【模板】KMP字符串匹配