• 后缀数组


    (sa)(rk) 数组

    定义
    (sa_i) 表示将所有后缀按字典序排序后第 (i) 小的后缀的编号, (rk_i) 表示后缀 (i) 的排名。(后缀 (i) 代指以 (i) 开头的后缀)
    有性质 (sa_{rk_i}=rk_{sa_i}=i)
    求法
    用倍增优化暴力做法。
    (rk_{w,i}) 表示以 (i) 开头的长度为 (w) 的串在所有长度为 (w) 的串中排名为多少。
    那么,以 (rk_{w,i}) 为第一关键字, (rk_{w,i+w}) 为第二关键字排序,即可求出 (rk_{2w})
    把字符串中每个字符排序,得到 (rk_1) 后即可推出 (rk) 数组。
    用 sort 排序,复杂度为 (O(n log^2 n))
    因为这个排序的值域为 (O(n)) ,考虑用基数排序代替 sort 的部分,复杂度为 (O(n log n))
    但是,第二关键字的排序其实并不需要计数排序。只需把空串放在前面,其它串按原顺序排好即可。
    这里就放一份用 sort 实现后缀排序的代码,比较清晰,方便理解。把各种排序丢进去之后太乱了。

    #include<algorithm>
    #include<iostream>
    #include<cstring>
    #include<cstdio>
    using namespace std;
    const int N=2e6+10;
    char a[N];
    int n,w,sa[N],rk[N],RK[N];
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    bool cmp(int x,int y)
    {
    	if(rk[x]!=rk[y]) return rk[x]<rk[y];
    	return rk[x+w]<rk[y+w];
    }
    int main()
    {
    	scanf("%s",a+1);
    	n=strlen(a+1);
    	int m=MAX(n,300);
    	for(int i=1;i<=n;i++) sa[i]=i,rk[i]=a[i];
    	//rk数组在代码中其实只关心大小关系,而并不关心具体的值 
            //进入循环后立刻就要排序,而且排序方法也和 sa 数组无关,所以 sa 数组的初值只要赋为 1~n 即可
    	for(w=1;w<n;w<<=1)
    	{
    		sort(sa+1,sa+n+1,cmp);
    		memcpy(RK,rk,sizeof(rk));
    		for(int t=0,i=1;i<=n;i++)
    			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
    	}
    	for(int i=1;i<=n;i++) printf("%d ",sa[i]);
    	return 0;
    }
    

    应用
    1
    把字符串复制一遍,后缀排序即可。

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    using namespace std;
    const int N=2e6+10;
    char a[N];
    int n,sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    int main()
    {
    	scanf("%s",a+1);
    	n=strlen(a+1);
    	for(int i=1;i<=n;i++) a[i+n]=a[i];n*=2;
    	int m=MAX(n,300);
    	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
    	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    	for(int w=1;w<n;w<<=1)
    	{
    		int sum=0;
    		for(int i=n;i>n-w;i--) id[++sum]=i;
    		for(int i=1;i<=n;i++)
    			if(sa[i]>w) id[++sum]=sa[i]-w;
    		memset(cnt,0,sizeof(cnt));
    		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
    		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
    		memcpy(RK,rk,sizeof(rk));
    		for(int t=0,i=1;i<=n;i++,m=t)
    			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
    	}
    	for(int i=1;i<=n;i++)
    		if(sa[i]<=n/2) putchar(a[sa[i]+n/2-1]);
    	return 0;
    }
    

    2
    先考虑暴力的做法。
    首先,当首尾字符不同时,显然可以贪心选。
    当首尾字符相同时,则把当前剩下的串和它的反串的字典序进行比较。
    考虑如何优化这一做法。
    把原串的反串接在原串后面,把两个串之间用一个字典序极小的字符隔开。
    求出这个串的后缀数组,比较时直接比较 (rk) 即可。

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    using namespace std;
    const int N=2e6+10;
    char a[N],ans[N];
    int n,sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    int main()
    {
    	scanf("%d",&n);
    	for(int i=1;i<=n;i++) getchar(),a[i]=getchar();
    	a[n+1]='#';
    	for(int i=1,j=n;i<=n;i++,j--) a[n+1+i]=a[j];
    	n*=2,n++;
    	int m=MAX(n,300);
    	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
    	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    	for(int w=1;w<n;w<<=1)
    	{
    		int sum=0;
    		for(int i=n;i>n-w;i--) id[++sum]=i;
    		for(int i=1;i<=n;i++)
    			if(sa[i]>w) id[++sum]=sa[i]-w;
    		memset(cnt,0,sizeof(cnt));
    		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
    		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
    		memcpy(RK,rk,sizeof(rk));
    		for(int t=0,i=1;i<=n;i++,m=t)
    			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
    	}
    	n--,n/=2;
    	int l=1,r=n,res=0;
    	while(l<r)
    	{
    		if(a[l]<a[r]) ans[++res]=a[l],l++;
    		else if(a[r]<a[l]) ans[++res]=a[r],r--;
    		else
    		{
    			if(rk[l]<rk[n*2+2-r]) ans[++res]=a[l],l++;
    			else ans[++res]=a[r],r--;
    		}
    	}
    	ans[++res]=a[l];
    	for(int i=1;i<=res;i++)
    	{
    		putchar(ans[i]);
    		if(i%80==0) puts("");
    	}
    	return 0;
    }
    

    3
    在线地在主串 (T) 中寻找模式串 (S)
    发现若 (S)(T) 中出现,(S) 一定是 (T) 某个后缀的前缀。
    求出后缀数组,在求的过程中我们已经将后缀排序了,
    在排序用的数组中二分,判断时暴力即可。
    复杂度 (O(|S| log |T|))
    若出现了很多次,发现每次出现时,我们要寻找的后缀在排序后一定是连续的,所以再二分一次即可。

    (height) 数组

    下面以 (lcp(i,j)) 表示后缀 (i) 和后缀 (j) 的最长公共前缀的长度。
    定义
    (height_i=lcp(sa_i,sa_{i-1}))
    求法
    (height_{rk_i} geq height_{rk_{i-1}}-1)
    根据这个式子,按照 (rk) 的顺序暴力求,容易证明复杂度是 (O(n)) 的。

    for(int i=1,t=0;i<=n;i++)
    {
    	if(t) t--;
    	while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
    	ht[rk[i]]=t;
    }
    

    应用
    1
    求任意后缀的 (lcp)
    (lcp(x,y)=min{height_k|rk_x <k leq rk_y})
    2
    求不同子串的数目。
    子串就是后缀的前缀。
    考虑容斥一下,用串的总数减去重复的串的个数。
    按排序得到的顺序枚举后缀,发现每次重复的子串的数量即为在与前一个后缀的 (lcp) 里的前缀的数量。
    所以,答案即为 (frac{n(n+1)}{2}-sum limits_{i=2}^n height_i)
    3
    出现至少 (k) 次可以转化为在排序后的后缀中有至少连续 (k) 个后缀的 (lcp) 是这个串。
    所以,只需求出每相邻 (k-1)(height) 的最小值,在求出它们的最大值即可。
    用单调队列实现。

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<deque>
    using namespace std;
    const int N=2e6+10;
    int sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
    int n,k,a[N],ht[N],ans;
    struct node
    {
    	int id,x;
    };
    deque <node> q;
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    int main()
    {
    	scanf("%d%d",&n,&k);
    	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    	int m=MAX(n,300);
    	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
    	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    	for(int w=1;w<n;w<<=1)
    	{
    		int sum=0;
    		for(int i=n;i>n-w;i--) id[++sum]=i;
    		for(int i=1;i<=n;i++)
    			if(sa[i]>w) id[++sum]=sa[i]-w;
    		memset(cnt,0,sizeof(cnt));
    		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
    		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
    		memcpy(RK,rk,sizeof(rk));
    		for(int t=0,i=1;i<=n;i++,m=t)
    			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
    	}
    	for(int i=1,t=0;i<=n;i++)
    	{
    		if(t) t--;
    		while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
    		ht[rk[i]]=t;
    	}
    	for(int i=2;i<=n;i++) 
    	{
    		while(q.size()&&ht[i]<=q.back().x) q.pop_back();
    		q.push_back((node){i,ht[i]});
    		while(q.front().id<=i-k+1) q.pop_front();
    		if(i>=k) ans=MAX(ans,q.front().x);
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    4
    给出一个文本串,问是否有字符串在文本串中至少不重叠地出现了两次。
    二分字符串的长度 (x) ,容易发现这一定是单调的,所以可以二分。
    (height) 数组中找出所有连续 (lcp) 大于等于 (x) 的段,
    对于每段找出后缀编号最小和最大的后缀,判断是否合法即可。
    5
    可以按照 (height) 数组大小的顺序合并答案,这部分用并查集维护。
    因为最大的乘积可能是由最小值相乘,或由最大值相乘得到,所以要维护下最小值、最大值。
    方案数即为合并时的两子树大小相乘。
    发现若两个串是 (r) 相似的,则它们一定也是 (1) 相似,(2) 相似, (cdots)(r-1) 相似的。
    所以最后要做前缀和。

    #include<algorithm>
    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #define int long long
    using namespace std;
    const int N=2e6+10;
    const int inf=1e18;
    char a[N];
    int sa[N],rk[N],RK[N],cnt[N],id[N],p[N],val[N];
    int n,ht[N],fa[N],sum[N],ans[N],mx[N],mn[N],sz[N];
    int MIN(int x,int y)
    {
    	return x<y?x:y;
    }
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    bool cmp(int x,int y)
    {
    	return ht[x]>ht[y];
    }
    int find(int x)
    {
    	if(x==fa[x]) return x;
    	return fa[x]=find(fa[x]);
    }
    void merge(int x,int y)
    {
    	int fx=find(x),fy=find(y);
    	sum[ht[x]]+=sz[fx]*sz[fy];
    	ans[ht[x]]=MAX(ans[ht[x]],MAX(mx[fx]*mx[fy],mn[fx]*mn[fy]));
    	mx[fx]=MAX(mx[fx],mx[fy]);
    	mn[fx]=MIN(mn[fx],mn[fy]);
    	fa[fy]=fx,sz[fx]+=sz[fy];
    }
    signed main()
    {
    	scanf("%lld",&n);
    	scanf("%s",a+1);
    	for(int i=1;i<=n;i++) scanf("%lld",&val[i]);
    	int m=MAX(n,300);
    	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
    	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    	for(int w=1;w<n;w<<=1)
    	{
    		int sum=0;
    		for(int i=n;i>n-w;i--) id[++sum]=i;
    		for(int i=1;i<=n;i++)
    			if(sa[i]>w) id[++sum]=sa[i]-w;
    		memset(cnt,0,sizeof(cnt));
    		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
    		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
    		memcpy(RK,rk,sizeof(rk));
    		for(int t=0,i=1;i<=n;i++,m=t)
    			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
    	}
    	for(int i=1,t=0;i<=n;i++)
    	{
    		if(t) t--;
    		while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
    		ht[rk[i]]=t;
    	}
    	for(int i=1;i<=n;i++)
    		id[i]=i,fa[i]=i,ans[i]=-inf,sz[i]=1,mx[i]=val[sa[i]],mn[i]=val[sa[i]];
    	sort(id+1,id+n+1,cmp);
    	for(int i=1;i<=n;i++)
    		if(find(id[i])!=find(id[i]-1)) merge(id[i],id[i]-1);
    	for(int i=n-2;i>=0;i--)
    		sum[i]+=sum[i+1],ans[i]=MAX(ans[i],ans[i+1]);
    	for(int i=0;i<n;i++)
    	{
    		if(sum[i]==0) puts("0 0");
    		else printf("%lld %lld
    ",sum[i],ans[i]);
    	}
    	return 0;
    }
    

    6
    这个式子前面的部分可以直接算,所以就是在求后缀两两之间的 (lcp) 之和。
    考虑 (lcp) 的求法,可以把求 (lcp) 之和转化为求 (height) 数组每段区间的区间最小值之和。
    这就是单调栈经典问题了。

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #define int long long
    using namespace std;
    const int N=2e6+10;
    const int inf=1e18;
    char a[N],b[N];
    int sa[N],rk[N],RK[N],cnt[N],id[N],p[N];
    int n,ht[N],ans,top,s[N],l[N],r[N];
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    signed main()
    {
    	scanf("%s",a+1);
    	n=strlen(a+1);
    	int m=MAX(n,300);
    	for(int i=1;i<=n;i++) cnt[rk[i]=a[i]]++;
    	for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    	for(int i=n;i>=1;i--) sa[cnt[rk[i]]--]=i;
    	for(int w=1;w<n;w<<=1)
    	{
    		int sum=0;
    		for(int i=n;i>n-w;i--) id[++sum]=i;
    		for(int i=1;i<=n;i++)
    			if(sa[i]>w) id[++sum]=sa[i]-w;
    		memset(cnt,0,sizeof(cnt));
    		for(int i=1;i<=n;i++) cnt[p[i]=rk[id[i]]]++;
    		for(int i=1;i<=m;i++) cnt[i]+=cnt[i-1];
    		for(int i=n;i>=1;i--) sa[cnt[p[i]]--]=id[i];
    		memcpy(RK,rk,sizeof(rk));
    		for(int t=0,i=1;i<=n;i++,m=t)
    			rk[sa[i]]=(RK[sa[i]]==RK[sa[i-1]]&&RK[sa[i]+w]==RK[sa[i-1]+w])?t:++t;
    	}
    	for(int i=1,t=0;i<=n;i++)
    	{
    		if(t) t--;
    		while(a[i+t]==a[sa[rk[i]-1]+t]) t++;
    		ht[rk[i]]=t;
    	}
    	ans=(n-1)*(n+1)*n/2;
    	ht[0]=-inf,ht[n+1]=-inf;
    	s[++top]=0;
    	for(int i=1;i<=n;i++)
    	{
    		while(top&&ht[i]<=ht[s[top]]) top--;
    		l[i]=s[top],s[++top]=i;
    	}
    	s[top=1]=n+1;
    	for(int i=n;i>=1;i--)
    	{
    		while(top&&ht[i]<ht[s[top]]) top--;
    		r[i]=s[top],s[++top]=i;
    	}
    	for(int i=1;i<=n;i++) ans-=2*(i-l[i])*(r[i]-i)*ht[i];
    	printf("%lld",ans);
    	return 0;
    }
    
  • 相关阅读:
    基数排序
    计数排序和桶排序
    部署Java Web项目到云服务器的步骤全解析
    IP地址0.0.0.0/0是什么意思
    Tomcat在阿里云Centos7上正常启动,但浏览器无法访问的解决方法
    eclipse光标怎么返回上一次浏览的位置
    IDEA设置方法自动显示参数提示
    socket通信模型、socket中的accept()阻塞与read()阻塞
    Ubuntu18.04 下修改 root密码
    Ubuntu18.04 安装 VMwareTools
  • 原文地址:https://www.cnblogs.com/zhs1/p/14262482.html
Copyright © 2020-2023  润新知