字尾陣列入門(二)——Height陣列與LCP
前言
看這篇部落格前,先去了解一下字尾陣列的基本操作吧:字尾陣列入門(一)——字尾排序。
這篇部落格的內容,主要建立於字尾排序的基礎之上,進一步研究一個\(Height\)陣列以及如何求\(LCP\)。
什麼是\(LCP\)
\(LCP\),即\(Longest\ Common\ Prefix\),是最長公共字首的意思。
而在後綴陣列中,\(LCP(i,j)\)表示字尾\(_{SA_i}\)與字尾\(_{SA_j}\)的最長公共字首的長度,注意是\(SA_i\)和\(SA_j\),而不是\(i\)和\(j\)。
\(LCP\)的性質
先是幾個比較簡單的基本性質:
\(LCP(i,j)=LCP(j,i)\)
這應該是比較顯然的。
\(LCP(i,i)=n-SA_i+1\)
這個性質非常重要,因為在求\(LCP\)的過程中要特判該情況,
不然會死得特別慘。
接下來,是一些比較複雜的性質:
\(LCP(i,j)=min(LCP(i,k),LCP(j,k))\)(對於任意\(1\le i\le k\le j\))
首先,設\(x=min(LCP(i,k),LCP(j,k))\),則可得\(LCP(i,k)\ge x,LCP(j,k)\ge x\)。
因此我們可以知道字尾\(_{SA_i}\),字尾\(_{SA_j}\)的前\(x\)個字元分別與字尾\(_{SA_k}\)的前\(x\)個字元相等
則字尾\(_{SA_i}\),字尾\(_{SA_j}\)的前\(x\)個字元相等,即\(LCP(i,j)\ge x\)。
而由於字尾\(_{SA_i}<\)字尾\(_{SA_k}<\)字尾\(_{SA_{j}}\),且由\(x=min(LCP(i,k),LCP(j,k))\)可得,\(LCP(i,j)\le x\)。
故\(LCP(i,j)=x\)。
\(LCP(i,j)=min_{k=i}^{j-1}LCP(k,k+1)\)
由\(LCP(i,j)=min(LCP(i,k),LCP(j,k))\)這個性質,我們可以把\(LCP(i,j)\)拆成\(j-i\)個部分,分別為\(LCP(i,i+1),LCP(i+1,i+2),...,LCP(j-1,j)\)
然後再取\(min\)即可。
這兩個性質雖然看似令人匪夷所思,但仔細理解其實還是能看懂的。
這兩個性質在\(LCP\)的求解過程中發揮著十分重要的作用。
\(Height\)陣列
為了方便求解\(LCP\),我們需要在定義一個新的陣列:\(Height\)陣列。
\(Height_i\)表示的是\(LCP(i,i+1)\)。
因此\(LCP(i,j)\)的結果就是\(min_{k=i}^{j-1}Height_i\),這似乎可以在知道\(Height\)陣列的情況下用\(RMQ\)實現\(O(1)\)求解。
於是關鍵來了:如何求出\(Height\)陣列。
如何求\(Height\)陣列
首先我們要知道一個性質\(Height_{SA_i}\ge Height_{SA_{i-1}}\)。
這個性質我也不會證,反正它還是挺簡單的,背一下就好了。
這樣一來,我們每次可以把\(Height_{SA_i}\)初始化為\(Height_{SA_{i-1}}\),然後每次儘量向外延長即可,這一過程似乎與\(Manacher\)演算法有點類似。
程式碼
放一份求\(Height\)陣列及\(LCP\)的模板程式碼:
class Class_SuffixArray
{
private:
int n,SA[N+5],Height[N+5],rk[N+5],pos[N+5],tot[N+5];
inline void RadixSort(int S)//基數排序
{
register int i;
for(i=0;i<=S;++i) tot[i]=0;
for(i=1;i<=n;++i) ++tot[rk[i]];
for(i=1;i<=S;++i) tot[i]+=tot[i-1];
for(i=n;i;--i) SA[tot[rk[pos[i]]]--]=pos[i];
}
inline void GetSA(char *s)//字尾排序,求SA陣列
{
register int i,k,Size=122,cnt=0;
for(i=1;i<=n;++i) rk[pos[i]=i]=s[i-1];
for(RadixSort(Size),k=1;cnt<n;k<<=1)
{
for(Size=cnt,cnt=0,i=1;i<=k;++i) pos[++cnt]=n-k+i;
for(i=1;i<=n;++i) SA[i]>k&&(pos[++cnt]=SA[i]-k);
for(RadixSort(Size),i=1;i<=n;++i) pos[i]=rk[i];
for(rk[SA[1]]=cnt=1,i=2;i<=n;++i) rk[SA[i]]=(pos[SA[i-1]]^pos[SA[i]]||pos[SA[i-1]+k]^pos[SA[i]+k])?++cnt:cnt;
}
}
inline void GetHeight(char *s)//求Height陣列
{
register int i,j,k=0;
for(i=1;i<=n;++i) rk[SA[i]]=i;//更新rk陣列
for(i=1;i<=n;++i)
{
if(k&&--k,!(rk[i]^1)) continue;//對於rk[i]=1的情況直接跳過
j=SA[rk[i]-1];//找到上一個字尾的座標
while(i+k<=n&&j+k<=n&&!(s[i+k-1]^s[j+k-1])) ++k;//儘量拓展
Height[rk[i]]=k;//存值
}
}
class Class_RMQ//RMQ求區間最值
{
private:
#define LogN 15
int Log2[N+5],Min[N+5][LogN+5];
public:
inline void Init(int len,int *data)
{
register int i,j;
for(i=2;i<=len;++i) Log2[i]=Log2[i>>1]+1;
for(i=1;i<=len;++i) Min[i][0]=data[i];
for(j=1;(1<<j-1)<=len;++j) for(i=1;i+(1<<j-1)<=len;++i) Min[i][j]=min(Min[i][j-1],Min[i+(1<<j-1)][j-1]);
}
inline int GetMin(int l,int r) {register int k=Log2[r-l+1];return min(Min[l][k],Min[r-(1<<k)+1][k]);}
}RMQ;
public:
inline void Init(int len,char *s) {n=len,GetSA(s),GetHeight(s),RMQ.Init(n,Height);}//初始化
inline int LCP(int x,int y) {return x^y?(rk[x]>rk[y]&&swap(x,y),RMQ.GetMin(rk[x]+1,rk[y])):n-x+1;}//求LCP,注意特判x=y的情況
};