<学习笔记> manacher 和 回文自动机

manacher#

对于奇串,考虑维护一个具有最大 r 值的回文串 (l,r),每次考虑一个新增加的点 i。若 i>r 那么直接暴力计算就可以;如果 in,设 mid=(l+r)/2,那么 i 点关于 mid 对称点 r+li 的回文串长度同样适用于 i,那么直接在此基础上更新就可以,奇偶同理。

code
int manacher(int n){
	for(int i=1,l=0,r=-1;i<=n;i++){
		int k;
		if(i>r) k=1;
		else k=min(r-i+1,f[r+l-i]);
		while( i-k>=1 && i+k<=n &&s[i-k]==s[i+k]) k++;
		f[i]=k--;
		ans=max(ans,f[i]*2-1);
		if(r<i+k){
			r=i+k;
			l=i-k;
		}
	}
	for(int i=1,l=0,r=-1;i<=n;i++){
		int k;
		if(i>r) k=0;
		else k=min(r-i+1,g[l+r-i+1]);
		while(i-k-1>=1 && i+k<=n && s[i-k-1]==s[i+k]) k++;
		g[i]=k--;
		ans=max(ans,g[i]*2);
		if(r<i+k){
			r=i+k;
			l=i-k-1;
		}
	}
	return ans; 
}

回文自动机(回文树,PAM)#

引入

对于一个新加入的字符 si,新产生的本质不同的串一定是新串的一个后缀,而且还是最长的后缀,因为如果不是最长的话,可以被最长的后缀中点对应到插入前的串中。可以发现每次新增最多为 1。那么我们可以根据这个性质,定义一个 fail[x] 表示为 x 代表的回文子串的最长回文后缀。
对于每个节点表示的回文串其实就是:从它到根再从根到它。

构建#

为了区分奇偶串,建两个根 0,10 为偶数根。定义 len[i] 为以 i 结尾的回文串的长度。len[1]=1.

fail[0]=1,奇数根可以挂,因为单独可以成为一个回文串。

每加入一个字符,在当前最长后缀回文串中找到最长的可以匹配新加字符的回文串,如果没有这个节点就新增一个节点,这样本质不同回文串个数加一,并更新 fail

code
char s[N];
int len[N],fail[N],num[N],tr[N][26],tot=1,cur=0,pos=0;
int getfail(int x,int i){
    while(i-len[x]-1<=0 || s[i-len[x]-1]!=s[i]) x=fail[x];
    return x;
}
void solve(){
    int n=strlen(s+1);
    fail[0]=1;
    len[1]=-1;
    int last=0;
    for(int i=1;i<=n;i++){
        pos=getfail(cur,i);
        if(!tr[pos][s[i]-'a']){
            fail[++tot]=tr[getfail(fail[pos],i)][s[i]-'a'];// 注意是 getfail(fail[pos],i),
                                                          // 如果是 getfail(pos,i) 就会匹配自己
            tr[pos][s[i]-'a']=tot;
            len[tot]=len[pos]+2;
            num[tot]=num[fail[tot]]+1;
        }
        cur=tr[pos][s[i]-'a'];
    }
}

例题#

双倍回文#

PAM做法:其实可以贡献的回文串是它存在一个后缀回文串长度为它的 12。所以在建 PAM 时每增加一个新串时,可以暴跳 fail 查看是否存在。这样复杂度是错的,但是 oj 可过。其实可以建一课 fail 树,我们遍历这棵树的时候开一个桶,维护长度出现次数就可以了。

