字尾陣列學習筆記

liuchanglc發表於2020-12-26

作用

對於一個字串的字尾按照字典序進行排序

通常的求法是 \(nlogn\) 的倍增做法

網上的部落格都很詳細

比如這篇這篇

這裡只放一下板子,並說一下幾種常見的題型

#define rg register
const int maxn=1e6+5;
int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn];
void Qsort(){
	for(rg int i=0;i<=m;i++) tax[i]=0;
	for(rg int i=1;i<=n;i++) tax[fir[i]]++;
	for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1];
	for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i];
}
void getsa(){
	m=10000;
	for(rg int i=1;i<=n;i++) fir[i]=s[i],sec[i]=i;
	Qsort();
	for(rg int len=1,p=0;p<n;m=p,len<<=1){
		p=0;
		for(rg int i=n-len+1;i<=n;i++) sec[++p]=i;
		for(rg int i=1;i<=n;i++) if(sa[i]>len) sec[++p]=sa[i]-len;
		Qsort();
		std::swap(fir,sec);
		fir[sa[1]]=p=1;
		for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p;
	}
}
void getheight(){
	rg int j,k=0;
	for(rg int i=1;i<=n;i++){
		if(k) k--;
		j=sa[fir[i]-1];
		while(s[i+k]==s[j+k]) k++;
		hei[fir[i]]=k;
	}
}

題型一:字尾排序

這種題一般給你一個長度為 \(n\) 的字串,你可以選擇將最前面的字元移到最後,求字典序最小的方案

解決方法就是把原陣列複製一遍,然後跑一下字尾排序

後兩道 \(USACO\) 的題則需要把原串翻轉接在最後,思想很巧妙

例題:P4051 [JSOI2007]字元加密 P1368 【模板】最小表示法 P6140 [USACO07NOV]Best Cow Line S P2870 [USACO07DEC]Best Cow Line G

題型二:不同性質子串個數

用總的子串的個數 \(\frac{n(n+1)}{2}\) 減去重複的子串的個數 \(\sum_{i=1}^{n}height[i]\)

例題:P2408 不同子串個數 P4070 [SDOI2016]生成魔咒 SP705 SUBST1 - New Distinct Substrings SP694 DISUBSTR - Distinct Substrings

第二道題要稍稍做一下轉化,把向結尾加字元轉化成向前加字元

這樣每次只會有一個新的字尾加入,我們只需要用一個 \(set\) 找該字尾的前驅後繼計算答案即可

題型三:利用\(height\)陣列的性質計算

對於\(height\)陣列,有如下的式子

\(height[i]=LCP(sa[i−1],sa[i])\)

\(LCP(j,k)=min_{l=j+1}^kheightl\)
例題:P4248 [AHOI2013]差異 #3879. SvT

這兩道題都利用了\(height\)陣列第二個取 \(min\) 的性質

對於 \(height\) 陣列中的每一個值,記錄一下它向左和向右能做的最遠的貢獻,可以用單調棧實現
核心程式碼

sta[++tp]=1;
for(rg int i=2;i<=n;i++){
	while(tp && heig[i]<=heig[sta[tp]]){
		r[sta[tp]]=i;
		tp--;
	}
	l[i]=sta[tp];
	sta[++tp]=i;
}
while(tp){
	r[sta[tp--]]=n+1;
}
ans=1LL*(n+1)*n*(n-1)/2;
for(rg int i=1;i<=n;i++){
	ans-=2LL*(i-l[i])*(r[i]-i)*heig[i];
}

題型四:求不同串的最長的公共子串的長度

我們把這些串連成一個長串,在串與串相接的地方插入一個沒有出現過的特殊符號,防止出現重合的問題

然後求出整個串的 \(height\) 陣列,並對於每一個\(height\) 陣列染色,標記它屬於原來的哪一個串

然後用雙指標從前到後掃一遍,當記錄到的不同串的個數等於總的串的個數時取一下最大值

