我学习的视频连接:【SDUACM-暑期专题div1】后缀自动机SAM_哔哩哔哩_bilibili (这位大佬讲的非常nice,墙裂推荐)
【一】板子(就是从视频上面抄的,配上自己写的注释)
struct SAM{ //(其中N是字符串最大长度,代码注释掉表示不是很常用,但也可能用到)
int tot=1, last=1; //最大点编号,上一个点编号
int len[N<<1], fa[N<<1], sz[N<<1];
// vector<int> g[N<<1]; //需要建立后缀树时要用到(有时候要用)
//int f[N<<1]; //记录子树的sz之和(不常用)
int to[N<<1][26];
// 为当前的SAM增加一个字符c
void extend(int c){
int p, cur = ++tot; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
sz[cur] = 1; //初始sz值都是1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=fa[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) fa[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) fa[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++tot; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
// to[cl] = to[q]; //复制所有原结点的trans连接
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q]; //后缀重连
fa[cur] = fa[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = fa[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
// //建立后缀树(有时候要用)
// void build(){
// FOR(i,2,tot) g[fa[i]].push_back(i);
// }
//dfs计算size
void dfs(int u){
for(int v:g[u]) dfs(v), sz[u]+=sz[v];
}
// //dfs2计算所能到达的点的点权和(不常用)
// int dfs2(int u){
// if(f[u]) return f[u]; //计算过,直接返回
// f[u] = sz[u];
// for(int i=0; i<26; i++){
// int v = to[u][i]; //u可以to的26个点
// if(v) f[u] += dfs2(v);
// }
// return f[u];
// }
// //当前在sam的u点,要求打印子树第k小(不常用)
// void print(int u,int k){
// if(k>f[u]) {cout<<"-1"; return;} //没有第k小,结束
// if(k<=sz[u]) return;
// k -= sz[u];
// for(int i=0; i<26; i++){
// int v = to[u][i];
// if(k>f[v]) k-=f[v];
// else{
// cout<<(char)('a'+i);
// print(v,k);
// return;
// }
// }
// }
} sam;
补充:另一套板子,不用struct的简便写法。
//SAM
int tot=1, las=1;
int len[N], fa[N], sz[N];
int to[N][26];
inline void ins(int c){
int cur = ++tot, p = las; las = cur;
len[cur] = len[p]+1; sz[cur] = 1;
for(; p && !to[p][c]; p=fa[p])
to[p][c] = cur;
if(!p) fa[cur] = 1;
else {
int q = to[p][c];
if(len[q] == len[p]+1) fa[cur] = q;
else{
int cl= ++tot; len[cl] = len[p]+1;
// to[cl] = to[q];
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q];
fa[q] = fa[cur] = cl;
for(; p && to[p][c]==q; p=fa[p]) to[p][c] = cl;
}
}
}
【二】一些例题
1.不同子串个数 - 洛谷 (就是视频part3的第一题)
#include <bits/stdc++.h>
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
using namespace std;
struct SAM{
int size, last; //最大点编号,上一个点编号
vector<int> len, link; //maxlen,后缀链接
vector<vector<int>> to; //trans数组,记录加一个字符后的转移
// 字符串长度*2(必须是两倍大小),字符集大小(一般是26或52)
SAM(int strLen, int chSize) : size(1), last(1) {
len.resize(strLen, 0);
link.resize(strLen, 0);
to.resize(strLen, vector<int>(chSize, 0));
}
// 为当前的SAM增加一个字符c
void extend(int c){
int p, cur = ++size; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=link[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) link[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) link[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++size; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
to[cl] = to[q]; //复制所有原结点的trans连接
link[cl] = link[q]; //后缀重连
link[cur] = link[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = link[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
};
SAM sam((int)2e5+10, 26); //特别注意,后缀自动机的第一位是两倍的字符串长度!!
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int n; cin>>n;
string s; cin>>s; for(char ch:s) sam.extend(ch-'a');
long long ans = 0;
FOR(i,1,sam.size){
ans += sam.len[i]-sam.len[sam.link[i]];
}
cout<<ans<<'\n';
}
补充:第二种做法,用到SAM一个性质:
在后缀自动机上从根节点开始的每一条路径都是一个子串。
所以直接在SAM上跑DP就好了,其实就是一遍DFS,因为SAM是一个DAG。
#include <bits/stdc++.h>
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
using namespace std;
struct SAM{
int size, last; //最大点编号,上一个点编号
vector<int> len, link; //maxlen,后缀链接
vector<vector<int>> to; //trans数组,记录加一个字符后的转移
vector<long long> ans; //ans数组,记录从i开始往后的点中不同子串数量
// 字符串长度*2(必须是两倍大小),字符集大小(一般是26或52)
SAM(int strLen, int chSize) : size(1), last(1) {
len.resize(strLen, 0);
link.resize(strLen, 0);
ans.resize(strLen, 0);
to.resize(strLen, vector<int>(chSize, 0));
}
// 为当前的SAM增加一个字符c
void extend(int c){
int p, cur = ++size; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=link[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) link[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) link[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++size; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
to[cl] = to[q]; //复制所有原结点的trans连接
link[cl] = link[q]; //后缀重连
link[cur] = link[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = link[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
//dfs树形dp,求从i状态往后的状态的点中不同子串个数
long long dfs(int x){
if(ans[x]) return ans[x];
for(int i=0; i<26; i++) if(to[x][i]) ans[x]+=dfs(to[x][i])+1; //to[x][i]后面的路径数量+当前这条路径
return ans[x];
}
};
SAM sam((int)2e5+10, 26); //特别注意,后缀自动机的第一位是两倍的字符串长度!!
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int n; cin>>n;
string s; cin>>s; for(char ch:s) sam.extend(ch-'a');
cout<<sam.dfs(1); //1是原点,统计全部答案
}
2.【模板】最小表示法 - 洛谷 (自己找的习题,用SAM解很方便)
这里用到SAM一个非常重要的性质:用字符串s构建出的SAM能匹配s的所有子串,且不是s的子串的串一定不能在SAM中匹配。
本题的“字符集”很大(因为是int,不是字符),所以用map来构建to数组。
由于可以把最前面的数字放到后面,所以用2*s的方法模拟环形,以此来构造SAM。
#include <bits/stdc++.h>
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
using namespace std;
const int N = 6e6+5;
int n, a[N<<1];
// 为当前的SAM增加一个字符c
int len[N<<1], link[N<<1];
map<int,int> to[N<<1];
int sz=1, last=1;
void extend(int c){
int p, cur = ++sz; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=link[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) link[cur] = 1; //如果一直到原点,都是没有连过,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) link[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++sz; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
to[cl] = to[q]; //复制所有原结点的trans连接
link[cl] = link[q]; //后缀重连
link[cur] = link[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = link[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin>>n; FOR(i,1,n) cin>>a[i];
FOR(i,1,2) FOR(j,1,n) extend(a[j]);
int p=1; //当前结点
FOR(i,1,n){
auto u = to[p].begin();
p = (*u).second;
cout<<(*u).first<<' ';
}
}
3.【模板】后缀自动机 (SAM) - 洛谷 (说是板子题,但还是有点难度的)
用到SAM的一个性质:每个结点都表示一个状态,同一状态中的字符串的endpos集合都相等,而endpos集合的大小就是串的出现次数,而同一个集合中,子串出现次数*子串长度最大的肯定是看长度最大那个(因为出现次数相同)。所以求出每个点endpos集合大小即可。
怎么求呢?代码里的sz数组就记录每个点endpos集合大小,具体为什么我暂时不知道。
#include <bits/stdc++.h>
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
using namespace std;
const int N = 2e6+100;
//SAM
int tot=1, las=1;
int len[N], fa[N], sz[N];
int to[N][26];
inline void ins(int c){
int cur = ++tot, p = las; las = cur;
len[cur] = len[p]+1; sz[cur] = 1;
for(; p && !to[p][c]; p=fa[p])
to[p][c] = cur;
if(!p) fa[cur] = 1;
else {
int q = to[p][c];
if(len[q] == len[p]+1) fa[cur] = q;
else{
int cl= ++tot; len[cl] = len[p]+1;
// to[cl] = to[q];
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q];
fa[q] = fa[cur] = cl;
for(; p && to[p][c]==q; p=fa[p]) to[p][c] = cl;
}
}
}
vector<int> g[N];
long long ans = 0;
void dfs(int u){
for(int v:g[u]) dfs(v), sz[u]+=sz[v];
if(sz[u] > 1) ans = max(ans,1ll*sz[u]*len[u]);
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
string s; cin>>s; for(char ch:s) ins(ch-'a');
for(int i=2; i<=tot; i++) g[fa[i]].push_back(i);
dfs(1);
cout<<ans;
}
当然,也可以用拓扑排序+树形dp求子树大小(见下)
#include <bits/stdc++.h>
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define ROF(i, a, b) for (int i = (a); i >= (b); i--)
using namespace std;
const int N = 2e6+100;
//SAM
int tot=1, las=1;
int len[N], fa[N], sz[N];
int to[N][26];
inline void ins(int c){
int cur = ++tot, p = las; las = cur;
len[cur] = len[p]+1; sz[cur] = 1;
for(; p && !to[p][c]; p=fa[p])
to[p][c] = cur;
if(!p) fa[cur] = 1;
else {
int q = to[p][c];
if(len[q] == len[p]+1) fa[cur] = q;
else{
int cl= ++tot; len[cl] = len[p]+1;
// to[cl] = to[q];
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q];
fa[q] = fa[cur] = cl;
for(; p && to[p][c]==q; p=fa[p]) to[p][c] = cl;
}
}
}
vector<int> g[N];
int in[N];
long long ans = 0;
queue<int> q;
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
string s; cin>>s; for(char ch:s) ins(ch-'a');
for(int i=2; i<=tot; i++) g[i].push_back(fa[i]), in[fa[i]]++;
FOR(i,1,tot) if(in[i]==0) q.push(i);
while(q.size()){
int u=q.front(); q.pop();
if(sz[u]>1) ans = max(ans, 1LL*len[u]*sz[u]);
sz[fa[u]] += sz[u]; //自底向上dp,求子树大小
if(--in[fa[u]]==0) q.push(fa[u]);
}
cout<<ans;
}
4.LCS - Longest Common Substring - 洛谷
题意:输入两个字符串,输出它们的最长公共子串长度,若不存在公共子串则输出 00。其中字符串长度不超过 2.5e5。
思路:还记得ac自动机吗?如果只把一个字符串放入ac自动机中,那就是建立一条trie树的链,并且给他做出fail指针用来失配跳转(此时ac自动机的作用类似于kmp,是单模匹配)。而sam是什么?某种程度上,我们可以把它理解为:把一个字符串的所有子串都放在一个trie上,并进行缩点加速,还做出了类似ac自动机的fail指针。那么我们就可以给其中一个字符串建sam,另一个字符串在该sam上进行匹配。具体怎么匹配呢?暴力枚举另一个字符串的所有字符,从这个字符开始的后缀在sam上跑,len(t)个匹配结果取最大即可。
具体细节见代码:
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
// #define int long long
const int N = 2.5e5+10;
char s[N], t[N];
int n,m;
struct SAM{ //(其中N是字符串最大长度,代码注释掉表示不是很常用,但也可能用到)
int tot=1, last=1; //最大点编号,上一个点编号
int len[N<<1], fa[N<<1], sz[N<<1];
// vector<int> g[N<<1]; //需要建立后缀树时要用到(有时候要用)
//int f[N<<1]; //记录子树的sz之和(不常用)
int to[N<<1][26];
// 为当前的SAM增加一个字符c
void extend(int c){
int p, cur = ++tot; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=fa[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) fa[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) fa[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++tot; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
// to[cl] = to[q]; //复制所有原结点的trans连接
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q]; //后缀重连
fa[cur] = fa[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = fa[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
int cal(){
int res=0, u=1, now=0; //最终答案,当前结点,当前匹配进度
FOR(i,1,m){ //遍历t字符串的每个字符,考虑它能匹配到的最远位置
int c=t[i]-'a';
if(to[u][c]) now++, u=to[u][c]; //能直接匹配
else{
while(u!=1 && to[u][c]==0) u=fa[u]; //暂时匹配不了,暴力跳fail
if(to[u][c]) now=len[u]+1, u=to[u][c]; //跳了几次之后匹配上了
else u=1, now=0; //一直匹配不了,回到原点,匹配进度清零
}
res=max(res,now);
}
return res;
}
} sam;
void solve(){
cin>>(s+1)>>(t+1);
n=strlen(s+1), m=strlen(t+1);
FOR(i,1,n) sam.extend(s[i]-'a');
cout<<sam.cal(); //重点是这里!
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T=1; //cin>>T;
while(T--) solve();
}
5.LCS2 - Longest Common Substring II - 洛谷
题意: 给定一些字符串,求出它们的最长公共子串 输入格式 输入至多10 行,每行包含不超过100000个的小写字母,表示一个字符串。输出格式: 一个数,最长公共子串的长度,若不存在最长公共子串,请输出0。
思路:这题仅仅是把上一题的两字符串匹配改成了多字符串匹配,做法自然也是类似的。考虑对第一个字符串建立sam,对于后续字符串,用mx[u]记录当前字符串在u结点(自动机上的点)的最大匹配进度,mn[u]记录对于所有字符串的mx[u]的最小值,那么就是最终答案。
#include <bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=(a), (i##i)=(b); i<=(i##i); ++i)
template<class T>inline bool cmax(T&a,const T&b){return a<b?a=b,1:0;}
template<class T>inline bool cmin(T&a,const T&b){return a>b?a=b,1:0;}
// #define int long long
const int N = 2.5e5+10;
char s[N]; int n;
struct SAM{ //(其中N是字符串最大长度,代码注释掉表示不是很常用,但也可能用到)
int tot=1, last=1; //最大点编号,上一个点编号
int len[N<<1], fa[N<<1], sz[N<<1];
int mx[N<<1], mn[N<<1]; //当前匹配最大值,全部匹配最小值
int to[N<<1][26];
void init(){
memset(mn,0x7f,sizeof(mn));
}
// 为当前的SAM增加一个字符c
void extend(int c){
int p, cur = ++tot; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=fa[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) fa[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) fa[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++tot; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
// to[cl] = to[q]; //复制所有原结点的trans连接
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q]; //后缀重连
fa[cur] = fa[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = fa[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
void cal(){
memset(mx,0,sizeof(mx)); //init
int u=1, now=0; //当前结点,当前匹配进度
FOR(i,1,n){
int c=s[i]-'a';
if(to[u][c]) now++, u=to[u][c], cmax(mx[u], now); //能直接匹配
else{
while(u!=1 && to[u][c]==0) u=fa[u], now=len[u], cmax(mx[u], now); //暂时匹配不了,暴力跳fail
if(to[u][c]) now=len[u]+1, u=to[u][c], cmax(mx[u], now); //跳了几次之后匹配上了
else u=1, now=0; //一直匹配不了,回到原点,匹配进度清零(清零不需要更新mx值)
}
}
FOR(i,1,tot) cmin(mn[i], mx[i]);
}
} sam;
void solve(){
sam.init();
cin>>(s+1); n=strlen(s+1);
FOR(i,1,n) sam.extend(s[i]-'a');
while(cin>>(s+1)){
n=strlen(s+1);
sam.cal();
}
int ans=0;
FOR(i,1,sam.tot) cmax(ans, sam.mn[i]);
cout<<ans;
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T=1; //cin>>T;
while(T--) solve();
}
一道很经典的sam题,要求输出一个字符串的第k小子串。
首先要熟知sam一个重要的性质:sam中从源点开始的任意一条沿着to边的路径,都代表一个不同的子串,每个子串和一条路径一一对应。
(图片来自算法笔记(5) 后缀自动机 - 知乎,侵删)
比如bb和aabb就是母串中不同的子串,尽管他们的终点都是4号点。
先说t=0的时候怎么做,此时所有子串都只出现一次,我们对2~tot的点(除了S点外所有点)记录sz[u]=1,然后树形dp记录子树的sz之和,比如这个图中,
为什么要这样呢?因为b是bb,ba,bd的前缀,那么b的字典序比后面三者小,并且b的首字母和bb,ba,bd相等(换成一般的例子的话,应该是“前面一段和儿子相等”)。
然后我们就可以用这个神奇的print函数解决问题了(直接看代码,很容易懂的)
//当前在sam的u点,要求打印子树第k小
//打印s串的第k小子串:main中调用print(1,k)
void print(int u,int k){
if(k>f[u]) {cout<<"-1"; return;} //没有第k小,结束
if(k<=sz[u]) return;
k -= sz[u];
for(int i=0; i<26; i++){
int v = to[u][i];
if(k>f[v]) k-=f[v];
else{
cout<<(char)('a'+i);
print(v,k);
return;
}
}
}
那么t=1又要怎么处理呢?其实就是把sz[u]=1改成这个这个子串实际出现次数就行,这里用到另一个重要性质:一个结点对应的子串出现次数等于它在后缀树上的子树大小。
于是就用到这两个函数:
//建立后缀树
void build(){
FOR(i,2,tot) g[fa[i]].push_back(i);
}
//dfs计算size,即这个点所对应子串的出现次数(要build建立后缀树之后用,作用是求后缀树的子树大小)
void dfs(int u){
for(int v:g[u])
dfs(v), sz[u]+=sz[v];
}
完整代码如下:
#include <bits/stdc++.h>
using namespace std;
#define FOR(i, a, b) for (int i = (a); i <= (b); i++)
#define int long long
typedef pair<int,int> pii;
const int N = 5e5+5;
char s[N<<1];
int t,k;
vector<int> g[N<<1];
int f[N<<1];
struct SAM{
int tot=1, last=1; //最大点编号,上一个点编号
int len[N<<1], fa[N<<1], sz[N<<1];
int to[N<<1][26];
// 为当前的SAM增加一个字符c
void extend(int c){
int p, cur = ++tot; //新建一个结点(因为S[1..i+1]必定是一个新状态)
len[cur] = len[last]+1; //S[1...i+1].len = S[1...i] + 1
sz[cur] = 1; //初始sz值都是1
//情况1,或者情况2的前半部分
for(p=last; p && !to[p][c]; p=fa[p]){
to[p][c] = cur; //如果前面的状态没有连c,那就现在连上
}
if(!p) fa[cur] = 1; //如果一直到原点,都是没有连过c,那就是满足情况1,link接到起始点1
else{ //否则就是中间有的点连过c,满足情况2
int q = to[p][c]; //p点有连过c,连了c的state在q这里
if(len[q]==len[p]+1) fa[cur]=q; //情况2-A类。不用拆分,直接连q
else{ //情况2-B类,需要拆分q点,拆分出一个cl结点
int cl = ++tot; //拆分出的新结点
len[cl] = len[p]+1; //把能连link的部分拆分到cl这里(就是满足len[p]+1的部分)
// to[cl] = to[q]; //复制所有原结点的trans连接
memcpy(to[cl], to[q], sizeof(to[q]));
fa[cl] = fa[q]; //后缀重连
fa[cur] = fa[q] = cl; //后缀重连
while(p && to[p][c]==q){ //把之前trans到q的点全部改成trans到cl(因为trans指向state中最短的字符串,而这个串在cl那里)
to[p][c] = cl;
p = fa[p];
}
}
}
last = cur; //最后一步,记录末尾结点
}
//建立后缀树
void build(){
FOR(i,2,tot) g[fa[i]].push_back(i);
}
//dfs计算size
void dfs(int u){
for(int v:g[u]) dfs(v), sz[u]+=sz[v];
}
//dfs2计算所能到达的点的点权和
int dfs2(int u){
if(f[u]) return f[u]; //计算过,直接返回
f[u] = sz[u];
for(int i=0; i<26; i++){
int v = to[u][i]; //u可以to的26个点
if(v) f[u] += dfs2(v);
}
return f[u];
}
//当前在sam的u点,要求打印子树第k小
void print(int u,int k){
if(k>f[u]) {cout<<"-1"; return;} //没有第k小,结束
if(k<=sz[u]) return;
k -= sz[u];
for(int i=0; i<26; i++){
int v = to[u][i];
if(k>f[v]) k-=f[v];
else{
cout<<(char)('a'+i);
print(v,k);
return;
}
}
}
} sam;
void solve(){
cin>>(s+1)>>t>>k;
int len = strlen(s+1);
FOR(i,1,len) sam.extend(s[i]-'a');
sam.build(); //建立后缀树
if(t==0) FOR(i,1,sam.tot) sam.sz[i]=1;
else sam.dfs(1);
sam.sz[1] = 0; //注意根结点是空字符串,按照题目意思,出现次数为0
sam.dfs2(1); //再次dfs,计算出现次数
sam.print(1,k);
}
signed main(){
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T=1; //cin>>T;
while(T--) solve();
}