后缀函数(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\) 次的子串最长长度
一个子串出现 \(k\) 次,即同时作为 \(k\) 个后缀的 \(\operatorname{lcp}\)。
于是考虑维护一个长度 \(k-1\) 的滑动窗口记录最小值,最后取所有区间答案的最大值。

浙公网安备 33010602011771号