Z算法


Z算法

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

(以下約定字符串下標從\(1\)開始)

\(z\)數組和Z-box

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

一個Z-box是一個區間。給定一個字符串\(a\),那么\(a\)上存在一個Z-box\([l,r]\)當且僅當滿足以下全部條件:

  • \(l\ne1\)
  • \(z_{a,l}\ne0\)
  • \(r=l+z_{a,l}-1\)

通俗來說,若從\(a\)的第\(i\)位開始能與\(a\)的前綴匹配至少\(1\)位,那么能匹配的最長的串覆蓋過的區間就是一個Z-box。(\(l\ne1\)是因為位置\(1\)很特殊,本身就是前綴,單獨考慮)

例如若\(a=\texttt{acactaac}\),那么\(z_{a}=[8,0,2,0,0,1,2,0]\),Z-box有\([3,4],[6,6],[7,8]\)

\(z\)數組的求法

給定字符串\(a\),現在我們需要求出\(z_{a}\)

由於\(z_{a,1}\)的值不用求,而且位置\(1\)比較特殊,就是前綴,所以我們單獨處理。

假設我們現在已經知道了\(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\)數組。

下面是求\(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算法的常數比哈希小(因為為了使哈希不被卡、不在CodeForces上FST,一般要寫雙重哈希),正確率也比哈希高(Z算法正確率當然是\(100\%\)啦)。

例題

CodeForces 526D - Om Nom and Necklace

題解傳送門

CodeForces 427D - Match & Catch

題解傳送門

CodeForces 955D - Scissors

題解傳送門


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM