学习笔记 后缀数组

【写在之前】

其实听说过这个东西 据说很牛B

dalao (1)

dalao (2)

dalao (3)

dalao (4)

但这其实是一个蒟蒻无力的挣扎

【正式开始】

后缀排序 求SA

这里只介绍倍增算法

\(SA[i]\) 表示排名为\(i\)的后缀的开头位置

\(rnk[i]\) 表示第\(i\)个位置的后缀的排名

其实二者互补

\(SA[\ rnk[i]\ ]=i\)

\(rnk[\ SA[i]\ ]=i\)

\(cdy[i]\) 表示第一关键字 也就是\(rnk\)

然后是\(wzy[i]\) 表示第二关键字对应的\(SA[i]\)

然后就是\(vis[i]\) 表示一个权值桶

TA的原理:

1.倍增的是比较跨度

2.使用二元数组进行比较机理 基数排序

然后就是这样

我们先上代码 (其实就这里不明白)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdlib>
#include<string>
#include<queue>
#include<map>
#include<stack>
#include<list>
#include<set>
#include<deque>
#include<vector>
#include<ctime>
#define ll long long
#define inf 0x7fffffff
#define N 500008
#define IL inline
#define M 1008611
#define D double
#define ull unsigned long long
#define R register
using namespace std;
template<typename T>void read(T &a)
{
    T x=0,f=1;char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')f=0;ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+ch-'0';ch=getchar();
    }
    a=f?x:-x;
}
/*-------------OI使我快乐-------------*/
char s[N<<1];
int n,key;
int SA[N<<1],rnk[N<<1],vis[N<<1],wzy[N<<1],cdy[N<<1];
//cdy 作为第一关键字
//wzy 作为第二关键字
IL void Rsort()
{
	for(R int i=0;i<=key;++i) vis[i]=0;
	for(R int i=1;i<=n;++i) vis[cdy[i]]++;
	for(R int i=1;i<=key;++i) vis[i]+=vis[i-1];
	for(R int i=n;i;--i) SA[vis[cdy[wzy[i]]]--]=wzy[i];
}
IL void get_SA()
{
	for(R int i=1;i<=n;++i) cdy[i]=s[i],wzy[i]=i;
	Rsort();
	for(R int x=1;x<=n;x<<=1)
	{
		int cnt=0;
		for(R int i=n-x+1;i<=n;++i) wzy[++cnt]=i;
		for(R int i=1;i<=n;++i) if(SA[i]>x) wzy[++cnt]=SA[i]-x;
		Rsort();swap(cdy,wzy);
		cdy[SA[cnt=1]]=1;
		for(R int i=2;i<=n;++i)
		cdy[SA[i]]=(wzy[SA[i]]==wzy[SA[i-1]] && wzy[SA[i]+x]==wzy[SA[i-1]+x] ? cnt:++cnt);
		if(cnt==n) break;
		else key=cnt;
	}
}
int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	scanf("%s",s+1);n=strlen(s+1);key=130;
	get_SA();
	for(R int i=1;i<=n;++i) printf("%d%c",SA[i],(i==n ? '\n':' '));
//	fclose(stdin);
//	fclose(stdout);
	return 0;
}

基数排序

一开始不怎么明白 但是看了看基数排序之后就懂了

首先 这里的基数排序是\(LSD\)(从低位到高位)

但是按照制胡窜来讲 显然是\(MSD\)更容易一些

但是随着序列长度的上升 \(MSD\)的性能远不及\(LSD\)

所以这里只好使用\(LSD\)

实在不懂的话就 。。。。。。

IL void Rsort()
{
	for(R int i=0;i<=key;++i) vis[i]=0;
	for(R int i=1;i<=n;++i) vis[cdy[i]]++;
    for(R int i=1;i<=key;++i) vis[i]+=vis[i-1];
    for(R int i=n;i;--i) SA[vis[cdy[wzy[i]]]--]=wzy[i];
}

前三行都是比较好理解 唯独最后一行不太好理解

首先我们看看第二关键字是如何让处理的

for(R int i=n-x+1;i<=n;++i) wzy[++cnt]=i;
for(R int i=1;i<=n;++i) if(SA[i]>x) wzy[++cnt]=SA[i]-x;

再看这张解惑图

\(wzy[i]=j\) 表示第二关键字排名为\(i\)位于第\(j\)个位置

第一行代码表示

上面由于倍增从而没有第二关键字的 那么就是空

自然排名也位于最前面

第二行代码表示

然后就是顺序存入 \(SA[i]-x\)表示TA是可以作为前面\(SA[i]-x\)的第二关键字

然后这样就可以了

然后再回来看第四行

一开始的处理就已经处理好了第二关键字

