• [数据结构]后缀自动机


    前言

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

    SAM

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

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

    没有后缀链接的SAM的样子

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

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

      ( iny exttt{借用一下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 > 字符串 > 后缀自动机

  • 相关阅读:
    微信小程序
    svn
    当滑动条滑动到某一位置触发js
    css固定页面
    css三级菜单
    h5时钟
    DOM节点
    应用r.js来优化你的前端
    浅谈javascript中的作用域
    javascript 中的 arguments,callee.caller,apply,call 区别
  • 原文地址:https://www.cnblogs.com/linzhengmin/p/11361325.html
Copyright © 2020-2023  润新知