Z函數&前綴函數的總結~

這篇總結全部的字符串都是以 0 爲下標起點

Z函數(ExKMP)

對於一個字符串 \(S\)c++

咱們規定一個函數 \(Z[i]\) 表示 \(S\)\(S[i...n-1]\) 的 LCP(最長公共前綴)的長度。算法

\(S[0.....Z[i]-1]\)\(S[i...i+Z[i]-1]\) 相等數組

先說構造 \(Z\) 函數,再說 \(Z\) 函數的應用函數

首先考慮暴力的構造 時間複雜度 \(O(n^2)\)優化

char s[N];
inline void GetZ(){
    int len=strlen(s);
    for(register int i=0;i<len;++i)
        while(i+z[i]-1<n&&s[z[i]]==s[i+z[i]]) z[i]++;
    
    for(register int i=0;i<len;++i)
        cout<<z[i]<<" ";
}

這就時一個根據定義的模擬,可是顯然 \(O(n^2)\) 的時間複雜度有些不太優秀,因此考慮優化:spa

擴展時的判斷條件根據上面的代碼,應該是:code

while(i+z[i]-1<n&&s[z[i]]==s[i+z[i]]) z[i]++;

這一步是用枚舉實現的,是 \(O(n)\) 的,那麼如何對這一步進行優化呢?blog

對於枚舉的優化:

這時考慮先考慮一下 \(Z\) 函數的性質:繼承

從定義來講:這是知足$Z[0.....Z[i]-1] $與 \(Z[i,i+Z[i]-1]\) 相等的最長長度ip

性質1:那麼對於一個區間\([l,r]\)\(l \in [i,i+Z[i]-1]\)\(r\in [i+Z[i]-1]\),它必定與區間 \([l-i,r-i]\) 相等(定義),

那麼考慮優化暴力的思路,即如何減小枚舉:

如何減小枚舉呢?大部分狀況來講是從當前已知的狀況去更新當前未知的狀況,若是不行,再枚舉

記錄下\(i+Z[i]-1\) 的最大值 \(r\) ,與這個最大值對應的 \(i\),下面出現的 \(l\),就是這個最大值對應的 \(i\)

若是對於當前的一個位置 $i $,若是 \(i \leq r\)。那麼根據性質 \(1\) , \(S[i....r]\) 是與 \(S[i-l.....r-l]\) 相等的

因此要麼 \(i\) 這個位置與 \(Z[i-l]\) 同樣,與 \(S\) 的LCP長度爲 \(Z[i-l]\),要麼它能夠匹配完整個 \(r-i+1\),還能夠繼續日後匹配。

簡單來講,就是\(Z[i] \geq min(r-i+1,Z[i-l])\)

那麼若是此時 \(Z[i-l]\) 還知足 \(Z[i-l] < r-i+1\) 也就是當前能夠繼承的範圍並無到達此時的邊界 \(r\) ,咱們選擇直接繼承。

if(Z[i-l]<r-i+1) Z[i]=Z[i-l];

根據上面的分析,若是不知足上面的這個條件話,證實它能夠匹配完整個 \(r-i+1\),而且還能向後匹配

因此代碼也就更簡單了:

if(Z[i-l]>=r-i+1){
	Z[i]=r-i+1;
	while(i+Z[i]-1<len&&S[Z[i]]==S[i+Z[i]])	Z[i]++;
}

可是咱們發現上面的兩個程序自己是沒有問題的,只是有一些狀況沒有考慮到:

1.好比當前的位置 \(i\),若是已經 \(>r\) 了,那麼上面的全部結論都不成立。這時就應該直接暴力匹配
2.咱們的 \(r\),表示的是當前匹配段最右邊的端點值,而 \(l\) 是它所對應的 \(i\) 值,因此在暴力匹配後,應該更新 \(l,r\) 的值。

因此整個求 \(Z\) 函數的代碼應該是這樣的:

int len=strlen(s);
	Z[0]=0;//其實根據定義這裏也珂以賦值爲 len。
	for(register int i=1;i<len;++i){
		if(i<=r&&Z[i-l]<r-i+1)	Z[i]=Z[i-l];
		else{
			Z[i]=max(0,r-i+1);
//由於可能有兩種狀況進來,一個是i>r,一個是Z[i-l]>=r-i+1,而兩種狀況對於Z[i]的賦值是不一樣的。因此這裏直接一個max(0,r-i+1)歸納兩種狀況
			while(i+Z[i]-1<len&&S[Z[i]]==S[i+Z[i]])	Z[i]++;
			if(r>i+Z[i]-1)	l=r,r=i+Z[i]-1;
		}
	}

爲何咱們的循環要從1開始呢?

由於若是從0開始的話,\(r\) 會直接擴展完,而整個算法也會隨之退化到 \(O(n^2)\)

Z函數的應用:

1.字符串匹配

一個字符串算法少不了的就是字符串匹配了。

一道經典例題:

求一個字符串 \(A\) ,在另外一個字符串 \(B\) 中的出現次數。

你先想了想 \(Z\) 函數,發現它儲存的都是 \(B\)的後綴與 \(B\) 匹配的信息,基本沒法應用到與 \(A\) 匹配上面。

那麼如何將 \(B\)\(B\) 匹配的信息變成 \(B\)\(A\) 統計的信息呢?

答案十分 \(Naive\)

\(A\) 加在 \(B\) 的前面不久好了?

此時在新的字符串中 \(A\) 是這個串的前綴,那麼此時匹配的就都是 \(A\) 了。

固然這樣是有問題的,好比位置 \(i\) 的後綴已經能夠把 \(A\) 所有匹配完了,他仍是會和本身匹配,那麼此時的信息根本沒法用到與 \(A\) 的匹配中去。

因此咱們還須要在 \(A\)\(B\) 之間加上一個特殊符號 '#',從而保證匹配長度不會超過 \(len_A\)

那麼統計出現次數時只須要統計在 \(B\) 串的範圍內,有多少個位置知足\(Z[i]=len_A\) 的就好了。

有了上面字符串匹配的知識,你就能夠 \(A\)掉一些簡單的模板題了!

題目:
P5410 【模板】擴展 KMP(Z 函數)
CF126B Password
UVA12604 Caesar Cipher

2.判斷循環節

幾個概念:

對字符串 \(S\)\(0> p \leq |S|\),若 \(S[i]=S[i+p]\) 對全部 \(i \in [0,|S|-p-1]\) 成立,則稱 \(p\)\(S\)週期

對字符串 \(S\)\(0 \leq r <|S|\),若 \(S\) 長度爲 \(r\) 的前綴 和 長度爲 \(r\) 的後綴相等,則稱長度爲$ r$ 的前綴爲 \(S\) 的 $ border$。

注意,週期不等價於循環節!

若是一個長度爲 \(k\) 的週期是循環節,那麼必定知足 \(len\% k=0\)

題目

求一個字符串 \(A\) 的最短循環節。

對於一個長度爲 \(k\) 的循環節,必定知足\(S[0......k-1]=S[len-k.....len-1]\)

若是轉化爲 \(Z\) 函數的話,就是 \(i+Z[i]==len\) 就是 \(i\) 的後綴爲 \(S\) 的一個Border,有一個長度爲 \(Z[i]\)\(border\) 等價於有一個長度爲 \(len-Z[i]\) 的週期。(證實略過)

那麼咱們能夠 \(O(n)\) 的掃,若是當前\(i+Z[i]==len\) 那麼判斷 \(len\%(len-Z[i])\) 是否等於 \(0\) 。由於知足 \(i+Z[i]=len\)\(len-Z[i]\) 是遞減的(由於 \(i\) 枚舉時遞增。)因此第一個知足上述條件的 \(len-Z[i]\) 就是最大的循環節,要找最小的能夠直接倒敘枚舉,而後第一個直接退出。

例題:
UVA455 週期串 Periodic Strings

(由於我太弱了,因此我沒有找到更多的循環節例題 )

3.判斷迴文

