后缀自动机学习

后缀自动机

学习参考:

一. ACM算法日常微信公众号-后缀自动机入门系列:

史上全网最清晰后缀自动机学习(一)基本概念入门

史上全网最清晰后缀自动机学习(二)后缀自动机的线性时间构造算法

史上全网最清晰后缀自动机学习(三)后缀自动机里的树结构

史上全网最清晰后缀自动机学习(四)后缀自动机里的DAG结构

史上全网最清晰后缀自动机学习(五)后缀自动机和最长公共子串问题

史上全网最清晰后缀自动机学习(六)后缀自动机杂题

二. OI Wiki - sam

三. 陈立杰后缀自动机PPT

模板

struct node{
    int trans[P], link, len;
    void clear(){
        memset(trans, 0, sizeof trans);
        link = len = 0;
    }
};
struct SAM{
    node S[N];
    int p, np, size;
    SAM():p(1),np(1),size(1){}
    void clear(){
        for(int i=0;i<=size;i++)S[i].clear();
        np = size = p = 1;
    }
    void insert(char ch){
        int x = ch - 'a';
        np = ++size;
        S[np].len = S[p].len + 1;
        while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
        if(p == 0)S[np].link = 1;
        else{
            int q, nq;
            q = S[p].trans[x];
            if(S[q].len == S[p].len + 1) S[np].link = q;
            else{
                nq = ++size;
                S[nq] = S[q]; 
                S[nq].len = S[p].len + 1;
                S[np].link = S[q].link = nq;
                while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
            }
        }
        p = np;
    }
}sam;

基础题

1. 求本质不同的子串个数

#1445 : 后缀自动机二·重复旋律5

SAM中每个状态的 \(len\) 表示该 \(endpos\) 等价类中最长字串的长度,所以这个状态的不同子串个数是 \(len-S[S[i].link].len\)

//在SAM结构体中添加该函数调用即可
ll get(){
    ll res = 0;
    for(int i=1;i<=size;i++){
        res += S[i].len - S[S[i].link].len;
    }
    return res;
}
int main() {
	scanf("%s",s);
	int n = strlen(s);
	for(int i=0;i<n;i++)sam.insert(s[i]);
	printf("%lld\n", sam.get());
    return 0;
}
#2033. 「SDOI2016」生成魔咒

本题特殊点在于字符集为[1,1e9], 所以并不能用数组下标直接查询,因为总结点数不会\(2*n\),边不超过\(3*n\), 所以只需要给每个点加一个map就可以了。

本题需要求每插入一个字符后,SAM中本质不同的字符串的个数,可以发现每次分裂产生的新结点只是变成了一个新的\(endpos\)类,但本质不同字符串个数并没有增加,所以该个数增加只会发生在新插入的结点上

#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
#include <unordered_map>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 200000 + 5;

