SA

这应该是第一节能够课上听懂的知识了

算法原理

SA 算法,著名的后缀数组

以下只讨论 O(nlogn) 的倍增构造

目标:求出 sai 表示后缀字典序排名为 i 的后缀的起始位置。

ababa --> 53142

算法核心:倍增法。

我们考虑先求出仅考虑 [i,i+2k1] 的串的排名,超出 n 的位置全部设为 ​。不妨记子串 [i,i+2k1]f(i,k)。同时记录 g(i,k)f(i,k)k 这一维度的排名。(去重后的排名)。

注意到 f(i,k)=f(i,k1)+f(i+2k1,k1)

因此我们考虑一个很 naive 的算法。

对于 kk+1,我们本质上只需要一个双关键字排序:第一关键字是 g(i,k),第二关键字是 g(i+2k,k)

然后排序后重新标记 g(i,k+1)

这个算法复杂度是 O(nlog2n) 的,我们发现瓶颈在于 sort

不难发现我们只需一个双关键字桶排序即可优化到 O(nlogn)

双关键字桶排序。

我们先对第一关键字建立桶。记录 cx 为第一关键字 x 的数的总数,这个可以先计数再前缀和。

然后将所有数字按照第二关键字排序,倒序加入即可。

void build(int n,int cnt){
	for(int i=1;i<=cnt;i++)c[i]=0;
	for(int i=1;i<=n;i++)c[a[i]]++,tmp[i]=a[b[i]];//tmp[i]减小缓存,很重要
	for(int i=1;i<=cnt;i++)c[i]+=c[i-1];
} 
void sa_sort(int n){
	for(int i=n;i;--i){//b_i:第二关键字排名为i的数的位置(也即g(i+2^k,k)的排名),tmp[i]:b_i所对应的数所对应的桶编号
		sa[c[tmp[i]]]=b[i];//sa[i]:排名为i的位置
		c[tmp[i]]--;
	}
}

然后我们考虑建立 SA。

void build_sa(char s[],int n){
	for(int i=1;i<=n;i++)a[i]=s[i],b[i]=i;
	int siz=512;build(n,siz);sa_sort(n);//第一轮排序初始化
	for(int len=1;len<n;len<<=1){//len=2^k
		int pos=0;
		for(int i=n-len+1;i<=n;i++)b[++pos]=i;//标记特殊的,也即g(i+2^k,k)为无穷小
		for(int i=1;i<=n;i++)if(sa[i]>len){
			b[++pos]=sa[i]-len;
		}//对于剩下的数,已知的数进行排序。
		build(n,siz);
		sa_sort(n);
		for(int i=1;i<=n;i++)d[i]=a[i];//copy,重标号第一关键字
		a[sa[1]]=1;pos=1;
		for(int i=2;i<=n;i++){
			if(d[sa[i]]!=d[sa[i-1]]||d[sa[i]+len]!=d[sa[i-1]+len])++pos;
			a[sa[i]]=pos;
		}
		siz=pos;
		if(len!=1&&pos==n)break;//剪枝,相当重要,已经排好
	}
}

算法思想应用基础题-CF1654F

rki[i,n] 在所有后缀的排名;

HEIGHT

借助 SA 数组,我们可以求出一个数组 h。限制 h1=0

hi=|LCP([sai1,n],[sai,n])|

有引理:hrkihrki11。证明留作习题,答案略,读者自证不难(tip:假设法+画图)。

void build_h(int n){
	for(int i=1;i<=n;i++){
		h[rk[i]]=max(0,h[rk[i-1]]-1);
		while(i+h[rk[i]]<=n&&sa[rk[i]-1]+h[rk[i]]<=n&&s[sa[rk[i]-1]+h[rk[i]]]==s[i+h[rk[i]]])++h[rk[i]];
	}
}

复杂度显然线性。

性质:

  1. hrkihrki11
  2. |LCP(sai,saj)|=mink[i+1,j]hk

所以可以通过 st 快速求两个后缀的 LCP 长度。

void get_lcp(int lcp[21][N]){
	memset(h,0,sizeof h);memset(rk,0,sizeof rk);memset(sa,0,sizeof sa);memset(a,0,sizeof a);memset(b,0,sizeof b);
	memset(d,0,sizeof d);
	build_sa(s,n);build_h(n);
	for(int i=1;i<=n;i++)lcp[0][i]=h[i];
	for(int j=1;(1<<j)<=n;j++){
		for(int i=1;i+(1<<j)-1<=n;i++)lcp[j][i]=min(lcp[j-1][i],lcp[j-1][i+(1<<j-1)]);
	}
}
int getlcp(int l,int r){
	l=rklcp[l],r=rklcp[r];//important
	if(l>r)swap(l,r);++l;
	int k=lg[r-l+1];
	return min(lcp[k][l],lcp[k][r-(1<<k)+1]);
}

