SAM

前言

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

OIP-C

  • \(\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\)
  1. 先得到 \(\text{endpos}\)
\(suffix\) \(\text{endpos}\)
\(a\) \(\{1,3\}\)
\(b\) \(\{2,4\}\)
\(ab\) \(\{2,4\}\)
\(ba\) \(\{3\}\)
\(aba\) \(\{3\}\)
\(bab\) \(\{4\}\)
\(abab\) \(\{4\}\)
  1. 合并成等价类
\(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}\)
  1. 连 \(\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\)
  • 图来

2026-10-14 07-44-12屏幕截图

引理
  • 每个子节点的 \(\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. 直接到了 \(-1\),即 \(p=-1\),\(\text{link}\) 置为 \(0\),\(last\) 置为 \(cur\),结束,称为情况 \(1\)
  2. 到了一个有边 \(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\),需要进行克隆分裂,将原等价类划给原节点,克隆节点放新的等价类,等会详细讲,克隆流程如下:
    1. 新建克隆节点 \(\text{clone}\),将 \(\text{len}(clone) = \text{len}(p)+1\),完全复制 \(q\) 的转移,\(\text{link}\)
    2. 进行重定向,条件为 \(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}\),此时图长这样:

Screenshot 2026-10-14 at 10-27-47 tldraw

加入 \(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\)
  • 此时图长这样:

Screenshot 2026-10-14 at 10-32-26 tldraw

加入 \(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\)
  • 此时图长这样:

Screenshot 2026-10-14 at 10-39-23 tldraw

加入 \(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\)
  • 此时图长这样:

Screenshot 2026-10-14 at 10-55-05 tldraw

为什么要克隆?

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

Screenshot 2026-10-14 at 11-01-02 tldraw

  • 到现在已经过去 \(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}\) 了,不过有几个问题:
  1. 这家伙 \(x_i\) 好大。。。
  2. 在线询问
  • 第一个问题直接请出 \(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}\)

  • 假的做法有两种
  1. 直接将串接串,中间加一个和一位的字符,对这个大串建 \(\text{SAM}\),通过魔法手段判断当前是哪个串
  2. 对每个串都建 \(\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}\) 没啥区别,有两个注意点:
  1. 在向 \(\text{SAM}\) 加入节点时,我们用的是 \(\text{Trie}\) 的节点,不需要像原来那样做 \(++tot\)
  2. 为了防止更新的 \(\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}\) 饭了一堆糖,包括但不限于:
  1. 对 \(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);
  1. 合并后将合并的节点直接扔了
点击查看代码
for (int y:e[x])
    dfs(y,n),merge(root[y],root[x],1,n);
  1. 跳 \(\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\)
  • 这个问题不好做欸,你发现链随便选,这个直接枚举爆炸,而且也无法维护
  • 我们尝试将一个链转化为一个从根到一个节点的后缀
  • 比如:

2026-10-19 21-50-33屏幕截图

  • 我们发现题目中说叶子节点个数不超过 \(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;
}

后话

Screenshot 2026-10-19 at 21-55-38 首页 - 洛谷 计算机科学教育新生态

  • 流萤我老婆
  • \(\text{sbmqwm}\):盯~
posted @ 2026-10-14 11:08  msjing  阅读(24)  评论(1)    收藏  举报