所以现在满足\(LSD\) 开始对第一关键字的排序

然后按照基数排序的逻辑

我们按照第二关键字的顺序

倒序存入之后 有里向外 数组下标发生了

**排名 --wzy--> 位置 **

**位置 ---cdy-> 排名 **

排名--vis--> 原始排名作为权值成为新的排名

排名 --SA--> 成为我们要的SA

更多的来说 是由于仅靠前面的字符无法分出胜负

所以我们使用后面的字符 作为第二关键字

在内部进行更深层次的划分

这就是基数排序的本质所在

大数之所以大 在于其有高位
但是没有低位的话 TA就会被别的大数压下去

维护关键字

for(R int x=1;x<=n;x<<=1)
{
	int cnt=0;
	for(R int i=n-x+1;i<=n;++i) wzy[++cnt]=i;
	for(R int i=1;i<=n;++i) if(SA[i]>x) wzy[++cnt]=SA[i]-x;
	Rsort();swap(cdy,wzy);
	cdy[SA[cnt=1]]=1;
	for(R int i=2;i<=n;++i)
	cdy[SA[i]]=(wzy[SA[i]]==wzy[SA[i-1]] && wzy[SA[i]+x]==wzy[SA[i-1]+x] ? cnt:++cnt);
	if(cnt==n) break;
	else key=cnt;
}

首先委会第二关键字我们已经透彻了

接下来就是第一关键字

为了节约空间 所以使用了\(swap()\) 反正也没有什么用了

然后排在第一的位置 当然排名就是第一了

然后的话每一次我们都同上一个比较

观察是否第一第二关键字都相同

是的话 TA们的排名就是相同的

否则的话 我们的倍增已经使得TA们互不相同了

那么接下来就没有必要了

所以就可以结束了

当然 后缀排序仅仅是一个板子

【update】

高度数组\(height[]\)

\(hei[rnk[i]]=LCP(rnk[i],rnk[i]-1)\)

最长公共前缀

\(hei[i]≥hei[i-1]+1\)

IL void get_he()
{
    int lat,k=0;
    for(R int i=1;i<=n;++i) rnk[SA[i]]=i;
    for(R int i=1;i<=n;++i)
    {
        if(k) k--;
        lat=SA[rnk[i]-1];
        while(s[i+k]==s[lat+k]) ++k;
        hei[rnk[i]]=k;
    }
}

最长公共子串

我们把两个串 接在一起 然后求\(SA\)

可以想象到的是

最长公共子串一定是某个后缀的前缀

然后两个最相近的前缀对应的位置 字典序一定是相邻的

所以我们枚举排名然后判断位置是否属于两个串 然后对于最长公共前缀取\(Max\)即可

int ans=0;
for(R int i=1;i<=n;++i)
{
    int x,y;
    x=SA[i-1];y=SA[i];
    if((x>len1&&y<len1)||(x<len1&&y>len1)) 
    //是否属于两个不同的串
    ans=max(ans,hei[i]);
}
printf("%d\n",ans);

求本质不同的的子串数量

本质不同的子串数量 :\(\sum_{i=1}^{n}n-SA[i]+1-hei[i]\)

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
    read(T);
    while(T--)
    {
        scanf("%s",s+1);n=strlen(s+1);key=130;ans=0;
        get_SA();get_he();
//		for(R int i=1;i<=n;++i) printf("%d%c",hei[i],(i==n ? '\n':' '));
        for(R int i=1;i<=n;++i) 
        ans+=1ll*(n-SA[i]+1-hei[i]);
        printf("%lld\n",ans);
    }
//	fclose(stdin);
//	fclose(stdout);
    return 0;
}

求本质不同的第\(k\)小子串

我们首先由排名从高到低

依次计算每一个位置的贡献 用前缀和以及二分查找来实现

然后找到对应的位置 暴力枚举即可

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
    scanf("%s",s+1);n=strlen(s+1);key=130;
    get_SA();get_hei();
//	for(R int i=1;i<=n;++i) printf("%d%c",SA[i],(i==n ? '\n':' '));
    for(R int i=1;i<=n;++i) sum[i]=sum[i-1]+(n-SA[i]+1-hei[i]);
    int T,k;
    read(T);
    while(T--)
    {
        read(k);
        int id=lower_bound(sum+1,sum+n+1,k)-sum;
        int bw=k-sum[id-1]+hei[id];
        for(R int i=0;i<bw;++i)
        printf("%c",s[i+SA[id]]);puts("");
    }
//	fclose(stdin);
//	fclose(stdout);
    return 0;
}

求第\(k\)大子串(本质相同的也要计算)

对于给出的排名