code
#include<bits/stdc++.h>
using namespace std;
const int N=5*1e5+10;
char s[N];
int fail[N],len[N],tr[N][26],tot=1,pos=0,cur=0;
int get_fail(int x,int i){
    while(i-len[x]-1<=0 || s[i-len[x]-1]!=s[i]) x=fail[x];
    return x;
}
int head[N*2],nex[N*2],ver[N*2],idx=0;
void add(int x,int y){
    ver[++idx]=y,nex[idx]=head[x],head[x]=idx;
}
int cnt[N];
int ans;
void dfs(int x){
    cnt[len[x]]++;
    if(len[x]%4==0){
        if(cnt[len[x]/2]) ans=max(ans,len[x]);
    }
    for(int i=head[x];i;i=nex[i]){
        int y=ver[i];
        dfs(y);
    }
    cnt[len[x]]--;
}
signed main(){
    int n;
    scanf("%d",&n);
    scanf("%s",s+1);
    fail[0]=1,len[1]=-1;
    add(1,0);
    ans=0;
    for(int i=1;i<=n;i++){
        pos=get_fail(cur,i);
        if(!tr[pos][s[i]-'a']){
            fail[++tot]=tr[get_fail(fail[pos],i)][s[i]-'a'];
            add(fail[tot],tot);
            tr[pos][s[i]-'a']=tot;
            len[tot]=len[pos]+2;
        }
        cur=tr[pos][s[i]-'a'];
    }
    dfs(1);
    printf("%d",ans);
}

P4762 [CERC2014] Virus synthesis#

可以发现加的逆串越多越优。

然后发现一个串加逆串就构成一个回文串,而且贡献就是一个回文串加上剩余的。设 f[i] 表示构成 i 这个回文串的最小次数,那么答案就是最小的 nlen[i]+f[i]

考虑有什么转移,假如树上有条边 y>x 那么 f[x]=min(f[y]+1)。还有什么情况就是设这个串后缀长度 len[x] 的节点 trans[x] 那么有转移 f[x]=min(len[x]/2len[trans[x]]+f[trans[x]]+1)。对于 trans[x] 的转移可以每次从 trans[y] 暴跳。最后建完树,一层一层更新就行。

code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
char s[N];
int tr[N][5],fail[N],len[N],pos,cur,f[N],tot=1;
int trans[N],rk[N];
int getfail(int x,int i){
    while(i-len[x]-1<=0 || s[i-len[x]-1]!=s[i]) x=fail[x];
    return x; 
}
signed main(){
    int T;
    scanf("%d",&T);
    rk['A']=0,rk['T']=1,rk['C']=2,rk['G']=3;
    while(T--){
        scanf("%s",s+1);
        int n=strlen(s+1);
        for(int i=0;i<=n+2;i++){
            fail[i]=len[i]=trans[i]=0;
            f[i]=(1<<25);
            tr[i][0]=tr[i][1]=tr[i][2]=tr[i][3]=0;
        }
        tot=1,pos=cur=0;
        fail[0]=1,len[1]=-1;
        int ans=n;
        f[0]=f[1]=1;
        for(int i=1;i<=n;i++){
            pos=getfail(cur,i);
            if(!tr[pos][rk[s[i]]]){
                fail[++tot]=tr[getfail(fail[pos],i)][rk[s[i]]];
                tr[pos][rk[s[i]]]=tot;
                len[tot]=len[pos]+2;
                if(len[tot]<=2) trans[tot]=fail[tot];
                else{
                    int tmp=trans[pos];
                    while(s[i-len[tmp]-1]!=s[i] || (len[tmp]+2)*2>len[tot]) tmp=fail[tmp];
                    trans[tot]=tr[tmp][rk[s[i]]];
                }
            }
            if(len[tot]%2==0){
                int tmp=trans[tot];
                f[tot]=min(f[pos]+1,f[tot]);
                f[tot]=min(f[tot],f[tmp]+len[tot]/2-len[tmp]+1);
                ans=min(ans,f[tot]+n-len[tot]);
            }
            cur=tr[pos][rk[s[i]]];
        }
        for(int i=2;i<=n;i++) f[i]=len[i];
        queue<int> q;
        for(int i=0;i<4;i++){
            if(tr[0][i]) q.push(tr[0][i]);
        }
        while(!q.empty()){
            int x=q.front();
            q.pop();
            f[x]=min(f[x],len[x]/2-len[trans[x]]+f[trans[x]]+1);
            for(int i=0;i<4;i++){
                if(tr[x][i]){
                    q.push(tr[x][i]);
                    f[tr[x][i]]=min(f[x]+1,f[tr[x][i]]);
                }
            }
            ans=min(ans,f[x]+n-len[x]);
        }
        printf("%d\n",ans);
    }
}

作者:bloss

出处:https://www.cnblogs.com/jinjiaqioi/p/17929421.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   _bloss  阅读(15)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu