SA

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

算法原理

SA 算法,著名的后缀数组

以下只讨论 \(O(n\log n)\) 的倍增构造

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

ababa --> 53142

算法核心:倍增法。

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

注意到 \(f(i,k)=f(i,k-1)+f(i+2^{k-1},k-1)\)

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

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

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

这个算法复杂度是 \(O(n\log^2 n)\) 的,我们发现瓶颈在于 sort

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

双关键字桶排序。

我们先对第一关键字建立桶。记录 \(c_x\) 为第一关键字 \(\le 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

\(rk_i\)\([i,n]\) 在所有后缀的排名;

HEIGHT

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

\(h_i=|LCP([sa_{i-1},n],[sa_i,n])|\)

有引理:\(h_{rk_i}\ge h_{rk_{i-1}}-1\)。证明留作习题,答案略,读者自证不难(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. \(h_{rk_i}\ge h_{rk_{i-1}}-1\)
  2. \(|LCP(sa_i,sa_j)|=\min_{k\in [i+1,j]} h_{k}\)

所以可以通过 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]);
}

基本应用

比较串 \([l_1,r_1],[l_2,r_2]\)

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

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

复杂度 \(O(1)\) 单次。

字符串匹配

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

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

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

本质不同子串数量

\(\frac{n(n+1)}{2}-\sum h_i\)。这是一个去重操作。

最小表示法

直接把串复制两倍,后缀排序,找到第一个 \(sa_i\le n\) 即可。

出现 \(k\) 次子串最大长度

本问题形式化描述是求出最大的 \(len\),满足存在 \(k\)\([l_i,r_i]\),使得 \(\forall i,r_i-l_i+1=len,[l_i,r_i]=[l_1,r_1]\)

问题答案显然是 \(\max_{i\in[1,n-k+2]}\lbrace \min_{j\in [i,i+k-2]}h_j\rbrace\)

滑动窗口即可。

最长公共子串

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

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

\(vaild(i)=([sa_i\in[1,|S|]]\times [sa_{i-1}\in [|S|+1,|G|]])+([sa_{i-1}\in[1,|S|]]\times [sa_{i}\in [|S|+1,|G|]])\),也即 \(sa_{i-1},sa_i\) 的起步位置分属 \(S,T\)

\(ans=\max h_i\times vaild(i)\)

最长回文子串

\(T=Rev(S)\) 即可。

\(\sum_{i,j} |LCP([i,n],[j,n])|\)

单调栈求出 \(l_i,r_i\) 满足:

\(\forall i,\forall j\in[l_i,i-1],h_j> h_i,\forall k\in[i+1,r_i],h_k\ge h_i\)。注意等号。

\(\sum_{i=1}^n (r_i-i+1)\times (i-l_i+1)\times h_i\)​ 即为答案。证明显然。

本质上是求 \(l\in [l_i-1,i-1],r\in [i,r_i]\) 的答案。全部都是 \(|LCP([l,n],[r,n])|=h_i\)

形如 \(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\)\([kt-i,k(t+2)-i]\) 的位置。

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

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

求解 \(f_i\) 为以 \(i\) 为开头的 \(AA\) 串个数。

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

由此,差分计算即可。

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

本质上还是上面那个问题,最多出现次数就是 \(\lfloor\frac{(lcs(l,r)+lcp(l,r)+k-1)}{k}\rfloor\)

综合应用

结合并查集

一个最基本的应用是对于所有的 \(d\in[0,n-1]\),统计有多少对长度为 \(d\) 的子串是相同的。

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

最naive的想法是把后缀数组进行分段,保留若干极长连续段 \((l_1,r_1)\sim (l_m,r_m)\),满足 \(\forall i\in[1,m],\min_{k\in[l_i+1,r_i]}h_k\ge d\)

答案显然是 \(\sum_{i=1}^m{r_i-l_i+1\choose 2}\)

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

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

利用并查集倒序 \(h_i\) 合并每一段即可。

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 @ 2025-02-02 17:12  spdarkle  阅读(53)  评论(1)    收藏  举报