[数据结构]后缀自动机

前言

对于字符串 \(s\) ,\(|s|\) 表示s的长度
对于字符集 \(A\) , \(|A|\) 表示 \(A\) 的大小
本文字符串下标一律从0开始
本文字数较多,如有错别字或者概念性错误,请联系博主或在下方回复。

SAM

后缀自动机 (suffix automaton, SAM) 是一种解决多种字符串问题的数据结构。
SAM基于一个字符串构建的,是给定字符串的所有子串的压缩形式。
标准定义为: 字符串 \(s\) 的SAM是一个接受 \(s\) 的所有后缀的最小 \(\texttt{DFA}\) (确定性有限自动机或确定性有限状态自动机)
我们记 \(t_0\) 为字符串的一个虚拟源点,事实上这种操作(构造虚拟节点)应用非常广泛。
那么SAM应当是:

  1. 有向无环图,节点为状态,边叫做转移
  2. 所有节点都可以由 \(t_0\) 到达
  3. 每个转移代表一个字母,且任意一个状态的出边的字母不同
  4. 存在一个或多个终止状态,使得从 $t_0 $ 到终止状态上的所有转移依访问顺序排列,对应原字符串 \(s\) 的某个后缀,且 \(s\) 的任何后缀均可以上述方式描述。
  5. SAM是满足以上的条件节点数最小的自动机
    简单来说,没有后缀链接的SAM是一棵以 \(t_0\) 为源点的无向图。这个图的名字叫做后缀链接树。

没有后缀链接的SAM的样子

目前您暂且不需要知道后缀链接是什么。

  • 空串
  • 字符串 s = "a"
  • 字符串 s = "abbb"

    \(\tiny\texttt{借用一下oi-wiki中的图片}\)

字串的性质

SAM上的任意路径与字符串 \(s\) 字串相互映射(可以互相表示)
注意上面这点非常重要。

结束位置

结束位置,通常记作endpos,就是子串在原串中的某一个匹配的最后一个字符在原串中的下标。
我们注意到一个子串的endpos并不唯一,所以结束位置应该时一个集合。
结束位置集合,通常记作endpos集合,表示一个子串在字符串中的所有结束位置。
举个例子,字符串 "abbbabbb" 的子串 "ab" 的endpos集合为{1,5},

后缀链接

长话短说,后缀链接连接了两个不同的endpos集合,
也就意味着从当前状态最长后缀的endpos集合,跳转到另外的endpos。
就是连接当前子串最长的"与当前子串 endpos 的不同的后缀"与该子串所属状态的边。

后缀链接树

SAM中所有后缀链接构成的树叫做后缀链接树。
后缀链接树非常重要,因为我们后续的针对题目的大部分操作都是在后缀链接树上进行的。

构造SAM

  • 读入需要的字符 c
  • 创建一个新的状态 \(cur\) ,并把它的 \(len\) 置为上一个状态+1
  • 从上一个状态延链接向前跳跃,更新沿途的 \(next\) ,直到跳跃到空或者发现了一个状态 \(p\) ,\(p\) 已经存在到字符 c 的转移
  • 将 \(p\) 通过字符 c 转移到状态即为 \(q\)
  • 如果\(len_p + 1 = len_q\),很简单,将 \(cur\) 的后缀链接连到 \(q\) 并结束算法。
  • 否则,"复制(copy)" \(q\) 至一个新的克隆节点记为 \(clone\) ,将 \(clone\) 的 \(len\) 重新设为 \(len_p+1\) ,然后将 \(cur\) 和 \(q\) 的后缀链接指向 \(clone\) 。最终我们需要使用后缀链接从状态 \(p\) 往回走,只要存在一条通过 \(p\) 到状态 \(q\) 的转移,就将该转移重定向到状态 \(clone\)

代码

原题参照洛谷[模板]后缀自动机

map版( \(O_2\) )

时空需求都较高,但是稳定,可以处理任意字符。
对 \(O_2\) 需求很高。

#include <cstdio>
#include <cstring>
#include <string>
#include <map>

using namespace std;

const int MAXN = 3000005;

int sz[3000005];

