后缀函数(SA)学习笔记

前置知识

计数排序、基数排序、倍增、字符串相关定义。

后缀函数(SA)

约定

  • 字符串下标从 \(1\) 开始。
  • 后缀 \(i\) 指的是 \(s_i\sim s_n\) 这一段后缀。
  • 排序/排名均是字典序排序意义下的。

定义

定义一个长度为 \(n\) 的字符串 \(s\) 的后缀数组和 \(sa\) 排名数组 \(rk\)

  • \(sa_i\) 的含义是,排名 \(i\) 的后缀是后缀 \(sa_i\)
  • \(rk_i\) 的含义是,后缀 \(i\) 的排名。

通过定义可知 \(sa_{rk_i}=i\)

用途

很多,比如求height数组 等。

暴力实现

考虑处理出 \(n\) 个前缀排序,字符串比较 \(O(n)\),复杂度 \(O(n^2\log n)\)

优化

考虑倍增。\(sa_k\) 表示每个 \(i\) 只考虑 \([i,i+2^k]\)\(sa\) 数组。

每次合并两个长度 \(2^{k-1}\) 的区间,分别将其作为第一关键字和第二关键字进行排序。

然后根据排序结果处理出 \(rk\)\(sa\) 数组,将 \(rk\) 数组作为处理 \(2^{k+1}\) 的关键字参与下一次排序。

其中因为值域 \(V\le n\) 所以排序使用计数排序(桶排),关键字的处理上本质是基数排序。

每次进行计数排序和基数排序均为 \(O(n)\),共处理 \(O(\log n)\) 次,复杂度 \(O(n\log n)\)

常数优化

其实就是这一部分

  • 优化基排过程
  • 减少循环次数
  • 优化值域

Code

#include<bits/stdc++.h>
#define sd std::
//#define int long long
#define F(i,a,b) for(int i=(a);i<=(b);i++)
#define f(i,a,b) for(int i=(a);i>=(b);i--)
#define MIN(x,y) (x<y?x:y)
#define MAX(x,y) (x>y?x:y)
#define me(x,y) memset(x,y,sizeof x)
#define pii sd pair<int,int>
#define X first
#define Y second
#define Fr(a) for(auto it:a)
int read(){int w=1,c=0;char ch=getchar();for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;for(;ch>='0'&&ch<='9';ch=getchar()) c=(c<<3)+(c<<1)+ch-48;return w*c;}
void printt(int x){if(x>9) printt(x/10);putchar(x%10+48);}
void print(int x){if(x<0) putchar('-'),printt(-x);else printt(x);}
void put(int x){print(x);putchar('\n');}
void printk(int x){print(x);putchar(' ');}
const int N=4e6+10,P=128;
int n,V,p;
char s[N];
int sa[N],id[N],rk[N],ork[N],cnt[N];
void solve()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	V=128;
	F(i,1,n) cnt[rk[i]=s[i]]++;
	F(i,1,V) cnt[i]+=cnt[i-1];
	f(i,n,1) sa[cnt[rk[i]]--]=i;
	for(int w=1;;w<<=1,V=p)
	{
		int cur=0;
		F(i,n-w+1,n) id[++cur]=i;
		F(i,1,n) if(sa[i]>w) id[++cur]=sa[i]-w;
		F(i,1,V) cnt[i]=0;
		F(i,1,n) cnt[rk[i]]++;
		F(i,1,V) cnt[i]+=cnt[i-1];
		f(i,n,1) sa[cnt[rk[id[i]]]--]=id[i];
		p=0;
		F(i,1,n) ork[i]=rk[i];
		F(i,1,n)
		{
			if(ork[sa[i]]==ork[sa[i-1]]&&ork[sa[i]+w]==ork[sa[i-1]+w])
			{
				rk[sa[i]]=p;
			}
			else
			{
				rk[sa[i]]=++p;
			}
		}
		if(p==n) break;
	}
	F(i,1,n) printk(sa[i]);
}
int main()
{
	int T=1;
//	T=read();
	while(T--) solve();
    return 0;
}

最长公共前缀(height 数组)

SA 的一个应用。

定义

\(\operatorname{height}_i=\operatorname{lcp}(sa_i,sa_{i-1})\)

特殊的,\(\operatorname{height}_1=0\)

定理

\(\operatorname{height}_{rk_i}\ge \operatorname{height}_{rk_{i-1}}-1\)

证明懒得背。

实现

利用这个引理暴力求即可。

Code

int k=0;
F(i,1,n)
{
	if(rk[i]==0) continue;
	if(k) --k;
	while(s[i+k]==s[sa[rk[i]-1]+k]) ++k;
	height[rk[i]]=k;
}

应用

  • 两后缀公共前缀

\(\operatorname{lcp}(sa_i,sa_j)=\min\{\operatorname{height}_{i+1\sim j}\}\)

  • 重复出现超过 \(k\) 次的子串最长长度

「USACO2006DEC」Milk Patterns

一个子串出现 \(k\) 次,即同时作为 \(k\) 个后缀的 \(\operatorname{lcp}\)

于是考虑维护一个长度 \(k-1\) 的滑动窗口记录最小值,最后取所有区间答案的最大值。

posted @ 2024-12-16 22:23  _E_M_T  阅读(255)  评论(0)    收藏  举报