最后我们来介绍 SAM 的转移函数。对于一个状态 st ,我们首先找到从它开始下一个遇到的字符可能是哪些。我们将 st 遇到的下一个字符集合记作 next(st) ,有 next(st)={S[i+1]|i∈endpos(st)} 。例如 next(S)={S[1],S[2],S[3],S[4],S[5],S[6],S[7]}={a,b,d} ,next(8)={S[4],S[7]}={b,d} 。
一些性质
对于一个状态 st 来说和一个 next(st) 中的字符 c ,你会发现 substrings(st) 中的所有子串后面接上一个字符 c 之后,新的子串仍然都属于同一个状态。
所以我们对于一个状态 st 和一个字符 c∈next(st) ,可以定义转移函数 trans(st,c)={x|longest(st)+c∈substrings(x)} 。换句话说,我们在 longest(st)(随便哪个子串都会得到相同的结果)后面接上一个字符 c 得到一个新的子串 s ,找到包含 s 的状态 x ,那么 trans(st,c) 就等于x 。
证明: 我们计算 连续的 转移个数。考虑以 S 为初始节点的自动机的最长路径树。这棵树将包含所有连续的转移,树的边数比结点个数小 1 ,这意味着连续的转移个数不超过 2n−2 。
我们再来计算 不连续 的转移个数。考虑每个不连续转移;假设该转移——转移 (p,q) ,标记为 c 。对自动机运行一个合适的字符串 u+c+w ,其中字符串 u 表示从初始状态到 p 经过的最长路径,w 表示从 q 到任意终止节点经过的最长路径。
一方面,对所有不连续转移,字符串 u+c+w 都是不同的(因为字符串 u 和 w 仅包含连续转移)。另一方面,每个这样的字符串 u+c+w ,由于在终止状态结束,它必然是完整串 s 的一个后缀。由于 s 的非空后缀仅有 n 个,并且完整串 s 不能是某个 u+c+w (因为完整串 s 匹配一条包含 n 个连续转移的路径),那么不连续转移的总共个数不超过 n−1 。
有趣的是,仍然存在达到转移个数上限的数据:abbb...bbbc–––––––––––– 。
这个证明其实我是没太懂的。。记下结论吧。
代码实现
我们令 id 为这次插入字符的编号,trans,maxlen,link 意义同上。Last 为上次最后插入的状态的编号,Size 为当前的状态总数,clone 为复制节点即上文的 y 。 具体来说如下代码所示:
minlen 可以最后计算 ,因为我们是从 link 处断开的,所以显然有 minlen[i]=maxlen[link[i]]+1 。
structSuffix_Automata {
int maxlen[Maxn], trans[Maxn][26], link[Maxn], Size, Last;
Suffix_Automata() { Size = Last = 1; }
inlinevoidExtend(int id){
int cur = (++ Size), p;
maxlen[cur] = maxlen[Last] + 1;
for (p = Last; p && !trans[p][id]; p = link[p]) trans[p][id] = cur;
if (!p) link[cur] = 1;
else {
int q = trans[p][id];
if (maxlen[q] == maxlen[p] + 1) link[cur] = q;
else {
int clone = (++ Size);
maxlen[clone] = maxlen[p] + 1;
Cpy(trans[clone], trans[q]);
link[clone] = link[q];
for (; p && trans[p][id] == q; p = link[p]) trans[p][id] = clone;
link[cur] = link[q] = clone;
}
}
Last = cur;
}
} T;
不难他的后缀链接组成了一个 DAG 图。并且它反向建那么就是一颗以 S 为根的树(因为除了 S 每个点有且仅有一个出边,并且不可能存在环,因为 maxlen[link[i]]<maxlen[i] ),我们称之为后缀树。
前面讲过了我们每次是暴力把路径上的所有点权值 +1 。我们就能转化成 DAG 每一个点对于它能走的路径上的所有点 +1 ,这个直接考虑在 DAG 图上进行拓扑 dp 就行了。
但注意 clone 的节点是不能对它到 S 的路径上有单独贡献的,因为它的贡献会在它的本体上计算一遍。
然后这题是要计算对于所有 i 长度为 i 子串个数,那么不难发现一个状态 st 包含的是长度为 [minlen(st),maxlen(st)] 的子串,那么它对于 minlen(st)≤k≤maxlen(st) 的长度的答案具有贡献。这个我们打个区间取 max 就行了。这样要写一个线段树比较麻烦,但我们发现对于长度更大 ans 我当前肯定也是可以使用的,一开始把标记打在 maxlen 上,直接最后倒着取 max 就行了。
至此这道题就做完啦。复杂度为 O(n) 比排序 len 的复杂度 O(nlogn) 要优秀。
vector<int> G[Maxn]; int indeg[Maxn];
voidBuild(){
For (i, 1, Size)
G[i].push_back(link[i]), ++ indeg[link[i]];
}
voidTopo(){
queue<int> Q; Build();
For (i, 0, Size) if (!indeg[i]) Q.push(i);
while (!Q.empty()) {
int u = Q.front(); Q.pop();
for (int v : G[u]) {
val[v] += val[u];
if (!(-- indeg[v])) Q.push(v);
}
}
For (i, 1, Size) chkmax(Tag[maxlen[i]], val[i]);
Fordown (i, n, 1) chkmax(Tag[i], Tag[i + 1]);
}
有些串会计算多次。例如 a–– ,将其倍长后为 aa––– 。我们会计算两次 a ,此时只要对 SAM 中被统计的状态打个标记就行了(也就是记一下现在被哪个版本统计过)。
有些串不该被算却被计算了。同上 a–– 倍长后为 aa––– ,我们会把 aa––– 也计算进来,这样显然是不行的。所以我们每次得到了一个 LCS 后如果长度 l≥|T| 那么我们不断尝试跳 link 直到第一个 u 刚好满足 l≥|T| 就可以了。
int version[Maxn];
ll Calc(char *str, int num){
ll res = 0; int u = 1, lcs = 0, len = strlen(str + 1), bas = len >> 1;
For (i, 1, len) {
int id = str[i] - 'a';
if (trans[u][id]) u = trans[u][id], ++ lcs;
else {
for (; u && !trans[u][id]; u = link[u]) ;
if (!u) { u = 1; lcs = 0; }
else lcs = maxlen[u] + 1, u = trans[u][id];
}
if (lcs >= bas) {
while (maxlen[link[u]] >= bas) lcs = maxlen[u = link[u]];
if (version[u] != num) version[u] = num, res += times[u];
}
}
return res;
}
我们首先确定 A 的串应该是什么,我们从高到低依次枚举每一位,判断是否在需要走入其中。具体来说我们假设当前到了 SAM 的第 u 个点需要取字典序第 k 小的字符串,在当前这个点的结束条件是 B.tot[A.SG[u]][S][0]≤k 也就是意味着对于这个点能取胜的总方案数是 B 中 SG 不和 A.SG[u] 相等的子串数。然后如果在当前节点结束不了,那么我们先减去这一部分的贡献。然后枚举接下来那一位,选择这个节点的贡献就是 c+1∑i=0A.tot[v][i][1]×B.tot[1][i][0] 也是就走完这一步后手必败的方案数之和,然后判一下大小就行了。
接下来只需要确定 B 串了,我们只需要用之前最后确定 A 串的 SG 函数去算就行了,具体见代码。(似乎写的有点长。。。凑合看吧。。。)
时间复杂度 O((|A|+|B|)c2) ,c 为字符集大小。
#include<bits/stdc++.h>#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)#define Set(a, v) memset(a, v, sizeof(a))#define Cpy(a, b) memcpy(a, b, sizeof(a))#define debug(x) cout << #x << ": " << x << endlusingnamespace std;
inlineboolchkmin(int &a, int b){return b < a ? a = b, 1 : 0;}
inlineboolchkmax(int &a, int b){return b > a ? a = b, 1 : 0;}
typedeflonglong ll;
inline ll read(){
ll x = 0, fh = 1; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * fh;
}
voidFile(){
#ifdef zjp_shadowfreopen ("1466.in", "r", stdin);
freopen ("1466.out", "w", stdout);
#endif}
constint N = 1e5 + 1e3, Maxn = N << 1, spc = 25;
structSuffix_Automata {
int trans[Maxn][spc + 1], maxlen[Maxn], minlen[Maxn], link[Maxn], Size, Last;
Suffix_Automata() { Last = Size = 1; }
inlinevoidExtend(int id){
int cur = (++ Size), p;
maxlen[cur] = maxlen[Last] + 1;
for (p = Last; p && !trans[p][id]; p = link[p]) trans[p][id] = cur;
if (!p) link[cur] = 1;
else {
int q = trans[p][id];
if (maxlen[q] == maxlen[p] + 1) link[cur] = q;
else {
int clone = (++ Size);
maxlen[clone] = maxlen[p] + 1;
Cpy(trans[clone], trans[q]);
link[clone] = link[q];
for (; p && trans[p][id] == q; p = link[p]) trans[p][id] = clone;
link[cur] = link[q] = clone;
}
}
Last = cur;
}
int SG[Maxn], lis[Maxn], indeg[Maxn], cnt; ll tot[Maxn][spc + 2][2];
voidGet_SG_Tot(){
queue<int> Q;
cnt = 0; Q.push(1);
For (i, 1, Size) For (j, 0, spc) if (trans[i][j]) ++ indeg[trans[i][j]];
while (!Q.empty()) {
int u; u = lis[++ cnt] = Q.front(); Q.pop();
For (i, 0, spc) {
int v = trans[u][i];
if (!v) continue ;
if (!(--indeg[v])) Q.push(v);
}
}
bitset<spc + 2> App;
Fordown (i, cnt, 1) {
int u = lis[i]; App.reset();
For (j, 0, spc) {
registerint v = trans[u][j];
if (v) {
App[SG[v]] = true;
For (k, 0, spc + 1)
tot[u][k][1] += tot[v][k][1];
}
}
for (int j = 0; ; ++ j)
if (!App[j]) { SG[u] = j; break; }
ll sum = 0;
++ tot[u][SG[u]][1];
For (i, 0, spc + 1) sum += tot[u][i][1];
For (i, 0, spc + 1) tot[u][i][0] = sum - tot[u][i][1];
}
}
voidOut(){
For (i, 1, Size) {
debug(i);
For (j, 0, 5) printf ("%lld%c", tot[i][j][1], j == jend ? '\n' : ' ');
debug(SG[i]);
}
}
} A, B;
char ansa[N], ansb[N];
ll k;
intGet_A(int u, int cur){
ll cnt = B.tot[1][A.SG[u]][0];
if (k <= cnt) return u; k -= cnt;
For (i, 0, spc) {
int v = A.trans[u][i]; if (!v) continue ;
ll now = 0;
For (i, 0, spc + 1)
now += 1ll * A.tot[v][i][1] * B.tot[1][i][0];
if (now < k) k -= now;
else { ansa[cur] = i + 'a'; returnGet_A(v, cur + 1); }
}
return0;
}
voidGet_B(int u, int cur, int val){
k -= (val != B.SG[u]);
if (!k) return ;
For (i, 0, spc) {
int v = B.trans[u][i]; if (!v) continue ;
ll now = B.tot[v][val][0];
if (now < k) k -= now;
else { ansb[cur] = i + 'a'; Get_B(v, cur + 1, val); return ; }
}
}
char str[N];
intmain(){
File();
k = read();
scanf ("%s", str + 1);
For (i, 1, strlen(str + 1)) A.Extend(str[i] - 'a');
A.Get_SG_Tot();
A.Out();
scanf ("%s", str + 1);
For (i, 1, strlen(str + 1)) B.Extend(str[i] - 'a');
B.Get_SG_Tot();
int pos = Get_A(1, 1); if (!pos) returnputs("NO"), 0; Get_B(1, 1, A.SG[pos]);
printf ("%s\n", ansa + 1);
printf ("%s\n", ansb + 1);
return0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效