struct SAM{
    int size, last;
    struct Node{
        int len, link;
        map<char, int> next;
    } nodes[MAXN];
    void init(){
        nodes[0].len = 0, nodes[0].link = -1;
        size = 1; last = 0;
    }
    void insert(char ch){
        int cur = size++, p; nodes[cur].len = nodes[last].len + 1; sz[cur] = 1;
        for (p = last; ~p && !nodes[p].next.count(ch); p = nodes[p].link)
            nodes[p].next[ch] = cur;
        if (p == -1)
            nodes[cur].link = 0;
        else{
            int q = nodes[p].next[ch];
            if (nodes[p].len + 1 == nodes[q].len)
                nodes[cur].link = q;
            else{
                int clone = size++;
                nodes[clone].len = nodes[p].len + 1;
                nodes[clone].next = nodes[q].next;
                nodes[clone].link = nodes[q].link;
                for ( ; ~p && nodes[p].next[ch] == q; p = nodes[p].link)
                    nodes[p].next[ch] = clone;
                nodes[q].link = nodes[cur].link = clone;
            }
        }
        last = cur;
    }
    void build(char *buf, int len = 0){
        if (!len) len = strlen(buf);
        for (int i = 0; i < len; ++i)
            insert(buf[i]);
    }
} sam;

struct Edge{
    int to, next;
} edges[6000005];

int head[3000005], edge_num;

inline void addEdge(int u, int v){
    edges[++edge_num] = (Edge){v, head[u]};
    head[u] = edge_num;
}

inline void buildParentTree(){
    for (int i = 1; i < sam.size; ++i)
        addEdge(sam.nodes[i].link, i);
}

long long ans = 0;

void DFS(int u){
    for (int c_e = head[u]; c_e; c_e = edges[c_e].next){
        int v = edges[c_e].to;
        DFS(v); sz[u] += sz[v];
    }
    if (sz[u] > 1)
        ans = max(ans, 1ll * sz[u] * sam.nodes[u].len);
}

char ch[1000005];

int main(){
    sam.init(); scanf("%s", ch);
    sam.build(ch); buildParentTree();
    string s = ch;
    DFS(0);
    printf("%lld", ans);
    return 0;
}

数组版

时空需求较低,但是只能处理子母类问题。

#include <cstdio>
#include <cstring>
#include <string>

using namespace std;

const int MAXN = 3000005;

int sz[3000005];

struct SAM{
    int size, last;
    struct Node{
        int len, link;
        int next[26];
    } nodes[MAXN];
    void init(){
        nodes[1].len = 0, nodes[1].link = 0;
        size = 2; last = 1;
    }
    void insert(char ch){
        int cur = size++, p; nodes[cur].len = nodes[last].len + 1; sz[cur] = 1;
        for (p = last; p && !nodes[p].next[ch - 'a']; p = nodes[p].link)
            nodes[p].next[ch - 'a'] = cur;
        if (!p)
            nodes[cur].link = 1;
        else{
            int q = nodes[p].next[ch - 'a'];
            if (nodes[p].len + 1 == nodes[q].len)
                nodes[cur].link = q;
            else{
                int clone = size++;
                nodes[clone].len = nodes[p].len + 1;
                memcpy(nodes[clone].next, nodes[q].next, sizeof(nodes[q].next));
                nodes[clone].link = nodes[q].link;
                for ( ; p && nodes[p].next[ch - 'a'] == q; p = nodes[p].link)
                    nodes[p].next[ch - 'a'] = clone;
                nodes[q].link = nodes[cur].link = clone;
            }
        }
        last = cur;
    }
    void build(char *buf, int len = 0){
        if (!len) len = strlen(buf);
        for (int i = 0; i < len; ++i)
            insert(buf[i]);
    }
} sam;

struct Edge{
    int to, next;
} edges[6000005];

int head[3000005], edge_num;

inline void addEdge(int u, int v){
    edges[++edge_num] = (Edge){v, head[u]};
    head[u] = edge_num;
}

inline void buildParentTree(){
    for (int i = 2; i < sam.size; ++i)
        addEdge(sam.nodes[i].link, i);
}

long long ans = 0;

void DFS(int u){
    for (int c_e = head[u]; c_e; c_e = edges[c_e].next){
        int v = edges[c_e].to;
        DFS(v); sz[u] += sz[v];
    }
    if (sz[u] > 1)
        ans = max(ans, 1ll * sz[u] * sam.nodes[u].len);
}

char ch[1000005];

int main(){
    sam.init(); scanf("%s", ch);
    sam.build(ch); buildParentTree();
    string s = ch;
    DFS(1);
    printf("%lld", ans);
    return 0;
}

参考资料及文献

①: oier-wiki > 字符串 > 后缀自动机

原文地址:https://www.cnblogs.com/linzhengmin/p/11361325.html

时间: 2024-07-31 00:06:12

[数据结构]后缀自动机的相关文章

51nod 1600 Simplr KMP(后缀自动机+维护树上的数据结构)