只要你理解了 \(Z\) 函數在字符串匹配的應用。若是要判斷一個串 \(S\) 是否爲迴文,只須要將它的反串 \(S'\) 拼在 \(S\) 前面,而後中間加上一個 '#' ,直接匹配,最後判斷 \(Z[0]\) 是否等於 \(len\) 就行了。

例題:
UVA11475 Extend to Palindrome

題意:

就是加最少的字母,使得原串變爲一個迴文串。

設當前的字符串爲 \(S\)\(S\) 必定能夠被分紅兩部分 \(A\)\(B\)

其中\(B\)是一個迴文串(也能夠是一個空串),\(A\) 是一個普通的字符串。

放一個圖方便理解吧:

\(A\) 的反串爲 $ A'$

並且 \(A+B+A'\) 必定是一個迴文串(想想爲何)

那麼咱們加上的字符串就是 \(A'\)

由於\(|A'|\) = \(|A|\),\(|A|=|S|-|B|\)

由於\(|S|\)必定,爲了讓\(|A'|\)更小,因此須要找到最大的\(|B|\)

也就是找出 \(S\) 的後綴中最長的迴文串。

這個利用 \(Z\) 函數很容易解決

咱們將 \(S\) 的反串 \(S'\) 拼在 \(S\) 的前面,那麼一個後綴迴文串左端點 \(i\) 必定知足 \(Z[i]=\)這個後綴迴文串的 \(len\) ,也就是\(i+Z[i]=\) 整個字符串的 \(len\),即\(i+Z[i]=len_S\)

記住,咱們找的是最長的後綴迴文串,也就是 \(|B|_{max}\)

但答案須要的是\(|A|\),而且還要將 \(S[0\)~\(|A|\)-\(1]\)倒過來輸出

最後輸出就能夠了。

Code:

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+3;
char s[N];
int len,z[N],siz;
inline void GetS(){
	z[0]=siz+1;
	for(int i=1,l=0,r=0;i<=siz;++i){
		if(i<=r&&z[i-l]<r-i+1)	z[i]=z[i-l];
		else{
			z[i]=max(0,r-i+1);
			while(i+z[i]<=siz&&s[z[i]]==s[i+z[i]]) ++z[i];
			if(i+z[i]-1>r)	l=i,r=i+z[i]-1;	
		}
	}
	return;
}
int main(){
	while(scanf("%s",s)!=EOF){
		len=strlen(s);siz=2*len;
		s[len]='#';
		for(register int i=len+1;i<=siz;++i)	s[i]=s[i-len-1];
		reverse(s,s+len);
		GetS();int maxn=0;
		for(register int i=siz;i>len;--i){if(z[i]==siz-i+1){maxn=z[i];}	}	
		maxn=len-maxn;
		for(register int i=len+1;i<=siz;++i)	cout<<s[i];
		reverse(s+len+1,s+len+1+maxn);
		for(register int i=len+1;i<=len+maxn;++i)	cout<<s[i];
		putchar('\n');
	}
	return 0;
}

4.完美子串?

對於一個串 \(S\),若是一個串既是它的前綴又是它的後綴,那麼他就是 \(S\) 的完美子串。用 \(Z\) 函數來講,就是 \(i\) 若是知足 \(i+Z[i]==len\)\(i\) 開頭的後綴爲完美子串。

一些變式

1.求完美子串的出現次數:

首先注意到,每個完美子串的長度都不相同,這就意味這咱們不須要判斷一個完美子串與另外一個完美子串是否本質相同。

並且大的完美子串中必定包含小的完美子串,這也就啓發咱們能夠利用 桶+後綴和 的思想來統計出現次數。

那麼如何判斷某一個子串能夠包含某一個大的完美子串( \(k\) )呢?很顯然,只須要這個點 \(i\)\(Z[i]\geq len_k\) 就好了(由於每個完美子串也是一個前綴。)

例題:
CF126B Password
CF432D Prefixes and Suffixes


//\(Z\) 函數蒟蒻會的就這麼點了。。。以爲好的點個讚唄~(贊在文章底部做者欄的右邊)

前綴函數

好吧其實前綴函數和 \(KMP\)\(next\) 數組沒什麼大區別,只不過一個是下標一個是長度罷了。

給定一個長度爲 \(len\) 的字符串 \(S\) , 其前綴函數被定義爲一個長度爲 \(n\) 的數組 \(\pi\)。其中\(\pi[i]\) 的定義爲:

1.若是 \(i\) 的前綴 \(S[0...i]\) 有一對相等的真前綴與真後綴,即 \(S[0.....k-1]=S[i-k+1.....i]\) 那麼 \(\pi[i]\) 就是這個相等的真前綴的長度,也就是 \(\pi[i]=k\)

2.若是有不止一對相等的,那麼 \(\pi[i]\) 就是其中最長的那一對的長度;

3.若是沒有相等的,那麼 \(\pi[i]=0\)

簡單來講 \(\pi[i]\) 表示的也就是以 \(i\) 爲右端點的前綴最長的 \(border\) 長度( \(border\) 的定義看上面)

特別的,咱們規定 \(\pi[0]=0\)

若是直接暴力計算前綴函數的話:

Code:

inline void Getpi(){
	string s;cin>>s;int len=s.size();
	for(register int i=1;i<len;++i){
		for(register int j=i;j>=0;--j){
			if(s.substr(0,j)==s.substr(i-j+1,j)){
				pi[i]=j;
				break;
			}
		}
	}
	return;
}

顯然上面的算法是 \(O(n^3)\) 的,不夠優秀

考慮優化

優化構造前綴函數

優化1:相鄰的兩個前綴函數值最多增長 1。

這個顯然,若是已經求出了當前的 \(\pi[i]\) 須要求出一個儘可能大的 \(\pi[i+1]\) 時。

\(S[i+1]=S[\pi[i]]\) 的(下標從 \(0\) 開始),此時的 \(\pi[i+1]=pi[i]+1;\)

因此從 \(i\)\(i+1\) 時,前綴函數值只可能增長 \(1\), 或者維持不變,或者減小。

此時能夠將整個代碼優化成這樣:

inline void Getpi(){
	string s;cin>>s;int len=s.size();
	for(register int i=1;i<n;++i){
		for(register int j=pi[i-1]+1;j>=0;--j){
			if(s.substr(0,j)==s.substr(i-j+1)){
				pi[i]=j;
				break;
			}	
		}
	}
	return;
}

這個時候,由於起始點變爲了 \(\pi[i-1]+1\) 因此只有在最好的狀況下才會在這個枚舉上限上 \(+1\) ,因此最多的狀況時會進行 \(n-1+n-2+2n-3\) 次比較

因此這個時候整個算法時間複雜度已是 \(O(n^2)\) 了。但仍是不夠優秀

優化2:能夠經過不斷地跳前綴函數來獲取一個合法的匹配長度

在優化1中,我討論了最優狀況下的轉移,那麼這時理所固然的就該來優化\(S[\pi[i]]!=S[i+1]\) 時的匹配了

咱們在 \(S[\pi[i]]!=S[i+1]\) 時,根據 \(\pi\) 函數的最優性,咱們應該找到第二長的長度 \(j\) 使得 \(S[0....j-1]==S[i-j+1.....i]\) 這樣咱們才能繼續用 \(S[i+1]=S[j]\) 時的拓展。

而當咱們觀察了一下能夠發現:

\(S[0.....\pi[i]-1]=S[i-\pi[i]+1....i]\) 因此第二長 \(j\) ,也就等價於\([0,\pi[i]-1]\) 這個區間中的最長 \(border\) 的長度 ,在一想,這不就是 $\pi[pi[i]-1] $ 嘛?(由於 \(\pi\) 函數,表明的必定是這個區間最長的 \(border\) 的長度)

因此這時咱們只須要不停地跳 \(\pi\) 函數,就能夠獲得當前的 \(\pi[i+1]\) 了。

Code:

inline void Getpi(){
	string s;cin>>s;int len=s.size();
	//由於下標從0開始,因此下標實際上是長度-1,因此格式與上文可能有些不符合,可是理解了就對了!
	for(register int i=1;i<len;++i){
		int j=pi[i-1];
		while(j&&S[i]!=S[j])	j=pi[j-1];
		if(S[i]==S[j]) ++j;
		pi[i]=j;
	}
	return;
}

發現:咱們枚舉的 \(i\) 最多讓 \(j\) 增長 \(n\),而咱們每次的跳至少會讓 \(j-1\),因此不管 \(j\) 減少多少次,總的次數也不會超過 \(O(n)\)

因此此時構造的時間複雜度就爲 \(O(n)\)

前綴函數的應用~

1.經典字符串匹配

求一個字符串 \(A\) ,在另外一個字符串 \(B\) 中的出現次數。

在前面 \(Z\) 函數匹配字符串的啓發下,很快就能想到:仍是將 \(A\) 拼到 \(B\) 前面,中間加上一個特殊字符 '#' 。

由於有一個 ‘#‘ 在中間,因此全部的 \(\pi[i]\) 必定是 \(\leq\) \(len_A\) 的。一樣的想法:那麼如何判斷 \(A\)\(B\)中出現過呢?

既然 \(\pi[i]\) 表示的是以 \(i\) 爲右端點的前綴長度,這個時候 \(A\) 爲整個串的前綴,那麼對於一個位置 \(i\),當 \(\pi[i]==len_A\) 時,表明着 \(S[i-len_A+1......i]\)\(A\) 相同 。

學會了這個你就能夠 \(A\) 下面的例題了!

例題:
P3375 【模板】KMP字符串匹配
CF126B Password
UVA12604 Caesar Cipher

一道字符串匹配的變式吧。。:

P6080 [USACO05DEC]Cow Patterns G

在不少普通的字符串匹配中,\(\pi\) 函數表示的是前綴中最長的 \(border\) ,也就是前綴中先後綴相等的最長長度。

但在這道題中,很明顯,沒法用相等來表示。

首先,將模式串(\(K\) )和數字串(\(N\))拼起來,中間插入一個特殊符號 「#」。

根據題意:咱們應該將 \(\pi\) 函數中的「相等」看作大小關係相同,因而$ \pi[i]$ 就表示當前 \(S[0\)~\(i]\) 中先後綴大小關係最長的長度,由於有個特殊符號 「#」 ,因此全部的 \(\pi[i] \leq K\),而知足「壞蛋團體」區間的右端點,必定知足 \(\pi[r]=K\)

那麼這時問題就出在瞭如何判斷大小關係相同了。

若是說當前 \(S[0\)~\(j-1]\)\(S[i-j,i-1]\) 大小關係相同。

那麼對於 \(j\)\(i\) 這兩個位置,(首先匹配時這個 \(j\) ,必定是\(\leq K\)的)

若是說 \([0,j-1]\) 中 比\(j\) 大的數與\([i-j,i-1]\)中比 \(i\) 大的數的個數相等

並且 \([0,j-1]\) 中 和\(j\) 相等的數與\([i-j,i-1]\)中和 \(i\) 相等的數的個數相等

又由於兩個區間長度是同樣的,那麼區間中大於 \(j\) ,與大於 \(i\) 的數的個數也是相等的。

那麼這\([0,j]\)\([i-j,i]\)兩個區間的大小關係相等。

如此咱們只須要用一個桶的前綴和,就能夠在 \(O(S)\) 的複雜度中求出區間中比它小的與相等的數的個數了。

Warning : 最後須要的是左端點,但利用 \(\pi\) 函數判斷的話,符合條件的是右端點.

Code:

與它類似的一道題:CF471D MUH and Cube Walls

2.判斷循環節:

\(Z\) 函數差很少,整個前綴函數判斷循環節也是經過不斷地判斷合法的 \(border\) 來肯定週期長度,從而肯定循環節長度的。

可是其實有一個定理(最長循環串長度=總長度-最長相同先後綴長度(前提是這個長度合法,不合法則不存在合法的循環節))

可是因爲 \(Z\) 函數的定義,因此 \(Z\) 函數並不能像前綴函數這樣 \(O(1)\) 求出最長循環節。

證實用的反證法。。這裏就不放了。。。有須要的能夠找我。。。

3.一個字符串中本質不一樣的子串個數

給定一個長度爲 \(n\) 的字符串 \(S\) ,咱們但願計算它的本質不一樣子串的數目。

咱們將用一種在 \(S\) 的末尾添加一個字符後從新計算該數目的方法。

\(k\) 爲當前 \(S\) 的本質不一樣子串的數量。咱們添加一個新的字符 \(c\)\(S\) 中。現然會有一些寫的子串以 \(c\) 結尾而且以前沒有出現過,咱們須要對這些字符串基數。

構造一個字符串 \(T+S+c\) 將它反轉獲得 \(T'\)。如今咱們的任務變成了計算有多少個 \(T'\) 的前綴沒有在 \(T'\) 中的其餘地方出現過,若是咱們計算了 \(T'\) 的前綴函數的最大值 \(\pi_max\),那麼最長的沒有在 \(S\) 中的前綴的長度就爲 \(\pi_max\)。那麼天然,全部更短的前綴也會出現

因此,當添加了一個新字符後出現的新字符串爲 \(|S|+1-\pi_max\)

因此對於每次加入的字符,咱們能夠 \(O(n)\) 的算出新出現的子串的數量,因此最終複雜度就爲 \(O(n^2)\)

這一段抄的老師的講義。。。(由於我描述不到這麼詳細,我太弱了)

相關文章
相關標籤/搜索