1. 程式人生 > 其它 >Z 函式學習筆記

Z 函式學習筆記

宣告,我不會 \(kmp\) 演算法。。。

於是我直接硬莽一波 \(Z\) 函式


(預設字串下標從 \(1\) 開始

\(z[i]\) 函式為 \(s[l,r]\)\(s\)\(lcp\)

這裡補充一下字串的關鍵思想:一切複雜度高的操作都儘量使用之前處理過的轉移函式,這樣大部分情況複雜度會被均攤。

我們當前處理 \(z[r]\),我們仿照 \(manacher\) 的思想,隨時維護一個 \(l\)\(r\)。在 \([l,r]\) 這個區間內,我們可以依賴之前的 \(z[i-l+1]\) 來儘量減少我們擴充套件當前 \(z[i]\) 所需的複雜度。

所以,對於一個 \(i\)

來說

\(i \leq r\),則有(注意等於的時候也要暴力擴充套件

進一步分類討論

\(z[i-l+1]<r-l+1\),則 \(z[i]=z[i-l+1]\)

否則,我們令 \(z[i]\)\(z[i-l+1]\) 處開始暴力擴充套件

\(r < i\),那麼我們暴力擴充套件 \(i\)\(z\) 函式

這樣我們就得到了一個 \(z\) 函式的 \(O(n)\)

複雜度分析

可以發現每次暴力擴充套件,\(r\) 都會隨著暴力擴充套件改變,而 \(r\) 最多隻能擴充套件到 \(n\),所以在均攤意義下,該演算法達到 \(O(n)\)

P5410 【模板】擴充套件 KMP(Z 函式)

第一個操作是 \(Z\) 函式基本操作,直接上板子。。

對於第二個操作,把兩個串拼起來,中間用特殊字元相接即可。

#include<bits/stdc++.h>

using namespace std;

#define int long long
#define ri register int
#define pb push_back
#define db double
#define pii pair<int,int>
#define ill long long
#define fi first
#define sc second

template<typename _T>
inline void read(_T &x)
{
	x=0;char s=getchar();int f=1;
	while(s<'0'||s>'9') {f=1;if(s=='-')f=-1;s=getchar();}
	while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
	x*=f;
}

const int np = 2e7 + 5;

char a[np];
char b[np * 2];
int z[np * 2];
int cas = 0;

inline void getz(char *c,int len)
{
	int r(0),l(0);
	int head(1);
	z[1] = cas;
	for(int i=2;i<=len;i++)
	{
		if(i < r)
		{
			if(z[i - l + 1] >= (r-i+1))
			{
				int pl = r;
				int head = r-i+1;
				while(c[head + 1] == c[pl + 1] && pl + 1 <= len) pl++,head++;
				z[i] = pl-i+1;
				l = i , r = i + z[i] - 1;
			}
			else z[i] = z[i - l + 1];
		}
		else
		{
			int pl = i-1;
			head = 0;
			while(c[head + 1] == c[pl + 1] && pl+1 <= len){pl++,head++;} 
			z[i] = head;
			if(pl > r) r = pl , l = i;
		}
	}
	int Ans =0 ;
	int flag(0);
	for(int i=1;i<=len;i++)
	{
		if(c[i] == '$')
		{
			cout<<Ans<<'\n';
			flag =i;
			break;
		}
		Ans ^= i * (z[i] + 1);
	}
	Ans = 0;
	for(int i=flag + 1;i<=len;i++)
	{
		Ans ^= (i - flag) * (z[i] + 1);
	}
	cout<<Ans;
}

signed main()
{
	scanf("%s",a + 1);
	scanf("%s",b + 1);
	int lena = strlen(a + 1);
	int lenb = strlen(b + 1);
	b[lenb + 1] = '$';
	cas = lenb;
	for(int i=1;i<=lena;i++) b[lenb + 1 + i] = a[i];
	int len = strlen(b + 1);
	getz(b,len);
}

CF432D Prefixes and Suffixes

求一遍 \(Z\) 函式,同時做出該串的 \(SAM\),提前預處理出所有子串的出現次數。

掃一遍字串,一邊掃一邊在 \(SAM\) 上走,隨時記錄是否滿足答案和該點出現次數

#include<bits/stdc++.h>

using namespace std;

#define int long long
#define ri register int
#define pb push_back
#define db double
#define pii pair<int,int>
#define ill long long
#define fi first
#define sc second

template<typename _T>
inline void read(_T &x)
{
	x=0;char s=getchar();int f=1;
	while(s<'0'||s>'9') {f=1;if(s=='-')f=-1;s=getchar();}
	while('0'<=s&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
	x*=f;
}

const int np = 1e5 + 5;

char c[np * 2];
int len[np * 2],siz[np * 2],link[np * 2],cnt = 1,la = 1;
int son[29][np * 2],sa[np * 2],z[np * 2],bac[np * 2],Ans;
int a1[np * 2],a2[np * 2],top;

inline void add(int x)
{
	int p,q,now,k,ch=1;
	p = la,now = la = ++cnt,len[now] = len[p] + (siz[now] = 1);
	for(;p && !son[x][p];son[x][p] = now,p=link[p]);
	if(!p) return (void)(link[now] = 1);
	if(len[p] + 1==len[q = son[x][p]]) return (void)(link[now] = q);
	len[k = ++cnt] = len[p] + 1,link[k] = link[q] , link[q] = link[now] = k;
	for(;ch<=26;ch++) son[ch][k] = son[ch][q];
	for(;p && !(son[x][p]^q);son[x][p]=k,p=link[p]);
}

inline void getz(char *c,int len)
{
	int l(0),r(0),head(0),pl(0);
	z[1] = len;
	for(int i=2;i<=len;i++)
	{
		if(i < r)
		{
			if(z[i-l+1] >= (r-i+1))
			{
				pl = r;
				head = r-i+1;
				while(c[head+1]==c[pl + 1] && pl + 1<=len) pl++,head++;
				z[i] = head,r = pl,l=i;
			}
			else z[i] = z[i-l+1];
		}
		else 
		{
			head = 0;
			pl = i-1;
			while(c[head + 1]==c[pl + 1] && pl + 1<=len) pl++,head++;
			z[i] = head,r = pl,l = i;
		}
	}
}

inline void solve(int le)
{
	int Now = 1;
	int plen = 0;
	for(int i=le;i>=1 && c[i] != '$';i--)
	{
		plen++;
		Now = son[c[plen]-'A'+1][Now];
		if(z[i] == plen) a1[++top] = plen,a2[top] = siz[Now];
	}
	printf("%lld\n",top);
	for(int i=1;i<=top;i++)
	{
		printf("%lld %lld\n",a1[i],a2[i]);
	}
	
}

signed main()
{
	scanf("%s",c+1);
	
	int le = strlen(c + 1);
	
	for(int i=1;i<=le;i++) add(c[i]-'A'+1);
	for(int i=1;i<=cnt;i++) bac[len[i]]++;
	for(int i=1;i<=cnt;i++) bac[i] += bac[i-1];
	for(int i=1;i<=cnt;i++) sa[bac[len[i]]--] = i;
	for(int i=cnt,x;i>=1;i--)
	{
		x = sa[i];
		siz[link[x]] += siz[x];
	}
	c[le + 1] = '$';
	for(int i=1;i<=le;i++) c[le + 1 + i] = c[i];
	le = strlen(c + 1);
	getz(c,le);
	solve(le);
}

感覺這個演算法的關鍵使用是如何用好字尾和整體的 \(\operatorname{lcp}\)

其實這個演算法本身並不是特別難,既沒有 \(SAM\) 那種晦澀的思想,也沒有 \(SA\) 頻出的騷操作……

(好吧只是因為我太菜了就只配做做板子了

以後發現比較有趣的 \(Z\) 函式題再往上添吧