zoukankan      html  css  js  c++  java
  • 回文自动机[学习笔记]

    回文自动机一一处理回文串问题的有力武器

    这几天一直沉迷字符串数据结构

    看了很多大佬的回文自动机学习笔记,稍微有点理解了,整理一下吧

    1.概念

    (quad)a.大概: 同其他自动机一样,回文自动机是个DAG,它用相当少((O(n)))的空间复杂度就存储了这个字符串的所有回文串信息。一个回文自动机包含不超过(|S|)个节点,每个节点都表示了这个字符串的一个不重复的回文子串,同时一个节点会有不超过字符集大小的边连向其他节点,以及一条fail边连向这个点的fail...这些都会在下面介绍

    (quad)b.森林: 和别的自动机不太一样,回文自动机是有两棵树的森林:其中一棵是长度为偶数的回文串集合,另一棵是长度为奇数的回文串集合,这两棵树的根节点分别表示长度为0(空串)和-1(无实际含义,便于运算)的回文串;

    (quad)c.边:自动机中每条有向边都有一个字符类型的权值,起点的串左右分别加上这个字符得到的就是终点的串。举个栗子:设一条边权为(c)的边连接的两个点分别是(A,B)(A)表示回文串(aba),则(B)表示的回文串就是(cabac) 。特别的,如果(A)是那个长度为(-1)的根,(B)串就是这条边的权值。。。

    (quad)d.点:当你插入一个字符的时候,插入的点代表的就是这个字符匹配的最长回文串,也就是说从根节点往下顺着边走,记着一个str一开始为空,一边走一边不停地往str左右两边添加新的字符,走到一个点,这个点代表的回文串就是str

    (quad)e.(fail)边:每个点都有个fail边,这条边指向这个点所代表的回文串的 最长回文后缀 所在的那个点(最长回文后缀:串中满足回文的最长的后缀,这个串自己不算)如果没有,则指向0(就是那个根节点)。特别的,0的fail节点就是那个长度为-1的点。

    2.构造:

    (quad)我是用的一个结构体存的,(len,fail,son[26],siz) 分别代表这个串的长,fail节点,连出来的每一条边以及这个回文串的数量,如下

    struct node{
    	int len,fail,son[26],siz;
    };
    node prt[maxn]; 
    

    我们把两个根下标设为0和1,并根据上面介绍的给他们赋值

    	prt[1].len=-1;
    	prt[0].fail=prt[1].fail=1;
    

    然后我们就可以把点一个一个加入到回文自动机中,这可以用一个函数(extend)来实现,具体实现方法如下:

    设我们以前插入的最后一个点为(last),这次要插入一个点x,首先要找到一个点(cur)为满足前面的字符等于新加入字符的,(last)的最长的回文后缀,这个过程可以不停地在(last)(fail)链上跑,因为(fail)所对应的正是串的最长回文后缀,这个可以用下面函数实现:

    int getfail(int x){
    	while(s[n-prt[x].len-1]!=s[n]) x=prt[x].fail;
    	return x;
    }
    

    (cur)已经包含权值为x的出边了,我们就可以简单地将出边终点的权值++,继续去加下一个点了。如果不包含权值x的边,我们就需要新建一个点(now)并让(cur)把边连向他,(now)代表的长度自然是(cur)的长度+2,然后我们只要求出(now)(fail)就完事了。

    (fail)的话可以用cur的(fail)来求,就用上面求(cur)的方法,但是不能用(cur)本身(想一想,为什么)

    当然最后千万不要忘记把(last)的值更新啊(qwq)

    void extend(int x){
    	int cur=getfail(last);
    	int now=prt[cur].son[x];
    	if(!now){
    		now=++num;
    		prt[now].len=prt[cur].len+2;
    		prt[now].fail=prt[getfail(prt[cur].fail)].son[x];
    		prt[cur].son[x]=now;
    	}
    	prt[now].siz++;
    	last=now;
    }
    

    累计答案可以从下往上把回文串数目加起来,显然上面的串一定是下面串的子串嘛(qwq)

    void count(){
    	for(int i=num;i>=2;i--)
    		prt[prt[i].fail].siz+=prt[i].siz;
    }
    

    4.举个栗子:

    如图,我们已经把串(abab)的回文自动机建好了,下面要添加一个点(a),此时(last=5)

    首先求出(cur)(last)所代表的回文串(bab)前边的字符正好与要加入的字符(a)相等,所以(cur)就是(last),我们发现(cur)不存在边权为(a)的出边,于是新建个点 6,从(cur)连一条边(a)到 6;

    6 的长度自然是5的长度+2((a'bab'a))

    然后求6的(last):5的(fail)指向3((b)),可以发现,3前面的那个字符(a)就是新加的字符(怎么那么巧...),于是我们把6的fail指向点3的(a)边所指向的点4;

    嗯,(last)更新为6,6的数量++,结束;

    最后累加答案,

    (siz(6)=1)(siz(4)=1)

    (siz(5)=1)

    ((siz(4)+=siz(6))=2)

    ((siz(3)+=siz(5))=2)

    ((siz(2)+=siz(4))=3)

    附:闲得自己也写了个造图的代码。。。

    void print(int x){
    	if(cz[x]) return;
    	cz[x]=1;
    	printf("	%d->%d[style="dashed"];
    ",x,sam[x].link);
    	for(int i=0;i<=25;i++)
    		if(sam[x].ch.count(i))
    			printf("	%d->%d[label=%d];
    ",x,sam[x].ch[i],i),
    			print(sam[x].ch[i]);
    }
    void Vz(){
    	printf("digraph zhy{
    	rankdir = LR;
    ");
    	print(0);
    	printf("}
    ");
    }
    

    5.例题(Luogu-1659):

    (quad)这道题的话就是把这些点按照长度从大到小排一遍序,然后前(k)个奇数长的乘起来就是答案啦,注意这题k较大,还要用快速幂,代码:

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    typedef long long ll;
    const int maxn=1e6+100,P=19930726;
    struct node{
    	int len,fail,son[26],siz;
    	node(){
    		len=fail=0;
    		for(int i=0;i<=25;i++)
    			son[i]=0;
    	}
    };
    node prt[maxn]; 
    int n,last,len,num;
    ll ans=1,k;
    char s[maxn];
    ll poww(ll x,int y){
    	ll base=1;
    	while(y){
    		if(y&1) base*=x,base%=P;
    		x*=x,x%=P;
    		y>>=1;
    	}
    	return base;
    }
    bool cmp(node x,node y){
    	return x.len>y.len;
    }
    int getfail(int x){
    	while(s[n-prt[x].len-1]!=s[n]) x=prt[x].fail;
    	return x;
    }
    void extend(int x){
    	int cur=getfail(last);
    	if(!prt[cur].son[x]){
    		int now=++num;
    		prt[now].len=prt[cur].len+2;
    		prt[now].fail=prt[getfail(prt[cur].fail)].son[x];
    		prt[cur].son[x]=now;
    	}
    	prt[prt[cur].son[x]].siz++;
    	last=prt[cur].son[x];
    }
    int main(){
    	scanf("%d%d",&len,&k);
    	scanf("%s",s);
    	last=num=1,prt[1].len=-1;
    	prt[0].fail=prt[1].fail=1;
    	for(n=0;n<len;n++) extend(s[n]-'a');
    	for(int i=num;i>=2;i--)
    		prt[prt[i].fail].siz+=prt[i].siz,prt[prt[i].fail].siz%=P;
    	sort(prt+1,prt+num+1,cmp);
    	int now=1;
    	while(k){
    		if(now>num){
    			printf("-1
    ");
    			return 0;
    		}
    		if(prt[now].len%2==0){
    			now++;
    			continue;
    		}
    		if(prt[now].siz<k){
    			k-=prt[now].siz;
    			ans*=poww(prt[now].len,prt[now].siz)%P;
    			ans%=P;
    			now++;
    		}
    		else{
    			ans*=poww(prt[now].len,k)%P;
    			ans%=P;
    			k=0;
    		}
    	}
    	printf("%lld
    ",ans);
    }
    
    
    
    
    
  • 相关阅读:
    Mybatis框架学习_6_mapper.xml 文件中的输入参数详解 (paraterType)
    Mybatis框架学习_5_自定义类型转换器
    Mybatis框架学习_4_属性文件、全局参数、别名
    Mybatis框架学习_3_基于约定或动态代理实现增删改查
    Mybatis框架学习_2_增删改查的简单实现
    Mybatis框架学习_1_简介以及入门示例
    Linux 系统下启动命名的书写过程
    spring-boot-Web学习2-模板引擎 Thymeleaf
    spring-boot-Web学习1-简介
    MacBook无法开机问题
  • 原文地址:https://www.cnblogs.com/nianheng/p/9814530.html
Copyright © 2011-2022 走看看