Z算法

Z算法

Z算法是一種用於字符串匹配的算法。此算法的核心在於$z$數組以及它的求法。html

(如下約定字符串下標從$1$開始)算法

$z$數組和Z-box

定義$z$數組:$z_{a,i}$表示從字符串$a$的第$i$位開始,日後能與$a$的前綴匹配的最長長度。顯然,$z_{a,1}=|a|$恆成立。數組

一個Z-box是一個區間。給定一個字符串$a$,那麼$a$上存在一個Z-box$[l,r]$當且僅當知足如下所有條件:spa

  • $l\ne1$;
  • $z_{a,l}\ne0$;
  • $r=l+z_{a,l}-1$。

通俗來講,若從$a$的第$i$位開始能與$a$的前綴匹配至少$1$位,那麼能匹配的最長的串覆蓋過的區間就是一個Z-box。($l\ne1$是由於位置$1$很特殊,自己就是前綴,單獨考慮)code

例如若$a=\texttt{acactaac}$,那麼$z_{a}=[8,0,2,0,0,1,2,0]$,Z-box有$[3,4],[6,6],[7,8]$。htm

$z$數組的求法

給定字符串$a$,如今咱們須要求出$z_{a}$。blog

因爲$z_{a,1}$的值不用求,並且位置$1$比較特殊,就是前綴,因此咱們單獨處理。ci

假設咱們如今已經知道了$z_{a,2\sim i-1}$和使得$zr$最大的Z-box$[zl,zr]$,要求出$z_{a,i}$並更新$zl,zr$,那麼分$2$種狀況:字符串

  1. $zr<i$。此時咱們直接暴力地從第$i$位向後匹配求出$z_{a,i}$。若是$z_{a,i}\ne0$,則令$zl=i,zr=i+z_{a,i}-1$;
  2. $zr\ge i$。設$i-zl+1=i'$,即$i'$是把跨越$i$的Z-box$[zl,zr]$平移至$a$的前綴處後$i$的位置。此時又分$2$種狀況:
    1. $i+z_{a,i'}\le zr$。顯然$\left[i,i+z_{a,i'}\right]\subsetneq[zl,zr]$。根據Z-box的定義,$\forall j\in\left[i,i+z_{a,i'}\right],a_j=a_{j-zl+1}$。那麼從$a$的第$i$位開始與$a$的前綴匹配的狀況和從第$i'$位開始是同樣的,直接令$z_{a,i}=z_{a,i'}$,$zl,zr$不變;
    2. $i+z_{a,i'}>zr$。同理,$\forall j\in[i,zr],a_j=a_{j-zl+1}$。那麼$a$的第$i\sim zr$位與$a$的前綴匹配的狀況和第$i'\sim zr-zl+1$位是同樣的,顯然$z_{a,i}$至少有$zr-i+1$這麼多,因而直接從第$zr+1$位開始暴力向後匹配求出$z_{a,i}$,並令$zl=i,zr=i+z_{a,i}-1$(由於$z_{a,i}$不可能爲$0$)。

這樣先令$z_1=|a|$,而後按上述方法從$i=2$遞推到$i=|a|$,即可求出$z_a$數組。get

下面是求$z$數組的代碼:

//|a|=n
void z_init(){//求z數組
	z[1]=n;//特殊處理z[1]
	int zl=0,zr=0;//右端點最大的Z-box
	for(int i=2;i<=n;i++)//從i=2遞推到i=n
		if(zr<i){//第1種狀況
			z[i]=0;
			while(i+z[i]<=n&&a[i+z[i]]==a[1+z[i]])z[i]++;//直接向後暴力匹配
			if(z[i])zl=i,zr=i+z[i]-1;//更新右端點最大的Z-box
		}
		else if(i+z[i-zl+1]<=zr)z[i]=z[i-zl+1];//第2種狀況的第1種狀況
		else{//第2種狀況的第2種狀況
			z[i]=zr-i+1;//z[i]至少有zr-i+1這麼多
			while(i+z[i]<=n&&a[i+z[i]]==a[1+z[i]])z[i]++;//後面再暴力匹配
			zl=i;zr=i+z[i]-1;//更新右端點最大的Z-box
		}
}

時間複雜度

按上述方法求$z$數組的時間複雜度是線性的$\mathrm{O}(|a|)$。

證實~~(感性)~~:觀察上述方法可發現,只有當$i>zr$時,纔可能將這個位置的字符與前綴匹配,而匹配結束後會把$zr$更新至最後一個匹配成功的位置,因此每一個字符最多會和前綴成功匹配$1$次,因此匹配成功的總次數爲$\mathrm{O}(|a|)$;算$z_{a,i}$時,若是日後暴力匹配(即遇到的不是第$2$種狀況的第$1$種狀況),那麼第$1$次匹配失敗就會停下來,因此匹配失敗的總次數也爲$\mathrm{O}(|a|)$。所以總時間就是匹配所花的時間$\mathrm{O}(|a|)+\mathrm{O}(|a|)=\mathrm O(|a|)$再加上一些賦值、更新$zl,zr$等一些$1$次只要$\mathrm O(1)$的操做,就仍是$\mathrm O(|a|)$了。得證。

應用

Z算法和ExKMP算法是徹底等價的,由於它們求的數組的意思是同樣的。可是哈希、KMP能求的東西卻有Z算法力所不及的。

Z算法最經常使用的用法就是字符串模式匹配(這個哈希和KMP也能夠作到線性複雜度)。考慮把模式串$b$隔一個不經常使用字符接到文本串$a$前面,即令$c=b+\texttt{!}+a$。而後求出$z_c$,從$i=|b|+2$到$i=|c|$掃一遍,若是$z_i=|b|$,那麼在該位置匹配成功。**注意:**所謂不經常使用字符必定不能在串中出現,否則會出bug。若是要用模式串$c$去匹配兩個文本串$a,b$,能夠令$d=c+\texttt{!}+a+\texttt @+b$,這時兩個分隔符不能相同,否則也會出bug。

爲何Z算法在字符串模式匹配上花的時間和哈希相同呢?Z算法算出了從每一位開始能與前綴匹配的最長長度,可是字符串模式匹配只須要知道可否與前綴$c_{1\sim|b|}$匹配,並未徹底使用$z$數組的價值。若是你就是想知道某一位開始能與前綴匹配的最長長度,哈希可就要二分的幫助了,複雜度是帶$\log$的,不如用Z算法預處理一下。具體的能夠參考下面$3$道例題。

不只如此,Z算法的常數比哈希小(由於爲了使哈希不被卡<del>、不在CodeForces上FST</del>,通常要寫雙重哈希),正確率也比哈希高(Z算法正確率固然是$100%$啦)。

例題

CodeForces 526D - Om Nom and Necklace

題解傳送門

CodeForces 427D - Match & Catch

題解傳送門

CodeForces 955D - Scissors

題解傳送門

原文出處:https://www.cnblogs.com/ycx-akioi/p/Z-algorithm.html

相關文章
相關標籤/搜索