最後一道題還需要差分一下
例題:SP1811 LCS - Longest Common Substring SP10570 LONGCS - Longest Common Substring SP1812 LCS2 - Longest Common Substring II [SDOI2008]Sandy的卡片
完整程式碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#define rg register
const int maxn=1e6+5;
int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn],t,l[maxn],r[maxn],col[maxn],cnt[maxn],js,q[maxn],head,tail,ans;
char s[maxn];
void Qsort(){
	for(rg int i=0;i<=m;i++) tax[i]=0;
	for(rg int i=1;i<=n;i++) tax[fir[i]]++;
	for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1];
	for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i];
}
void getsa(){
	memset(sa,0,sizeof(sa));
	memset(fir,0,sizeof(fir));
	memset(sec,0,sizeof(sec));
	m=300;
	for(rg int i=1;i<=n;i++) fir[i]=s[i]-'0'+1,sec[i]=i;
	Qsort();
	for(rg int len=1,p=0;p<n;m=p,len<<=1){
		p=0;
		for(rg int i=n-len+1;i<=n;i++) sec[++p]=i;
		for(rg int i=1;i<=n;i++) if(sa[i]>len) sec[++p]=sa[i]-len;
		Qsort();
		std::swap(fir,sec);
		fir[sa[1]]=p=1;
		for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p;
	}
}
void getheight(){
	memset(hei,0,sizeof(hei));
	rg int j,k=0;
	for(rg int i=1;i<=n;i++){
		if(k) k--;
		j=sa[fir[i]-1];
		while(s[i+k]==s[j+k]) k++;
		hei[fir[i]]=k;
	}
}
void xg(rg int now,rg int op){
	if(col[now]==0) return;
	if(cnt[col[now]]==0) js++;
	cnt[col[now]]+=op;
	if(cnt[col[now]]==0) js--;
}
int T;
int main(){
	scanf("%d",&T);
	while(T--){
		memset(col,0,sizeof(col));
		memset(l,0,sizeof(l));
		memset(r,0,sizeof(r));
		memset(cnt,0,sizeof(cnt));
		ans=js=0;
		scanf("%d",&t);
		rg int len;
		for(rg int i=1;i<=t;i++){
			l[i]=n+1;
			scanf("%s",s+n+1);
			len=strlen(s+n+1);
			n+=len;
			r[i]=n;
			s[++n]='A'+i;
		}
		if(t==1){
			printf("0\n");
			return 0;
		}
		getsa();
		getheight();
		for(rg int i=1;i<=t;i++){
			for(rg int j=l[i];j<=r[i];j++){
				col[fir[j]]=i;
			}
		}
		rg int nl=1;
		xg(1,1);
		for(rg int nr=2;nr<=n;nr++){
			while(head<=tail && hei[nr]<=hei[q[tail]]) tail--;
			q[++tail]=nr;
			xg(nr,1);
			if(js==t){
				while(js==t && nl<nr) xg(nl++,-1);
				nl--;
				xg(nl,1);
			}
			while(head<=tail && q[head]<=nl) head++;
			if(js==t){
				ans=std::max(ans,hei[q[head]]);
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

題型五:求不同串的子串相同的方案數

利用上一個題型的方法把不同的串合併

利用單調佇列求出每一個 \(height\) 陣列能貢獻的最左和最右的距離

最後再容斥一下,減去兩個單獨子串的上述貢獻

例題:P3181 [HAOI2016]找相同字元

題型六:求出現次數為 \(k\) 的子串的最長長度和長度為 \(k\) 的子串出現的最大次數

分別對應下面的兩道題

我們還是用單調棧維護當前的 \(height\) 能向右和向左擴充套件的最長的長度

然後 \(dp\) 轉移即可
核心程式碼(第二道)

sta[++tp]=1;
for(rg int i=2;i<=n;i++){
	while(tp && heig[i]<=heig[sta[tp]]){
		r[sta[tp]]=i;
		tp--;
	}
	l[i]=sta[tp];
	sta[++tp]=i;
}
while(tp){
	r[sta[tp--]]=n+1;
}
for(rg int i=1;i<=n;i++) f[i]=1;
for(rg int i=1;i<=n;i++){
	f[heig[i]]=std::max(f[heig[i]],r[i]-l[i]);
}
for(rg int i=n;i>=1;i--){
	f[i]=std::max(f[i],f[i+1]);
}

例題:P2852 [USACO06DEC]Milk Patterns G SP8222 NSUBSTR - Substrings

題型七:一些綜合性比較強的題目

P1117 [NOI2016]優秀的拆分

主要考察怎麼利用字首和字尾的性質求類似於 \(AA\) 的子串的個數

考慮列舉一個 \(Len\) ,然後對於每個點求出他是否是一個 \(2 \times Len\)\(AA\) 串的開頭 / 結尾。

我們每隔 \(Len\) 放一個點,這樣每一個 長度為 \(2 \times Len\)\(AA\) 串都至少會經過兩個相鄰的點。

所以再轉換為每兩個相鄰的點會對 \(a, b\) 產生多少貢獻。

先求出這對相鄰點所代表的字首的最長公共字尾 \(LCS\) 和 所代表的字尾的最長公共字首 \(LCP\)

如果 \(LCP + LCS < Len\) 肯定不合法

否則給合法的區間整體加一

參考洛谷題解

程式碼實現

#include<cstdio>
#include<cstring>
#include<algorithm>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=3e4+5;
int n,m,sa[maxn],fir[maxn],sec[maxn],tax[maxn],hei[maxn],hei1[maxn],hei2[maxn],fir1[maxn],fir2[maxn],lg[maxn],mmin1[maxn][20],mmin2[maxn][20],t;
char s[maxn];
void Qsort(){
	for(rg int i=0;i<=m;i++) tax[i]=0;
	for(rg int i=1;i<=n;i++) tax[fir[i]]++;
	for(rg int i=1;i<=m;i++) tax[i]+=tax[i-1];
	for(rg int i=n;i>=1;i--) sa[tax[fir[sec[i]]]--]=sec[i];
}
void getsa(){
	memset(sa,0,sizeof(sa));
	memset(fir,0,sizeof(fir));
	memset(sec,0,sizeof(sec));
	m=3e4+1;
	for(rg int i=1;i<=n;i++) fir[i]=s[i],sec[i]=i;
	Qsort();
	for(rg int len=1,p=0;p<n;m=p,len<<=1){
		p=0;
		for(rg int i=n-len+1;i<=n;i++) sec[++p]=i;
		for(rg int i=1;i<=n;i++) if(sa[i]>len) sec[++p]=sa[i]-len;
		Qsort();
		memcpy(sec,fir,sizeof(fir));
		fir[sa[1]]=p=1;
		for(rg int i=2;i<=n;i++) fir[sa[i]]=(sec[sa[i]]==sec[sa[i-1]] && sec[sa[i]+len]==sec[sa[i-1]+len])?p:++p;
	}
}
void getheight(){
	memset(hei,0,sizeof(hei));
	rg int j,k=0;
	for(rg int i=1;i<=n;i++){
		if(k) k--;
		j=sa[fir[i]-1];
		while(s[i+k]==s[j+k]) k++;
		hei[fir[i]]=k;
	}
}
int cf1[maxn],cf2[maxn];
long long ans;
int getans1(rg int l,rg int r){
	rg int k=lg[r-l+1];
	return std::min(mmin1[l][k],mmin1[r-(1<<k)+1][k]);
}
int getans2(rg int l,rg int r){
	rg int k=lg[r-l+1];
	return std::min(mmin2[l][k],mmin2[r-(1<<k)+1][k]);
}
int main(){
	for(rg int i=2;i<maxn;i++) lg[i]=lg[i>>1]+1;
	t=read();
	while(t--){
		ans=0;
		memset(cf1,0,sizeof(cf1));
		memset(cf2,0,sizeof(cf2));
		scanf("%s",s+1);
		n=strlen(s+1);
		getsa();
		getheight();
		memcpy(hei1,hei,sizeof(hei));
		memcpy(fir1,fir,sizeof(fir));
		std::reverse(s+1,s+1+n);
		getsa();
		getheight();
		memcpy(hei2,hei,sizeof(hei));
		memcpy(fir2,fir,sizeof(fir));
		std::reverse(s+1,s+1+n);
		for(rg int i=1;i<=n;i++) mmin1[i][0]=hei1[i],mmin2[i][0]=hei2[i];
		for(rg int j=1;j<=15;j++){
			for(rg int i=1;i+(1<<j)-1<=n;i++){
				mmin1[i][j]=std::min(mmin1[i][j-1],mmin1[i+(1<<(j-1))][j-1]);
				mmin2[i][j]=std::min(mmin2[i][j-1],mmin2[i+(1<<(j-1))][j-1]);
			}
		}
		rg int ac1,ac2,ac3,ac4,ac5;
		for(rg int len=1;len<=n;len++){
			for(rg int i=len,j=i+len;j<=n;i+=len,j+=len){
				ac1=fir2[n-i+1],ac2=fir2[n-j+1];
				if(ac1>ac2) std::swap(ac1,ac2);
				ac3=getans2(ac1+1,ac2);
				ac1=fir1[i],ac2=fir1[j];
				if(ac1>ac2) std::swap(ac1,ac2);
				ac4=getans1(ac1+1,ac2);
				ac3=std::min(ac3,len);
				ac4=std::min(ac4,len);
				if(ac3+ac4-1<len) continue;
				ac5=ac3+ac4-len;
				cf1[i-ac3+1]++;
				cf1[i-ac3+1+ac5]--;
				cf2[j+ac4-1-ac5+1]++;
				cf2[j+ac4]--;
			}
		}
		for(rg int i=1;i<=n+1;i++) cf1[i]+=cf1[i-1],cf2[i]+=cf2[i-1];
		for(rg int i=1;i<n;i++){
			ans+=1LL*cf2[i]*cf1[i+1];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

P2178 [NOI2015]品酒大會

\(height\) 陣列從小到大排序後倒序列舉

用並查集維護聯通塊最大/最小值

每次把 \(height\) 陣列所掌管的兩個元素所在的集合合併

P4094 [HEOI2016/TJOI2016]字串

主席樹+字尾陣列+二分

利用了 \(lcp\) 這個函式是單峰的,並且峰值在自己這裡

相關文章