1. 程式人生 > 實用技巧 >exkmp(Z函式) 筆記

exkmp(Z函式) 筆記

exkmp 用於求解這樣的問題:

求文字串 \(T\) 的每一個字尾與模式串 \(M\) 的匹配長度(即最長公共字首長度)。特別的,取 \(M=T\),得到的這個長度被稱為 \(Z\) 函式。“函式”只是一個叫法,它本質上是個陣列...為了好聽,後面叫他“\(Z\) 陣列” (網際網路上的確有人這麼叫)

符號(字串)

\(|S|\) 表示 \(S\) 的長度

\(S[l:r]\) 表示 \(S\)\(l\)\(r\) 的子串。如果 \(l\) 空著,預設為 \(1\);同理 \(r\) 預設為 \(|S|\)

也就是 \(S[:x]\) 表示 \(S\)\(x\) 結束的字首,\(S[x:]\)

表示 \(S\)\(x\) 開始的字尾。

\(LCP(S_1,S_2)\) 表示 \(S_1,S_2\)最長公共字首Longest Common Prefix

演算法講解

\(p_i=LCP(T_i,M)\)

定義從 \(l\) 開始的匹配區間為 \([l,l+p_l-1]\) (設 \(l+p_l-1=r\)

我們列舉處理。假設現在已經求好了 \([1,i-1]\)\(p\) 陣列,要求 \(p_i\)。記錄一個 最靠後 的匹配區間 \([l,r]\)\(l<i\),以 \(r\) 靠後為第一關鍵字,\(l\) 靠後為第二關鍵字),考慮直接從 \([l,r]\)

中繼承點答案來,那很顯然一個前提就是 \(i\le r\) (你 \(i\)\(r\) 外面繼承啥)

顯然,\(p_i\ge LCP(T[i:r],M)\) (因為 \(T[i:r]\)\(T[i:]\) 字首)

由定義, \([l,r]\) 是最長匹配長度,可知 \(T[l:r]=M[1:r-l+1]\)

然後現在假如 \(l<i\le r\),那麼顯然 \(T[i:r]=M[i-l+1:r-l+1]\)

那麼 \(LCP(T[i:r],M)=LCP(M[i-l+1:r-l+1],M)\)

簡單想一下,\(LCP(A[l:r],A)=min(LCP(A[l:],A),r-l+1)\)

我們要求 \([l,r]\) 子串與整個串的 \(LCP\),可以先求以 \(l\) 開頭的整個字尾的與整個串的 \(LCP\),然後和區間長度取 \(min\)。這顯然正確。

然後有:

\(LCP(M[i-l+1:r-l+1],M)=min(LCP(M[i-l+1:],M),(r-l+1)-(i-l+1+1))\)

右邊的 \(-l+1\) 兩個抵消了,就變成 \(r-i+1\)

然後前面是 \(LCP(M[i-l+1:],M)\) 。這不就是 \(M\)\(Z\) 陣列的第 \(i-l+1\) 個位置嗎!(還記得 \(Z\) 陣列的定義嗎?)

覺得看字母理解不了的看圖(自己畫的)(純滑鼠):

紅色的部分就是我們推出來的匹配部分。然後現在我們把 \(M\) 移到 \(i\) 開頭的位置來匹配,就相當於把 \(M[i-l+1:r-l+1]\) 這一段(紅色)移到 \(M\) 的開頭處匹配。這一段匹配的長度就是 \(min(Z_{i-l+1},r-i+1)\)

假設我們現在能求這個 \(Z\) 陣列,那麼我們已經知道 \(p_i\) 的最小值了 ,就是 \(min(Z_{i-l+1},r-i+1)\) 。從這個位置開始暴力即可。這樣就不用每次從 \(1\) 開始匹配了。

求完 \(p_i\) 之後,記得用 \([i,i+p_i-1]\) 更新 \([l,r]\)

時間是線性的,我不會證,可以參考網上的證明。

如何求 Z 陣列

我們發現 \(Z\) 陣列就是自己和自己匹配的過程。然後我們把上面過程中 \(M\) 換成 \(T\) 即可。

所以我們還是記錄一個最靠後的匹配區間 \([l,r]\),然後 \(p_i\) 就相當於 \(Z_i\) 了。

易得:

\(Z_i=min(LCP(M[i-l+1:],M),r-i+1)=min(LCP(T[i-l+1:],T),r-i+1)=min(Z_{i-l+1},r-i+1)\)

求完 \(Z_i\) 之後,記得用 \([i,i+Z_i-1]\) 來更新 \([l,r]\)

一樣,也是從這裡開始暴力即可。時間複雜度依然是線性的,可以參考網上的證明。

模板

洛谷板子

#include <bits/stdc++.h>
using namespace std;
#define N 20000007
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.Start(u),v=G.To(i);~i;i=G.Next(i),v=G.To(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define all(a) a.begin(),a.end()
#define iter(a,p) (a.begin()+p)
#define Flandre_Scarlet int
#define IsMyWife main
char _c;
int I()
{
    int x=0; int f=1;
    while(_c<'0' or _c>'9') f=(_c=='-')?-1:1,_c=getchar();
    while(_c>='0' and _c<='9') x=(x<<1)+(x<<3)+(_c^48),_c=getchar();
    return (x=(f==1)?x:-x);
}
void Rd(int cnt,...)
{
    va_list args; va_start(args,cnt);
    F(i,1,cnt) {int* x=va_arg(args,int*);(*x)=I();}
    va_end(args);
}

char a[N],b[N];
void Input()
{
	scanf("%s%s",a+1,b+1);
}
int z[N];
void Z(char s[]) // 求 Z 函式
{
	int n=strlen(s+1);
	z[1]=n; F(i,2,n) z[i]=0;
    // Z[1]=n 特判,同時也是遞推邊界
	int l=0,r=0;
	F(i,2,n) 
	{
		if (i<=r) z[i]=min(z[i-l+1],r-i+1); // 推理出下界
		while(i+z[i]<=n and s[i+z[i]]==s[z[i]+1]) ++z[i]; // 暴力
		if (i+z[i]-1>=r) l=i,r=i+z[i]-1; // 更新最靠後的匹配位置
	}
}
int p[N];
void ExKmp(char s[],char t[])
{
	int n=strlen(s+1);
	Z(t);
	int l=0,r=0;
	F(i,1,n)
	{
		if (i<=r) p[i]=min(z[i-l+1],r-i+1); // 推理出下界
		while(i+p[i]<=n and s[i+p[i]]==t[p[i]+1]) ++p[i]; // 暴力
		if (i+p[i]-1>r) l=i,r=i+p[i]-1; // 更新最靠後的匹配位置
	}
}
void Soviet()
{
	ExKmp(a,b);
	int n=strlen(a+1),m=strlen(b+1);
	long long ans=0;
	F(i,1,m) ans^=1ll*i*(z[i]+1);
	printf("%lld\n",ans);
	ans=0;
	F(i,1,n) ans^=1ll*i*(p[i]+1);
	printf("%lld\n",ans);
}
Flandre_Scarlet IsMyWife()
{
	Input();
	Soviet();
	getchar();
	return 0;
}