zoukankan      html  css  js  c++  java
  • 后缀数组学习笔记

    作用

    对于一个字符串的后缀按照字典序进行排序

    通常的求法是 (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
    ");
    			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
    ",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 imes Len)(AA) 串的开头 / 结尾。

    我们每隔 (Len) 放一个点,这样每一个 长度为 (2 imes 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
    ",ans);
    	}
    	return 0;
    }
    

    P2178 [NOI2015]品酒大会

    (height) 数组从小到大排序后倒序枚举

    用并查集维护联通块最大/最小值

    每次把 (height) 数组所掌管的两个元素所在的集合合并

    P4094 [HEOI2016/TJOI2016]字符串

    主席树+后缀数组+二分

    利用了 (lcp) 这个函数是单峰的,并且峰值在自己这里

  • 相关阅读:
    CocoaPod 常用命令
    Runloop
    RxSwift学习笔记7:buffer/window/map/flatMap/flatMapLatest/flatMapFirst/concatMap/scan/groupBy
    RxSwift学习笔记6:Subjects/PublishSubject/BehaviorSubject/ReplaySubject/Variable
    RxSwift学习笔记5:Binder
    RxSwift学习笔记4:disposeBag/scheduler/AnyObserver/Binder
    RxSwift学习笔记3:生命周期/订阅
    RxSwift学习笔记2:Observable/生命周期/Event/oneNext/onError/onCompleted/
    RxSwift学习笔记1:RxSwift的编程风格
    iOS处理视图上同时添加单击与双击手势的冲突问题
  • 原文地址:https://www.cnblogs.com/liuchanglc/p/14190528.html
Copyright © 2011-2022 走看看