【例題傳送門:caioj1461】
【EXKMP】最長共同前綴長度
【題意】
給出模板串A和子串B,長度分別為lenA和lenB,要求在線性時間內,對於每個A[i](1<=i<=lenA),求出A[i..lenA]與B的最長公共前綴長度
【輸入文件】
輸入A,B兩個串,(lenB<=lenA<=1000000)
【輸出文件】
輸出lenA個數,表示A[i...lenA]與B的最長公共前綴長度,每個數之前有空格
【樣例輸入】
aabbabaaab
aabb
【樣例輸出】
4 1 0 0 1 0 2 3 1 0
算法分析:
學EXKMP前,必須將KMP學透,如果仍未學KMP,請出門左轉【傳送門】
我們在KMP算法中可以理解,p數組是KMP的核心,p[i]代表着以i為結尾和以開頭為首的最長公共子串長度,也就是說對於st字符串數組的p[i]代表的就是st字符串數組從1開始到p[i]和從i-p[i]+1到i是完全相同的(st[1...p[i]]=st[i-p[i]+1...i])
那么擴展KMP就高級了。一樣還是p數組(還是原來的配方,還是熟悉的味道!),但是既然是擴展KMP就不要用p,我就改成extend數組了,表示的意義與普通KMP就大有不同。extend[i]表示的是以i為首和以開頭為首的最長前綴,也就是說對於st字符串數組中的extend[i]表示的就是st字符串數組從1開始到extend[i]和從i到i+extend[i]-1是完全相同的(st[1...extend[i]]=st[i...i+extend[i]-1])
首先extend[1]就不用說了,直接等於len這個沒有問題吧,那么因為extend[1]這個是具有一定性,所以我們基本把這東西廢掉..那么我們就直接從extend[2]開始。然后我們就定義一個k,這個k表示的就是在當前搜索過的范圍以內(因為這是線性算法,所以是從1到len)能到達最遠(也就是說k+extend[k]-1最大的)的編號,為什么要定義一個這樣子的東西,等下你們就知道了。
我們先定義一個p,讓它等於k+extend[k]-1,那么由extend這個數組的定義我們就可以得到一個等式:st[1...extend[k]]=st[k...p]。因為p=k+extend[k]-1,所以我們又可以得到一條等式:extend[k]=p-k+1,把這個代換到上一條等式上,就會——瞬間爆炸!(好吧開個玩笑..)就會變成:st[1...p-k+1]=st[k...p],如下圖:
因為我們現在要求從extend[i],那么由上面這條等式又可以得到另一條等式:st[i-k+1...p-k+1]=st[i...p],如下圖:
在看此證明過程中請各位一直記住extend數組所表達的含義,不然會有很多地方不懂的。那么我們再定義一個L=extend[i-k+1],我們又可以得到一條等式:st[i-k+1...i-k+L](注意:本來得到的應該是i-k+1+L-1,我直接把+1-1省略了)=st[1...L],如下圖:
這個時候有人就會問了:為什么你上面的圖不和前幾個合在一起呢?這個就是本算法的一個難點了!因為L的不定性,這個時候我們需要考慮i-k+L和p-k+1的大小!那我們就來分開來考慮。
(1)i-k+L<p-k+1,如下圖:
從上圖我們可以看到因為st[1...L]=st[i-k+1...i-k+L],st[i-k+1...p-k+1]=st[i...p],所以在st[i...p]中肯定含有一段(從i開始)和st[1...L]是完全相同的,也就是上圖標出來的藍色部分st[i...i+L-1]。因為st[i-k+1...p-k+1]=st[i...p]又因為i-k+L<p-k+1,所以我們又可以得到:st[i-k+L+1]=st[i+L](也就是上圖所標的黃色位置),而因為extend[i-k+1]的定義,所以st[L+1](也就是上圖所標棕色位置)!=st[i-k+L+1],所以我們可以得到:st[i+L]!=st[L+1],那么extend[i]就直接等於L了。
(2)i-k+L>p-k+1,如下圖:
從上圖中我們看到因為st[1...L]=st[i-k+1...i-k+L],又因為i-k+L>p-k+1,所以在st[1...L]中肯定含有一段和st[i-k+1...p-k+1]完全相同的(圖中綠色部分)。因為extend[k]的意義,所以st[p+1]!=st[p-k+2](圖中第二個棕色和黃色位置,為什么不相同不用解釋吧),又因為st[1...L]=st[i-k+1...i-k+L],所以st[p-i+2](圖中並未標出,第一個棕色位置)=st[p-k+2],那么就會得到:st[p-i+2]!=st[p+1],所以extend[i]就等於p-i+1了。
(3)i-k+L=p-k+1,如下圖:
從上圖我們可以發現因為st[i-k+1...i-k+L]=st[1..L],st[i-k+1...p-k+1]=st[i...p],又因為i-k+L=p-k+1,所以st[1...L]=st[i-k+1...p-k+1(可換成i-k+L)]=st[i...p](也就是指上圖中三段綠色部分)。那么由於extend[i-k+1]和extend[k]的意義表達,我們可以得到:st[L+1]!=st[i-k+L+1],st[i-k+L+1]!=st[p+1],但是我們不能確定st[L+1]和st[p+1]是否相同,我們只能確定從i開始和從1開始有p-i+1這么長的公共前綴但並不一定是最長的(這句話要好好理解,這很重要)。
那么我們就設一個變量j=p-i+1,表示當前從i開始和從1開始的公共前綴長度,由於上面加粗的那句話,我們可以直接從st[j+1]和st[i+j]來累加j的值來得出最長的公共前綴。
注意事項:
因為p是一個不定的數(由k和extend[k]來定),所以說有可能p-i+1是有可能為負數,那么第二種情況顯然不對,公共前綴怎么樣也不能等於負數吧,最小也會是0吧!這個時候也許你就會冒出一個想法,在第二種情況下取p-i+1和0的最大值。很顯然這是不對的。請看下圖:
從上圖我們可以發現,因為p-i+1是負數,所以上面所有條件都用不了,因為對i后面的字符沒有作任何的計算,但這個時候是絕對不可以肯定st[1]和st[i]是不同的(也就是最長公共前綴為0),那么我們需要從st[1]和st[i]開始繼續判斷后面的字符是否相同。於是我們把第二種情況和第三種情況歸納為一種情況,因為兩者都是要暴力處理未知的點,只是起始點不同
這僅僅是求extend數組的證明,也僅僅是在同一個字符串里的基本操作,接下來我就來講下擴展KMP(簡稱EXKMP)的實際用途。EXKMP主要是利用於解決處理兩個字符串的最長公共前綴長度,假如A是主串,B是副串,那么這時我們定義一個ex數組,ex[i]就表示A[i...Alen]和B[1...Blen]的最長公共前綴長度(這個概念需要好好注意)
其實在處理兩者的匹配時,只需要注意將A串中的子串轉移到B串中進行處理,那么這樣我們實際上在求ex數組時,操作仍與上面的步驟相似
參考代碼:
#include<cstdio> #include<cstring> #include<cstdlib> #include<algorithm> #include<cmath> using namespace std; char sa[1100000],sb[1100000]; int lena,lenb; int p[1100000],ex[1100000]; //p數組是用來讓B串自己匹配自己的 void exkmp() { p[1]=lenb; int x=1; while(sb[x]==sb[x+1]&&x+1<=lenb) x++;//因為我們p[1]是具有一定性,所以我們不能直接用,所以要先暴力求出p[2] p[2]=x-1; int k=2; for(int i=3;i<=lenb;i++) { int pp=k+p[k]-1,L=p[i-k+1];//pp實際上是p if(i+L<pp+1) p[i]=L;//i-k+L<pp-k+1化簡后i+L<pp else { int j=pp-i+1; if(j<0) j=0; while(sb[j+1]==sb[i+j]&&i+j<=lenb) j++; p[i]=j; k=i; } } x=1; while(sa[x]==sb[x]&&x<=lenb) x++;//ex[1]並不具有一定性,所以我們暴力求出ex[1] ex[1]=x-1; k=1; for(int i=2;i<=lena;i++) { int pp=k+ex[k]-1,L=p[i-k+1]; if(i+L<pp+1) ex[i]=L; else { int j=pp-i+1; if(j<0) j=0; while(sb[j+1]==sa[i+j]&&i+j<=lena&&j<=lenb) j++; ex[i]=j; k=i; } } } int main() { scanf("%s%s",sa+1,sb+1); lena=strlen(sa+1);lenb=strlen(sb+1); exkmp(); for(int i=1;i<lena;i++) printf("%d ",ex[i]); printf("%d\n",ex[lena]); return 0; }