題目傳送門:戳我進入ios
KMP算法是用來處理字符串匹配的問題的,也就是給你兩個字符串,你須要回答B串是不是A串的子串,B串在A串中出現了幾回,B串在A串中出現的位置等問題。算法
KMP算法的意義在於,若是你在洛谷上發了一些話,kkksc03就能夠根據KMP算法查找你是否說了一些不和諧的字,而且屏蔽掉你的句子裏的不和諧的話(好比cxk雞你太美就會被屏蔽成cxk****),還會根據你句子中出現不和諧的字眼的次數對你進行處罰spa
舉個栗子:A:GCAKIOI B:GC ,那麼咱們稱B串是A串的子串指針
咱們稱等待匹配的A串爲主串,用來匹配的B串爲模式串。code
通常的樸素作法就是枚舉B串的第一個字母在A串中出現的位置並判斷是否適合,而這種作法的時間複雜度是O(mn)的,當你處理一篇較長文章的時候顯然就會超時。blog
咱們會發如今字符串匹配的過程當中,絕大多數的嘗試都會失敗,那麼有沒有一種算法可以利用這些失敗的信息呢?ip
KMP算法就是字符串
KMP算法的關鍵是利用匹配失敗後的信息,儘可能減小模式串與主串的匹配次數以達到快速匹配的目的get
設主串(如下稱爲T)string
設模式串(如下稱爲W)
用暴力算法匹配字符串過程當中,咱們會把T[0] 跟 W[0] 匹配,若是相同則匹配下一個字符,直到出現不相同的狀況,此時咱們會丟棄前面的匹配信息,而後把T[1] 跟 W[0]匹配,循環進行,直到主串結束,或者出現匹配成功的狀況。這種丟棄前面的匹配信息的方法,極大地下降了匹配效率。
咱們來看一看KMP是怎麼工做的
在KMP算法中,對於每個模式串咱們會事先計算出模式串的內部匹配信息(也就是說這個東西只和模式串有關,能夠預處理,這個處理咱們後面會提到),在匹配失敗時最大的移動模式串,以減小匹配次數。
好比,在簡單的一次匹配失敗後,咱們會想將模式串儘可能的右移和主串進行匹配。右移的距離在KMP算法中是如此計算的:在已經匹配的模式串子串中,找出最長的相同的前綴和後綴,而後移動使它們重疊。
咱們用兩個指針i和j分別表示A[i-j+1......i]和B[1......j]徹底相等,也就是說i是不斷增長的,而且隨着i的增長,j也相應的變化,而且j知足以A[j]結尾的長度爲j的字符串正好匹配B串的前j個字符,如今須要看A[i+1]和B[j+1]的關係
舉個栗子:
T: a b a b a b a a b a b a c b
W:a b a b a c b
當i=j=5時,此時T[6]!=W[6],這代表此時j不能等於5了,這個時候咱們要改變j的值,使得W[1...j]中的前j'個字母與後j'個字母相同,由於這樣j變成j'後(也就是將W右移j'個長度)才能繼續保持i和j的性質。這個j'顯然越大越好。在這裏W[1...5]是匹配的,咱們發現當ababa的前三個字母和後三個字母都是aba,因此j'最大也就是3,此時狀況是這樣
T: a b a b a b a a b a b a c b
W: a b a b a c b
那麼此時i=5,j=3,咱們又發現T[6]與W[4]是相等的,而後T[7]與W[5]是相等的(這裏是兩步)
因此如今是這種狀況:i=7,j=5
T: a b a b a b a a b a b a c b
W: a b a b a c b
這個時候又出現了T[8]!=W[6]的狀況,因而咱們繼續操做。因爲剛纔已經求出來了當j=5時,j'=3,因此咱們就能夠直接用了(經過這裏咱們也能夠發現j'是多少和主串沒有什麼關係,只和模式串有關係)
因而又變成了這樣
T: a b a b a b a a b a b a c b
W: a b a b a c b
這時,新的j=3依然不能知足A[i+1]=B[j+1],因此咱們還須要取j'
咱們發現當j=3時aba的第一個字母和最後一個字母都是a,因此這時j'=1
新的狀況:
T: a b a b a b a a b a b a c b
W: a b a b a c b
仍然不知足,這樣的話j須要減少到j'就是0(咱們規定當j=1時,j'=0)
T: a b a b a b a a b a b a c b
W: a b a b a c b
終於,T[8]=B[1],i變爲8,j變爲1,咱們一位一位日後,發現都是相等的,最後當j=7還知足條件時,咱們就能夠下結論:W是T的子串,而且還能夠找到子串在主串中的位置(i+1-m+1,由於下標從0開始)
這一部分的代碼其實很短,由於用了for循環
inline void kmp() { int j=0; for(int i=0;i<n;i++) { while(j>0&&b[j+1]!=a[i+1]) j=nxt[j]; if(b[j+1]==a[i+1]) j++; if(j==m) { printf("%d\n",i+1-m+1); j=nxt[j]; //當輸出第一個位置時 直接break掉 //當輸出全部位置時 j=nxt[j]; //當輸出區間不重疊的位置時 j=0 } } }
這裏就有一個問題:爲何時間複雜度是線性的?
咱們從上述的j值入手,由於每執行一次while循環都會使j值減少(但不能到負數),以後j最多+1,所以整個過程當中最多加了n個1.因而j最多隻有n個機會減少。這告訴咱們,while循環最多執行了n次,時間複雜度平攤到for循環上後,一次for循環的複雜度是O(1),那麼總的時間複雜度就是O(n)的(n是主串長度)。這樣的分析對於下文的預處理來講一樣有效,也能夠獲得預處理的時間複雜度是O(m)(m是模式串長度)
接下來是預處理
預處理並不須要按照定義寫成O(m2)甚至O(m3),窩們能夠經過nxt[1],nxt[2]....nxt[n-1]來求得nxt[n]的值
舉個栗子
W :a b a b a c b
nxt:0 0 1 2 ??
假如咱們有一個串,而且已經知道了nxt[1~4]那麼如何求nxt[5]和nxt[6]呢?
咱們發現,因爲nxt[4]=2,因此w[1~2]=w[3~4],求nxt[5]的時候,咱們發現w[3]=w[5],也就是說咱們能夠在原來的基礎上+1,從而獲得更長的相同先後綴,此時nxt[5]=nxt[4]+1=3
W :a b a b a c b
nxt:0 0 1 2 3?
那麼nxt[6]是否也是nxt[5]+1呢?顯然不是,由於w[nxt[5]+1]!=w[6],那麼此時咱們能夠考慮退一步,看看nxt[6]是否能夠由nxe[5]的狀況所包含的子串獲得,便是否nxt[6]=nxt[nxt[5]]+1?
事實上,這樣一直推下去也不行,因而咱們知道nxt[6]=0
那麼預處理的代碼就是這樣的
inline void pre() { nxt[1]=0;//定義nxt[1]=0 int j=0; rep(i,1,m-1) { while(j>0&&b[j+1]!=b[i+1]) j=nxt[j]; //不能繼續匹配而且j尚未減到0,就退一步 if(b[j+1]==b[i+1]) j++; //若是能匹配,就j++ nxt[i+1]=j;//給下一個賦值 } }
完整的代碼:
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<string> #include<cmath> #include<queue> #include<algorithm> #include<iomanip> using namespace std; #define rep(i,a,n) for(int i=a;i<=n;i++) #define per(i,n,a) for(int i=n;i>=a;i--) typedef long long ll; ll read() { ll ans=0; char last=' ',ch=getchar(); while(ch<'0'||ch>'9') last=ch,ch=getchar(); while(ch>='0'&&ch<='9') ans=ans*10+ch-'0',ch=getchar(); if(last=='-') ans=-ans; return ans; } char a[1000005],b[1000005]; int nxt[1000005],n,m; inline void pre() { nxt[1]=0; int j=0; rep(i,1,m-1) { while(j>0&&b[j+1]!=b[i+1]) j=nxt[j]; if(b[j+1]==b[i+1]) j++; nxt[i+1]=j; } } inline void kmp() { int j=0; for(int i=0;i<n;i++) { while(j>0&&b[j+1]!=a[i+1]) j=nxt[j]; if(b[j+1]==a[i+1]) j++; if(j==m) { printf("%d\n",i+1-m+1); j=nxt[j]; } } rep(i,1,m) printf("%d ",nxt[i]); } int main() { scanf("%s%s",a+1,b+1); n=strlen(a+1),m=strlen(b+1); pre(); kmp(); return 0; }