• 【数据结构】后缀自动机(SAM)小记


    后缀自动机(SAM)小记

    介绍

    简单来说,就是使用一个 \(DAG\) 以及一棵树维护一个字符串所有子串(压缩的)信息。

    其中 \(DAG\) 的点称为状态

    endpos

    个人认为 \(SAM\) 的核心在于 \(endpos\)

    子串(终点)在原串出现的下标集合称为 \(endpos\) 集合。

    例如,对于原串 \(\texttt{aabaaba}\)\(endpos(\texttt{ab}) = \{3, 6\}\)

    性质

    \(endpos\) 有如下性质:

    • 对于任意子串 \(a, b\),二者 \(endpos\) 的关系必为:

      1. \(endpos(a) \subseteq endpos(b)\)

      2. \(endpos(a)\cap endpos(b) = \empty\)

      之一

    • \(endpos\) 相同的两个子串属于一个等价类。(这意味着:\(SAM\) 中每个状态会对应 \(endpos\) 相同的若干个子串)

    • 原串中 \(endpos\) 对应的等价类数量级为 \(O(N)\)

    • 对于一个状态 \(st\) 及任意的 \(longest(st)\) 的后缀 \(s\) ,若有:$|shortest(st)|≤|s|≤|longsest(st)| $,则 \(s\in substrings(st)\)

      \(|shortest(st)|\)\(minlen(st)\)\(|longest(st)|\)\(len(st)\)

    由上面的 \(endpos\) 性质二可知,每个 \(endpos\) 集合可以被划分为若干个集合(每个集合当然可以同样地划分下去),这样我们就可以按照划分的过程得到一个树形的结构,这被称为 \(link\)

    对于一个状态 \(st\),我们有:\(minlen(st) = len(link(st)) + 1\)

    后缀自动机的建立

    鉴于会涉及代码,先放模板题链接:

    https://www.luogu.com.cn/problem/P3804

    这是一个在线的过程,就是按照每次加一个点(对应字符串中加一个字符)然后相应地连边构造的。

    建立过程见代码注释部分。

    每个状态维护了:

    • \(link\) 树的父节点 \(fa\)
    • 该状态对应的等价类中最长的子串长 \(len\)
    • \(DAG\) 中连接的字符集所到的状态 \(ch[]\)

    性质

    • 起点开始沿蓝边任意路径所到的状态为该状态的子串。
    • \(fa(st)\) 的任意子串一定是 \(st\) 任意子串的后缀

    更多性质见下图:

    (这是 \(\texttt{aabbabd}\) 的后缀自动机)

    image

    参考代码

    树上计数。

    #include<bits/stdc++.h>
    using namespace std;
    
    const int N=2e6+5, M=N<<1;
    
    #define int long long
    
    struct Edge{
    	int to, next;
    }e[M];
    
    int h[N], idx;
    
    void add(int u, int v){
    	e[idx].to=v, e[idx].next=h[u], h[u]=idx++;
    }
    
    char str[N];
    
    struct Node{
    	int len, fa; // longest length of state, link
    	int ch[26];
    }node[N];
    int tot=1, last=1; // 初始的根
    int f[N];
    
    void ins(int c){
    	int p=last, np=last=++tot; // np 代表新插入的点(状态)
    	f[tot]=1; // 新状态的大小(即子串个数)
    	node[np].len=node[p].len+1; // 新插入字符后对应的最长子串当然比前一个状态的长 1。
    	for(; p && !node[p].ch[c]; p=node[p].fa) node[p].ch[c]=np; // 跳 link,没有 c 儿子的状态连边。
    	if(!p) node[np].fa=1; // 跳到根则将 np 的 link 指向根(因为 c 没有出现过)
    	else{
    		int q=node[p].ch[c]; // q 为第一个有 c 儿子的状态的 c 儿子
    		if(node[q].len==node[p].len+1) node[np].fa=q; // 找到正好接受新串后缀的状态
    		else{
    			int nq=++tot; // 开新点
    			node[nq]=node[q], node[nq].len=node[p].len+1;
    			node[q].fa=node[np].fa=nq;
    			for(; p && node[p].ch[c]==q; p=node[p].fa) node[p].ch[c]=nq;
    		}
    	}
    }
    
    int res=0;
    
    void dfs(int u){
    	for(int i=h[u]; ~i; i=e[i].next){
    		int go=e[i].to;
    		dfs(go);
    		f[u]+=f[go];
    	}
    	if(f[u]>1) res=max(res, f[u]*node[u].len);
    }
    
    signed main(){
    	scanf("%s", str);
    	for(int i=0; str[i]; i++) ins(str[i]-'a');
    	memset(h, -1, sizeof h);
    	for(int i=2; i<=tot; i++) add(node[i].fa, i);
    	dfs(1);
    	
    	cout<<res<<endl;
    		
    	return 0;
    }
    
  • 相关阅读:
    Android周学习Step By Step(6)Android的数据库SQLite
    Android周学习Step By Step(2)HelloWorld
    解决方案(.sln)文件
    浅谈测试(1)单元测试
    批量上传功能的实现
    分页控件AspNetPager的用法
    .net下验证码的简单实现
    window.alert重写实现友好的对话框(支持IE)
    网页上自定义运行和测试HTML脚本
    数据库行转列的sql语句(zt)
  • 原文地址:https://www.cnblogs.com/Tenshi/p/16408718.html
Copyright © 2020-2023  润新知