后缀自动机 SAM
学习可以参考oi-wiki和luogu的一些题解。
本问只是对一些应用上的思考。
文章目录
学习材料
https://oi-wiki.org/string/sam/#_8 反正就是要反复看。
##报错调试的角度
1. 空间算错
因为空间是2N的,所以要考虑清楚最长的串多大。
我会设置一个专门的全局常量给SAM的数据数组。
有时候,SAM之外申请的数组很容易搞错。(尤其是拓扑排序用的数组)
2. last 和 size(total)
last的编号不一定等于状态个数,因为clone时新建点的编号是大于last的。
3. 关于初始化
看到一种做法是对数组空间随用随清的做法,和AC自动机类似。我觉得比较清晰。
// new node
int nuw(int id) {
memset(nxt[id], 0, sizeof(nxt[id]));
len[id] = link[id] = 0;
return id;
}
快速手模后缀链接(Parent Tree)
本节的【后缀链接】均指代所有后缀链接构成的“Parent Tree”
一般情况下手模的时候,也是需要画出所有后缀链接。
自动机在构建的过程中会有clone操作,对连边进行修改。如果直接按照代码逻辑去模拟会有很多涂改,并不方便。
一般手模是从自动机状态的定义出发:(参考证明类教程)
- 把原串的所有子串按照
END_POS
进行分组,每个组就是自动机里的一个状态,每个状态取最长的子串来辨识。 - 后缀链接就是从一个状态连向它的最长后缀所在的状态。
还有一些小技巧:
- 第一层的
END_POS
就是每种字母出现的位置分组。不过,注意他们的不一定就是1。比如下面这个例子中的{2,5}
,实际是用肉眼算出每个位置对应前缀的公共后缀。 - 每个状态下面的分支都是对当前状态的子集进行划分,子状态的辨识字符串尽量长,并且保持同个状态里
END_POS
一致。
在比较短的串里,肉眼观察其实很快,多画几次就会熟悉了。我觉得手模样例也是解题找思路的重要一步。
应用1:子串出现次数(与对应长度)
给定一个只包含小写字母的字符串S,
请你求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。
树(针对后缀链接)上每个节点,代表一些子串。
每个节点所包含的子串,在原串中出现的次数,等于这个点所在子树endpos
的大小。
每个节点的len表示这个节点状态最长串的长度。
拓扑序遍历后缀链接就可以枚举出答案了。
// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cctype>
using namespace std;
const int N = 1000010;
#define rg int
typedef long long ll;
/*
* @notice:
* - tot计数从1开始,1号节点代表空节点(也就是根)。
*
* */
struct SAM {
const int begin = 1;
int nxt[N << 1][26];
int link[N << 1];
int len[N << 1];
int siz[N << 1];
int las = begin;
int tot = 1;
inline void init(const int n) {
len[begin] = 0;
link[begin] = 0;
las = begin;
tot = 1;
}
// start from 0
inline void build(const char s[], const int &length) {
for (int i = 0; i < length; ++i) {
this->extend(s[i] - 'a');
}
}
inline void extend(int c) {
int p = las;
int cur = ++tot;
len[cur] = len[p] + 1;
siz[cur] = 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = ++tot;
memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
const int MAXN = 1000059;
char s[MAXN];
ll ans;
int n;
int stk[MAXN << 1], top;
int deg[MAXN << 1];
int main() {
ans = 0;
scanf("%s", s + 1);
n = strlen(s + 1);
// cerr << n << " " << (s + 1) << endl;
sam.build(s + 1, n);
for (int i = 1; i <= sam.tot; ++i) {
deg[sam.link[i]]++;
}
for (int j = sam.tot; j >= 1; --j) {
if (deg[j] == 0) {
stk[top++] = j;
}
}
while (top) {
int cur = stk[--top];
int pre = sam.link[cur];
if (sam.siz[cur] > 1) {
ans = max(ans, 1ll * sam.len[cur] * sam.siz[cur]);
}
sam.siz[pre] += sam.siz[cur];
deg[pre]--;
if (deg[pre] == 0 && pre != 1) {
stk[top++] = pre;
}
}
// cerr << sam.tot << endl;
printf("%lld", ans);
return 0;
}
应用2:最小循环移位
给定一个字符串s。找出字典序最小的循环移位。
套路就是S+S包含s的所有是循环似移位。
在自动机的next图上按照顺序搜索长度为n的路径。
但是这种题要小心,自动机的空间应该是四倍范围。
// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<map>
#include<vector>
using namespace std;
const int N = 6e5 + 59;
typedef long long ll;
/*
* @notice:
* - tot计数从1开始,1号节点代表空节点(也就是根)。
*
* */
struct SAM {
const int begin = 1;
map<int, int> nxt[N << 1];
int link[N << 1];
int len[N << 1];
int siz[N << 1];
int las = begin;
int tot = 1;
inline void init(const int n) {
len[begin] = 0;
link[begin] = 0;
las = begin;
tot = 1;
}
// start from 0
inline void append(const vector<int> &a) {
for (auto ai : a) {
extend(ai);
}
}
inline void extend(int c) {
int p = las;
int cur = ++tot;
len[cur] = len[p] + 1;
siz[cur] = 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = ++tot;
nxt[clone] = nxt[q];
// memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
int n;
int stk[N], top = 0;
bool DFS(int u) {
for (auto pi : sam.nxt[u]) {
stk[top++] = pi.first;
if (top == n) return true;
if (DFS(pi.second)) return true;
--top;
}
return false;
}
int main() {
scanf("%d", &n);
vector<int> a(n);
for (auto &ai : a) {
scanf("%d", &ai);
}
sam.append(a);
sam.append(a);
DFS(sam.begin);
for (int i = 0; i < top; ++i) {
printf("%d%c", stk[i], " \n"[i + 1 == top]);
}
return 0;
}
应用3:本质不同的子串个数
按顺序输出所有前缀的本质不同子串的个数。
性质:一个串的本质不同子串个数等于
Σ
[
l
e
n
(
u
)
−
l
e
n
(
l
i
n
k
(
u
)
)
]
\Sigma{[len(u) - len(link(u))]}
Σ[len(u)−len(link(u))]
再简化一点,只要针对last维护答案即可。
// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<map>
#include<vector>
using namespace std;
const int N = 1e5 + 59;
typedef long long ll;
/*
* @notice:
* - tot计数从1开始,1号节点代表空节点(也就是根)。
*
* */
struct SAM {
const int begin = 1;
map<int, int> nxt[N << 1];
int link[N << 1];
int len[N << 1];
int siz[N << 1];
int las = begin;
int tot = 1;
inline void init(const int n) {
len[begin] = 0;
link[begin] = 0;
las = begin;
tot = 1;
}
// start from 0
inline void append(const vector<int> &a) {
for (auto ai : a) {
extend(ai);
}
}
inline void extend(int c) {
int p = las;
int cur = ++tot;
len[cur] = len[p] + 1;
siz[cur] = 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = ++tot;
nxt[clone] = nxt[q];
// memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
int n;
int main() {
scanf("%d", &n);
vector<int> a(n);
sam.init(n);
ll ans = 0;
for (auto &ai : a) {
scanf("%d", &ai);
sam.extend(ai);
ans += sam.len[sam.las] - sam.len[sam.link[sam.las]];
printf("%lld\n", ans);
}
return 0;
}
广义后缀自动机
luogu一个模板
题解的文章中说的比较细致,但是也有点口语化。而OIwiki上的介绍比较正式,可以结合起来看。
它有两种构造方法,注意写的时候一些细节。
应用:统计多个串间每个串的独有子串
P4081 [USACO17DEC]Standing Out from the Herd P
输入n个串,对每个串统计有多少种不同子串是这个串独有的。
本来只想做狭义SAM的,但是看到题解里说广义SAM在线做只要每次加入串的时候重置last为root就可以了。
我觉得很不合理,于是去调查了一下,正确的做法是要额外特判的,如果直接这样做,会导致不明显的漏洞。(会创造许多无效状态污染整个自动机,详细看模板题的题解)
构造好自动机以后,在next图上遍历每个串,同时要遍历后缀链接,标记每个状态出现在哪个串中。(出现超过一次就可以认为无效了,比如-1)
统计个数是对应标记的len-len(link)
求和。
但是我加了一个检查状态没有标记的断言居然生效了,让我怀疑这份代码的正确性了。
// luogu-judger-enable-o2
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<map>
#include<vector>
#include<cassert>
#include <algorithm>
using namespace std;
const int N = 1e5 + 59;
typedef long long ll;
/*
* @notice:
* - tot计数从1开始,1号节点代表空节点(也就是根)。
*
* */
struct SAM {
const int begin = 1;
int nxt[N << 1][26];
int link[N << 1];
int len[N << 1];
// int las = begin;
int tot = 1;
/*inline void init() {
len[begin] = 0;
link[begin] = 0;
las = begin;
tot = 1;
}*/
// start from 0
inline void append(const string &s) {
int last = begin;
for (auto c : s) {
last = extend(c - 'a', last);
}
}
inline int extend(int c, int las) {
if (nxt[las][c] && len[las] + 1 == len[nxt[las][c]]) {
return nxt[las][c];
}
bool flag = false;
int p = las;
int cur = ++tot;
len[cur] = len[p] + 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
int clone;
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
if (len[p] + 1 == len[cur]) {
flag = true;
}
clone = ++tot;
memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
return flag ? clone : cur;
}
} sam;
int n;
int tag[N << 1];
char str[N];
int res[N];
vector<string> a;
int main() {
scanf("%d", &n);
getchar();
for (int i = 1; i <= n; ++i) {
gets(str);
a.emplace_back(string(str));
sam.append(a.back());
}
int id = 0;
for (auto s : a) {
// cerr << s << endl;
int cur = sam.begin;
id++;
for (auto c : s) {
cur = sam.nxt[cur][c - 'a'];
int tmp = cur;
while (tmp > 1 && tag[tmp] != id && tag[tmp] != -1) {
if (tag[tmp] == 0) tag[tmp] = id;
else tag[tmp] = -1;
tmp = sam.link[tmp];
}
}
}
for (int i = sam.begin + 1; i <= sam.tot; ++i) {
// assert(tag[i] != 0);
if (tag[i] != -1) {
res[tag[i]] += sam.len[i] - sam.len[sam.link[i]];
}
}
for (int i = 1; i <= n; ++i) {
printf("%d\n", res[i]);
}
return 0;
}
实战1 :2019 ICPC 南昌站 网络赛 F
https://nanti.jisuanke.com/t/41353
https://blog.csdn.net/weixin_43823767/article/details/101149717
https://blog.csdn.net/m0_37809890/article/details/100639953
思路是推倒期望公式,统计前缀的本质不同子串。
题意
初始有一个长为L的字符串S。
现在执行M次操作,每次操作在S末尾添加一个字符c。
初始和每次操作后,求长N的随机串T的【美丽值】的期望,答案模998244353。
定义T的【美丽值】是所有子串的【价值】之和。
定义一个字符串str的【价值】如下:
如果它不是S子串,那么价值为0,否则为G(|str|),其中G是一个给定的K次多项式函数。
思路
因为是对S扩展,所以从S的所有子串对期望的贡献出发。
对于某个长度为
l
e
n
len
len的子串,其贡献的期望是:
f
(
l
e
n
)
=
(
n
−
l
e
n
+
1
)
∗
G
(
l
e
n
)
2
6
l
e
n
f(len) = \frac{(n - len + 1) * G(len)}{26^{len}}
f(len)=26len(n−len+1)∗G(len)
问题转化成对当前S求,所有本质不同子串的贡献和。
并且这个贡献是和长度有关的一个函数。
回顾本质不同串长度和的求法,SAM中每个状态都代表长度连续的一组子串等价类,类似的可以对 f f f求个前缀和,比如记作 F ( l e n ) F(len) F(len),只要预处理就能每次快速计算出答案了。
每个节点u的贡献可以就这样算:
F
(
l
e
n
[
u
]
)
−
F
(
l
e
n
[
l
i
n
k
[
u
]
]
)
F(len[u]) - F(len[link[u]])
F(len[u])−F(len[link[u]])
可能套路的点就在于,不断扩展一个字符,并且求子串长度有关的东西。
代码
// luogu-judger-enable-o2
#include<stdio.h>
#include<string.h>
using namespace std;
const int N = 6e5 + 59;
typedef long long ll;
struct SAM {
const int begin = 1;
int nxt[N << 1][26];
int link[N << 1];
int len[N << 1];
int las = begin; // last state
int tot = 1; // total states
inline void init() {
las = nuw(begin);
tot = 1;
}
// start from zero
inline void build(char s[], int length) {
for (int i = 0; i < length; ++i) {
this->extend(s[i] - 'a');
}
}
// new node
int nuw(int id) {
memset(nxt[id], 0, sizeof(nxt[id]));
len[id] = link[id] = 0;
return id;
}
inline void extend(int c) {
int p = las;
int cur = nuw(++tot);
len[cur] = len[p] + 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = ++tot;
memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
const int MAXN = 3e5 + 59;
const ll MOD = 998244353;
ll fpow(ll a, ll b) {
ll res = 1;
a %= MOD;
while (b) {
if (b & 1) res = res * a % MOD;
a = a * a % MOD;
b >>= 1;
}
return res;
}
int kase = 0;
int l, k, n, m;
char s[MAXN];
ll p[55];
ll a[MAXN + MAXN];
char ch[2];
int main() {
scanf("%d", &kase);
while (kase--) {
scanf("%d%d%d%d", &l, &k, &n, &m);
scanf("%s", s);
for (int i = 0; i <= k; ++i) {
scanf("%lld", p + i);
}
int slen = l + m;
for (ll i = 1; i <= slen; ++i) {
ll x = 1;
a[i] = 0;
if (i <= n) {
for (ll j = 0; j <= k; ++j) {
a[i] = (a[i] + p[j] * x) % MOD;
x = x * i % MOD;
}
a[i] = (n - i + 1) * a[i] % MOD *
fpow(fpow(26, i), MOD - 2) % MOD;
}
a[i] = (a[i] + a[i - 1]) % MOD;
}
sam.init();
sam.build(s, l);
ll res = 0;
for (int i = 2; i <= sam.tot; ++i) {
res = (res + a[sam.len[i]] + MOD - a[sam.len[sam.link[i]]]) % MOD;
}
printf("%lld\n", res);
while (m--) {
scanf("%s", ch);
sam.extend(ch[0] - 'a');
int i = sam.las;
res = (res + a[sam.len[i]] + MOD - a[sam.len[sam.link[i]]]) % MOD;
printf("%lld\n", res);
}
}
return 0;
}
/*
abbbabbb
*/
实战2 :2019 CCPC Final C (大算法题)
https://blog.csdn.net/m0_37809890/article/details/103515142
对我来说过难,skip了(冠军题)
实战3 :2019 CCPC 网络赛 C (大算法题)
https://blog.csdn.net/weixin_43823767/article/details/102924625
题意
对一个字符串S,询问Q次,每次求子串S[l,r]在S中第k次出现的位置。
思路
这不是就是SAM的parent树dfs序建主席树吗?(震声)
我完全了解了。
套路归套路,其实不难想:
应用SAM的Parent树的一个性质,结点u的子树大小就代表这个结点所代表的子串的出现次数。
然后因为dfs序就可以用主席树求子树上的第k大的位置,所以这道题主要做法就出来了。
但还有个问题是快速找到询问的串所在的最小节点。
因为如果每个子串都反复跳link是比较慢的,可以结合树上ST的操作优化跳link。
思路很明确但就是编码有很大难度。
我对主席树还不是很熟练,skip。
实战 4 :2017 ICPC 沈阳站 网络赛 A
https://blog.csdn.net/zhenlingcn/article/details/77926019
acm.hdu.edu.cn/showproblem.php?pid=6194
题意
给一个字符串,问出现K次的子串有多少个。
思路
统计每个节点子树endpos
的数量,表示节点所代表的每个子串的出现的次数。
在做这个题的过程中,因为我代码经过了一些修整,导致错误地认为子树大小就代表出现次数。实则不然,因为clone
节点不属于endpos
。
endpos
就是那些代表了前缀的点。
代码
/**
* @Source: myself
* @Author: Tieway59
* @Complexity: $O(|S|)$
* @Description:
* 求原串S中出现次数恰号是K的子串的种数
* !注意修改 build 中的字符类型
* !注意extend是int参数
* !注意SAM外数组的大小
*
* @Example:
* 2
* 2
* abcabc
* 3
* abcabcabcabc
*
* 6
* 9
*
* @Verification:
* 2017 ICPC 沈阳站 网络赛 A
* acm.hdu.edu.cn/showproblem.php?pid=6194
*
*/
#include<stdio.h>
#include<string.h>
#include<iostream>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 59;
const ll MOD = 998244353;
const int SAMN = MAXN << 1;
struct SAM {
const int begin = 1;
int nxt[SAMN][26];
int link[SAMN];
int len[SAMN];
int cnt[SAMN];
int las = begin; // last state
int tot = 1; // total states
inline void init() {
las = nuw(begin, false);
tot = 1;
}
// start from zero
inline void build(char s[], int length) {
for (int i = 0; i < length; ++i) {
extend(s[i] - 'a');
}
}
// new node
int nuw(int id, bool is_endpos) {
memset(nxt[id], 0, sizeof(nxt[id]));
len[id] = link[id] = 0;
cnt[id] = int(is_endpos);
return id;
}
inline void extend(int c) {
int p = las;
int cur = nuw(++tot, true);
len[cur] = len[p] + 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = nuw(++tot, false);
memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
int kase;
int n;
int k;
char s[MAXN];
int deg[SAMN];
int stk[SAMN], top;
int main() {
scanf("%d", &kase);
while (kase--) {
scanf("%d", &k);
scanf("%s", s);
n = strlen(s);
top = 0;
sam.init();
sam.build(s, n);
memset(deg, 0, (sam.tot + 5) * sizeof(deg[0]));
for (int i = sam.begin + 1; i <= sam.tot; ++i) {
deg[sam.link[i]] += 1;
}
ll ans = 0;
for (int u = sam.tot; u > sam.begin; --u) {
if (deg[u] == 0 && u != sam.begin) {
stk[top++] = u;
}
}
while (top) {
int v = stk[--top];
int u = sam.link[v];
if (sam.cnt[v] == k) {
ans += sam.len[v] - sam.len[u];
}
sam.cnt[u] += sam.cnt[v];
if (0 == (--deg[u]) && u != sam.begin) {
stk[top++] = u;
}
}
printf("%lld\n", ans);
}
return 0;
}
实战 5:2019 ICPC Latin American Regional Contests G
https://codeforc.es/gym/102428/problem/G
题意
给定一个串,然后有n个串,对每个串询问最少需要几个原串中的子串可以拼成目标串
思路
SAM有个性质在于,一个串T从SAM的初始状态开始匹配,能找到与T最长前缀相同的子串。
对于每个询问,从SAM的出是状态开始匹配,到找不到就回到初始状态,就可以保证每次都是取到最长的匹配,也就能保证匹配段数最少了。
代码
/**
* @Source: myself
* @Author: Tieway59
* @Complexity:
* @Description:
* 给一个串S,询问Q个串ti最少需要几个S的字串可以拼成。
* |S| -> 2e5, sum(|ti|) -> 2e5
* @Example:
*
* @Verification:
* https://codeforces.com/gym/102428/problem/G
*/
#include<stdio.h>
#include<string.h>
#include<iostream>
using namespace std;
typedef long long ll;
const int MAXN = 2e5 + 59;
const ll MOD = 998244353;
const int SAMN = MAXN << 1;
struct SAM {
const int begin = 1;
int nxt[SAMN][26];
int link[SAMN];
int len[SAMN];
// int cnt[SAMN];
int las = begin; // last state
int tot = 1; // total states
inline void init() {
las = nuw(begin, false);
tot = 1;
}
// start from zero
inline void build(char s[], int length) {
for (int i = 0; i < length; ++i) {
extend(s[i] - 'A'); // char type
}
}
// new node
int nuw(int id, bool is_endpos) {
memset(nxt[id], 0, sizeof(nxt[id]));
len[id] = link[id] = 0;
// cnt[id] = int(is_endpos);
return id;
}
inline void extend(int c) {
int p = las;
int cur = nuw(++tot, true);
len[cur] = len[p] + 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = nuw(++tot, false);
memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
int n;
char s[MAXN], t[MAXN];
int main() {
scanf("%s", s);
scanf("%d", &n);
sam.build(s, strlen(s));
for (int _ = 0; _ < n; ++_) {
scanf("%s", t);
int len = strlen(t);
int i = 0;
int j = sam.begin;
int res = 1;
// printf("%s\n", t);
while (i < len) {
int ch = t[i] - 'A';
// printf("%c %d\n", t[i], j);
if (sam.nxt[j][ch] == 0) {
j = sam.begin;
if (sam.nxt[j][ch] == 0) {
res = -1;
break;
} else {
res++;
}
}
j = sam.nxt[j][ch];
i = i + 1;
}
printf("%d\n", res);
}
return 0;
}
/*
SANTIAGO
3
TITA
SANTIAGO
NAS
*/
实战 6:2018 ICPC 焦作站 网络赛 H
https://nanti.jisuanke.com/t/A2018
题意
求原串S中出现次数在[A,B]之间的子串的个数
思路
统计cnt,拓扑序即可。
但是这道题数据范围其实不太明确,得赌一下1e5。
代码
/**
* @Source: myself
* @Author: Tieway59
* @Complexity: $O(|S|)$
* @Description:
* 求原串S中出现次数在[A,B]之间的子串的种数
* !注意修改 build 中的字符类型
* !注意extend是int参数
* !注意SAM外数组的大小
*
* @Example:
* AAA 2 3
* ABAB 2 2
*
* 2
* 3
*
* @Verification:
* 2018 ICPC 焦作站 网络赛 H
* https://nanti.jisuanke.com/t/A2018
*
*/
#include<stdio.h>
#include<string.h>
#include<iostream>
using namespace std;
typedef long long ll;
const int MAXN = 1e5 + 59;
const ll MOD = 998244353;
const int SAMN = MAXN << 1;
struct SAM {
const int begin = 1;
int nxt[SAMN][26];
int link[SAMN];
int len[SAMN];
int cnt[SAMN];
int las = begin; // last state
int tot = 1; // total states
inline void init() {
las = nuw(begin, false);
tot = 1;
}
// start from zero
inline void build(char s[], int length) {
for (int i = 0; i < length; ++i) {
extend(s[i] - 'A'); // char type
}
}
// new node
int nuw(int id, bool is_endpos) {
memset(nxt[id], 0, sizeof(nxt[id]));
len[id] = link[id] = 0;
cnt[id] = int(is_endpos);
return id;
}
inline void extend(int c) {
int p = las;
int cur = nuw(++tot, true);
len[cur] = len[p] + 1;
las = cur;
while (p > 0 && !nxt[p][c]) {
nxt[p][c] = cur;
p = link[p];
}
if (p == 0) {
link[cur] = 1;
} else {
int q = nxt[p][c];
if (len[q] == len[p] + 1) {
link[cur] = q;
} else {
int clone = nuw(++tot, false);
memcpy(nxt[clone], nxt[q], sizeof(nxt[clone]));
link[clone] = link[q];
len[clone] = len[p] + 1;
link[q] = link[cur] = clone;
while (p > 0 && nxt[p][c] == q) {
nxt[p][c] = clone;
p = link[p];
}
}
}
}
} sam;
int A, B;
char s[MAXN];
int stk[SAMN], top; // 小心范围
int deg[SAMN]; // 小心范围
int main() {
while (scanf("%s %d %d", s, &A, &B) != EOF) {
sam.init();
sam.build(s, strlen(s));
top = 0;
for (int i = 0; i <= sam.tot; ++i) {
deg[i] = 0;
}
for (int i = 2; i <= sam.tot; ++i) {
deg[sam.link[i]]++;
}
for (int i = 2; i <= sam.tot; ++i) {
if (deg[i] == 0) {
stk[++top] = i;
}
}
ll ans = 0;
while (top) {
int u = stk[top--];
sam.cnt[sam.link[u]] += sam.cnt[u];
if (0 == (--deg[sam.link[u]]) &&
sam.link[u] != sam.begin) {
stk[++top] = sam.link[u];
}
if (A <= sam.cnt[u] && sam.cnt[u] <= B) {
ans += sam.len[u] - sam.len[sam.link[u]];
}
}
printf("%lld\n", ans);
}
return 0;
}
代办
广义后缀自动机,我想分一篇来做了。
https://blog.csdn.net/qq_43544481/article/details/103451148
https://www.cnblogs.com/SovietPower/p/10772320.html#hberos-file-suggestion%E5%90%8E%E7%BC%80%E8%87%AA%E5%8A%A8%E6%9C%BA