我们用二分答案确定其本质不同时对应的排名

然后以此我们可以确定其开头对应的\(x\)以及长度\(len\)

那么比其小的子串的个数就是

\(\sum_{i=1}^{x=1}(n-SA[i]+1)+\sum_{i=x}^{n}min(LCP(x,i),len)\)

顺道提一下

\(LCP(x,y)=min(LCP(x,x+1),LCP(x+1,x+2),......,LCP(y-1,y))\)


IL int check(int x)
{
    int s1=0,s2=0;
    //s1 统计答案 
    //s2 本质不同 
    for(R int i=1;i<=n;++i)
    {
        if(s2+n-SA[i]+1-hei[i]>=x)
        {//我们找到了
            int len=0,tox=x-s2+hei[i];
            for(R int j=0;j<tox;++j) len++;
            int minx=hei[i+1];s1+=len;
            for(R int j=i+1;j<=n;++j)
            {
                minx=min(minx,hei[j]);
                if(hei[j]<len)
                {
                    for(R int k=j;k<=n;++k)
                    {
                        minx=min(minx,hei[k]);
                        s1+=minx;
                    }
                    return s1;
                } 
                s1+=len;
            } 
        }
        s1+=n-SA[i]+1;s2+=n-SA[i]+1-hei[i]; 
    }
}

int le=1,ri=k,ans;
while(le<=ri)
{
    int mid=(le+ri)>>1;
    if(check(mid)>=k) {ans=mid;ri=mid-1;}
    else le=mid+1;
}

求旋转字典序排序之后字符串

我们还是在其后面再接一个同样的串

然后跑\(SA\) 然后我们直接看位置是否合法

然后求结尾字符即可

int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
    scanf("%s",s+1);n=strlen(s+1);key=130;
    for(R int i=1;i<=n;++i) s[n+i]=s[i];n<<=1;
    get_SA();
//	for(R int i=1;i<=n;++i) printf("%d%c",SA[i],(i==n ? '\n':' '));	
    for(R int i=1;i<=n;++i)
    if(SA[i]<=(n>>1)) ans[++tot]=s[SA[i]+(n>>1)-1];
    for(R int i=1;i<=n;++i)
    printf("%c",ans[i]);
//	fclose(stdin);
//	fclose(stdout);
    return 0;
}

求两个串的相同子串的数量

答案就是$$\sum_{i=1}{n}\sum_{j=1}m LCP(i,j)$$

其中\(i,j\)属于两个不同的串

我们把两个串拼在一起之后

就是求任意两个后缀的最长公共前缀

由于 $$LCP(i,j)=min{LCP(k,k+1)}(i≤k<j)$$

所以我们可以使用单调栈维护

也就是T2

然后再减去两个串自己的贡献即可

IL ll qury(char *s,int len)
{
	memset(SA,0,sizeof SA);memset(cdy,0,sizeof cdy);
	memset(wzy,0,sizeof wzy);memset(rnk,0,sizeof rnk);
	memset(hei,0,sizeof hei);
	ll sum=0;key=130;get_SA(s,len);get_hei(s,len);
//	for(R int i=1;i<=len;++i) printf("%d%c",SA[i],(i==len ? '\n':' '));
	for(R int i=2;i<=len;++i) hei[i-1]=hei[i];len--;
	for(R int i=1;i<=len;++i) Le[i]=0,Ri[i]=len+1;
	top=0;memset(sta,0,sizeof sta);
	for(R int i=1;i<=len;++i)
	{
		while(top&&hei[sta[top]]>=hei[i]) Ri[sta[top--]]=i;
		sta[++top]=i;
	}
	top=0;memset(sta,0,sizeof sta);
	for(R int i=len;i;--i)
	{
		while(top&&hei[sta[top]]>hei[i]) Le[sta[top--]]=i;
		sta[++top]=i;
	} 
	for(R int i=1;i<=len;++i) sum+=1ll*(Ri[i]-i)*(i-Le[i])*hei[i];
	return sum;
}
int main()
{
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	scanf("%s%s",sx+1,sy+1);
	le=strlen(sx+1);ri=strlen(sy+1);
	for(R int i=1;i<=le;++i) sk[++n]=sx[i];
	sk[++n]='#';for(R int i=1;i<=ri;++i) sk[++n]=sy[i];
//	for(R int i=1;i<=n;++i) cout<<s[i];
	ans=qury(sk,n)-qury(sx,le)-qury(sy,ri);
	printf("%lld\n",ans);		
//	fclose(stdin);
//	fclose(stdout);
    return 0;
}

HEOI 2019 RP++

posted @ 2020-10-01 11:03  tcswuzb  阅读(163)  评论(0编辑  收藏  举报