SAM
前言
- 我将用一天来研究流萤
- \(\text{sbmqwm}\):这是哪个萨姆?!

- \(\text{Upd}\space \text{on}\space 10.30\):修 \(\LaTeX\)
\(\text{SAM}\)
- 流萤可爱捏
我博客园图片库居然没有流萤的库存- 对于一个字符串的所有后缀,我们希望在 \(O(n)\) 的时空构建出一种结构记录,\(\text{SAM}\) 就是维护这个的东西,本质是一张 \(\text{DAG}\)(有向无环图),含有一个起始节点和多个终点,保存字符串的所有后缀的最小 \(\text{DFA}\)(确定性有限状态自动机)
在构建 \(\text{SAM}\) 前
- 后文为了好理解,将原本定义的状态对应为节点,转移对应为边
- 直接维护这个东西怎么做
- 我们可以构建后缀 \(\text{Trie}\),这样就方便维护,但是这样时空复杂度有 \(O(n^2)\) 是爆炸的
- 我们发现其实 \(\text{Trie}\) 上记录了过多的重复节点导致爆炸,所以 \(\text{SAM}\) 就将这些节点压缩来保证复杂度
与 \(\text{SAM}\) 有关的定义
- 字符串下标从 \(1\) 开始
不一致就是左右脑互搏了
\(\text{endpos}\)
- 我们记 \(\text{endpos}(t)\) 为字符串 \(s\) 中子串 \(t\) 的结束位置的集合
- 比如 \(s=qwqwq\),\(\text{endpos}(qw) = \{2,4\}\)
- 等价类:\(s\) 的所有非空子串可以根据它们的 \(\text{endpos}\) 分为若干等价类
- 有三个结论
结论 \(1\)
- \(s\) 的两个非空子串 \(t_1,t_2\space (\mid t_1 \mid \le \mid t_2 \mid)\) 的 \(\text{endpos}\) 相同(即 \(\text{endpos}(t_1) = \text{endpos}(t_2)\)),此时 \(t_1\) 一定是 \(t_2\) 的后缀
- 这个结论挺简单,不证了
结论 \(2\)
- \(s\) 的两个非空子串 \(t_1,t_2\space (\mid t_1 \mid \le \mid t_2 \mid)\),有:
\[\begin{cases}
\text{endpos}(t_2) \subseteq \text{endpos}(t_1) & t_1 \in t_2'\space s\> gather\> of\> suffix\\
\text{endpos}(t_2) \cap \text{endpos}(t_1) = \varnothing & other
\end{cases}\]
- 你发现和结论 \(1\) 非常有关系,也不证了
结论 \(3\)
- 对于任意的等价类,将其中的所有子串按长度从大到小排序,则每一个子串的长度均为上一个子串的长度 \(-1\),且为上一个子串的后缀,即等价类中短的是长的后缀,子串长度连续
证明
- 定理 \(1\) 其实就是等价类,从中我们知道了等价类中所有元素均不等长
- 从定理 \(2\) 的结论倒退,得到后缀的证明,不写了咕咕咕
\(link\)
- 对于一个节点 \(x\) 表示的等价类,令 \(w\) 为这个等价类元素中长度最长的那个
- 对于 \(w\) 的后缀,有一部分属于这个等价类,一部分不属于,我们令不属于这个等价类最长的 \(w\) 的后缀为 \(t\)
- 定义 \(\text{link}(x) = endpos(t)\)
- 并定义初始节点 \(t_0\) 对应的等价类只包含一个空串,且 \(\text{endpos}(t_0)=\{0,\ldots,\mid s\mid\}\)
- 我们发现所有的 \(\text{link}\) 会连成一棵以 \(t_0\) 为根的树,称为 \(\text{parent}\) 树
- 这坨石太难理解了,我们模个例子
- 例子:\(s=abab\)
- 先得到 \(\text{endpos}\)
| \(suffix\) | \(\text{endpos}\) |
|---|---|
| \(a\) | \(\{1,3\}\) |
| \(b\) | \(\{2,4\}\) |
| \(ab\) | \(\{2,4\}\) |
| \(ba\) | \(\{3\}\) |
| \(aba\) | \(\{3\}\) |
| \(bab\) | \(\{4\}\) |
| \(abab\) | \(\{4\}\) |
- 合并成等价类
| \(equivalence\space class\) | \(suffix\) |
|---|---|
| \(\{1,3\}\) | \(a(longest)\) |
| \(\{2,4\}\) | \(b,ab(longest)\) |
| \(\{3\}\) | \(ba,aba(longest)\) |
| \(\{4\}\) | \(bab,abab(longest)\) |
- 其中 \(0\) 号节点 \(\text{endpos}(t_0) = {0,1,2,3,4}\)
- 连 \(\text{link}\)
- \(t_0\) 编号为 \(0\)
- 第一个等价类 \(\{1,3\}\),编号为 \(1\),\(a\) 的真后缀是
滚木空,即 \(1 \to 0\) - 第二个等价类 \(\{2,4\}\),编号为 \(2\),\(ab\) 的真后缀是 \(b\),同等价类,再往后是空集,即 \(2 \to 0\)
- 第三个等价类 \(\{3\}\),编号为 \(3\),\(aba\) 的真后缀是 \(ba\),同等价类,往后是 \(a\),即 \(3 \to 1\)
- 第四个等价类 \(\{4\}\),编号为 \(4\),\(abab\) 的真后缀是 \(bab\),同等价类,往后是 \(ab\),即 \(4 \to 2\)
- 图来

引理
- 每个子节点的 \(\text{endpos}\) 都包含在父节点的 \(\text{endpos}\) 中,这个从 \(\text{link}\) 的定义可知
- \(\text{parent}\) 树构建了 \(\text{endpos}\) 间的关系,使各个等价类产生联系
\(\text{SAM}\) 的构建
- 终于开始构造 \(\text{SAM}\) 了。。。
- 构建需要 \(\text{DAG}\) 和 \(\text{parent}\) 树协同进行
- 我们将初始节点 \(t_0\) 的 \(\text{len}(t_0) = 0,\text{link}(t_0) = -1\)
- 设 \(last\) 表示在加入新的字符 \(c\) 之前,整串对应的节点
- 设 \(cur\) 为新建的节点
- 流程如下:
- 创建新节点 \(cur\),将 \(\text{len}(cur) = \text{len}(last)+1\)
- 从 \(last\) 开始跳 \(\text{link}\),直到 \(-1\) 或此节点有 \(c\) 的边停止
- 停止时分三种情况,设当前节点为 \(p\):
- 直接到了 \(-1\),即 \(p=-1\),\(\text{link}\) 置为 \(0\),\(last\) 置为 \(cur\),结束,称为情况 \(1\)
- 到了一个有边 \(c\) 的节点,设 \(q\) 为这个节点经过 \(c\) 转移到的节点,开始检查是否需要进行 \(\text{clone}\) 操作(为什么要克隆等会讲)
- 当 \(\text{len}(p)+1 = \text{len}(q)\) 时,可以被直接转移,将 \(\text{link}(cur) = q\),\(last\) 置为 \(cur\),结束,称为情况 \(2\)
- 当 \(\text{len}(p)+1 < \text{len}(q)\) 时,原来的 \(\text{endpos}\) 无法表示等价类,即产生了不同的等价类,称为情况 \(3\),需要进行克隆分裂,将原等价类划给原节点,克隆节点放新的等价类,等会详细讲,克隆流程如下:
- 新建克隆节点 \(\text{clone}\),将 \(\text{len}(clone) = \text{len}(p)+1\),完全复制 \(q\) 的转移,\(\text{link}\)
- 进行重定向,条件为 \(p \neq -1 \land transfer\space to\space q\),操作是将转移向的节点从 \(q\) 改成 \(clone\)
- 完成后,将 \(\text{link}(q) = clone,\text{link}(cur) = clone\),\(last\) 置为 \(cur\),结束
- 其实你发现情况 \(1\) 和情况 \(2\) 是相似的,但是为了说克隆操作,把这俩分开了
- 难理解,我们算一个例子
- \(\text{sbmqwm}\):我将,____
- \(\text{msjing}\):别捣乱你起开
例:\(s=aabb\)
初始
- \(cur=0,\text{len}(cur)=0,\text{link}(cur)=-1,last=0\)
加入 \(c=\>'\!a'\)
- \(cur=1,\text{len}(cur)=\text{len}(last)+1=1\)
- \(p=last=0\) 开始跳 \(\text{link}\)
- \(p=0\) 没有 \(c\) 的边,添加从 \(p\) 经过 \(c\) 到 \(cur\) 的边,跳 \(\text{link}\)
- \(p=-1\),停止,满足情况 \(1\),\(\text{link}(cur)=0,last=1\)
- 用红色表示 \(\text{DAG}\) 的边(转移),蓝色表示 \(\text{link}\),此时图长这样:

