zoukankan      html  css  js  c++  java
  • PAM / 回文自动机(回文树)略解

    回文自动机可以处理一个字符串的回文子串的信息,复杂度为 (O(n))

    参考资料:翁文涛 《回文树及其应用》

    结构

    回文自动机的每个节点代表一个回文子串,本质相同的回文子串在一个节点上。记节点 (i) 代表的回文串为 (s_i)(实现时不用记录),长度为 (len_i)

    回文自动机的结构可以看成两棵树,它们的根分别为 (even)(odd)(even) 对应空回文串,(odd) 对应长度为 (-1) 的,实际上并不存在的回文串。

    树上的一条边,也就是自动机的转移边对应一个字符。若 (i)(j) 有一条对应字符 (c) 的转移边,则 (s_j=cs_ic),即在字符串前后各加一个 (c)。特别的,(odd) 经过一条边后得到单个字符。

    与其它自动机类似,回文自动机的节点也有 (fail) 指针(失配指针 / 后缀链接),它指向这个节点最长的、不是自身的后缀回文串。特别的,(fail_{even}=odd)。通常记 (fail_{odd}=odd),虽然 (odd) 节点不可能失配(添加一个字符后成为单个字符,它一定是回文的)。

    (fail) 指针显然构成一棵 (fail) 树。

    下图来自于翁文涛的论文《回文树及其应用》。

    节点数和转移数

    显然,若字符串 (s)(n) 个本质不同的回文子串,状态数为 (n+2)。于是分析节点数即分析 (s) 本质不同的回文子串个数。下面是论文中的证明方法:

    定理 3.1 对于一个字符串 (s),不同的回文子串个数最多只有 (|s|) 个。

    证明 . 使用数学归纳法。

    • (|s|=1)时,只有 (s[1dots 1]) 一个子串,并且他是回文的,所以结论成立。
    • (|s| > 1) 时,设 (s = s'c),其中 (c)(s) 的最后一个字符,并且结论对 (s') 成立。考虑以末尾字符 (c) 为结尾的回文子串,假设他们的左端点从左到右依次为 (l_1,l_2,dots,l_k),那么由于(s[l_1dots |s|]) 为回文串,那么对于所有的位置 (l_1 leq p leq |s|),都会有 (s[pdots|s|] = s[l_1dots l_1+|s|-p]),所以对于回文子串(s[l_idots |s|]),都会有 (s[l_idots |s|]=s[l_1dots l_1+|s|-l_i]),当 (i eq 1) 时,总会有(l_1+|s|-l_i <|s|),从而 (s[l_idots |s|]) 已经在 (s[1dots|s|-1]) 中出现,因此每次不同的回文串最多新增一个,即 (s[l_idots |s|])。因此结论对于 (s) 依然成立。

    由数学归纳法可知定理 3.1 成立。

    翻译一下就是:假如有多个以 (c) 结尾的回文子串,较短的那些肯定在最长的那个里面出现过至少一次。

    因此状态数是 (O(|s|)) 的。由于每个状态只会有一个转移通向它、每个状态只会有一个 (fail),因此转移数也是 (O(|s|)) 的。

    构造

    使用增量法。记录目前所在的节点 (cur) 指向目前字符串的最长回文后缀,初始值为 (even)。考虑新加入第 (p) 个字符 (c),显然由定理 3.1 可得,最多新增 1 个状态。我们反复跳 (fail) 来找到 (s[p]=s[p-len_i-1]),即该回文串再往前一个字符与新加的字符相等,显然最多跳到 (odd) 就找到了。假如找到的节点 (x) 没有对应的儿子 (y),新建一个节点:

    • (len_y=len_x+2)
    • (fail_x) 开始往上跳,找到 (fail_y)

    (cur) 设为 (y),然后添加下一个字符。

    由于 (cur) 的深度每次至多 (+1),因此时间复杂度是 (O(|s|)) 的(忽略字符集大小)。

    性质

    • 本质不同的回文串数量等于回文自动机的节点数 (-2)
    • 一个回文串出现的次数等于以之为根的子树的各节点作为 (cur) 的次数之和;
    • 当前字符串的回文后缀的数量等于 (cur) 的深度;
    • 位置不同的回文串的数量等于各个节点(除 (even)(odd))对应的回文串出现的次数之和;
      • 或者是在每加入一个字符后累加当前字符串的回文后缀的数量。

    模板题洛谷 P5496 【模板】回文自动机(PAM)

    代码:

    /**********
    Author: WLBKR5
    Problem: luogu 5496
    Name: 回文自动机 
    Source: 模板 
    Algorithm: 回文自动机 
    Date: 2020/06/05
    Statue: accepted
    Submission: https://www.luogu.com.cn/record/34145336
    **********/
    #include<bits/stdc++.h>
    using namespace std;
    int getint(){
        int ans=0,f=1;
        char c=getchar();
        while(c>'9'||c<'0'){
            if(c=='-')f=-1;
            c=getchar();
    	}
        while(c>='0'&&c<='9'){
            ans=ans*10+c-'0';
            c=getchar();
        }
        return ans*f;
    }
    const int N=5e5+10;
    const int rt_1=1,rt0=0;
    int ch[N][26],fail[N],cnt=2,cur=rt0;
    int len[N],sz[N];
    char s[N]; 
    void init(){
    	len[rt_1]=-1;	fail[rt_1]=0;	//sz[rt_1]=1;
    	len[rt0]=0;		fail[rt0]=rt_1;	//sz[rt0]=1;
    }
    void extend(int pos,char c){
    	int p=cur;
    	while(s[pos-len[p]-1]!=c)p=fail[p];
    	if(!ch[p][c-'a']){
    		int t=cnt++;
    		len[t]=len[p]+2;
    		fail[t]=fail[p];
    		while(s[pos-len[fail[t]]-1]!=c)fail[t]=fail[fail[t]];
    		fail[t]=ch[fail[t]][c-'a'];
    		sz[t]=sz[fail[t]]+1;
    		ch[p][c-'a']=t;
    	}
    	cur=ch[p][c-'a'];
    }
    int main(){
    	scanf("%s",s+1);
    	int n=strlen(s+1);
    	init();
    	for(int i=1;i<=n;i++){
    		extend(i,s[i]);
    		printf("%d ",sz[cur]);
    		s[i+1]=(s[i+1]-97+sz[cur])%26+97;
    	}
    	return 0;
    }
    
    

    在开头插入字符

    假如要求支持在字符串开头、结尾插入字符(LOJ #141.回文子串),一个简单的想法是维护 (cur')(fail'),分别代表当前字符串的最长回文前缀和各个节点的最长回文前缀。

    考虑到回文串正着看、反着看都一样,实际上回文串的最长回文前缀,也就是其最长回文后缀。所以 (fail'=fail),只维护 (fail) 即可。

    在末尾(开头)插入字符时,只有整个串成=成为了一个回文串,(cur')(cur))才会受影响。特殊处理这种情况。

    模板题LOJ #141.回文子串

    代码:

    /**********
    Author: WLBKR5
    Problem: loj 141
    Name: 回文子串 
    Source: 模板 
    Algorithm: 回文自动机 
    Date: 2020/06/06
    Statue: accepted
    Submission: loj.ac/submission/826336
    ********/
    #include<bits/stdc++.h>
    using namespace std;
    int getint(){
        int ans=0,f=1;
        char c=getchar();
        while(c>'9'||c<'0'){
            if(c=='-')f=-1;
            c=getchar();
    	}
        while(c>='0'&&c<='9'){
            ans=ans*10+c-'0';
            c=getchar();
        }
        return ans*f;
    }
    const int N=4e5+10;
    const int rt_1=1,rt0=0;
    int ch[N][26],fail[N],cnt=2,cur=rt0,ruc=cur;
    int len[N],sz[N];
    char s[N<<1];
    char tmp[N];
    int l=N,r=N-1; 
    void init(){
    	len[rt_1]=-1;	fail[rt_1]=0;	//sz[rt_1]=1;
    	len[rt0]=0;		fail[rt0]=rt_1;	//sz[rt0]=1;
    }
    long long ans=0;
    void push_back(char c){
    	s[++r]=c;
    	int p=cur;
    	while(s[r-len[p]-1]!=c)p=fail[p];
    	if(!ch[p][c-'a']){
    		int t=cnt++;
    		len[t]=len[p]+2;
    		fail[t]=fail[p];
    		while(s[r-len[fail[t]]-1]!=c)fail[t]=fail[fail[t]];
    		fail[t]=ch[fail[t]][c-'a'];
    		sz[t]=sz[fail[t]]+1;
    		ch[p][c-'a']=t;
    	}
    	cur=ch[p][c-'a'];
    	if(len[cur]==r-l+1)ruc=cur;
    	ans+=sz[cur];
    }
    void push_front(char c){
    	s[--l]=c;
    	int p=ruc;
    	while(s[l+len[p]+1]!=c)p=fail[p];
    	if(!ch[p][c-'a']){
    		int t=cnt++;
    		len[t]=len[p]+2;
    		fail[t]=fail[p];
    		while(s[l+len[fail[t]]+1]!=c)fail[t]=fail[fail[t]];
    		fail[t]=ch[fail[t]][c-'a'];
    		sz[t]=sz[fail[t]]+1;
    		ch[p][c-'a']=t;
    	}
    	ruc=ch[p][c-'a'];
    	if(len[ruc]==r-l+1)cur=ruc;
    	ans+=sz[ruc];
    }
    int main(){
    	scanf("%s",tmp+1);
    	int n=strlen(tmp+1);
    	init();
    	for(int i=1;i<=n;i++)push_back(tmp[i]);
    	int q=getint();
    	while(q--){
    		int op=getint();
    		if(op<=2){
    			scanf("%s",tmp+1);
    			int n=strlen(tmp+1);
    			for(int i=1;i<=n;i++)(op==1?push_back:push_front)(tmp[i]);
    		}
    		if(op==3){
    			printf("%lld
    ",ans);
    		}
    	}
    	return 0;
    }
    
    

    更高深的技术(如支持删除字符、可持久化 etc.)就不写了(其实是看不懂)。

    知识共享许可协议
    若文章内无特别说明,公开文章采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。
  • 相关阅读:
    Java入门——数组和方法
    Java入门——选择与循环语句
    Java入门——面向对象基础
    十天学会Oracle数据库(Day2)
    Java入门——理解面向对象:UML设计
    十天学会Oracle数据库(Day1)
    Codeforces Round #482 (Div. 2) :B
    Codeforces Round #482 (Div. 2) :C
    Codeforces Round #490 (Div. 3) :F. Cards and Joy(组合背包)
    POJ-2155:Matrix(二维树状数祖)
  • 原文地址:https://www.cnblogs.com/wallbreaker5th/p/13905622.html
Copyright © 2011-2022 走看看