题意:对于每个位置,统计有多少个相同的字串. 分析:按照题目的意思,把fail树画出来就会发现,对于第i个字符:ans[i] = ans[i-1] + (ans[i-1]-ans[i-1]) + cal(i); cal(i)是计算s[1…i-1]所有子串与s[1…i]的最长公共后缀的和.换句话说,根据后缀自动机性质,沿着parent树往上走可以知道对于后缀s[1…i]的所有位置的公共后缀长度以及个数(right集合的大小).很容易可以计算出cal(i), 只要每次新增一个字符的时候,在paren

【字符串数据结构后缀系列Part3】后缀自动机的性质和应用

学会了构建SAM之后,我们要开始学如何使用SAM来处理各种问题了. 我们先来整体看一下SAM的性质(引自2015国家集训队论文集张天扬<后缀自动机及其应用>): 1.每个状态s代表的串的长度是区间(lenfas,lens]. 2.对于每个状态s,它代表的所有串在原串中出现次数和每次出现的右端点相同. 3.在后缀自动机的Parent树中,每个状态的right集合都是其父状态right集合的子集. 4.后缀自动机的Parent树是原串的反向前缀树 . 5.两个串的最长公共后缀,位于这两个串对应状态

[转]后缀自动机

原文地址:http://blog.sina.com.cn/s/blog_8fcd775901019mi4.html 感觉自己看这个终于觉得能看懂了!也能感受到后缀自动机究竟是一种怎样进行的数据结构了... 笔者自己的话会用楷体表示出来...[说不定能帮助大家理解,但是可能也破坏了大家的自主理解力?所以...看不懂的话再来看好咯...] 常用的字符串处理工具: 1.       整词索引:排序+二分:Hash表.可以解决整词匹配,但不支持前缀搜索:Hash表在模式串定长的情况下可以用RK解决多模式

Codeforces 235C Cyclical Quest - 后缀自动机

Some days ago, WJMZBMR learned how to answer the query "how many times does a string x occur in a string s" quickly by preprocessing the string s. But now he wants to make it harder. So he wants to ask "how many consecutive substrings of s

后缀自动机学习小记

简介 后缀三姐妹:后缀数组,后缀自动机,后缀树. 后缀自动机:Suffix Automation,也叫SAM. 创立算法的思路来源:能不能构出一个自动机(本质就是一个有向图),能识别一个串的所有后缀. 识别所有后缀基础想法 把所有的后缀都放进一个trie里面,比如串aabbabd. 这样的状态太多了,怎么把状态数缩小. 减小状态数的方法 定义一个子串的right集合为这个子串在原串中出现的右端点集合. 如果两个子串A和B的right集合完全相同的话,那么他们明显一个是另一个的后缀,假设A是B的后

【模板整合】SAM后缀自动机的构建

太弱了QAQ学完SAM这么久才学会构建QAQ #include<iostream> #include<cstdio> #include<cstring> #include<cmath> #include<algorithm> #define MAXN 200010 using namespace std; struct sam { int p,q,np,nq; int cnt,last; int a[MAXN][26],len[MAXN],fa[

后缀自动机详解 -----转载

转载于:http://blog.csdn.net/qq_35649707/article/details/66473069 原论文(俄文)地址:suffix_automata 后缀自动机 后缀自动机(单词的有向无环图)——是一种强有力的数据结构,让你能够解决许多字符串问题. 例如,使用后缀自动机可以在某一字符串中搜索另一字符串的所有出现位置,或者计算不同子串的个数——这都能在线性 时间内解决. 直觉上,后缀自动机可以被理解为所有子串的简明信息.一个重要的事实是,后缀自动机以压缩后的形式包含了一个

后缀自动机的一点点理解

后缀自动机的一点点理解 前言 最近心血来潮,想学学SAM,于是花了一晚上+一上午 勉强打了出来(但是还是不理解) 虽说张口就讲我做不到 但是一些其他的东西还是有所感触的 索性,乱口胡点东西,谢谢关于SAM的一些简单的理解 资料 丽洁姐WC PPT hihocoder上的后缀自动机 一些概念 这些概念都不读懂,接下来真的是步履维艰 本来我们要的是一个能够处理所有后缀的数据结构 但是我们发现,如果对于每一个后缀都要插入进Trie树 空间复杂度完全背不动(\(O(n^2)\)级别) 于是,后缀自动机出

BZOJ 3926: [Zjoi2015]诸神眷顾的幻想乡 广义后缀自动机 后缀自动机 字符串

https://www.lydsy.com/JudgeOnline/problem.php?id=3926 广义后缀自动机是一种可以处理好多字符串的一种数据结构(不像后缀自动机只有处理一到两种的时候比较方便). 后缀自动机可以说是一种存子串的缩小点数的trie树,广义后缀自动机就是更改了一下塞点的方式让它可以塞多个子串. 1 #include<iostream> 2 #include<cstdio> 3 #include<algorithm> 4 #include<