【模板】后缀自动机 SAM、后缀树、广义后缀自动机

posted on 2022-08-07 20:50:14 | under 模板 | source
updated on 20230727:增补更多细节,合并广义 SAM 相关。

点击查看闲话

广义后缀自动机,建议看这篇理解会更深(口语化警告) <- 已经加入后缀自动机豪华套餐

保存:基本子串结构 https://www.cnblogs.com/crashed/p/17382894.html 2023 xtq

炫酷后缀树魔术 https://www.luogu.com.cn/blog/EternalAlexander/xuan-ku-hou-zhui-shu-mo-shu

众所周知,后缀自动机有 \(114514\) 种理解方式,我也无法理解,但是明天模拟赛要考,所以要学习一下。

感谢 @sshwy 的课件。/bx

UPD:考了,不会。

后缀自动机

后缀自动机(Suffix Automaton,SAM)是一种有限状态自动机,接受字符串 \(S\) 的所有后缀。若认为字符集大小为常数,则它有着 \(O(n)\) 的时间复杂度 和 \(O(n)\) 的空间复杂度(两倍)。

概念

在后缀自动机中,每一个状态对应着 \(S\) 的一类子串,它们有着相同的出现位置(右边),长度互不相同,若按长度排序,则前一个是后一个的后缀。定义 \(\delta(p,r)\) 为状态 \(p\) 末尾加上字符 \(r\) 后转移到的状态,容易发现 SAM 是一个 DAG。

  • Endpos 集合:\(S\) 的子串 \(s\) 的 Endpos 集合定义为它在 \(S\) 中出现的位置的右端点。以下记为 \(endpos(s)\)。显然 \(|endpos(s)|\)\(s\)\(S\) 中的出现次数。
  • 等价类:一个等价类中的所有子串都有着完全一致的 \(endpos\),且它们的长度是连续的。一个状态(点)表示的正是一个等价类,并且 \(S\) 的任意子串都唯一归属一个等价类。显然,两个等价类要么包含要么不交(反证法)。我们可以用 \((s,len)\) 表示一个最子串是 \(s\),最子串长度为 \(len\) 的一个等价类,显然它是唯一的。如果 \(endpos=\{r\}\),可以简单认为这个等价类的所有子串以 \(r\) 为右端点,左端点在左边连续,最左的左端点对应的长度为 \(len\),最右的是 \(len_{link}+1\)(等会交 \(link\) 的定义)
  • 包含类:若两个等价类 \(s,s'\) 满足 \(endpos(s)\subset endpos(s')\),则称 \(s'\)\(s\) 的包含类。(注意这里是反过来的包含)若 \(s'\)\(s\) 的包含类,那么 \(s'\) 的最长子串比 \(s\) 的最长子串短,\(endpos\) 更多,出现次数更多,或者更加迫真,\(s'\) 全是 \(s\) 的后缀。
  • Link 指针 / 后缀链接:所有状态都有 Link 指针(根状态没有),它们构成一棵 Link 树,在 Link 树上状态 \(u\) 的所有祖先都是它的包含类,\(u\) 的 Link 指针指向了 \(u\) 的所有包含类中最长子串长度最大的,记为 \(link(u)\)。或者更加迫真,\(link(u)\)\(u\) 砍掉一段前缀的结果。
  • 转移指针 \(\delta(u,r)\) 是将 \(u\) 这个等价类后面加一个字符 \(r\) 形成的新的等价类。可以确定的是 \(u+r\subseteq \delta(u,r)\)。(\(u+r\) 这里表示等价类所有子串全部加上 \(r\)

构造(理解一)

现在开始构造后缀自动机,我们使用增量构造法,现在我们有了 \(S\) 的 SAM,我们试图构造 \(S+r\) 的 SAM。(\(r\) 是一个字符)

首先新建状态节点 \(u\) 表示一个等价类 \((S+r,|S|+1)\)。考虑两件事情:

  • \(link(u)\) 是什么?
  • 哪些状态会转移到 \(u\)?/ 哪些 \(\delta(q,r)\) 会被更新?

\(p=(S,|S|)\)。将 \(p\) 不断地在 Link 树上跳,遇到的第一个 \(\delta(p',r)\neq\varnothing\) 的点记为 \(p'\)。那么 \(q=\delta(p',r)\)\(u\) 的等价类。

重新看一遍 \(\delta\),可以发现 \(\delta(p,r)\) 还包含除了 \(p+r\) 外的其他串。即 \(\delta(p,r)\) 是包含 \(p+r\) 的串。那么我们往 SAM 里放了一个 \(r\),对应的 \(q\) 中属于 \(p'+r\) 的字符串的出现次数都增加一,其它的不能加。遂分类讨论:

  • \(p'+r=q\),整个 \(q\) 都可以留着,出现次数一起加一,\(link(u)=q\)
  • \(p'+r\subsetneq q\),那么 \(q\) 中有些字符串要加一,有些不用,我们把它们 split 开,原本 \(q=(s,len)\) 会被划分成 \(q_1=(s,|p'|+2)\)\(q_2=(s[|s|-|p'|,|s|],len)\),其中 \(q_2\)\(q_1\) 的包含类,\(link(u)=q_2\)。代码实现中我们可以令 \(q=q_1\),新开节点 \(q_2\)

那么怎么更新 \(\delta\)?只跟 \(q,u\) 有关的需要更新,具体地,

  • 更新 \(p\)\(p'\) Link 树上的点 \(p''\)\(\delta(p'',r)=u\)
  • \(q\) 分裂了,若 \(p''\in[p',root]\) 满足 \(\delta(p'',r)=q\),则更新成 \(q_2\)

上面的我看不懂,照抄了黄队课件,Orz

构造(理解二)

现在开始增量构造。假如我们有一个 \(tail\) 表示上一次加入的那个整串。加入字符 \(r\),新开一个 \(u\) 表示新的整串。时刻记住,我们要维护出边和后缀链接。显然 \(ch_{tail,r}=u\),从 \(tail\) 往上跳 \(link\)\(p\),这是在砍掉前缀,如果 \(ch_{p,r}=\varnothing\) 那么接上 \(ch_{p,r}=u\)。如果 \(ch_{p,r}\neq\varnothing\),因为我们在砍前缀,再砍它的出边也存在,所以我们在这里停下来更新。令 \(q=ch_{p,r}\)。我们很天真地想要 \(link_u=q\),这不好。

对于一个出现位置,有颜色的部分,是可能的子串左端点。显然 \(p\) 在这里面。如果 \(p+r\) 刚好是 \(q\)(绿色图),那么直接 \(link_u=q\) 是正确的。否则(红色图)我们就会发现,对于黑线右边的这一部分子串,它们的 \(endpos\) 的集合加上 \(n\),但是左边这一部分子串不是,这两部分已经不一样了。所以我们要分裂,假设我们分裂出 \(cq\),那么使 \(cq\) 成为黑线右边这一部分,\(q\) 成为黑线左边的,根据定义,此时 \(link_{cq}\) 为原来的 \(link_q\)\(link_q\) 改成 \(cq\)(因为要保证 \(endpos\) 的大小,越往上跳越多;\(q\) 的后缀确实是 \(cq\));\(link_u=cq\),就是说 \(u\) 贡献的这个 \(endpos\) 不会给到 \(q\),这是正确的。对于 \(cq\) 的出边,和 \(q\) 是一模一样的。对于 \(p\) 剩下的 \(link\),如果有 \(ch_{p',r}=q\) 那么改成 \(ch_{p',r}=cq\)。完成。

广义 SAM 相关

这里不对什么东西进行证明,只说正确的。广义 SAM 其实就是插入新的字符串时令 \(tail=1\) 重新插入即可。但这样会有问题:\(ch_{tail,r}\) 一开始就存在怎么办?我们对此进行特判即可,直接分裂 \(ch_{tail,r}\),和刚才讲的一样。然后代码中有点细节要改改,分裂的时候不一定要使得 \(link_u=cq\)(你连 \(u\) 都没有)。参考的实现将 expand 和 split 分成两个函数,可参考。

code

注意双倍空间!

SAM
template<int N,int M=26> struct suffixam{
    int tot,ch[N*2+10][M],link[N*2+10],len[N*2+10],siz[N*2+10],last;
    suffixam():tot(1),last(1){memset(ch,0,sizeof ch),memset(siz,0,sizeof siz),len[1]=link[1]=0;}
    void expand(int r){
        int u=++tot,p=last;last=u;
        len[u]=len[p]+1,siz[u]=1;
        while(p&&!ch[p][r]) ch[p][r]=u,p=link[p];
        if(!p) return void(link[u]=1);
        int q=ch[p][r];
        if(len[q]==len[p]+1) return void(link[u]=q);
        int cq=++tot;memcpy(ch[cq],ch[q],sizeof ch[cq]);
        len[cq]=len[p]+1,link[cq]=link[q];
        link[u]=link[q]=cq;
        while(p&&ch[p][r]==q) ch[p][r]=cq,p=link[p];//不要写错!
    }
};
广义 SAM
template <int N, int M>
struct suffixam {
  int ch[N << 1][M], tot, link[N << 1], len[N << 1];
  suffixam() : tot(1) {
    len[1] = link[1] = 0;
    memset(ch[1], 0, sizeof ch[0]);
  }
  int split(int p, int q, int r) {
    if (len[q] == len[p] + 1) return q;
    int u = ++tot;
    len[u] = len[p] + 1;
    memcpy(ch[u], ch[q], sizeof ch[0]);
    link[u] = link[q];
    link[q] = u;
    for (; p && ch[p][r] == q; p = link[p]) ch[p][r] = u;
    return u;
  }
  int expand(int p, int r) {
    if (ch[p][r]) return split(p, ch[p][r], r);
    int u = ++tot;
    len[u] = len[p] + 1;
    memset(ch[u], 0, sizeof ch[0]);
    for (; p; p = link[p]) {
      if (ch[p][r]) {
        link[u] = split(p, ch[p][r], r);
        return u;
      } else {
        ch[p][r] = u;
      }
    }
    link[u] = 1;
    return u;
  }
};
广义 SAM 使用 `unordered_map` 存转移边(这个代码很慢,除非字符集大小不可接受否则不应选用,若字符串总长过大考虑换用 map)
template <int N>
struct suffixam {
  unordered_map<int, int> ch[N << 1];
  int tot, link[N << 1], len[N << 1];
  suffixam() : tot(1) {
    ch[1].clear();
    len[1] = link[1] = 0;
  }
  int split(int p, int q, int r) {
    if (len[q] == len[p] + 1) return q;
    int u = ++tot;
    len[u] = len[p] + 1;
    ch[u] = ch[q];
    link[u] = link[q];
    link[q] = u;
    for (; p && ch[p][r] == q; p = link[p]) ch[p][r] = u;
    return u;
  }
  int expand(int p, int r) {
    if (ch[p][r]) return split(p, ch[p][r], r);
    int u = ++tot;
    len[u] = len[p] + 1;
    ch[u].clear();
    for (; p; p = link[p]) {
      if (ch[p][r]) {
        link[u] = split(p, ch[p][r], r);
        return u;
      } else {
        ch[p][r] = u;
      }
    }
    link[u] = 1;
    return u;
  }
  template <bool rev>
  vector<int> bucketsort() {
    vector<int> per(tot), buc(tot + 1);
    for (int i = 1; i <= tot; i++) buc[len[i]] += 1;
    for (int i = 1; i <= tot; i++) buc[i] += buc[i - 1];
    for (int i = 1; i <= tot; i++) per[--buc[len[i]]] = i;
    if (rev) reverse(per.begin(), per.end());
    return per;
  }
};

更多代码参见 https://www.cnblogs.com/caijianhong/p/18153828/solution-UOJ577

最后可能需要用到得到 \(endpos\) 集合的大小:需要建图预处理,具体的见下一节:

点击查看代码
//method 1:暴力建图 dfs
LL dfs(int u,int fa=0){
    LL ans=0;
    for(int i=g.head[u];i;i=g.nxt[i]){
        int v=g[i].v; if(v==fa) continue;
        ans=max(ans,dfs(v,u)),t.siz[u]+=t.siz[v];
    }
    if(t.siz[u]>1) ans=max(ans,1ll*t.siz[u]*t.len[u]);
    return ans;
}
for(int i=2;i<=t.tot;i++) g.add(t.link[i],i);
//method 2:拓扑排序
int q[N*2+10],inn[N*2+10];
LL toposort(){
    int L=1,R=0;LL ans=0;
    for(int i=1;i<=tot;i++) inn[link[i]]++;
    for(int i=1;i<=tot;i++) if(!inn[i]) q[++R]=i;
    while(L<=R){
        int u=q[L++];
        siz[link[u]]+=siz[u];
        if(siz[u]>1) ans=max(ans,1ll*siz[u]*len[u]);
        if(--inn[link[u]]==0) q[++R]=link[u];
    }
    return ans;
}
//method 3:桶排序求拓扑序
int per[N*2+10],buc[N*2+10];
LL toposort(){
    memset(buc,0,sizeof buc);
    for(int i=1;i<=tot;i++) buc[len[i]]++;
    for(int i=1;i<=tot;i++) buc[i]+=buc[i-1];
    for(int i=tot;i>=1;i--) per[buc[len[i]]--]=i;
    LL ans=0;
    for(int i=tot;i>=1;i--){
        int u=per[i];
        if(siz[u]>1) ans=max(ans,1ll*siz[u]*len[u]);
        siz[link[u]]+=siz[u];
    }
    return ans;
}

要求得 \(endpos(u)\) 的大小或者具体值,我们需要在每次插入后在返回的 \(last\) 节点中打上一个标记(用一个数组或者线段树)表示这个 \(last\)\(endpos(u)\) 里面有一个 [插入的第几次] 位置,且 \(last\) 的所有后缀链接上,由于是包含类,所以都有这个 \(endpos\)。插入完了之后,每个点真实的 \(endpos\) 是它在 Link 树上的子树的 \(endpos\) 的并。(如果代码没有写错,则每个点都有不为空的 \(endpos\))在求这个真实 \(endpos\) 时可以考虑将所有点按照 \(len\) 倒序排序,那么这就是 Link 根向树的拓扑序。

后缀树

我们考虑一个无脑的东西:后缀树。它把 \(S\) 的的所有后缀插入到 Trie 里,并拿出结束点(下文称关键点)建虚树,空间是 \(O(n)\) 的。

SAM 与后缀树的联系

我们有定理:

反串 SAM 的 Link 树是其正串的后缀树。
简单推论:反串 SAM 向下跳 \(link\) 等价于正串上跳 \(\delta\)

那么所有概念都清晰了:

  • 等价类:后缀树上一个关键点和上方的非关键点。(不建虚树)
  • 包含类:后缀树上的祖先。
  • \(q\) 分裂的分类讨论:本质上是判断 \(q\) 是否为关键点,不是就分裂。
  • 增量构造:例如原串 bbaba,插入字符 b,实际上就是反串前面插入这个字符,即在后缀树上插入 bababb

一模一样。

那么它们有什么区别?其实没有(如果不涉及什么匹配),正串 SAM 是在 Link 树上搞事情,反串 SAM 是在 Trie 树上搞事情。

SAM 建立后缀树

在 SAM 的代码中,我们用到了几个数组,它们有对应的后缀树的含义:

数组 SAM 后缀树
\(len_u\) \(root\to u\) 的最长路长度 后缀树上 \(u\) 的深度
\(siz_u\) \(|endpos(u)|\) \(subtree(u)\) 中关键点个数
\(link_u\) \(u\) 最长的包含类 \(u\) 的父亲
\(ch_{u,r}\) \(\delta(u,r)\) \(r+u\) 在这棵 Trie 的位置

(注:\(siz_u\) 需要 dfs 预处理才有这个实际含义,其实它是 \(u\) 的出现次数。)

如果想求得后缀树上 \(u\) 下面那些儿子的边的第一个字母(或者叫后缀排序),请按以下方法执行:

  1. 建反串 SAM,记录 \(pos_u\) 表示 \(u\) 这个等价类是在插入第 \(i\) 个字符(先插入第 \(n\) 个最后插第 \(1\) 个)时建立的,split 的时候新的等价类的 \(pos\) 等于分裂那个。
  2. 连边:\(link_u\to^{S[pos[u]+len[link[u]]]}u\),连出来是棵树,证明很简单,你这个 \(pos[u]\) 相当于等价类 \(u\)\(leftpos\) 中的一个,\(u\) 的最长串是 \(S[pos[u]+len[u]-1]\),对应 \(u\) 头上的;最短串是 \(S[pos[u]+len[link[u]]]\),对应 \(link_u\) 底下的。

SAM 与后缀树的互换

其实可以这样说:正串 SAM 维护的是 \(endpos\) 或者 \(rightpos\),反串 SAM 维护的是 \(leftpos\);正串 SAM 的 Link 树,往上跳的时候 \(endpos\) 变多,长度变短,砍掉了前缀,所以它可以认为是将所有前缀从右到左插入的 Trie(意思是对于一个前缀 \([1,i]\),从 \(S[i]\) 开始向下插到 \(S[1]\));反串 SAM 的 Link 树,自然就是后缀树了。

各种自动机的比较

除了子序列自动机,我们现在见过的自动机有 ACAM(字符串匹配)、SAM(后缀自动机)、PAM(回文自动机)。它们的相同点是:状态转移集都形成一个 DAG。

自动机 状态集合 Link 树(若 \(link_y=x\)
SAM 原串的所有子串 则等价类 \(x\) 是等价类 \(y\) 的后缀
ACAM 所有字符串的前缀 则前缀 \(x\) 是前缀 \(y\) 的后缀
PAM 所有回文串 则回文子串 \(x\) 是回文子串 \(y\) 的后缀

简单应用

全都离不开 DAG 和 Link 树,二选一进行 DP 或匹配,甚至两个一起。基本子串结构将这些东西有机联合,见 https://www.cnblogs.com/crashed/p/17382894.html 对它的基本介绍。(据说原文是 2023 年许庭强论文)

两个后缀的 LCP

反串 SAM:后缀树上这两个后缀的 LCA 的深度。停停,怎么找这两个后缀?

for(int i=n;i>=1;i--) ap[i]=s.expand(a[i]-'a',ap[i+1]);

这样 \(ap_i\) 就是 \([i,n]\) 这个后缀对应后缀树位置。

同理我们可以找到前缀的最长公共后缀:正着做一遍就是了。

\(S[l,r]\) 子串在原串的出现次数

正串 SAM 上等价类的 \(|endpos|\),可以倍增。

具体说一下这个等价类怎么找:\(S[l,r]\),首先取 \(u\) 为插入 \(S_r\) 时返回的那个等价类,在后缀链接上,我们要找到一个点 \(p\) 使得 \(len_p\geq r-l+1\)(就是砍前缀,砍到它应该在的等价类)。然后取它的 \(endpos\) 大小就是答案了。

如果是反串 SAM 呢?反过来就行了。

int locate(int l,int r){
    int len=r-l+1,u=pos[r];
    for(int j=20;j>=0;j--) if(s.len[fa[j][u]]>=len) u=fa[j][u];
    return u;
}

本质不同的子串个数

这里注意到这个串是正串还是反串答案都不变。

  1. SAM,\(\sum\limits_{u}len_u-len_{link_u}\)
  • 反串 SAM,原后缀树的每一个点对应一个子串,那么两个关键点之间的所有点加起来就是子串个数。
  • 正串 SAM,每个等价类中最长长度为 \(len_u\),最短长度为 \(len_{link_u}+1\)(再短它的 \(endpos\) 增加,跳到上面),一共 \(len_u-len_{link_u}\) 个。对每个等价类分别计算。
  1. SAM,DAG 上 DP,倒推,\(f_u=1+\sum_{\delta(u,v)} f_v\)

最小表示法

倍长并使用正串 SAM,即寻找 SAM 中字典序最小的长为 \(|S|\) 的路径,DAG 上贪心走字典序最小的。

\(S,T\) 的最长公共子串 / 字符串匹配

  1. 反串 SAM,建出 \(S+\#+T\) 的后缀树,求 dfn 序,求相邻两个不同字符串的后缀的 LCP。就是找一个点,使得它的子树中既有来自 \(S\) 的又有来自 \(T\) 的,使 \(len\) 最大。这相当于是把 SA 搬到了树上。
  2. 考虑建出 \(S\) 的正串 SAM,对 \(T\) 的每个前缀求最大匹配,每次一点点地加入 \(T_i\),动态更新当前走到的状态的长度,一直跳 Link 直到找到转移边就转移,取最优值即可。这相当于把 ACAM 的方法搬到了 SAM 上,比较相似。这里细说一下怎么匹配:动态维护 \(u=1,l=0\) 初始,每次加入 \(T\) 的字符 \(r\),向上跳后缀链接找第一个 \(\delta(u,r)\) 存在的,同时如果跳了,则 \(l\) 更新为新的 \(len_u\)(没跳不更新)。然后如果存在 \(\delta(u,r)\),则 \(u:=\delta(u,r),l:=l+1\);否则 \(u=1,l=0\) 从头来过。注意 SAM 的匹配字符串一定要记录当前匹配的字符个数,因为一个等价类里的长度是不确定的。
  3. 考虑对 \(S,T\) 建广义 SAM,那么我们就是要找最大的 \(len_u\) 使得 \(endpos(u)\) 既有 \(S\) 又有 \(T\)

第二种方法就是字符串匹配,由此我们可以使用广义 SAM 薄纱 ACAM(虽然要乘个字符集是个大问题)

另外说一下怎么删字符,非常简单,\(l:=l-1\),搞完之后如果不在这个等价类了(\(l=len_{link_u}\)),那么 \(u=link_u\)

例题 CF235C Cyclical Quest

本质不同第 \(k\) 小子串

输出字符串

正串 SAM。我们将子串看作 DAG 上的一条路径,那么第 \(k\) 小子串自然就是第 \(k\) 小路径了。首先 DP 求出每个等价类 \(u\) 能走出去多少条路径:\(f_u=1+\sum_{v=\delta(u,r)} f_v\)。然后 dfs,到达等价类 \(u\) 时,如果 \(k=1\) 那么就是它自己,输出;否则按字典序遍历它的转移,如果 \(f_v<k\) 那么 \(k:=k-f_v\),否则 \(dfs(v,k)\)。这个 \(k\) 是会变的。另外记得空串算一个串。

输出区间

对着 DAG 做链剖分,比较阴间,展开说说。考虑向重链剖分一样,对于每个点选出重儿子 \(son_u\),使得它是所有出边(儿子)中 \(f\) 最大的。考虑优化刚才 dfs 的过程,首先倍增跳重链,跳到 \(k\) 不能再继续减,这时再跳轻边。这样的复杂度是 \(O(n\log^2n)\) 的。

有一个例题 ABC280H 是没有本质相同的,有另外一种后缀数组做法,这里说的 SAM 做法也能过。(如果用下一节说的后缀排序还原后缀树那么可以线性遍历树,和后缀数组一样了)

点击查看代码

https://atcoder.jp/contests/abc280/submissions/43972309

void print(int u,int len){printf("%d %d %d\n",ep[u].first,ep[u].second-len+1,ep[u].second);}
void solve(int u,LL x,int len){
    for(int j=18;j>=0;j--) if(x>g[j][u]){
        if(x-g[j][u]<=f[to[j][u]]) x-=g[j][u],u=to[j][u],len+=1<<j;
    }
    if(x<=siz[u]) return print(u,len);
    x-=siz[u];
    for(int i=0;i<26;i++){
        int v=s.ch[u][i];
        if(x<=f[v]) return solve(v,x,len+1);
        else x-=f[v];
    }
}
void init(){
    m=s.tot;
    iota(p+1,p+m+1,1);
    sort(p+1,p+m+1,[&](int i,int j){return s.len[i]<s.len[j];});
    for(int j=m;j>=1;j--){
        int u=p[j]; int*ch=s.ch[u],son=0;
        siz[s.link[u]]+=siz[u],ep[s.link[u]]=ep[u],f[u]=siz[u];
        for(int i=0;i<26;i++) if(ch[i]) f[u]+=f[ch[i]];
        for(int i=0;i<26;i++) if(f[ch[son]]<f[ch[i]]) son=i;
        g[0][u]=siz[u],to[0][u]=ch[son];
        for(int i=0;i<son;i++) g[0][u]+=f[ch[i]];
    }
    for(int j=1;j<=18;j++){
        for(int i=1;i<=m;i++) to[j][i]=to[j-1][to[j-1][i]];
        for(int i=1;i<=m;i++) g[j][i]=g[j-1][i]+g[j-1][to[j-1][i]];
    }
}

询问的时候记得加 siz[1] 哦

后缀排序

那这位更是重量级,学会了可以薄纱后缀数组。方法就是还原后缀树。

  1. 建反串 SAM,记录 \(pos_u\) 表示 \(u\) 这个等价类是在插入第 \(i\) 个字符(先插入第 \(n\) 个最后插第 \(1\) 个)时建立的,split 的时候新的等价类的 \(pos\) 等于分裂那个。(实际上 \(pos_u=\min\{x|x\in startpos(u)\}\)
  2. 连边:\(link_u\to^{S[pos[u]+len[link[u]]]}u\),连出来是棵树。
  3. dfs 树,按照边的字典序,先访问到的 \(pos\) 的后缀排序更前。
  4. 和 SA 一样求出 \(height\) 数组。
点击查看代码

https://uoj.ac/submission/646959

//split pos[u]=pos[q];
//expand pos[u]=now
void dfs(int u){
    if(key[u]) sa[rnk[pos[u]]=++cnt]=pos[u];
    for(int i=0;i<26;i++) if(to[u][i]) dfs(to[u][i]);
}
void gethei(){
    for(int i=1,k=0;i<=n;i++){
        if(rnk[i]==1) continue;
        int j=sa[rnk[i]-1]; k=max(k-1,0);
        while(max(i,j)+k<=n&&a[i+k]==a[j+k]) k++;
        hei[rnk[i]]=k;
    }
}
int main(){
    scanf("%s",a+1),n=strlen(a+1);
    for(int i=n,last=1;i>=1;i--) key[last=s.expand(a[i]-'a',last,i)]=1;
    for(int i=2;i<=s.tot;i++) to[s.link[i]][a[pos[i]+s.len[s.link[i]]]-'a']=i;
    dfs(1),gethei();
    for(int i=1;i<=n;i++) printf("%d%c",sa[i]," \n"[i==n]);
    for(int i=2;i<=n;i++) printf("%d%c",hei[i]," \n"[i==n]);
    return 0;
}

基本子串结构

https://www.cnblogs.com/caijianhong/p/18153828/solution-UOJ577

posted @ 2022-11-06 19:17  caijianhong  阅读(242)  评论(0编辑  收藏  举报