后缀数组学习笔记

Introduction

DP 题刷不动,补以前的题发现题题不会,所以来学习新算法充实大脑。

SA 码量小,应用广,思路妙,是难得一见的好字符串算法!

Description

给你一个长度为 n 的字符串,要你对他的所有后缀进行排序,输出每一个后缀的排名。

Solution

后缀数组的经典实现方法为倍增法,主要利用的原理是将一个字符串拆成两个关键字再进行比较,为了后文方便描述,我们先将各个数组的含义做一个定义。

  • sa(i) 表示排名为 i 的后缀的起始位置

  • rk(i) 表示起始位置为 i 的后缀的排名

对于这两个数组,显然存在 sa(rk(i))=rk(sa(i))=i,这就是下面做法中这两个数组互相转化的原理。

我们首先将问题特殊化,考虑每个后缀如果都只有一个字符的话,那么我们可以直接对他排序,得到排名。

这时候如果存在两个字符,我们可以将这两个字符视作两个关键字,那么这时候就是双关键字了 (rk(i),rk(i+1))

这样子的话会存在一个问题,我们是无法对直接对 rk 数组进行排序的,这时候 sa 数组的作用就体现出来了:

sa(x)sa(y) 需要交换,就是当 (rk(sa(x)),rk(sa(x)+1)>(rk(sa(y)),rk(sa(y)+1))

那么这个时候就可以排序了,然后 rk 也可以通过与 sa 的关系求出。

然后我们考虑如果存在长度为 2k,那么也可以通过类似的方法从 2k1rk 数组中得到,这个时候我们的复杂度是 O(nlog2n),不够优秀。

目前的代码是这样的:

 scanf("%s",S+1);n=strlen(S+1);
 for (i=1;i<=n;i++) sa[i]=i;
 for (i=1;i<=n;i++) rk[i]=S[i]-'a'+1;len=0;
 	sort(sa+1,sa+n+1,cmp);
 	for (i=1;i<=n;i++) lst[i]=rk[i];tot=0;
 	for (i=1;i<=n;i++)
 	    if ((lst[sa[i]]==lst[sa[i-1]])&&(lst[sa[i]+len]==lst[sa[i-1]+len])) rk[sa[i]]=tot;
 	    else rk[sa[i]]=++tot;
 for (len=1;len<=n;len<<=1){
    sort(sa+1,sa+n+1,cmp);
 	for (i=1;i<=n;i++) lst[i]=rk[i];tot=0;
 	for (i=1;i<=n;i++)
 	    if ((lst[sa[i]]==lst[sa[i-1]])&&(lst[sa[i]+len]==lst[sa[i-1]+len])) rk[sa[i]]=tot;
 	    else rk[sa[i]]=++tot;
 }
  for (i=1;i<=n;i++) printf("%d ",sa[i]);puts("");

如何优化到 O(nlogn)

我们注意到这道题的值域不是很大(与 n 几乎相等),这时候应该想到计数排序,但是有一个问题就是计数排序是单关键字的,而这里是双关键字的,那么是否可以将计数排序进行拓展?

必然是可以的,这种排序叫做基数排序,复杂度是 O(kn+i=1kwi),其中 k 是关键字的个数,我们会发现在后缀数组需要的排序下,这种排序是要快于快排的。

那么基数排序又应该如何实现呢?

我们首先将数列按照第二关键字排序,然后按第一关键字排序的时候,如果相同保持原来的顺序不动,这就是双关键字时的实现方式,然后每一个关键字排序都用一个计数排序维护就可以了。

那么这个时候的代码是这样的:

void sort(int l,int r){
	tot=0;
	for (i=n;i>n-len;i--) B[++tot]=i;
	for (i=1;i<=n;i++)
	    if (sa[i]>len) B[++tot]=sa[i]-len;
	for (i=1;i<=max(n,250);i++) fre[i]=0;
	for (i=1;i<=n;i++) fre[rk[B[i]]]++;
	for (i=1;i<=max(n,250);i++) fre[i]=fre[i-1]+fre[i];
	for (i=n;i>=1;i--) sa[fre[rk[B[i]]]--]=B[i];
}
int main()
{
 scanf("%s",S+1);n=strlen(S+1);
 for (i=1;i<=n;i++) sa[i]=i;
 for (i=1;i<=n;i++) rk[i]=S[i];len=0;
 sort(1,n);
 for (i=1;i<=n;i++) lst[i]=rk[i];tot=0;
 for (i=1;i<=n;i++)
   if ((lst[sa[i]]==lst[sa[i-1]])&&(lst[sa[i]+len]==lst[sa[i-1]+len])) rk[sa[i]]=tot;
   else rk[sa[i]]=++tot;
 for (len=1;len<=n;len<<=1){
    sort(1,n);
 	for (i=1;i<=n;i++) lst[i]=rk[i];tot=0;
 	for (i=1;i<=n;i++)
 	    if ((lst[sa[i]]==lst[sa[i-1]])&&(lst[sa[i]+len]==lst[sa[i-1]+len])) rk[sa[i]]=tot;
 	    else rk[sa[i]]=++tot;
 }
  for (i=1;i<=n;i++) printf("%d ",sa[i]);puts("");
 return 0;
}

Extra

后缀数组还有一个东西叫做 Height 数组,这个东西又是干什么的呢?

还是先上定义:

  • LCP(i,j) 表示后缀 i 和后缀 j 的最长公共前缀

  • Height(i)=LCP(sa(i),sa(i1))

我们有一个结论:Height(rk(i))Height(rk(i1))1

Proof

咕咕咕

然后暴力做就好了呀。

神奇的习题

P4070 [SDOI2016]生成魔咒

动态加字符,询问本质不同子串个数

如果单次询问,就是求 Height 数组的和,而这里是往后面动态加点,这样子会导致每一个后缀的排名都有可能移动不止一位,显然是不优的,那么我们考虑将字符串倒置,那么这样子每次操作就是往最前面加点。

我们考虑快速算出这个新后缀的排名,好像可以用 set 解决,就是考虑把首位和除首位外的后缀的排名弄成一个二元组,然后 set 就可以找到他第一个符合条件的位置,但是我们知道了新的排名,怎么更新 Height 数组呢?

EricQian 曾经说过,正着不好做,我们就倒着做,但是这道题显然不能倒着做,删点肯定更加麻烦,但是我们可以把所有点先全部加进去,求出 sa,那么在之后的运算中,其实 sa 的值虽然会改变,他是他们的相对大小是一定不会改变的,然后好像就可以做了?

EricQian 又告诉了我,不要局限于 Height 数组的改变,我们考虑加进这个数会产生什么样的贡献,采用正难则反的策略,我们求出他的前驱和后继,那么这个点和这两个点的 LCPmax 就是重复出现过的,那么总数减去这个多余的就好了。

for (now=1;now<=n;now++){
    G.insert(rk[n-now+1]);
    it=G.find(rk[n-now+1]);up=-1;
    if (it!=G.begin()){
         it--;
         int lft=*it;
         up=max(up,find(lft+1,rk[n-now+1]));
    }it=G.find(rk[n-now+1]);it++;
    if (it!=G.end()){
    	int rit=*it;
    	up=max(up,find(rk[n-now+1]+1,rit));
	}
	if (up==-1) ans=1;
	else ans+=now-up;
	printf("%lld\n",ans);
}

P1117 [NOI2016] 优秀的拆分

给定一个字符串 S,问每个子串拆分成 A+A+B+B 形式的方案的总和

一道题耗了我 6 个小时,最后还是只能看题解,我只能说为什么这道题是紫题?

我们假设这次做的长度为 len,考虑到 len 的位置恰好是第一的长度为 len 的子串的结尾,那么我们考虑从这个点开始每 len 个设一个关键点。

而这个时候就是本题最难的部分了,证明不难,但是想到这个结论十分的困难啊!

我们考虑计算相邻两个关键点的 LCPLCS,下面一张图,会解释所有:
q4vTBQ.png

但是不是只要理解了这张图就好了,实现过程中还是有无数的细节,LCP 自然是简单的,但是这个 LCS 就会产生一个细节,还是看图:

q5Ax00.png
但是给了 95 分暴力分是好的

总共用了 10 个小时以上在这道【】【】题上

Code
#include<bits/stdc++.h>
using namespace std;
inline long long read()
{
  long long x=0,f=1;char ch=getchar();
  while(!isdigit(ch)&&ch!='-')ch=getchar();
  if(ch=='-')f=-1,ch=getchar();
  while(isdigit(ch))x=(x<<1)+(x<<3)+ch-'0',ch=getchar();
  return x*f;
}
int st1[1001010][22],st2[1001010][22];
char S[1001010];
long long G1,G2,G,st,ed,tot,rk1[1001010],rk2[1001010],k,lg[1001010],lft,rit,tn,ans,mn,j,lst[1001010],n,i,pre[1001010],suf[1001010],B[1001010],fre[1001010],sa[1001010],rk[1001010],len,K,H[1001010],a[1001010];
void sort(int l,int r){tot=0;
	for (int i=n;i>n-len;i--) B[++tot]=i;
	for (i=1;i<=n;i++)
	    if (sa[i]>len) B[++tot]=sa[i]-len;
	for (i=1;i<=n;i++) fre[rk[B[i]]]++;
	for (i=1;i<=max(n,(long long)444);i++) fre[i]=fre[i-1]+fre[i];
	for (i=n;i>=1;i--) sa[fre[rk[B[i]]]--]=B[i];
	for (i=0;i<=max(n,(long long)444);i++) fre[i]=0;
}void Remake(){
	for (i=1;i<=n;i++) lst[i]=rk[i];tot=0;
	for (i=1;i<=n;i++)
	    if ((lst[sa[i]]==lst[sa[i-1]])&&((lst[sa[i]+len]==lst[sa[i-1]+len]))) rk[sa[i]]=tot;
	    else rk[sa[i]]=++tot;
}
int find1(int l,int r){int len=lg[r-l+1];return min(st1[l][len],st1[r-(1<<len)+1][len]);}
int find2(int l,int r){int len=lg[r-l+1];return min(st2[l][len],st2[r-(1<<len)+1][len]);}
void solve(int opt){n=tn;
    memset(lst,0,sizeof(lst));//memset(rk,0,sizeof(rk));memset(sa,0,sizeof(sa));
	for (i=1;i<=n;i++) sa[i]=i,rk[i]=S[i];
	len=0;
	sort(1,n);Remake();
    for (len=1;len<=n;len<<=1) sort(1,n),Remake();
	K=0;S[n+1]='&';
    for (i=1;i<=n;i++){
      	if (rk[i]==0) continue;if (K) K--;
	    while ((S[sa[rk[i]]+K]==S[sa[rk[i]-1]+K])&&(K<=n)) K++;H[rk[i]]=K;
        }
    if (opt==1){
    	for (i=1;i<=n;i++) st1[i][0]=H[i],rk1[i]=rk[i];
    	for (j=1;j<=lg[n];j++)
    	    for (i=1;i<=n-(1<<j)+1;i++)
    	         st1[i][j]=min(st1[i][j-1],st1[i+(1<<(j-1))][j-1]);
    	return ;
	}
	for (i=1;i<=n;i++) st2[i][0]=H[i],rk2[i]=rk[i];
	for (j=1;j<=lg[n];j++)
	    for (i=1;i<=n-(1<<j)+1;i++)
	         st2[i][j]=min(st2[i][j-1],st2[i+(1<<(j-1))][j-1]);
}
void Main(){
	scanf("%s",S+1);n=strlen(S+1);tn=n;
	for (i=2;i<=n;i++) lg[i]=lg[i>>1]+1;
	solve(1);
	for (i=1;i<=n/2;i++) swap(S[i],S[n-i+1]);solve(-1);
	for (len=1;len<=n;len++)
	    for (st=len;st+len<=n;st+=len){
	   	    ed=st+len;lft=rk1[st];rit=rk1[ed];if (lft>rit) swap(lft,rit);G1=find1(lft+1,rit);
	   	    lft=rk2[n-st+2];rit=rk2[n-ed+2];if (lft>rit) swap(lft,rit);G2=find2(lft+1,rit);
	   	    G1=min(G1,len);G2=min(G2,len-1);
	   	    if (G1+G2>=len){
	   	    	G=G1+G2-len+1;
				   suf[st-G2]++;suf[st-G2+G]--;pre[ed+G1-G]++;pre[ed+G1]--;
			   }
	   }
	for (i=1;i<=n;i++) pre[i]=pre[i-1]+pre[i],suf[i]=suf[i-1]+suf[i];
	ans=0;  
	for (i=2;i<n;i++) ans=ans+pre[i]*suf[i+1];
	for (i=0;i<=n+1;i++) pre[i]=suf[i]=0;
	printf("%lld\n",ans);
	return; 
}
int main()
{
	for (int Testing=read();Testing;Testing--) Main();
return 0;
}
posted @   OIer_Albedo  阅读(57)  评论(2编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示