基本应用

比较串 [l1,r1],[l2,r2]

先求解 LCP([l1,n],[l2,n])。如果长度大于等于两个串的长度的较小值,则直接比较长度。

否则等价于找到了第一个失配位置。直接比较

复杂度 O(1) 单次。

字符串匹配

在串 T 中找到所有串 S 的出现位置。多次操作。

直接后缀数组上二分,暴力比较即可。复杂度 O(|S|log|T|)

二分第一个匹配位置和第一个失配位置即可。

本质不同子串数量

n(n+1)2hi。这是一个去重操作。

最小表示法

直接把串复制两倍,后缀排序,找到第一个 sain 即可。

出现 k 次子串最大长度

本问题形式化描述是求出最大的 len,满足存在 k[li,ri],使得 i,rili+1=len,[li,ri]=[l1,r1]

问题答案显然是 maxi[1,nk+2]{minj[i,i+k2]hj}

滑动窗口即可。

最长公共子串

求串 S,T 的最长公共子串。

建立串 G=S+&+T,求其 h

vaild(i)=([sai[1,|S|]]×[sai1[|S|+1,|G|]])+([sai1[1,|S|]]×[sai[|S|+1,|G|]]),也即 sai1,sai 的起步位置分属 S,T

ans=maxhi×vaild(i)

最长回文子串

T=Rev(S) 即可。

i,j|LCP([i,n],[j,n])|

单调栈求出 li,ri 满足:

i,j[li,i1],hj>hi,k[i+1,ri],hkhi。注意等号。

i=1n(rii+1)×(ili+1)×hi​ 即为答案。证明显然。

本质上是求 l[li1,i1],r[i,ri] 的答案。全部都是 |LCP([l,n],[r,n])|=hi

形如 AA 循环串处理

基本策略

下面我们简记 lcp(i,j)=|LCP([i,n],[j,n])|,lcs(i,j)=|LCS([1,i],[1,j])|

注意到如果存在一个串 AA,则它一定经过某两个点 k|A|,(k+1)|A|

所以如果我们枚举 k=|A|,然后选取 k,2k,3k 为特殊点。

那么如果存在一个串 AA[kti,k(t+2)i] 的位置。

则必然有 lcp(kt,k(t+1))ki,lcs(kt,k(t+1))i+1。这是因为 [kti,kt]=[k(t+1)i,k(t+1)],[kt,k(t+1)i1]=[k(t+1),k(t+2)i1]

因此存在一个串 AA 的对称轴在 [i,i+1],i[kt,kt+k1] 之间的充要条件是 lcp(kt,kt+t)+lcs(kt,kt+t)k+1

求解 fi 为以 i 为开头的 AA 串个数。

我们可以得到影响区间,令 l=kt,r=kt+k,也即这个 AA 串的起始点就是在 [max(lk+1,rlcp(l,r)k+1),min(l+lcs(l,r)k,l)]

由此,差分计算即可。

求解重复次数最多连续重复子串

本质上还是上面那个问题,最多出现次数就是 (lcs(l,r)+lcp(l,r)+k1)k

综合应用

结合并查集

一个最基本的应用是对于所有的 d[0,n1],统计有多少对长度为 d 的子串是相同的。

这个可以使用后缀数组进行操作。

最naive的想法是把后缀数组进行分段,保留若干极长连续段 (l1,r1)(lm,rm),满足 i[1,m],mink[li+1,ri]hkd

答案显然是 i=1m(rili+12)

这里我们完全是对 h 数组进行操作,已经和原数组没啥关系了

而我们需要对于每个 d 求解答案,需要用到并查集。

利用并查集倒序 hi 合并每一段即可。

int find(int x){
	return x==f[x]?x:f[x]=find(f[x]);
}
int get(int a){
	int len=rpos[a]-lpos[a]+1;
	return len*(len-1)/2;
}
void merge(int a,int b){
	a=find(a),b=find(b);
	if(a==b)return ;
	nowcnt-=get(a)+get(b);
	lpos[a]=min(lpos[a],lpos[b]);
	rpos[a]=max(rpos[a],rpos[b]);
	f[b]=a;nowcnt+=get(a);
}
//solve 函数中
    for(int i=1;i<=n;i++)f[i]=i,lpos[i]=rpos[i]=i;
	for(int i=2;i<=n;i++)posval[h[i]].push_back(i);
	for(int p=n-1;p>=0;--p){
		for(auto cut:posval[p]){
			merge(cut,cut-1);
		}
		anscnt[p]=nowcnt;
	}

https://www.luogu.com.cn/problem/CF1923F

https://www.luogu.com.cn/problem/CF524F

posted @   spdarkle  阅读(16)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
历史上的今天:
2023-02-02 树链剖分习题集
2023-02-02 树链剖分入门
点击右上角即可分享
微信分享提示