SA SAM
SA
即一个字符串后缀的排名数组,\(sa_i\)表示所有后缀中,排名为 \(i\) 的后缀的起始位置,\(rk_i\)表示以 \(i\) 为开头的后缀的排名
考虑倍增求,假设当前的长度为 \(w\) 的串已经拍好序,考虑用当前的信息给长度为 \(2 \times w\) 的后缀排序
那么在比较两个字符串时就可以用当前的排名比较,长度为 \(w\) 前半部分的排名为第一关键字,后半部分的排名为第二关键字,我们用桶排先对第二关键字排序,再对第一关键字排序,这样如果两个串的前半部分不同,能直接比较出大小,相同的就会保持原顺序不变,即按照第二关键字从小到大排好序(这是因为桶排为稳定的排序,即相同的值会保持原来的相对顺序)
最后的 \(sa,rk\) 一定两两不同,但在过程中可能会相同
一些常数优化
-
如果当前值域已经为 \(n\),说明已经排好序了,就可以直接跳出
-
发现对第二关键字的排序实际上就是将第二关键字不满的全的放到前面,剩下的由于在上一层以及那个排好序,保持原顺序不变即可
【模板】后缀排序
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+100;
string s;
int n,V,cnt[N],sa[N],rk[N],Rk[N],Sa[N];
signed main() {
cin>>s;n=s.size();s=" "+s;V=200;
for(int i=1;i<=n;i++) cnt[rk[i]=s[i]]++;
for(int i=1;i<=V;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) sa[cnt[s[i]]--]=i;
int p=0;
for(int i=1;i<=n;i++) {
if(s[sa[i]]==s[sa[i-1]]) ;
else p++;
rk[sa[i]]=p;
}
int w;
for(w=1;w<n;w<<=1) {
if(V==n) break;
for(int i=1;i<=V;i++) cnt[i]=0;
for(int i=1;i<=n;i++) Sa[i]=sa[i];
for(int i=1;i<=n;i++) cnt[rk[Sa[i]+w]]++;
for(int i=1;i<=V;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) Sa[cnt[rk[sa[i]+w]]--]=sa[i];
for(int i=1;i<=n;i++) sa[i]=Sa[i];
for(int i=1;i<=V;i++) cnt[i]=0;
for(int i=1;i<=n;i++) Sa[i]=sa[i];
for(int i=1;i<=n;i++) cnt[rk[Sa[i]]]++;
for(int i=1;i<=V;i++) cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--) Sa[cnt[rk[sa[i]]]--]=sa[i];
for(int i=1;i<=n;i++) sa[i]=Sa[i],Rk[i]=rk[i];
int p=0;
for(int i=1;i<=n;i++) {
if(Rk[sa[i]]==Rk[sa[i-1]]&&Rk[sa[i]+w]==Rk[sa[i-1]+w]) ;
else p++;
rk[sa[i]]=p;
}
V=p;
}
for(int i=1;i<=n;i++) cout<<sa[i]<<' ';
return 0;
}
SAM
就是一个原串的所有后缀的一个高度压缩的 \(AC\) 自动机,用 \(endpos\) 即结束位置的集合来划分每一个节点,每个节点表示结束位置的集合为 \(endpos\) 的所有子串
在 \(SAM\) 中用 \(fa_i\) 来表示和节点 \(i\) 的\(endpos\) 不同的最长后缀,那么就构成一棵树,每个节点的 \(endpos\) 集合都真包含其子树内所有节点的 \(endpos\) 集合,且每个节点的最大长度都小于其子树内的所有节点的最大长度,还有一个转移数组 \(ch_{u,i}\) 表示节点 \(u\) 所代表的所有子串中走一条 \(i\) 的边会到达的子树内的节点编号
至于 \(endpos\) 集合,用该节点的最大长度 \(len\) 表示
考虑增量法构造 \(SAM\) ,在原有的字符串的后面接上一个字符 \(c\) ,那么所有的以最后一个字符为结尾的字符串的 \(endpos\) 都会改变,那些在原串中没有的后缀都会用当前节点 \(nw\) 表示,而那些在原串中存在的后缀,只需要更新最大后缀的节点即可
整个过程就是从 \(las\) 节点开始跳 \(fa\) ,直到跳到一个有 \(c\) 的出边的节点 \(p\),\(ch_{p,c}=q\) ,那么考虑 \(q\) 的所有子串中 \(endpos\) 集合是否会全部改变:
-
如果\(len_p+1=len_q\) ,那么就代表这个节点可以表示最长后缀,直接让 \(fa_{nw}=q\) 即可
-
如果\(len_{p+1} \neq len_q\),这个节点中长度小于等于这个最长后缀 \(t\) 的串的 \(endpos\) 会改变,而那些比 \(t\) 的长度大的串的 \(endpos\) 不会改变,所以就要将 \(q\) 点拆出一个新节点\(nq\) 来表示这个最长后缀,转移边就直接继承,由于 \(nq\) 所代表的 \(endpos\) 集合相对于 \(q\) 节点多了一个 \(|s|+1\) ,根据含义,将 \(fa_q\) 指向 \(nq\) 而\(fa_nq\) 指向\(q\)原来的\(fa\)
在跳祖先的时候要连接转移边
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
char s[N];int las=1;
struct String{
int ch[N][26],fa[N],len[N],tim[N],cnt=1,deg[N];
void Ins(char c) {
int nw=++cnt;
len[nw]=len[las]+1;
int p=las;
for(;p;p=fa[p]) {
if(ch[p][c-'a']!=0) break;
ch[p][c-'a']=nw;
}
if(p!=0) {
int q=ch[p][c-'a'];
if(len[p]+1==len[q]) fa[nw]=q,deg[q]++;
else {
int nq=++cnt;
len[nq]=len[p]+1;
fa[nq]=fa[q];
fa[q]=fa[nw]=nq;
deg[nq]+=2;
for(int i=0;i<26;i++) ch[nq][i]=ch[q][i];
int op=p;
while(op) {
if(ch[op][c-'a']==q) ch[op][c-'a']=nq;
op=fa[op];
}
}
}
else fa[nw]=1,deg[1]++;
ch[las][c-'a']=nw;
tim[nw]++;las=nw;
}
void Query() {
queue<int>q;long long ans=0;
for(int i=1;i<=cnt;i++) if(deg[i]==0) q.push(i);
while(q.size()) {
int u=q.front();q.pop();
// cout<<u<<":"<<tim[u]<<"*"<<len[u]<<'\n';
int v=fa[u];tim[v]+=tim[u];
deg[v]--;
if(tim[u]!=1)
ans=max(ans,1ll*tim[u]*len[u]);
if(deg[v]==0) q.push(v);
}
cout<<ans<<'\n';
return;
}
}SAM;
signed main() {
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=1;i<=n;i++) SAM.Ins(s[i]);
SAM.Query();
return 0;
}
posted on 2025-07-20 20:45 Pearblossom 阅读(19) 评论(0) 收藏 举报
浙公网安备 33010602011771号