回文自动机(PAM)入门路线 + P3649 【模板】[APIO2014] 回文串(PAM)

个人比较推荐的回文自动机学习路径:

回文自动机学习博客:
回文树(讲的最严谨,oiwiki上的)
回文自动机(Palindrome Automanton PAM)(讲的最通俗易懂,知乎上的)
回文自动机(PAM)学习笔记(代码最简洁,洛谷上的)

PAM 的定义:
Palindrome Automaton ( 回文自动机,回文树)是一种能够识别所有回文子串的数据结构,结构十分类似于 ACAM。

  • 节点:节点数至多 N 个,每个节点代表了一种回文串。用 S(u) 表示节点 u 代表的回文串 len[u] = |S(u)|。

  • 后继边 ch:每个后继边上有一个字母。用 trans(u, ch) = v 表示 u 节点有后继边 ch 指向 v 节点。则有 S(v) = chS(u)ch,以及 len[v] = len[u] + 2。

  • 失配边 fa:每个节点都有一一个失配边,用 fail[ul = v 表示 u 节点的失配边指向了 v 节点。则有 S(v) 是 S(u) 的最大 Border,即最长回文后缀(不含本身,如果你学过 KMP 的话你会发现这十分类似于 KMP 的失配指针 )。

  • 奇根和偶根:对于奇回文串和偶回文串需要有两个根节点。由于长度为 2 的回文串是偶根的后继节点,长度为 1 的回文串是奇根的后继节点。因此偶根长度应设为 0,奇根应该设为 -1。方便起见,可以令偶根的失配边指向奇根,奇根的失配边指向偶根。

PAM 的增量构造:

  • PAM 在构造时,实际上有两个核心步骤
    求每个前缀 S[i] 的最长回文后缀,即跳一次 fail 链
    求每个前缀 S[i] 最长回文后缀的最长回文后缀,就是确定它的 fail,即又跳一次 fail 链
  • 步骤 ① 方法:枚举上一个前缀 S[i - 1] 的回文后缀,即 fail 链,每跳到一个节点,观察这个节点代表的子串左边相邻的一个字符(是左边相邻,并不属于这个子串)是否等于 S[i],如果等于就代表两头都可以接得上 S[i],即找到了前缀 S[i] 的最大回文后缀。如果不等,则将子串缩短,继续跳 fail 链。
  • 举个例子:比如某个前缀:abaca,模拟上面的过程后发现,它的最大回文后缀(不包括自身)是 aca,PAM 构造的时候就是求这玩意。
  • 步骤 ② 方法:与步骤 ① 一致,也是跳上一个节点的 fail 链。
  • 注意:不一定每次构造的时候都有新的节点产生,如果当前点的最大回文后缀是个已经出现过的字符串,那就没必要新建了,直接使用即可。

总结一下,PAM 的增量构造实际上就是跳两次 fail 链的过程
在这里插入图片描述

例题:

[APIO2014] 回文串

题目描述

给你一个由小写拉丁字母组成的字符串 s s s。我们定义 s s s 的一个子串的存在值为这个子串在 s s s 中出现的次数乘以这个子串的长度。

对于给你的这个字符串 s s s,求所有回文子串中的最大存在值。

输入格式

一行,一个由小写拉丁字母(a~z)组成的非空字符串 s s s

输出格式

输出一个整数,表示所有回文子串中的最大存在值。

样例 #1

样例输入 #1

abacaba

样例输出 #1

7

样例 #2

样例输入 #2

www

样例输出 #2

4

提示

【样例解释1】

∣ s ∣ \lvert s \rvert s 表示字符串 s s s 的长度。

一个字符串 s 1 s 2 … s ∣ s ∣ s_1 s_2 \dots s_{\lvert s \rvert} s1s2ss 的子串是一个非空字符串 s i s i + 1 … s j s_i s_{i+1} \dots s_j sisi+1sj,其中 1 ≤ i ≤ j ≤ ∣ s ∣ 1 \leq i \leq j \leq \lvert s \rvert 1ijs。每个字符串都是自己的子串。

一个字符串被称作回文串当且仅当这个字符串从左往右读和从右往左读都是相同的。

这个样例中,有 7 7 7 个回文子串 a,b,c,aba,aca,bacab,abacaba。他们的存在值分别为 4 , 2 , 1 , 6 , 3 , 5 , 7 4, 2, 1, 6, 3, 5, 7 4,2,1,6,3,5,7

所以回文子串中最大的存在值为 7 7 7

第一个子任务共 8 分,满足 1 ≤ ∣ s ∣ ≤ 100 1 \leq \lvert s \rvert \leq 100 1s100

第二个子任务共 15 分,满足 1 ≤ ∣ s ∣ ≤ 1000 1 \leq \lvert s \rvert \leq 1000 1s1000

第三个子任务共 24 分,满足 1 ≤ ∣ s ∣ ≤ 10000 1 \leq \lvert s \rvert \leq 10000 1s10000

第四个子任务共 26 分,满足 1 ≤ ∣ s ∣ ≤ 100000 1 \leq \lvert s \rvert \leq 100000 1s100000

第五个子任务共 27 分,满足 1 ≤ ∣ s ∣ ≤ 300000 1 \leq \lvert s \rvert \leq 300000 1s300000

思路:

PAM 可以求出所有本质不同子串,且只有 N 个,那么本题实际上就是要统计每种回文串的出现次数。

可以在构建 PAM 的时候额外维护一个 cnt 数组,表示每个 PAM 节点对应多少个位置的最长回文后缀,也就是每个点有多少次成为了 lstp 节点(lstp 是什么,看代码)。网上有些这部分讲的很好的教程,可以去了解一下。

这样每个节点代表的回文串的出现次数等于,以每个节点为根的 fail 树子树的和。

卡常小技巧:节点编号从大到小就是这棵 fail 树的拓扑排序。

时间复杂度: O ( n ) O(n) O(n)

代码:

#include<bits/stdc++.h>

using namespace std;
const int N = 3e5 + 10;
//lstp:每次刚开始插入字符 s[i] 时,表示的是 s[i - 1] 后缀的最大回文子串节点
//tot:最新创建节点编号
//fa:fa[u] 表示 u 节点代表的回文串的最大回文后缀(不含自己)的节点位置
//len:len[u] 为节点 u 代表的回文串的长度
//cnt:每个节点代表回文子串的出现次数
//ch:转移边
//初始已存在奇偶两根,tot 初始化为 1
int lstp, tot = 1, fa[N], ch[N][26], len[N];
long long cnt[N];
char s[N];

int getfa(int p, int idx)
{
	//检查 s[i - len[p] - 1] 是否等于 s[i]
    while(idx - len[p] - 1 < 0 || s[idx - len[p] - 1] != s[idx]){
    	//不等于,则令 p = fa[p] 重复上面的检查,直到满足。
        p = fa[p];
    }
    //如果等于,得到了以 s[i] 结尾的最长回文串,返回所在节点
    return p;
}

void insert(int c, int idx)	//增量构建PAM,和SAM类似
{
	//先通过 getfa 找出以 s[i] 结尾的最长回文串所在节点 p
    int p = getfa(lstp, idx);
    //找到这个 p 以后,检查 ch[p][s[i]] 是否存在
	//如果不存在
    if (!ch[p][c]) { 
		int np = ++tot;	// trie 树中创建并加入一个新节点 np
		//更新 fa
		int v = getfa(fa[p], idx);	//先找到 p 后缀中最长的回文后缀,且满足前一个字符是 s[i] 的后缀对应节点 v
		fa[np] = ch[v][c];	// fa[np] 指向 v 的儿子 ch[v][s[i]],因为它就是当前后缀 s[1, i] 最长的回文后缀
		//更新 len
		len[np] = len[p] + 2;	//首尾加
		//更新后继边的指向
		ch[p][c] = np;
	}
	//如果存在,说明之前已经处理过了。无需处理,只需根据需求更新一些必要统计信息
    lstp = ch[p][c];
    cnt[lstp]++;	//不是cnt = 1,是++ 表示每个节点有多少次成为过 lstp 节点
}

signed main()
{
    fa[0] = 1, fa[1] = 0, len[0] = 0, len[1] = -1;
    scanf("%s", s);
    for(int i = 0; s[i]; ++i){
        insert(s[i] - 'a', i);
    }
    long long mx = 0;
    for(int i = tot; i > 0; --i){
        cnt[fa[i]] += cnt[i];	//拓扑更新
        mx = max(mx, cnt[i] * len[i]);
    }
    printf("%lld\n", mx);
    
    return 0;
}

空白代码:

#include<bits/stdc++.h>

using namespace std;
const int N = 3e5 + 10;
int lstp, tot = 1, fa[N], ch[N][26], len[N];
long long cnt[N];
char s[N];

int getfa(int p, int idx)
{
    while(idx - len[p] - 1 < 0 || s[idx - len[p] - 1] != s[idx]){
        p = fa[p];
    }
    return p;
}

void insert(int c, int idx)	
{
    int p = getfa(lstp, idx);
    if (!ch[p][c]) { 
		int np = ++tot;	
		int v = getfa(fa[p], idx);
		fa[np] = ch[v][c];
		len[np] = len[p] + 2;	
		ch[p][c] = np;
	}
    lstp = ch[p][c];
    cnt[lstp]++;	
}

signed main()
{
    fa[0] = 1, fa[1] = 0, len[0] = 0, len[1] = -1;
    scanf("%s", s);
    for(int i = 0; s[i]; ++i){
        insert(s[i] - 'a', i);
    }
    long long mx = 0;
    for(int i = tot; i > 0; --i){
        cnt[fa[i]] += cnt[i];	
        mx = max(mx, cnt[i] * len[i]);
    }
    printf("%lld\n", mx);
    
    return 0;
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值