SAM后缀自动机学习笔记(附板子)

 我学习的视频连接:【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]的最小值,那么max(mn[u]), 1<=u<=tot就是最终答案。

#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();
}

6.[TJOI2015]弦论 - 洛谷

一道很经典的sam题,要求输出一个字符串的第k小子串。

首先要熟知sam一个重要的性质:sam中从源点开始的任意一条沿着to边的路径,都代表一个不同的子串,每个子串和一条路径一一对应。

(图片来自算法笔记(5) 后缀自动机 - 知乎,侵删)

比如bb和aabb就是母串中不同的子串,尽管他们的终点都是4号点。

先说t=0的时候怎么做,此时所有子串都只出现一次,我们对2~tot的点(除了S点外所有点)记录sz[u]=1,然后树形dp记录子树的sz之和,比如这个图中,f[5]=sz[5]+f[4]+f[6]+f[9]

为什么要这样呢?因为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();
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值