struct node{
    int link, len;
    unordered_map<int,int> trans;
	void clear(){
        trans.clear();
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
    ll res;
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(int ch){
		int x = ch;
		np = ++size;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
        res += S[p].len - S[S[p].link].len; // res只会在这里更新
	}
}sam;

int main() {
    int n;scanf("%d",&n);
    sam.res = 0;
    while(n--){
        int x;scanf("%d",&x);
        sam.insert(x);
        printf("%lld\n",sam.res);
    }
    return 0;
}

2. 求 \(endpos\) 集合大小

#1449 : 后缀自动机三·重复旋律6

沿后缀\(link\) 建立反向树,\(dfs\) 进行统计

using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 3000000 + 5;
const int P = 26;
int head[N], ver[N], nxt[N], tot;
ll res[N];
void add(int x, int y){
    ver[++tot] = y, nxt[tot] = head[x], head[x] = tot;
}
struct node{
    int trans[P], link, len;
    ll sum;
	void clear(){
		memset(trans, 0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
        S[np].sum = 1; // np 为一个新的endpos类,大小为1
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].sum = 0; // 分离出的结点,赋值为0
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void dfs(int x){
        for(int i=head[x];i;i=nxt[i]){
            int y = ver[i];
            dfs(y);
            S[x].sum += S[y].sum;
        }
    }
	void get(){
		for(int i=2;i<=size;i++){ //沿后缀link反向建树
            int to = S[i].link;
            if(to != 0) add(to, i);
		}
        dfs(1); // dfs
        for(int i=2;i<=size;i++){ // 更新答案
            res[S[i].len] = max(res[S[i].len], S[i].sum);
        }
            
	}
}sam;
char s[N];
int main() {
	scanf("%s",s);
	int n = strlen(s);
	for(int i=0;i<n;i++)sam.insert(s[i]);
    sam.get();
	for(int i=n-1;i>=1;i--) res[i] = max(res[i], res[i+1]);
    for(int i=1;i<=n;i++){
        printf("%lld\n",res[i]);
    }   
    return 0;
}
P3804 【模板】后缀自动机 (SAM)

求出 \(endpos\) 之后扫一遍所有结点即可

ll get(){
    for(int i=2;i<=size;i++){
        int to = S[i].link;
        if(to != 0) add(to, i);
    }
    dfs(1);
    ll res = 0;
    for(int i=2;i<=size;i++){
        if(S[i].sum == 1) continue;
        res = max(res, S[i].len * S[i].sum);
    }
    return res;
}

3. SAM上DP

#1457 : 后缀自动机四·重复旋律7

SAM是一个天然的DAG,某个结点作为一个最终结果接纳状态,可以表示若干个子串,\(endpos\) 的大小则表示了他们的出现次数。

本题中先考虑一个串的情况,DP时,某个结点沿着某条出边走向下一个结点,这个转移意味着给一些子串后面加一个字符,如果这个结点的数字之和为 \(sum\)\(endpos\) 大小表示这些数字在原串中共出现了多少次,那么沿着 \('1'\) 这条边走到下一个状态,贡献将产生 :\(sum*10 + 1 * endpos\)

这里的\(endpos\) 可以顺便用拓扑序 \(dp\) 求出,当多个串连起来时,不考虑非法字符的转移即可。

typedef long long ll;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int N = 3000000 + 5;
const int P = 11;
int tot;
ll res[N];
struct node{
    int trans[P], link, len;
    ll sum, validnum;
	void clear(){
		memset(trans, 0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size, deg[N];
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - '0';
		np = ++size;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    ll get(){
        queue<int> q;
        q.push(1);
        S[1].validnum = 1;
        for(int i=1;i<=size;i++){
            for(int j=0;j<P;j++){
                if(S[i].trans[j]) deg[S[i].trans[j]]++; //初始化结点入度
            }
        }
        //按照拓扑序进行DP,DAG上的DP
        ll res = 0;
        while(q.size()){
            int x = q.front();q.pop();
            for(int i=0;i<P;i++){
                int y = S[x].trans[i];
                if(!y)continue;
                if(--deg[y] == 0)q.push(y);
                // i = 10时表示非法字符,不进行转移
                if(i != P-1){ 
                    S[y].validnum += S[x].validnum;
                    S[y].sum += S[x].sum * 10 % mod + i * S[x].validnum % mod;
                    S[y].sum %= mod;
                }
            }
            res = (res + S[x].sum)%mod;
        }
        return res;
    }
}sam;
int n;
char s[N];
int main() {
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",s);
        int len = strlen(s);
        for(int j=0;j<len;j++)sam.insert(s[j]);
        if(i < n) sam.insert(':'); //‘:'的 ascii码为58,刚好等于 '9' + 1
    }
    printf("%lld\n", sam.get());
    return 0;
}

4. 公共子串

Longest Common Substring

SPOJ - LCS

给两个串,求最长公共子串的长度。

将第一个串构建SAM,然后对第二个串的每个位置 i, 维护以 i 为结尾的最长字串的长度 l, 那么整个过程 l 的最大值就是答案。

如果维护?设当前状态结点为 \(u\),长度为$ l$,如果匹配不到当前字符,那 \(u = u.link\), 以直到 \(u = 1\),此过程中 $l $一直在缩小,令 \(l = min(l, u.len)\), 如果最终也没能匹配,那么\(u=1,l=0\),否则\(l++,u = u.trans[i]\);

#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 5000000 + 5;
const int P = 26;
char a[N], b[N];
struct node{
    int link, len, trans[P];
	void clear(){
        memset(trans,0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void solve(char *b){
        int u = 1, l = 0, n = strlen(b);
        int res = 0;
        for(int i=0;i<n;i++){
            int c = b[i] - 'a';
            while(u != 1 && !S[u].trans[c]) u = S[u].link, l = min(l, S[u].len);
            if(S[u].trans[c]){ 
                u = S[u].trans[c];
                l++;
            }
            res = max(res, l);
        }
        cout << res << endl;
    }
}sam;
int main() {
    scanf("%s%s",a,b);
    int n = strlen(a);
    for(int i=0;i<n;i++)sam.insert(a[i]);
    sam.solve(b);
    return 0;
}
#1465 : 后缀自动机五·重复旋律8

给出一个字符串,有若干次询问,每次询问给出一个字符串,求该字符串的所有循环表示在原串中出现过多少次。

循环表示一般的套路就是复制一遍接到尾巴上,\(s[1...n] \Rightarrow S[1...n~1....n]\) , 枚举每个位置作为循环串的结尾,假设为 S[1...i],在SAM中找到一个结点 \(u\) , \(u\) 在SAM中表示的状态是 \(S[1...i]\) 的后缀,并且在 \(u\)\(len >= n\) 前提下,使得\(endpos\) 尽可能大,在这样情况下求出来的 \(u\) 就是一个以字符\(S[i]\) 结尾的循环子串,在原串中可以出现的所有位置。

typedef long long ll;
const int inf = 0x3f3f3f3f;
const int mod = 1e9 + 7;
const int N = 3000000 + 5;
const int P = 26;
ll res[N];
int head[N], ver[N], nxt[N], tot;
void add(int x, int y){
    ver[++tot] = y, nxt[tot] = head[x], head[x] = tot;
}
struct node{
    int trans[P], link, len;
    int endpos;
	void clear(){
		memset(trans, 0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
    int vis[N];
    vector<int> vislist;
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
        S[np].endpos = 1;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q];
                S[nq].endpos = 0; 
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void dfs(int x){
        for(int i=head[x];i;i=nxt[i]){
            dfs(ver[i]);
            S[x].endpos += S[ver[i]].endpos;
        }
    }
    void getEndpos(){
        for(int i=2;i<=size;i++){
            add(S[i].link, i);
        }
        dfs(1);
    }
    void solve(char *s){
        int len = strlen(s), newLen = len * 2 - 1; // 长度为2*len-1的原因是防止重复计算
        for(int i=len;i<newLen;i++)s[i] = s[i-len];
        int u = 1, l = 0, res = 0;
        for(int i=0;i<newLen;i++){
            int c = s[i] - 'a';
            while(u != 1 && !S[u].trans[c]){ // 找不到可以匹配 c 的 u
                u = S[u].link;
                l = S[u].len;
            }
            if(S[u].trans[c]){ // 可以匹配 c 的 u 存在
                u = S[u].trans[c];
                ++ l;
            }else { //不存在能匹配 c 的 u,可以可以break了
                break;
            }
            if(l > len){
                while(S[S[u].link].len >= len){
                    u = S[u].link;
                    l = S[u].len;
                }
            }
            if(l >= len && !vis[u]){
                vis[u] = 1;
                vislist.push_back(u);
                res += S[u].endpos;
            }
        }
        for(int i=0;i<vislist.size();i++){
            vis[vislist[i]] = 0;
        }
        printf("%d\n",res);
    }
}sam;
int n;
char s[N];
int main() {
    scanf("%s",s);
    n = strlen(s);
    for(int i=0;i<n;i++)sam.insert(s[i]);
    scanf("%d",&n);
    sam.getEndpos();
    for(int i=1;i<=n;i++){
        scanf("%s",s);
        sam.solve(s);
    }
    return 0;
}
Longest Common Substring II

SPOJ - LCS2

多个串之间的LCS,与第一个问题大同小异,先构建第一个串的SAM,然后对于后来的每个串去按照第一个题进行一样的处理,并标记SAM中每个点能够匹配的最大长度,这也意味着这些点的link也具有这样的长度,所以要在扫描结束后自底向上的更新max值,然后对于所有的串,取min值即可。

#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 200000 + 5;
const int P = 26;
char a[N], b[N];
struct node{
    int link, len, trans[P], mx, mi;
	void clear(){
        memset(trans,0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
    int b[N], c[N];
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void sort(){//按照len值排序,保证从后往前遍历能够正确更新mx值
        for(int i=1;i<=size;i++)++c[S[i].len];
        for(int i=1;i<=size;i++)c[i] += c[i-1];
        for(int i=1;i<=size;i++)b[c[S[i].len]--] = i;
        for(int i=1;i<=size;i++)S[i].mi = inf;
    }
    void solve(char *s){
        int u = 1, l = 0, n = strlen(s);
        int res = 0;
        for(int i=0;i<n;i++){
            int c = s[i] - 'a';
            while(u != 1 && !S[u].trans[c]) u = S[u].link, l = S[u].len;
            if(S[u].trans[c]){ 
                u = S[u].trans[c];
                l++;
                S[u].mx = max(S[u].mx, l);
            }else u = 1, l = 0;
        }
        for(int i=size;i>=1;i--){
            int u = b[i], fa = S[u].link;
            S[fa].mx = max(S[fa].mx, min(S[u].mx, S[fa].len));
            S[u].mi = min(S[u].mi, S[u].mx);
            S[u].mx = 0;
        }
    }
}sam;


int main() {
    scanf("%s",a);
    int n = strlen(a);
    for(int i=0;i<n;i++)sam.insert(a[i]);
    sam.sort();
    while(~scanf("%s",a))sam.solve(a);
    int res = 0;
    for(int i=1;i<=sam.size;i++) res = max(res, sam.S[i].mi);
    printf("%d\n",res);
    return 0;
}

5. 字典序第 K 大子串

SPOJ - SUBLEX

在SAM中的DAG上可以直接求出每个结点以及其后续状态的所有子串,然后依次从小到大试填。

#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 200000 + 5;
const int P = 26;
char s[N], a[N];
struct node{
    int link, len, trans[P];
	void clear(){
        memset(trans,0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
    ll d[N];
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void dfs(int x){
        if(d[x])return; // 记忆化
        d[x] = 1; // 它本身肯定算作一个
        for(int i=0;i<P;i++){
            if(S[x].trans[i]){
                dfs(S[x].trans[i]);
                d[x] += d[S[x].trans[i]];
            }
        }
    }
    void init(){
        dfs(1);
        d[1] --;//空串本身不算,所以减一
    }
    void get(int k){
        int pos = 0, u = 1;
        while(k){
            if(u != 1) k --; //非空串占用一个
            if(k == 0)break;
            for(int i=0;i<P;i++){
                int y = S[u].trans[i];
                if(!y)continue;
                if(d[y] >= k){
                    u = S[u].trans[i];
                    a[pos++] = 'a' + i;
                    break;
                }
                k -= d[y];
            }
        }
        a[pos] = 0;
        puts(a);
    }
}sam;


int main() {
    scanf("%s",s);
    int n = strlen(s);
    for(int i=0;i<n;i++)sam.insert(s[i]);
    sam.init();
    int q;scanf("%d",&q);
    while(q--){
        int k;scanf("%d",&k);
        sam.get(k);
    }
    return 0;
}
#2102. 「TJOI2015」弦论

这个题稍稍复杂一些,包含了对于不同位置的相同子串的计算,那么SAM上每个结点都有 \(endpos\) 次出现,所以我们在计算出\(endpos\)之后再DP就好了。

// TJOI 2015
#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 1000000 + 5;
const int P = 26;
int head[N], ver[N], nxt[N], tot;
void add(int x, int y){ver[++tot] = y, nxt[tot] = head[x], head[x] = tot; }
char s[N], a[N];
struct node{
    int link, len, trans[P], endpos;
	void clear(){
        memset(trans,0, sizeof trans);
		link = len = 0;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
    ll d[N][2]; // 0 表示不同位置子串算一个,1 表示不同位置子串算多个
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
        S[np].endpos = 1;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q]; 
                S[nq].endpos = 0;
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void getendpos(int x){
        for(int i=head[x];i;i=nxt[i]){
            getendpos(ver[i]);
            S[x].endpos += S[ver[i]].endpos;
        }
    }
    void dfs(int x){
        if(d[x][0])return; // 记忆化
        d[x][0] = 1; // 它本身肯定算作一个
        d[x][1] = S[x].endpos;
        for(int i=0;i<P;i++){
            if(S[x].trans[i]){
                dfs(S[x].trans[i]);
                d[x][0] += d[S[x].trans[i]][0];
                d[x][1] += d[S[x].trans[i]][1];
            }
        }
    }
    void init(){
        for(int i=2;i<=size;i++){
            if(S[i].link) add(S[i].link, i);
        }
        getendpos(1);
        dfs(1);
        d[1][0] --;//空串本身不算,所以减一
    }
    void get(int k, int t){
        int pos = 0, u = 1;
        if(d[u][t] < k){
            puts("-1");
            return;
        }
        while(k){
            if(u != 1){
                if(t == 0) k--; // t = 0 时 只占用一个串
                else k -= S[u].endpos; //t = 1 时,占用endpos个串
            }
            if(k <= 0)break;
            for(int i=0;i<P;i++){
                int y = S[u].trans[i];
                if(!y)continue;
                if(d[y][t] >= k){
                    u = S[u].trans[i];
                    a[pos++] = 'a' + i;
                    break;
                }
                k -= d[y][t];
            }
        }
        a[pos] = 0;
        puts(a);
    }
}sam;


int main() {
    scanf("%s",s);
    int n = strlen(s);
    for(int i=0;i<n;i++)sam.insert(s[i]);
    sam.init();
    int t,k;
    scanf("%d%d",&t,&k);
    sam.get(k,t);
    return 0;
}

综合题

1. SAM与DAG上的博弈

#1466 : 后缀自动机六·重复旋律9

请先阅读史上全网最清晰后缀自动机学习(六)后缀自动机杂题

这个题等价于同时进行两个DAG有向图游戏,出度为 0 的点是必败态,每次只能往前走一步。所以可以直接\(DFS\)求每个点的\(SG\)函数,而有向图游戏的和就是简单的将他们现在的状态的\(SG\)值异或起来,如果等于0则为必败态。

要求字典序第 \(k\) 小的答案,所以我们还要统计两个DAG中某个状态以及该状态的后继状态中的所有必胜态个数,由于每个状态最多只有26条出边,所以\(sg\) 值并不是很大。用 \(S[x].cnt[i]\) 表示\(x\) 以及 \(x\) 的所有后继状态的 \(sg\) 值等于 i 的个数,\(S[x].rcnt[i]\) 表示 x 以及 x 的所有后继状态中 sg 值不等于 i 的个数。

统计所有必胜态个数,如果小于 k,那么输出 NO。

然后从 a 为空串开始试填,假设填到某个地方,a串成为答案,a串SAM中结点走到了 u(因为每次匹配一个字符都要往前走一格),那么必胜态只有 \(B.S[1].rcnt[A.S[u].sg]\) , 其中 S[1] 表示SAM中的源点,这个值跟 k 去作比较,如果 k 小于等于它,那么这时的 a 串就已经是答案了,否则考虑 a 的后继状态,也是一样的思考方法,就不再赘述了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 200000 + 5;
const int P = 27;
struct node{
    int trans[P], link, len, sg;
    ll cnt[P], rcnt[P], tot;
    node(){
        clear();
    }
	void clear(){
		memset(trans, 0, sizeof trans);
        memset(cnt,0, sizeof cnt);
        memset(rcnt, 0, sizeof rcnt);
		link = len = 0;
        sg = -1;
	}
};
struct SAM{
    node S[N];
    int p, np, size;
	SAM():p(1),np(1),size(1){}
	void clear(){
		for(int i=0;i<=size;i++)S[i].clear();
		np = size = p = 1;
	}
	void insert(char ch){
		int x = ch - 'a';
		np = ++size;
		S[np].len = S[p].len + 1;
		while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
		if(p == 0)S[np].link = 1;
		else{
			int q, nq;
			q = S[p].trans[x];
			if(S[q].len == S[p].len + 1) S[np].link = q;
			else{
                nq = ++size;
				S[nq] = S[q];
                S[nq].len = S[p].len + 1;
				S[np].link = S[q].link = nq;
				while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
			}
		}
		p = np;
	}
    void getSg(int x){
        //要记得记忆化,防止复杂度暴增
        if(S[x].sg != -1)return;
        S[x].sg = 0;
        bool st[P] = {false};
        for(int i=0;i<P;i++){
            int y = S[x].trans[i];
            if(y){
                getSg(y);
                st[S[y].sg] = true;
            }
        }
        while(st[S[x].sg])S[x].sg++;
    }
    void getcnt(int x){
        if(S[x].cnt[S[x].sg])return;
        ll sum = 0;
        S[x].cnt[S[x].sg]++;
        for(int i=0;i<P;i++){
            int y = S[x].trans[i];
            if(y){
                getcnt(y);
                for(int j=0;j<P;j++){
                    S[x].cnt[j] += S[y].cnt[j];
                }
            }
        }
        for(int i=0;i<P;i++) sum += S[x].cnt[i];
        for(int i=0;i<P;i++) S[x].rcnt[i] = sum - S[x].cnt[i];
    }
    void insert(char *s){
        int n = strlen(s);
        for(int i=0;i < n;i++){
            insert(s[i]);
        }
        getSg(1); //递归求SG
        getcnt(1); //递归求cnt以及rcnt
    }
} A, B;
ll k;
char a[N], b[N];

ll calc(int u){
    if(A.S[u].tot) return A.S[u].tot;
    ll res = 0;
    for(int i = 0;i<P;i++){
        res += A.S[u].cnt[i] * B.S[1].rcnt[i];
    }
    return A.S[u].tot = res;
}
bool get(){
    ll sum = 0;
    bool flag = false;
    for(int i=0;i<P;i++){
        sum = sum + A.S[1].cnt[i] * B.S[1].rcnt[i];
        if(sum >= k){ // 防止sum爆ll,大于等于 k就立即退出
            flag = true;
            break;
        }
    }
    if(!flag) return false;
    // 开始依次试填 a, u 为 A中当前结点,pos表示a串试填长度
    int pos = 0, u = 1; 
    while(k){
        sum = B.S[1].rcnt[A.S[u].sg]; // 计算a串为u状态下,所有的必胜态
        if(sum >= k) break; // a 为 u 状态可行
        k -= sum;
        for(int i=0;i<P;i++){
            int y = A.S[u].trans[i];
            if(!y)continue;
            sum = calc(y);//计算 y后继所有必胜态
            if(sum >= k){ // 可以用这个来作为前缀
                a[pos++] = i + 'a';
                u = y;
                break;
            }
            k -= sum;
        }
    }
    // 此时 A 的状态为 u, 要在 B 中找 第 k 个 不等于 sg 的结点
    int sg = A.S[u].sg;
    pos = 0, u = 1;
    while(k){
        if(B.S[u].sg != sg){
            k --;
            if(k == 0) break;
        }
        for(int i=0;i<P;i++){
            int y = B.S[u].trans[i];
            if(!y)continue;
            sum = B.S[y].rcnt[sg];
            if(sum >= k){
                b[pos++] = i + 'a';
                u = y;
                break;
            }
            k -= sum;
        }
    }
    return true;
}
int main() {
    scanf("%lld%s%s",&k,a,b);
    A.insert(a);B.insert(b);
    memset(a, 0, sizeof a);
    memset(b, 0, sizeof b);
    if(get()){
        printf("%s\n%s",a,b);
    }else{
        puts("NO");
    }
    return 0;
}

2. SAM维护字符串DP

HDU-6583

2019年杭电多校的题目,粘一下出题人题解:

对于\(i\)从小到大处理,维护使得\(s[j:i]\in s[1:j-1]\)的最小的\(j\)(\(s[l:r]\)表示子串\(s_ls_{l+1}...s_{r}\)),那么记\(f[i]\)为输出前\(i\)个字符的最小代价,则\(f[i]=\min\{f[i-1]+p,f[j-1]+q\}\)

用SAM维护\(s[1:j-1]\),若\(s[1:j-1]\)中包含\(s[j:i+1]\),即加入第\(i+1\)个字符仍然能复制,就不需要做任何处理。否则,重复地将第\(j\)个字符加入后缀自动机并\(j=j+1\),相应维护\(s[j:i+1]\)在后缀自动机上新的匹配位置,直到\(s[j,i+1]\in s[1,j-1]\)

时限给了\(1.5s\),应该是卡掉了所有非线性做法

#include <cstdio>
#include <iostream>
#include <cmath>
#include <string>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
const int N = 400000 + 5;
const int P = 26;
char a[N];
ll p, q, d[N];
struct node{
    int trans[P], link, len;
    ll sum;
    void clear(){
        memset(trans, 0, sizeof trans);
        link = len = 0;
    }
};
struct SAM{
    node S[N];
    int p, np, size, u;
    SAM():p(1),np(1),size(1){}
    void clear(){
        for(int i=0;i<=size;i++)S[i].clear();
        u = np = size = p = 1;
    }
    void insert(char ch){
        int x = ch - 'a';
        np = ++size;
        S[np].sum = 1;
        S[np].len = S[p].len + 1;
        while(p != 0 && !S[p].trans[x]) S[p].trans[x] = np, p = S[p].link;
        if(p == 0)S[np].link = 1;
        else{
            int q, nq;
            q = S[p].trans[x];
            if(S[q].len == S[p].len + 1) S[np].link = q;
            else{
                nq = ++size;
                S[nq] = S[q]; 
                S[nq].sum = 0;
                S[nq].len = S[p].len + 1;
                S[np].link = S[q].link = nq;
                while(p != 0 && S[p].trans[x] == q) S[p].trans[x] = nq, p = S[p].link;
            }
        }
        p = np;
    }
    bool find(char ch){
        return S[u].trans[ch-'a'];
    }
    void solve(char *a, ll p, ll q){
        int n = strlen(a+1);
        int j = 1;
        u = 1;
        d[0] = 0;
        for(int i=1;i<=n;i++){
            while(!find(a[i])){//如果[1..j-1]所构成的SAM无法匹配到 a[j..i]
                if(u == 1){
                    insert(a[j++]);
                }else{
                    //刚到这里 u 表示最长的可匹配到 a[j...i-1],然后进一步缩短 u 所表示的长度,增加字符到SAM
                    u = S[u].link;
                    while(j < i - S[u].len){//[i-S[u].len,....i-1] 为u匹配的字符串,所以最多添加到 i-S[u].len-1
                        insert(a[j++]);
                    }
                }
            }
            u = S[u].trans[a[i]-'a'];
            d[i] = d[i-1] + p;
            if(j <= i)
	            d[i] = min(d[i], d[j-1] + q);
        }
        printf("%lld\n",d[n]);
    }
}sam;

int main() {
    while(~scanf("%s",a+1)){
        scanf("%lld%lld",&p,&q);
        sam.solve(a, p, q);
        sam.clear();
    }
    return 0;
}
posted @ 2020-03-04 21:16  kpole  阅读(276)  评论(3编辑  收藏  举报