• 后缀自动机详解


    当想让学妹看博客时,怕旧的写的太烂被嫌弃,又怕新的看不懂……哎

    一、定义:

    单词的有向无环图

    二、作用

    从原点出发形成的所有路径即为单词的所有子串,并且通过维护endpos和endpos类,得知每个串出现的次数和出现的位置

    三、构建后缀自动机

    一些性质:

    1. endpos :数集,一些子串他们出现的位置相同,这些位置的集合称为endpos

    2. endpos类 :串集,一些子串他们出现的位置相同,这些子串的集合称为endpos类

    3. fail树

    • 树上节点的endpos是他父亲节点endpos的真子集

    • 树上节点的父亲包含的子串是当前节点包含的子串的后缀

    • 树上节点的endpos类中的子串长度连续

    • 树上节点的endpos类中的最小长度等于他父亲节点endpos类中的最长长度+1

    后缀自动机维护的东西:

    1. fail树上的父亲

    2. c边连向的点

    3. endpos类中最长串的长度

    以上是一些概念,由他的性质和用途决定,就像1+1=2一样解释不了,早出生一点也许就不这样了呢

    这些概念性的东西介绍完毕之后,就要开始讲如何维护了

    考虑每次新添加一个字符进后缀自动机中(新开一个节点),改变的只有原串中的新字串的所有后缀节点,就相当于在原来的终止节点后添加一个字符的所有节点。

    新开节点的长度+1,且每个终止节点都要向他引出一个c边,而由性质我们可以知道当前终止节点的所有祖先既终止节点集,当最终跳到源点的时候就表示,旧串中没有新形成的字符串的后缀,说明没有节点会发生改变,所以新节点的父亲指向1就可以了,那么如下代码的意义就解释完毕:

                    int preNode = lastNode;
                    int nowNode = lastNode = ++tot;
                    mac[nowNode].len = mac[preNode].len+1;
                    for(;preNode&&!mac[preNode].ch[c];preNode = mac[preNode].fa) mac[preNode].ch[c] = nowNode;
                    if (!preNode) mac[nowNode].fa = 1;
    

    如果旧串中存在新形成的字符串的后缀呢?另tmpNode = ch[preNode][c],说明tmpNode为现在的终止节点,两种情况:len[tmpNode] = len[preNode]+1或者len[tmpNode] != len[preNode]+1

    第一种情况说明tmpNode的endpose类中的所有字符串都是新串的后缀,把nowNode的父亲指向tmpNode

    第二种情况说明tmpNode的endpose类中的字符串有一部分是新串的后缀,而这些串又是剩下那些串的后缀,为了方便维护,我们想到了拆点,把是后缀的一部分单独拎出来,把两部分分别维护的后缀自动机的内容都更新就可以了

    突然发现最后一句让我也有点不理解的样子,但是经过我的深思熟虑,终于思考明白了:

    为什么我会出现不等的情况,说明有其他的不经过终止节点的路径连向了这个点,并且拆分后的点的endpose类一个只包含那些路径所形成的字符串,一个只包含经过终止节点的路径所形成的字符串,因为第一种路径本来就连向tmpNode,所以我们只需要把第二种路径从指向tmpNode变成指向tmpNode1即可,代码如下:

                    int tmpNode = mac[preNode].ch[c];
    		if (mac[tmpNode].len == mac[preNode].len+1) mac[nowNode].fa = tmpNode; 
    		else{
    			int tmpNode1 = ++tot;
    			mac[tmpNode1] = mac[tmpNode];
    			mac[tmpNode1].len = mac[preNode].len+1;
    			mac[tmpNode].fa = mac[nowNode].fa = tmpNode1;
    			for (;preNode&&mac[preNode].ch[c] == tmpNode;preNode = mac[preNode].fa) mac[preNode].ch[c] = tmpNode1;
    		}
    

    四、应用

    1. 如何求endpose集合的大小,每次新增加一个节点的时候说明出现了一个前缀,说明他会比他的父亲少一个endpose,初始化为1,在fail树上dp

    2. 如何求本质不同的子串的个数,l[nowNode] - l[fa[nowNode]]

    3. 求第k大/小子串 弦论

    五、广义后缀自动机

    先建个Trie树然后在Trie树上添加新的节点和边来构建后缀自动机,不想说了,就这样吧

    六、代码

    void add(int c){
    	int preNode = lastNode;
    	int nowNode = lastNode = ++tot;
    	dp[nowNode] = 1;
    	mac[nowNode].len = mac[preNode].len+1;
    	for(;preNode&&!mac[preNode].ch[c];preNode = mac[preNode].fa) mac[preNode].ch[c] = nowNode;
    	if (!preNode) mac[nowNode].fa = 1;	
    	else{
    		int tmpNode = mac[preNode].ch[c];
    		if (mac[tmpNode].len == mac[preNode].len+1) mac[nowNode].fa = tmpNode; 
    		else{
    			int tmpNode1 = ++tot;
    			mac[tmpNode1] = mac[tmpNode];
    			mac[tmpNode1].len = mac[preNode].len+1;
    			mac[tmpNode].fa = mac[nowNode].fa = tmpNode1;
    			for (;preNode&&mac[preNode].ch[c] == tmpNode;preNode = mac[preNode].fa) mac[preNode].ch[c] = tmpNode1;
    		}
    	}
    }
    
    struct SAM{
    	int ch[maxn][30],len[maxn],fa[maxn],siz[maxn],tot;
    	void init(){
    		tot = 1;
    		memset(ch,0,sizeof(ch));
    		memset(len,0,sizeof(len));
    		memset(fa,0,sizeof(fa));
    		memset(siz,0,sizeof(siz));
    	}
    	void insert(string s){
    		int len = s.length(),root = 1;
    		for (int i = 0;i < len;i++){
    			int nxt = s[i]-'a';
    			if (!ch[root][nxt]) ch[root][nxt] = ++tot;
    			root = ch[root][nxt];
    		}
    	}
    	int add(int c,int lastNode){
    		int nowNode = ch[lastNode][c];
    		if (len[nowNode]) return nowNode;
    		len[nowNode] = len[lastNode] +1;
    		int preNode = fa[lastNode];
    		for (;preNode&&!ch[preNode][c];preNode = fa[preNode]) ch[preNode][c] = nowNode;
    		if (!preNode) fa[nowNode] = 1;
    		else{
    			int tmpNode = ch[preNode][c];
    			if (len[tmpNode] == len[preNode]+1) fa[nowNode] = tmpNode;
    			else{
    				int tmpNode1 = ++tot;
    				for (int i = 0;i < 26;i++){
    					if (!len[ch[tmpNode][i]]) continue;
    					ch[tmpNode1][i] = ch[tmpNode][i];
    				}
    				len[tmpNode1] = len[preNode]+1;
    				fa[tmpNode1] = fa[tmpNode];
    				fa[tmpNode] = fa[nowNode] = tmpNode1;
    				for (;preNode&&ch[preNode][c] == tmpNode;preNode = fa[preNode]) ch[preNode][c] = tmpNode1;
    			}
    		}
    		return nowNode;
    	}
    	void build(){
    		queue<pair<int,int> > q;
    		for (int i = 0;i < 26;i++){
    			if (ch[1][i]){
    				pair<int,int> x;
    				x.first = i,x.second = 1;
    				q.push(x);
    			} 
    		}
    		while (!q.empty()){
    			pair<int,int> x = q.front();q.pop();
    			int lastNode = add(x.first,x.second);
    			for (int i = 0;i < 26;i++){
    				if (ch[lastNode][i]){
    					pair<int,int> tmp;
    					tmp.first = i,tmp.second = lastNode;
    					q.push(tmp);
    				}
    			}
    		}
    	}
    }sam;
    
  • 相关阅读:
    win10自带输入法突然变成了繁体
    Eclipse 包视图折叠
    Unknown column '字段名' in 'field list' 错误解决方案
    The method getContextPath() from the type HttpServletRequest
    Eclipse 设置新建文件默认编码为 utf-8 的方法
    Java 混淆器
    程序员,不能缺少的几张图
    北漂程序员,扬帆起航的地方
    数据是啥?数据都去哪儿了?
    7行代码搞定WEB服务
  • 原文地址:https://www.cnblogs.com/little-uu/p/14507070.html
Copyright © 2020-2023  润新知