加入 \(c=\>'\!a'\)
- \(cur=2,\text{len}(cur)=\text{len}(last)+1=2\)
- \(p=last=1\) 开始跳 \(\text{link}\)
- \(p=1\) 没有 \(c\) 的边,添加从 \(p\) 经过 \(c\) 到 \(cur\) 的边,跳 \(\text{link}\)
- \(p=0\) 有 \(c\) 的边,将 \(q\) 置为 \(1\),进入是否需要进行克隆的判断
- \(\text{len}(p)+1=\text{len}(q)=1\),不需要克隆,情况 \(2\),直接接上,\(\text{link}(cur)=q=1,last=2\)
- 此时图长这样:

加入 \(c=\>'\!b'\)
- \(cur=3,\text{len}(cur)=\text{len}(last)+1=3\)
- \(p=last=2\) 开始跳 \(\text{link}\)
- \(p=2\) 没有 \(c\) 的边,添加从 \(p\) 经过 \(c\) 到 \(cur\) 的边,跳 \(\text{link}\)
- \(p=1\) 没有 \(c\) 的边,添加从 \(p\) 经过 \(c\) 到 \(cur\) 的边,跳 \(\text{link}\)
- \(p=0\) 没有 \(c\) 的边,添加从 \(p\) 经过 \(c\) 到 \(cur\) 的边,跳 \(\text{link}\)
- \(p=-1\),停止,满足情况 \(1\),\(\text{link}(cur)=0,last=3\)
- 此时图长这样:

加入 \(c=\>'\!b'\)
- \(cur=4,\text{len}(cur)=\text{len}(last)+1=4\)
- \(p=last=3\) 开始跳 \(\text{link}\)
- \(p=3\) 没有 \(c\) 的边,添加从 \(p\) 经过 \(c\) 到 \(cur\) 的边,跳 \(\text{link}\)
- \(p=0\) 有 \(c\) 的边,将 \(q\) 置为 \(3\),进入是否需要进行克隆的判断
- \(\text{len}(p)+1=1,\text{len}(q)=3,\text{len}(p)+1 \neq \text{len}(q)\),需要克隆,情况 \(3\)
- 哦哦哦哦哦哦哦哦哦开始克隆!!哦哦哦哦哦哦哦哦哦!!!
- \(clone=5,\text{len}(clone)=\text{len}(p)+1=1\)
- 将所有 \(q\) 的转移(这里说转移没有歧义)全部赋给 \(clone\),由于 \(q\) 有一个经过 \('b'=c\) 到 \(4=cur\) 的边,所以 \(clone\) 也具有
- 同样将 \(q\) 的 \(\text{link}\) 赋给 \(clone\),有 \(\text{link}(clone) = \text{link}(q) = 0\)
- 开始重定向嗷嗷嗷嗷嗷嗷嗷啊啊啊啊啊啊!!!
- \(\text{sbmqwm}\):\(\text{msjing}\) 魔了
- \(p=0\),满足条件,重置从 \(p\) 经过 \(c\) 到 \(q\) 成从 \(p\) 经过 \(c\) 到 \(clone\)
- \(p=-1\),停止,将 \(\text{link}(q)\) 和 \(\text{link}(cur)\) 赋为 \(clone\),\(last=4\)
- 此时图长这样:

为什么要克隆?
- 在上面的例子中,我们加入第二个 \(b\),原来的 \(\text{endpos}\{3\} = \{b,ab,aab\}\),但是加入了第二个 \(b\),\(b\) 的 \(\text{endpos} = \{3,4\}\),与原来的不为同种等价类,必须分出这些

- 到现在已经过去 \(5h\) 了我还什么代码都没写。。。
- \(2h\) 后终于写出来了。。。
- \(\text{Upd}\space \text{on}\space 10.30\):统一马蜂
点击查看代码
namespace SAM
{
int p,q,cur,lt,cl,tot;
int t[maxn][26],lk[maxn],len[maxn],siz[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
len[0]=0,lk[0]=-1;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot;
len[cur]=len[lt]+1,siz[cur]=1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p)) lk[cur]=0;
else
{
if (len[q] == len[p]+1)
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
}
}
}using namespace SAM;
- \(\text{sbmqwm}\):我将,____
- \(\text{msjing}\):巡征追猎
- \(\text{sbmqwm}\):?
复杂度
- 忘写了。。。
- 不给严格证明了,这玩意没啥用
- 时间复杂度为 \(O(n)\),总空间复杂度为 \(O(n\mid \Sigma \mid)\)
应用 \(\text{SAM}\)
- \(\text{SAM}\) 啥都能干!
检查子串
- 直接对文本串建立 \(\text{SAM}\),在边(不是 \(\text{parent}\) 树哦,\(\text{parent}\) 树是维护 \(\text{endpos}\) 的)上按模板串走,走不通就不是,走得通就是
其实有个函数能判
统计子串个数
板子
- 做法是直接在 \(\text{parent}\) 树上统计子树大小,因为一个子串出现次数为对应的 \(\text{endpos}\) 大小,\(\text{parent}\) 树上子是父的子集,统计时统计答案,注意克隆节点不能加入贡献,要不然重复统计,不能全挂 \(long\space long\) 要不然爆空间,不能不开要不然掉,答案开就行
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define pb push_back
using namespace std;
constexpr int maxn=2e6+10;
namespace SAM
{
int p,q,cur,lt,cl,tot;
int t[maxn][26],lk[maxn],len[maxn],siz[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
len[0]=0,lk[0]=-1;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot;
len[cur]=len[lt]+1,siz[cur]=1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p)) lk[cur]=0;
else
{
if (len[q] == len[p]+1)
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
}
}
}using namespace SAM;
vector<int> e[maxn];
long long ans;
void dfs(int x)
{
for (int y:e[x])
dfs(y),siz[x]+=siz[y];
if (siz[x]!=1) ans=max(ans,1ll*siz[x]*len[x]);
}
int main()
{
#ifdef LOCAL
freopen("sample.in","r",stdin);
freopen("sample.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
string s;cin >> s;
BuildSAM(s);
for (int i=1;i<=tot;i++)
e[lk[i]].pb(i);
dfs(0);
cout << ans << endl;
return 0;
}
P4070 [SDOI2016] 生成魔咒
- 这个动态加点太像 \(\text{SAM}\) 了,不过有几个问题:
- 这家伙 \(x_i\) 好大。。。
- 在线询问
- 第一个问题直接请出 \(map\) 大人创过去,第二个有点说法
- 由于 \(\text{SAM}\) 上每个等价类中的子串长度恰好覆盖一个整数区间,所以每加入一个节点,贡献为 \(\text{len}(cur)-\text{len}(\text{link}(cur))\)
- 这个结论挺重要,说一下
- 首先,一个 \(v\) 覆盖的区间是 \((\text{len}(\text{link}(v)),\text{len}(v)]\),这是因为一个等价类是一段后缀,而这一段后缀每次减少 \(1\) 的长度,这个后缀被分裂出去(就是构建的情况 \(3\))的可能性变大,而其会变成 \(v\) 在 \(\text{parent}\) 树上的父亲,即 \(\text{link}(v)\),假设分裂出去的后缀长 \(k\),所有比 \(k\) 小的全部都被分裂出去,被分裂出去的最长的后缀成为了分界,故有上述结论的前半句
- 后半句就简单了,由于我们新加入了一个元素,\(cur\) 表示的是全新的,而 \(\text{link}(cur)\) 是之前的,差值为本质不同的后缀数量
- 注意输入不是一个字符
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define pb push_back
#define endl '\n'
using namespace std;
constexpr int maxn=2e5+10;
int ans;
namespace SAM
{
int cur,lt,p,q,cl,tot;
int len[maxn];
int lk[maxn];
map<int,int> t[maxn];
int rk[maxn];
void BuildSAM(int c)
{
cur=++tot;
len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
t[cl]=t[q];
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[q]=lk[cur]=cl;
}
}
lt=cur;
ans+=len[cur]-len[lk[cur]];
}
}using namespace SAM;
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int n;cin >> n;
len[cur]=0,lk[cur]=-1,lt=0;
for (int i=1;i<=n;i++)
{
int c;cin >> c;
BuildSAM(c);
cout << ans << endl;
}
return 0;
}
求子串排名
- 求后缀排名 \(\text{SA}\) 还挺好用的,我等会看看子串排名咋用 \(\text{SA}\)。。。
P3975 [TJOI2015] 弦论
- 子串排名,偶吼吼或或或吼吼吼吼哦哦哦
- 我们统计一个 \(w\) 表示本质不同的子串个数,说白了就是 \(siz\) 的前缀,\(siz\) 后面还用
- \(t=0\) 时是将所有的 \(siz\) 和 \(w\) 全置为 \(1\)(当然,根除外)
- \(t=1\) 时就是正常统计
- 现在是查 \(\text{kth}\)
- 对于一个节点 \(x\),先减去这个节点的 \(siz\),再找所有 \(x\) 的转移边,假设转移到 \(y\)
- 如果 \(k \le w_y\),可以,记录或直接输出字符,并递归继续查找
- 否则直接让 \(k\) 减去 \(w_y\)
- 无解是 \(k>w_x\)
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
constexpr int maxn=1e6+10;
namespace SAM
{
int cur,p,q,cl,lt,tot;
int len[maxn],t[maxn][26],lk[maxn];
int cpy[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
len[cur]=0,lk[cur]=-1,lt=0;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot,len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
cpy[cl]=1;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
}
}
}using namespace SAM;
int h[maxn],to[maxn],nxt[maxn],total;
void addedge(int x,int y)
{
total++;
to[total]=y;
nxt[total]=h[x];
h[x]=total;
}
int w[maxn],siz[maxn];
void dfs1(int x)
{
for (int i=h[x];i;i=nxt[i])
{
int y=to[i];
dfs1(y);
siz[x]+=siz[y];
}
w[x]=siz[x];
}
int vis[maxn];
int dfs2(int x)
{
if (vis[x]) return w[x];
vis[x]=1;
for (int i=0;i<26;i++)
{
if (!t[x][i]) continue;
w[x]+=dfs2(t[x][i]);
}
return w[x];
}
void kth(int x,int k)
{
if (k>w[x]) {cout << "-1" << endl;exit(0);}
if (k<=siz[x]) return;
k-=siz[x];
for (int i=0;i<26;i++)
{
if (!t[x][i]) continue;
if (k<=w[t[x][i]])
{
cout << (char)(i+'a');
kth(t[x][i],k);
return;
}
else
k-=w[t[x][i]];
}
}
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
string s;cin >> s;
BuildSAM(s);
int t,k;cin >> t >> k;
for (int i=1;i<=tot;i++)
addedge(lk[i],i);
if (!t)
for (int i=1;i<=tot;i++)
w[i]=siz[i]=1;
else
{
for (int i=1;i<=tot;i++)
if (!cpy[i])
w[i]=siz[i]=1;
dfs1(0);
w[0]=siz[0]=0;
}
dfs2(0);
kth(0,k),cout << endl;
return 0;
}
- \(\text{msjing}\) \(on \space 26.10.14 \space 20:50\):好累啊。。。
最小循环移位
- 把串复制一遍建 \(\text{SAM}\) 贪心跳最小的字符
子串出现次数
- 对文本串建立 \(\text{SAM}\),然后直接拿模式串上去匹配,匹配完之后这个节点的 \(\text{endpos}\) 大小就是,匹配不上就是 \(0\)
两串最长公共子串
- 杭电 \(\text{OJ}\) 不能用万能头,还是用 \(\text{SPOJ}\) 吧,严肃忍受
SPOJ LCS - Longest Common Substring
- 设俩串分别为 \(s,t\)
- 对 \(s\) 建 \(\text{SAM}\),拿 \(t\) 上去匹配,能转移就让长度 \(+1\),转移不了就一直跳 \(\text{link}\) 直到可以或者到根,过程中最大的长度为答案
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
constexpr int maxn=5e5+10;
string S,T;
namespace SAM
{
int cur,p,q,cl,lt,tot;
int len[maxn],t[maxn][26],lk[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
len[cur]=0,lk[cur]=-1,lt=0;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot,len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
}
}
}using namespace SAM;
int ans;
void dfs(int &x,int id,int &ln)
{
int c=T[id]-'a';
if (t[x][c])
ln++,x=t[x][c];
else
{
while ((~x) && !t[x][c])
x=lk[x];
if (!(~x))
x=0,ln=0;
else
ln=len[x]+1,x=t[x][c];
}
ans=max(ans,ln);
}
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> S >> T;
BuildSAM(S);
int x=0,ln=0;
for (int i=0;i<T.size();i++)
dfs(x,i,ln);
cout << ans << endl;
return 0;
}
多串间的最长公共子串
- 踏马 \(\text{SPOJ}\) 数据好像是错的
QOJ #1948. 最长公共子串
- 对于第一个串建立 \(\text{SAM}\) 拿每个串上去匹配,统计所有转移到一个节点 \(v\) 的最长长度,向上更新其在 \(\text{parent}\) 树上的父亲,父亲可被转移的最长长度为其子树中最长长度的最大值
- 完成后,对于每个节点 \(v\),将每个串转移到 \(v\) 的最大长度取最小
- 最后对所有节点由上面得到的长度取 \(\max\) 为答案
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define pb push_back
#define mem(a) memset((a),0,sizeof((a)))
using namespace std;
constexpr int maxn=5e5+10;
namespace SAM
{
int cur,p,q,lt,cl,tot,len[maxn],lk[maxn],t[maxn][26],cpy[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
len[cur]=0,lk[cur]=-1,lt=0,cpy[0]=1;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot,len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
cpy[cl]=1;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
}
}
}using namespace SAM;
vector<int> e[maxn];
int mx[maxn],mn[maxn];
string T;
void dfs(int &x,int id,int &ln)
{
int c=T[id]-'a';
if (t[x][c])
ln++,x=t[x][c];
else
{
while ((~x) && !t[x][c])
x=lk[x];
if (!(~x))
x=0,ln=0;
else
ln=len[x]+1,x=t[x][c];
}
}
void dfs2(int x)
{
if (!e[x].size()) return;
for (int y:e[x])
dfs2(y),mx[x]=max(mx[x],mx[y]);
}
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
memset(mn,0x7f,sizeof(mn));
int n;cin >> n;
n--;
string s;cin >> s;
if (!n) return cout << s.size() << endl,0;
BuildSAM(s);
for (int i=1;i<=tot;i++)
e[lk[i]].pb(i);
while (n--)
{
cin >> T;
mem(mx);
int x=0,ln=0;
for (int i=0;i<T.size();i++)
dfs(x,i,ln),mx[x]=max(mx[x],ln);
dfs2(0);
for (int i=1;i<=tot;i++)
mn[i]=min(mn[i],mx[i]);
}
int ans=0;
for (int i=1;i<=tot;i++)
ans=max(ans,mn[i]);
cout << ans << endl;
return 0;
}
- 终于完成了。。。
广义 \(\text{SAM}\)
说在前面
- 本来不打算学的,但是找 \(\text{SAM}\) 的题的时候发现大多是广义 \(\text{SAM}\) 就来了。。。
- 请确保你已经完全会 \(\text{SAM}\) 后再来学
- \(\text{SAM}\) 可以高效维护单串信息,而广义 \(\text{SAM}\) 可以维护多串信息(即将串建出 \(\text{Tire}\) 后建 \(\text{SAM}\))
- 广义 \(\text{SAM}\) 有在线和离线做法,在线以后补(万一真补了呢)
- 好吧其实广义 \(\text{SAM}\) 没有 \(\text{BSAM}\) 这个叫法
但我就这么叫了咋地。。。
假的广义 \(\text{SAM}\)
- 假的做法有两种
- 直接将串接串,中间加一个和一位的字符,对这个大串建 \(\text{SAM}\),通过魔法手段判断当前是哪个串
- 对每个串都建 \(\text{SAM}\),不过是在同一个 \(\text{SAM}\) 上,每次将 \(last\) 初始化
- 这俩玩意复杂度有点假,但是建立方法简单
真的广义 \(\text{SAM}\)
- 先建 \(\text{Trie}\),然后在 \(\text{Trie}\) 上建 \(\text{SAM}\)
- 然后对 \(\text{Trie}\) 跑 \(\text{BFS}\),得到一个序列(用 \(\text{BFS}\) 的原因是 \(\text{DFS}\) 在直接给出 \(\text{Trie}\) 的情况下会被卡)
- 按序列顺序修改 \(\text{Trie}\) 使其成为 \(\text{SAM}\)
- 很懵,我们结合代码
- 实际上和普通 \(\text{SAM}\) 没啥区别,有两个注意点:
- 在向 \(\text{SAM}\) 加入节点时,我们用的是 \(\text{Trie}\) 的节点,不需要像原来那样做 \(++tot\)
- 为了防止更新的 \(\text{len}\) 大于当前节点,我们需要判断那个 \(q\) 是否非 \(0\),非 \(0\) 可以更新,否则不行
- 还有小细节,标代码里了
- 破案了,\(\text{SAM}\) 要开 \(2\) 倍空间。。。
点击查看代码
namespace BSAM
{
int t[maxn][26];
int cur,p,q,cl,tot;
int lk[maxn],len[maxn];
void init() {len[0]=0,lk[0]=-1;}
int BuildBSAM(int lt,int c)
{
cur=t[lt][c];/*1*/
if (len[cur]) return cur;
len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (p == lt) continue;/*2*/
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
for (int i=0;i<26;i++)
t[cl][i]=len[t[q][i]]!=0 ? t[q][i] : 0;/*3*/
len[cl]=len[p]+1;
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
return cur;
}
void BuildTrie(string s)
{
int n=s.size(),x=0;
for (int i=0;i<n;i++)
{
int &son=t[x][s[i]-'a'];
if (!son) son=++tot;
x=son;
}
}
void Build()
{
queue<pii > q;
for (int i=0;i<26;i++)
if (t[0][i])
q.push({i,0});
while (!q.empty())
{
pii x=q.front();q.pop();
int last=BuildBSAM(x.se,x.fi);
for (int i=0;i<26;i++)
if (t[last][i])
q.push({i,last});
}
}
}using namespace BSAM;
- 直接给出板子题的总代码
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
constexpr int maxn=2e6+10;
int n;
string s;
namespace BSAM
{
int t[maxn][26];
int cur,p,q,cl,tot;
int lk[maxn],len[maxn];
void init() {len[0]=0,lk[0]=-1;}
int BuildBSAM(int lt,int c)
{
cur=t[lt][c];
if (len[cur]) return cur;
len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (p == lt) continue;
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
for (int i=0;i<26;i++)
t[cl][i]=len[t[q][i]]!=0 ? t[q][i] : 0;
len[cl]=len[p]+1;
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
return cur;
}
void BuildTrie(string s)
{
int n=s.size(),x=0;
for (int i=0;i<n;i++)
{
int &son=t[x][s[i]-'a'];
if (!son) son=++tot;
x=son;
}
}
void Build()
{
queue<pii > q;
for (int i=0;i<26;i++)
if (t[0][i])
q.push({i,0});
while (!q.empty())
{
pii x=q.front();q.pop();
int last=BuildBSAM(x.se,x.fi);
for (int i=0;i<26;i++)
if (t[last][i])
q.push({i,last});
}
}
}using namespace BSAM;
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> n;
init();
for (int i=1;i<=n;i++)
cin >> s,BuildTrie(s);
Build();
int ans=0;
for (int i=1;i<=tot;i++)
ans+=len[i]-len[lk[i]];
cout << ans << endl << tot+1 << endl;
return 0;
}
做题
P4770 [NOI2018] 你的名字
- 前置:\(\text{SAM}\),线段树合并
- 牛子题
- 太优美了 \(SAM\) 尤其是配上优美的 \(\text{DS}\) 我想不到世界上有什么东西比这更优美
- 欸为啥我部分分只有 \(54pts\)。。。
- 先考虑 \(l=1,r=\mid S \mid\)
- 我们对 \(S\) 建 \(\text{SAM}\),对每个 \(T\) 上 \(\text{SAM}\) 匹配前缀匹配长度,上面提到怎么做了
- 假设 \(i\) 能匹配到 \(len\),会有 \(i−len\) 匹配不到,直接算贡献
- 但是你发现假完了
- 因为要求种类,你没去重
- 那你对于 \(T\) 也建一个 \(\text{SAM}\),为方便统计贡献
- 对于每个点,统计最长能匹配长度,就是上面求的最长长度在 \(T\) 的 \(\text{parent}\) 树上求子树最大值,上面好像也提过,设求出的数组为 \(mx\)
- 一个点 \(x\) 的贡献为:
\[\max(0,len_x - \max(len_{link_x},mx_x))
\]
- 不知道为啥只有 \(54pts\),期望是 \(68pts\)
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define mem(a) memset((a),0,sizeof((a)))
#define pb push_back
using namespace std;
constexpr int maxn=1e6+10;
int m;
string S,T;
struct _
{
int lt,p,q,cur,tot,cl,t[maxn][26],lk[maxn],len[maxn],pos[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
lt=p=q=cur=tot=cl=0;
mem(t[0]);
len[cur]=0,lk[cur]=-1,lt=0;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot;
len[cur]=len[lt]+1;
mem(t[cur]);
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[q]=lk[cur]=cl;
}
}
lt=cur;
pos[i-1]=cur;
}
}
}s,t;
int mx1[maxn],mx2[maxn];
void getlen(int &x,int id,int &ln)
{
int c=T[id]-'a';
if (s.t[x][c])
ln++,x=s.t[x][c];
else
{
while ((~x) && !s.t[x][c])
x=s.lk[x];
if (!(~x))
ln=0,x=0;
else
ln=s.len[x]+1,x=s.t[x][c];
}
}
vector<int> e[maxn];
void dfs(int x)
{
for (int y:e[x])
{
dfs(y);
mx2[x]=max(mx2[x],mx2[y]);
}
}
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> S;
s.BuildSAM(S);
cin >> m;
while (m--)
{
cin >> T;
int l,r;cin >> l >> r;
for (int i=0;i<=T.size();i++)
mx1[i]=0;
t.BuildSAM(T);
for (int i=0;i<=t.tot;i++)
mx2[i]=0,e[i].clear();
int x=0,ln=0;
for (int i=0;i<T.size();i++)
{
getlen(x,i,ln);
mx1[i]=ln;
mx2[t.pos[i]]=max(mx2[t.pos[i]],mx1[i]);
}
for (int i=t.tot;i;i--)
e[t.lk[i]].pb(i);
dfs(0);
int ans=0;
for (int i=1;i<=t.tot;i++)
ans+=max(0ll,t.len[i]-max(t.len[t.lk[i]],mx2[i]));
cout << ans << endl;
}
return 0;
}
- \(upd\space on \space 26.10.19\):我学过线段树合并吗。。。
- 现在考虑加上区间限制后怎么办
- 我们需要将 \(T\) 在 \(S\) 的 \(\text{SAM}\) 上匹配,但是一个节点可能根本不在 \([l,r]\) 内,具体处理方法是通过线段树合并,维护每个点的 \(\text{endpos}\) 的区间,\(\text{parent}\) 树上的点的区间包含其子节点,需要进行合并,所以需要线段树合并维护,每到一个点进行转移的时候,我们不止要判断是否有转移(边),还要看转移到的节点是否有在区间合法的后缀,还有就是我们不能直接一直跳 \(\text{link}\) 了,因为在每次匹配长度 \(-1\) 时都可能出现答案
- 对了在判转移到的状态是否合法时,区间要查 \([l+ln,r]\)(即加上已经匹配的长度)
- 线段树合并正常写就行
点击查看代码
namespace mergeofsegtree
{
int root[maxn<<5];
int seg;
int ls[maxn<<5],rs[maxn<<5],sum[maxn<<5];
#define mid ((l+r)>>1)
void Upd(int &rt,int l,int r,int pos)
{
if (!rt) rt=++seg;
if (l == r) return sum[rt]=1,void();
if (pos<=mid) Upd(ls[rt],l,mid,pos);
else Upd(rs[rt],mid+1,r,pos);
sum[rt]=sum[ls[rt]]+sum[rs[rt]];
}
int merge(int x,int y,int l,int r)
{
if (!x || !y) return x|y;
int rt=++seg;
if (l == r) return sum[rt]=1,rt;
ls[rt]=merge(ls[x],ls[y],l,mid);
rs[rt]=merge(rs[x],rs[y],mid+1,r);
sum[rt]=sum[ls[rt]]+sum[rs[rt]];
return rt;
}
int que(int rt,int l,int r,int L,int R)
{
if (!rt) return 0;
int res=0;
if (L<=l && r<=R) return sum[rt];
if (L<=mid) res+=que(ls[rt],l,mid,L,R);
if (R>mid) res+=que(rs[rt],mid+1,r,L,R);
return res;
}
#undef mid
}using namespace mergeofsegtree;
vector<int> e[maxn<<1];
void dfs(int x,int n)
{
for (int y:e[x])
dfs(y,n),root[x]=merge(root[y],root[x],1,n);
}
int mx1[maxn],mx2[maxn];
void getlen(int &x,int id,int &ln,int l,int r,int n)
{
int c=T[id]-'a';
while (1)
{
if (s.t[x][c] && que(root[s.t[x][c]],1,n,l+ln,r))
return ln++,x=s.t[x][c],void();
if (!ln) return x=0,void();
ln--;
if (ln == s.len[s.lk[x]]) x=s.lk[x];
}
}
- 剩下的就和上面说的 \(l=1,r=\mid S \mid\) 没区别了
- 小心饭堂,\(\text{msjing}\) 饭了一堆糖,包括但不限于:
- 对 \(S\) 建立了线段树合并后用 \(T\) 覆盖掉
点击查看代码
s.BuildSAM(S);
for (int i=s.tot;i;i--)
e[s.lk[i]].pb(i);
dfs(0,S.size());
cin >> m;
while (m--)
{
cin >> T;
int l,r;cin >> l >> r;
for (int i=0;i<=T.size();i++)
mx1[i]=0;
t.BuildSAM(T);
- 合并后将合并的节点直接扔了
点击查看代码
for (int y:e[x])
dfs(y,n),merge(root[y],root[x],1,n);
- 跳 \(\text{link}\) 到根不重置节点编号
点击查看代码
if (!ln) return;
点击查看代码
#include <bits/stdc++.h>
#define int long long
#define endl '\n'
#define mem(a) memset((a),0,sizeof((a)))
#define pb push_back
using namespace std;
constexpr int maxn=1e6+10;
int m;
string S,T;
namespace mergeofsegtree
{
int root[maxn<<5];
int seg;
int ls[maxn<<5],rs[maxn<<5],sum[maxn<<5];
#define mid ((l+r)>>1)
void Upd(int &rt,int l,int r,int pos)
{
if (!rt) rt=++seg;
if (l == r) return sum[rt]=1,void();
if (pos<=mid) Upd(ls[rt],l,mid,pos);
else Upd(rs[rt],mid+1,r,pos);
sum[rt]=sum[ls[rt]]+sum[rs[rt]];
}
int merge(int x,int y,int l,int r)
{
if (!x || !y) return x|y;
int rt=++seg;
if (l == r) return sum[rt]=1,rt;
ls[rt]=merge(ls[x],ls[y],l,mid);
rs[rt]=merge(rs[x],rs[y],mid+1,r);
sum[rt]=sum[ls[rt]]+sum[rs[rt]];
return rt;
}
int que(int rt,int l,int r,int L,int R)
{
if (!rt) return 0;
int res=0;
if (L<=l && r<=R) return sum[rt];
if (L<=mid) res+=que(ls[rt],l,mid,L,R);
if (R>mid) res+=que(rs[rt],mid+1,r,L,R);
return res;
}
#undef mid
}using namespace mergeofsegtree;
struct _
{
int lt,p,q,cur,tot,cl,t[maxn][26],lk[maxn],len[maxn],pos[maxn];
void BuildSAM(string s,int op)
{
int n=s.size();s=" "+s;
lt=p=q=cur=tot=cl=0;
mem(t[0]);
len[cur]=0,lk[cur]=-1,lt=0;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot;
len[cur]=len[lt]+1;
mem(t[cur]);
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[q]=lk[cur]=cl;
}
}
lt=cur;
pos[i-1]=cur;
if (op) Upd(root[cur],1,n,i);
}
}
}s,t;
vector<int> e[maxn<<1];
void dfs(int x,int n)
{
for (int y:e[x])
dfs(y,n),root[x]=merge(root[y],root[x],1,n);
}
int mx1[maxn],mx2[maxn];
void getlen(int &x,int id,int &ln,int l,int r,int n)
{
int c=T[id]-'a';
while (1)
{
if (s.t[x][c] && que(root[s.t[x][c]],1,n,l+ln,r))
return ln++,x=s.t[x][c],void();
if (!ln) return x=0,void();
ln--;
if (ln == s.len[s.lk[x]]) x=s.lk[x];
}
}
vector<int> e2[maxn];
void dfs2(int x)
{
for (int y:e2[x])
dfs2(y),mx2[x]=max(mx2[x],mx2[y]);
}
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> S;
s.BuildSAM(S,1);
for (int i=s.tot;i;i--)
e[s.lk[i]].pb(i);
dfs(0,S.size());
cin >> m;
while (m--)
{
cin >> T;
int l,r;cin >> l >> r;
for (int i=0;i<=T.size();i++)
mx1[i]=0;
t.BuildSAM(T,0);
for (int i=0;i<=t.tot;i++)
mx2[i]=0,e2[i].clear();
int x=0,ln=0;
for (int i=0;i<T.size();i++)
{
getlen(x,i,ln,l,r,S.size());
mx1[i]=ln;
mx2[t.pos[i]]=max(mx2[t.pos[i]],mx1[i]);
}
for (int i=t.tot;i;i--)
e2[t.lk[i]].pb(i);
dfs2(0);
int ans=0;
for (int i=1;i<=t.tot;i++)
ans+=max(0ll,t.len[i]-max(t.len[t.lk[i]],mx2[i]));
cout << ans << endl;
}
return 0;
}
P3346 [ZJOI2015] 诸神眷顾的幻想乡
- 前置:广义 \(\text{SAM}\)
- 密码的变量冲突虚空调试 \(30min\)
- 这个问题不好做欸,你发现链随便选,这个直接枚举爆炸,而且也无法维护
- 我们尝试将一个链转化为一个从根到一个节点的后缀
- 比如:

- 我们发现题目中说叶子节点个数不超过 \(20\)
- 所以这题做法为:以每个叶子(注意不是任意节点,这里的叶子就是树中度为 \(1\) 的点)为根做 \(\text{DFS}\),将一条到叶链直接插入 \(\text{Trie}\) 中(即将整个的字符串的前缀全部插入),直接上广义 \(\text{SAM}\),统计本质不同后缀个数为答案
- 请注意时空限制和数组大小
点击查看代码
#include <bits/stdc++.h>
#define endl '\n'
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
constexpr int maxn=2e6+10,maxm=1e5+10;
int n,c;
int w[maxm];
vector<int> e[maxm];
namespace BSAM
{
int t[maxn][10];
int cur,p,q,cl,tot;
int lk[maxn],len[maxn];
void init() {len[0]=0,lk[0]=-1;}
int BuildBSAM(int lt,int c)
{
cur=t[lt][c];
if (len[cur]) return cur;
len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (p == lt) continue;
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
for (int i=0;i<10;i++)
t[cl][i]=len[t[q][i]]!=0 ? t[q][i] : 0;
len[cl]=len[p]+1;
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
return cur;
}
void BuildTrie(string s)
{
int n=s.size(),x=0;
for (int i=0;i<n;i++)
{
int &son=t[x][s[i]-'0'];
if (!son) son=++tot;
x=son;
}
}
void Build()
{
queue<pii > q;
for (int i=0;i<10;i++)
if (t[0][i])
q.push({i,0});
while (!q.empty())
{
pii x=q.front();q.pop();
int last=BuildBSAM(x.se,x.fi);
for (int i=0;i<10;i++)
if (t[last][i])
q.push({i,last});
}
}
}using namespace BSAM;
vector<string> str;
string tp;
void dfs(int x,int fa)
{
tp+=(char)(w[x]+'0');
int cnt=0;
for (int y:e[x])
{
if (y == fa) continue;
cnt++;
dfs(y,x);
}
if (!cnt) BuildTrie(tp);
tp.pop_back();
}
int main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> n >> c;
init();
for (int i=1;i<=n;i++)
cin >> w[i];
for (int i=1;i<n;i++)
{
int x,y;cin >> x >> y;
e[x].pb(y),e[y].pb(x);
}
vector<int> leaf;
for (int i=1;i<=n;i++)
if (e[i].size() == 1)
leaf.pb(i);
for (int i:leaf)
dfs(i,0);
Build();
long long ans=0;
for (int i=1;i<=tot;i++)
ans+=len[i]-len[lk[i]];
cout << ans << endl;
return 0;
}
P2336 [SCOI2012] 喵星球上的点名
- 前置:\(\text{SAM}\),莫队
- e,这题不好写
- 用啥的都有,我们用 \(\text{SAM}\)
- 我们发现一只哈基米有名和姓,不好弄,所以我们在每只哈基米的姓名之间加一个神秘分隔符,每个哈基米都将姓名合成一个串,再在每只哈基米的名字合起来(还是通过神秘分隔符相接)形成一个超级大串,对这个大串直接建立 \(\text{SAM}\),这就是上面提到的伪广义 \(\text{SAM}\)
- 现在我们需要处理询问,我们将每个串的每个前缀标记上是哪只哈基米对应的(插入时标记就行),对于询问,我们可以直接上 \(\text{parent}\) 树询问,不强制在线,莫队可以维护询问范围内出现颜色种类,不过我们没必要直接跑树上莫队,可以将 \(\text{parent}\) 树通过 \(dfn\) 序拍扁成序列,然后问题转化为区间数颜色
- 具体地,对于一个 \(\text{parent}\) 树上的节点,我们求出经过它的颜色集合,记得去重要不然算重
- 然后开始对 \(\text{parent}\) 树拍扁
点击查看代码
int dfn[maxn],siz[maxn];
void dfs(int x)
{
dfn[x]=color.size();
for (int co:bel[x]) color.pb(co);
for (int y:e[x])
dfs(y);
siz[x]=color.size()-dfn[x];
}
- 对于询问,我们将其上 \(\text{parent}\) 匹配,如果走不通就是无解不将其列入莫队维护对象,匹配完后,询问区间就是 \([dfn_x,dfn_x+siz_x-1]\),注意不要发现无法匹配后直接退出,要读完
点击查看代码
for (int i=1;i<=m;i++)
{
int k;cin >> k;
int x=0,f=1;
while (k--)
{
int ch;cin >> ch;
if (!f) continue;
f=check(x,ch);
}
if (f && siz[x])
qu[++tt]={dfn[x],dfn[x]+siz[x]-1,i};
}
- 统计时,第一个询问直接统计,第二个我们可以统计一个颜色第一次进入和最后出去的时间,做一个类似差分的东西维护存在时间,由于我们只数区间,并不用关心原区间的顺序,在莫队过程中可以方便维护
点击查看代码
void add(int x,int id)
{
int cl=color[x];
if (!cl) return;
if (!col[cl])
as++,ans2[cl]+=(tt-id+1);
col[cl]++;
}
void del(int x,int id)
{
int cl=color[x];
if (!cl) return;
col[cl]--;
if (!col[cl])
as--,ans2[cl]-=(tt-id+1);
}
- 没了
点击查看代码
#include <bits/stdc++.h>
#define endl '\n'
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second
#define mem(a) memset((a),0,sizeof((a)))
using namespace std;
constexpr int maxn=1e6+10;
int n,m;
vector<int> bel[maxn];
namespace SAM
{
map<int,int> t[maxn];
int lt,cur,p,q,cl,tot;
int lk[maxn],len[maxn];
void init() {len[0]=0,lk[0]=-1;}
void BuildSAM(int c)
{
cur=++tot;
len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
t[cl]=t[q];
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
}
}using namespace SAM;
vector<int> e[maxn];
struct _ {int l,r,id;}qu[maxn];
int tt;
vector<int> color;
int dfn[maxn],siz[maxn];
void dfs(int x)
{
dfn[x]=color.size();
for (int co:bel[x]) color.pb(co);
for (int y:e[x])
dfs(y);
siz[x]=color.size()-dfn[x];
}
int check(int &x,int c)
{
if (!t[x][c]) return 0;
x=t[x][c];
return 1;
}
int L[maxn],R[maxn],pos[maxn],cnt;
void initt()
{
cnt=sqrt(n);
for (int i=1;i<=cnt;i++)
L[i]=R[i-1]+1,R[i]=i*cnt;
if (R[cnt]<n)
cnt++,L[cnt]=R[cnt-1]+1,R[cnt]=n;
for (int i=1;i<=cnt;i++)
for (int j=L[i];j<=R[i];j++)
pos[j]=i;
}
int col[maxn];
int as,ans1[maxn],ans2[maxn];
void add(int x,int id)
{
int cl=color[x];
if (!cl) return;
if (!col[cl])
as++,ans2[cl]+=(tt-id+1);
col[cl]++;
}
void del(int x,int id)
{
int cl=color[x];
if (!cl) return;
col[cl]--;
if (!col[cl])
as--,ans2[cl]-=(tt-id+1);
}
int main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
init();
cin >> n >> m;
for (int i=1;i<=n;i++)
{
int k;cin >> k;
int ch;
while (k--)
cin >> ch,BuildSAM(ch),bel[cur].pb(i);
BuildSAM(1145);
cin >> k;
while (k--)
cin >> ch,BuildSAM(ch),bel[cur].pb(i);
BuildSAM(1145);
}
for (int i=1;i<=tot;i++)
{
sort(bel[i].begin(),bel[i].end());
auto st=unique(bel[i].begin(),bel[i].end());
bel[i].erase(st,bel[i].end());
}
for (int i=1;i<=tot;i++)
e[lk[i]].pb(i);
dfs(0);
for (int i=1;i<=m;i++)
{
int k;cin >> k;
int x=0,f=1;
while (k--)
{
int ch;cin >> ch;
if (!f) continue;
f=check(x,ch);
}
if (f && siz[x])
qu[++tt]={dfn[x],dfn[x]+siz[x]-1,i};
}
initt();
sort(qu+1,qu+1+tt,[](_ a,_ b)
{
return pos[a.l] == pos[b.l] ?
(pos[a.l]&1) ? a.r<b.r : a.r>b.r
: pos[a.l]<pos[b.l];
});
int l=1,r=0;
for (int i=1;i<=tt;i++)
{
while (l>qu[i].l) add(--l,i);
while (r<qu[i].r) add(++r,i);
while (l<qu[i].l) del(l++,i);
while (r>qu[i].r) del(r--,i);
ans1[qu[i].id]=as;
}
for (int i=1;i<=m;i++)
cout << ans1[i] << endl;
for (int i=1;i<=n;i++)
cout << ans2[i] << " ";
cout << endl;
return 0;
}
P2178 [NOI2015] 品酒大会
- 前置:\(\text{SAM}\)
- 饿啊好久没写 \(\text{SAM}\) 了
- 其实本来没打算写的,但是 \(\text{wang79}\) 说这题要用 \(\text{SA}\) 做,并打算今天做掉
\(\text{wang79}\):这个和 \(\text{SA}\) 的 \(height\) 有关
\(\text{msjing}\):woc \(\text{SA}\) 能做 \(\text{SAM}\) 一定能做
- \(\text{sbmqwm}\):典型的 \(\text{SAM}\) 思维
- 题意需要转化
- 我们转化一下,将问题变成求两串的后缀的 \(\text{LCP} \ge r\)
- 你发现这个东西对于 \(\text{SAM}\) 来说不好维护,但是 \(SAM\) 擅长维护两串前缀的 \(\text{LCS}\)
- 所以我们对反串建立 \(\text{SAM}\)
- 由于 \(\text{SAM}\) 的优良性质,对于两个节点,其 \(\text{LCS}\) 为其 \(\text{LCA}\) 的 \(\text{len}\)
- 现在考虑如何计算答案
- 首先我们应该先把权值挂上 \(\text{parent}\) 树
- 对于一个原串中对应位置为 \(i\)(\(1-based\)),其在反串的位置为 \(n-i+1\),所以我们对于建立在 \(\text{SAM}\) 上的节点,将其 \(val \leftarrow a_{n-i+1}\),并标记其上挂了权值
vis[cur]=1,val[cur]=a[n-i+1];
- 注意不要给克隆节点挂,其不为结束状态
- 接下来该统计答案了
- 我们需要从下向上在 \(\text{parent}\) 树上统计答案,采用拓扑的方法
- 我们需要维护三个东西:子节点个数,最大值,最小值
- 具体做法为:对于一个节点及其父亲,如果这个节点没有权值,直接跳过,如果其父亲没有权值,直接将其给到其父亲,如果都有的话,需要进行合并,下面说合并
- 设这个节点为 \(x\),其父亲为 \(p\),会对答案方案数 \(cnt\) 产生 \(siz_x \times siz_p\) 的贡献,累加到 \(len_p\) 上
- 最大值可能由 \(mx_x \times mx_p\) 或者 \(mn_x \times mn_p\) 得到(因为有负数,负负得正),注意不要少算了,同样贡献到 \(len_p\) 上
- 最后将 \(x\) 合并到 \(p\)
点击查看代码
int k=len[p];
cnt[k]+=siz[p]*siz[x];
allmx[k]=max({allmx[k],mx[x]*mx[p],mn[x]*mn[p]});
mx[p]=max(mx[p],mx[x]);
mn[p]=min(mn[p],mn[x]);
siz[p]+=siz[x];
- 最后是最后答案的统计,其实就是求个后缀和和后缀最大值的事,注意当后缀和为 \(0\) 时,后缀最大值为 \(0\),别把负极大输出了
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
constexpr int maxn=1e6+10;
int n;
string s;
int a[maxn];
namespace SAM
{
int cur,lt,p,q,cl,tot;
int t[maxn][26],lk[maxn],len[maxn];
int vis[maxn],val[maxn];
void BuildSAM(string s)
{
int n=s.size();s=" "+s;
len[0]=0,lk[0]=-1;
for (int i=1;i<=n;i++)
{
int c=s[i]-'a';
cur=++tot;
len[cur]=len[lt]+1;
for (p=lt;~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p)) lk[cur]=0;
else
{
if (len[q] == len[p]+1)
lk[cur]=q;
else
{
cl=++tot;
len[cl]=len[p]+1;
memcpy(t[cl],t[q],sizeof(t[q]));
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
lt=cur;
vis[cur]=1,val[cur]=a[n-i+1];
}
}
}using namespace SAM;
int d[maxn];
int mx[maxn],mn[maxn],siz[maxn];
int cnt[maxn],allmx[maxn];
int cntans[maxn],mxans[maxn];
signed main()
{
// freopen("sample.in","r",stdin);
// freopen("sample.out","w",stdout);
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> n;
cin >> s;
for (int i=1;i<=n;i++)
cin >> a[i];
reverse(s.begin(),s.end());
BuildSAM(s);
memset(mx,128,sizeof(mx));
memset(mn,127,sizeof(mn));
memset(allmx,128,sizeof(allmx));
for (int i=1;i<=tot;i++)
if (vis[i])
siz[i]=1,mx[i]=mn[i]=val[i];
for (int i=1;i<=tot;i++)
d[lk[i]]++;
queue<int> q;
for (int i=1;i<=tot;i++)
if (!d[i])
q.push(i);
while (!q.empty())
{
int x=q.front();q.pop();
p=lk[x];
if (!(~p)) continue;
if (!siz[x]) continue;
if (!siz[p])
siz[p]=siz[x],mx[p]=mx[x],mn[p]=mn[x];
else
{
int k=len[p];
cnt[k]+=siz[p]*siz[x];
allmx[k]=max({allmx[k],mx[x]*mx[p],mn[x]*mn[p]});
mx[p]=max(mx[p],mx[x]);
mn[p]=min(mn[p],mn[x]);
siz[p]+=siz[x];
}
d[p]--;
if (!d[p])
q.push(p);
}
memset(mxans,128,sizeof(mxans));
for (int i=n-1;~i;i--)
{
cntans[i]+=cntans[i+1]+cnt[i];
mxans[i]=max(mxans[i+1],allmx[i]);
}
for (int i=0;i<n;i++)
cout << cntans[i] << " " << (cntans[i] ? mxans[i] : 0) << endl;
return 0;
}
P3181 [HAOI2016] 找相同字符
- 前置:广义 \(\text{SAM}\)
- 其实最开始胡的是建伪广义 \(\text{SAM}\),然后再对两个串分别建 \(\text{SAM}\),分别统计本质不同子串,然后做差减去自己配自己,但是有点假。。
- 假的原因是我们上述做法只关心了本质不同的子串,数量会算少,对于容斥做法请参考 \(\text{SA}\) 做法
- 其实建立广义 \(\text{SAM}\),答案为:
\[\sum siz_{x,1} \times siz_{x,2} \times (len_x-len_{link_x})
\]
- \(siz_{x,1}\) 表示 \(x\) 这个节点对应 \(1\) 串的 \(siz\),具体统计方法是在建 \(\text{Trie}\) 时直接记录这个节点对应哪个串,然后在建 \(\text{SAM}\) 时统计就行,具体看看代码
- 答案统计拓扑就行
- 开 \(long\space long\)
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second
using namespace std;
constexpr int maxn=8e5+10;
string s1,s2;
namespace GSAM
{
int t[maxn][26],lk[maxn],len[maxn],siz[maxn][2];
int cur,p,q,lt,cl,tot;
pii bel[maxn];
void init() {len[0]=0,lk[0]=-1;}
int BuildSAM(int lt,int c)
{
cur=t[lt][c];
siz[cur][0]=bel[cur].fi,siz[cur][1]=bel[cur].se;
if (len[cur]) return cur;
len[cur]=len[lt]+1;
for (p=lk[lt];~p;p=lk[p])
{
if (!t[p][c]) t[p][c]=cur;
else {q=t[p][c];break;}
}
if (!(~p))
lk[cur]=0;
else
{
if (len[p]+1 == len[q])
lk[cur]=q;
else
{
cl=++tot;
for (int i=0;i<26;i++)
t[cl][i]=len[t[q][i]]!=0 ? t[q][i] : 0;
len[cl]=len[p]+1;
lk[cl]=lk[q];
for (;(~p) && (t[p][c] == q);p=lk[p])
t[p][c]=cl;
lk[cur]=lk[q]=cl;
}
}
return cur;
}
void BuildTrie(string s,int op)
{
int n=s.size();s=" "+s;
int x=0;
for (int i=1;i<=n;i++)
{
int &son=t[x][s[i]-'a'];
if (!son) son=++tot;
x=son;
if (op == 1) bel[x].fi=1;
else bel[x].se=1;
}
}
void Build()
{
queue<pii > q;
for (int i=0;i<26;i++)
if (t[0][i])
q.push({i,0});
while (!q.empty())
{
pii x=q.front();q.pop();
int last=BuildSAM(x.se,x.fi);
for (int i=0;i<26;i++)
if (t[last][i])
q.push({i,last});
}
}
}using namespace GSAM;
int d[maxn];
int main()
{
#ifdef LOCAL
freopen("sample.in","r",stdin);
freopen("sample.out","w",stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
init();
cin >> s1;
BuildTrie(s1,1);
cin >> s2;
BuildTrie(s2,2);
Build();
queue<int> q;
for (int i=1;i<=tot;i++)
d[lk[i]]++;
for (int i=1;i<=tot;i++)
if (!d[i])
q.push(i);
long long ans=0;
while (!q.empty())
{
int x=q.front();q.pop();
if (!x) continue;
ans+=1ll*siz[x][0]*siz[x][1]*(len[x]-len[lk[x]]);
siz[lk[x]][0]+=siz[x][0];
siz[lk[x]][1]+=siz[x][1];
d[lk[x]]--;
if (!d[lk[x]]) q.push(lk[x]);
}
cout << ans << endl;
return 0;
}
后话

- 流萤我老婆
- \(\text{sbmqwm}\):盯~
$\frak{msjing}$

浙公网安备 33010602011771号