首页 > 其他分享 >Manacher 和 回文自动机

Manacher 和 回文自动机

时间:2022-10-10 14:47:33浏览次数:67  
标签:子串 字符 Manacher len fail 自动机 sim 回文

引入

求串 \(s\) 中的回文子串数量。\(|s|\le 10^7\)。

做法

定义一个长为 \(2k-1(k\in N)\) 的回文串 \(s\) 的回文中心为 \(s_k\)。则子串 \(s_2\sim s_{2k-2}\),\(s_3\sim s_{2k-3}\),一直到 \(s_k\) 均为回文串,回文中心也均是 \(s_k\)。对于任意一个字符 \(s_b\),若 \(s_{b-c}\sim s_{b+c}\) 非回文串,则 \(\forall p\ge c\),\(s_{b-p}\sim s_{b+p}\) 也不会是回文串。故我们对于串的任意一个字符 \(s_p\),求 \(\max_{k=0} (1+k\prod_{i=0}^k[s_{p+i}=s_{p-i}])\)(也就是以 \(s_p\) 为回文中心的最长回文串的长度的一半),即为 \(d_p\),则所有奇回文串的长度/数量之和即为 \(\sum d_p\)。

对于偶回文串,直接在原串中每两个相邻的字符之间加上间隔符,以间隔符为回文中心的长度大于一的奇回文串就对应了原串中的偶回文串。注意这样后原串中的每个回文串均被计算了两遍,需要将 \(\sum d\) 除以 \(2\)。

不使用下述算法的情况下,一种简单的方法是 hash + 二分,单次处理一个 \(d\) 值的时间复杂度为 \(O(\log |s|)\)。

Manacher 算法

原理

为了方便,我们按照上述方式处理原串,在原串中每两个相邻的字符之间加上间隔符,这样就只用考虑奇回文串。令处理后的串长为 \(n\)。

考虑某个性质:在一个回文串中,若某个子串是回文串,则对称位置长度相同的子串也是回文串。由此可得:若某个回文串为 \(s_l\sim s_r\),回文中心为 \(s_p\),且 \(s_{p-i-d_{p-i}+1}\sim s_{p-i+d_{p-i}-1}\) 被完全包含在 \(s_{l+1}\sim s_{r-1}\) 内,记 \(rev(s)\) 为 \(s\) 的反串,则

\[\begin{aligned}&{\color{white}=\ }s_{p-i-d_{p-i}+1}\sim s_{p-i+d_{p-i}-1}\\&=rev(s_{p-i-d_{p-i}+1}\sim s_{p-i+d_{p-i}-1})\\&=rev(rev(s_{p+i-d_{p-i}+1}\sim s_{p+i-d_{p-i}-1}))\\&=s_{p+i-d_{p-i}+1}\sim s_{p+i-d_{p-i}-1}\end{aligned} \]

且由于 \(s_{p-i-d_{p-i}}\ne s_{p-i+d_{p-i}}\) 有 \(s_{p+i+d_{p-i}}\ne s_{p+i-d_{p-i}}\),故 \(d_{p+i}=d_{p-i}\)。

故我们可以维护目前由 \(d_1\sim d_{i-1}\) 得出的右端最靠右的回文子串 \(s_l\sim s_r\),以此从 \(d_1\sim d_{i-1}\) 推出 \(d_i\)。记 \(r(i)=l+r-i\),分类讨论一下各种情况:

  • \(i>r\)。此时显然只能暴力处理。
  • \(s_{r(i)-d_{r(i)}+1}>l\)。由于 \(\frac{l+r}2<i\),此时显然有 \(s_{r(i)-d_{r(i)}+1}<r\)。按上述的方式处理。
  • \(s_{r(i)-d_{r(i)}+1}\le l\)。此时不能直接得出 \(d_i\),但是由于 \(s_{r(i)-d_{r(i)}+1}\sim s_{r(i)+d_{r(i)}-1}\) 的子串 \(s_l\sim s_{2r(i)-l}\) 是回文串,我们可以得出 \(d_i\) 的下界为 \(r(i)-l+1\)。在这个下界的基础上,继续暴力处理即可。

综上,我们就得到了 Manacher 算法的全流程。同时由于第一种和第三种情况的暴力处理 \(x\) 位,新的 \(r\) 就会增加 \(x\),且处理到最后一位时的 \(r\) 显然是 \(n\),故整个算法的时间复杂度是 \(O(n)\) 的。同时这种方法较其他方法常数小得多,理解和操作也很简单。

代码实现中,\(d_p\) 为 \(\max_{k=0} k\prod_{i=0}^k[s_{p+i}=s_{p-i}]\),但原理与上述相同。

模板代码

#include <bits/stdc++.h>
using namespace std;
char ch,p[22000030];
int d[22000030],i,l,r=-1,k,mx,siz=1;
bool o;
int main(){
	p[1]='T';
	for(;;){
		ch=getchar();
		if(ch<'a'||ch>'z') break;
		p[++siz]=ch;
		p[++siz]='M';
	}
	p[siz]='D';
	for(i=1;i<=siz;++i){
		if(i>r) k=1;
		else k=min(d[l+r-i],r-i+1);
		while(i-k>=0&&i+k<siz&&p[i-k]==p[i+k]) ++k;
		d[i]=k--;if(i+k>r){l=i-k;r=i+k;}
	}
	for(i=1;i<=siz;++i){
		o=!((i==d[i]+1)||(i+d[i]==siz));
		if(mx<d[i]-o) mx=d[i]-o;
	}
	printf("%d\n",mx);
	return 0;
}

引入

给定一个空串 \(s\),每次在 \(s\) 的末尾加入一个字符,询问末尾为 \(s\) 末尾的回文子串个数。强制在线。保证最后 \(s\) 的总长度不超过 \(5\times 10^5\)。

回文树/回文自动机(PAM,Palindromic Tree/Palindromic AutoMaton)

特点

同 Manacher 对字符串处理,只考虑奇回文串不同,回文树同时处理长度为奇数和偶数的回文串,分别用两棵树维护。由于从根节点出发可以遍历到原串所有的本质不同的回文子串,故回文树又称回文自动机。

构造

同 SAM 一样,构造 PAM 时我们仍然可以采用增量法,依次插入原串的每一个字符。然而 PAM 本质上是两棵树,每个节点均只代表一个回文子串。

下面令 \(len(p)\) 表示 \(p\) 节点对应的回文子串长度,\(fail(p)\) 表示 \(p\) 节点对应的回文子串最长回文后缀对应的节点编号,\(son(p,c)\) 表示在 \(p\) 节点对应的回文子串 两边 加上字符 \(c\) 形成的回文子串对应的编号。

引理:

​ 每次在当前字符串后添加一个字符,若新增了回文串,则新增的本质不同的回文串最多只有只有一个;若有,就是添加字符后的字符串的最长回文后缀。

​ 证明:这个最长回文后缀的任意回文后缀一定会与其某个回文前缀对称,而这个回文前缀一定是原串的回文子串,在 PAM 内出现过。

同时这证明了 PAM 的点数为 \(O(n)\) 级别的。这也证明了一个字符串的所有回文子串的数量为 \(O(n)\) 的。由于 PAM 为两棵树的形态,故边数也是 \(O(n)\) 的。

具体地,最开始时没有插入字符,只有两个根节点(令奇回文串的根节点为 \(t_1\),偶回文串的根节点为 \(t_0\))。由于保证插入字符合法,故 \(len(t_1)=-1\),\(len(t_0)=0\)。为何方便后面的操作,我们令 \(fail(t_1)=t_0\),\(fail(t_0)=t_1\)。

令 \(last\) 为插入上一个字符之后,最长回文后缀的编号,\(last\) 开始时可以为 \(t_0\) 或 \(t_1\)。注意回文串两边删除一个字符后仍然是回文串(或是空串)。设当前插入的是字符串 \(s\) 的第 \(k\) 个字符 \(c\),则我们需要从 \(last\) 开始不断跳 fail 直到找到第一个 \(p\) 满足 \(s_{k-len(p)-1}=c\)。

若 \(son(p,c)\) 存在,则这个最长后缀已经出现在之前的部分中。否则首先需要新建节点 \(q\) 代表这个新的最长回文后缀,然后将 \(son(p,c)\) 赋为 \(q\),最后从 \(p\) 开始不断跳 \(fail\) 直到跳到第一个节点 \(p'\) 满足 \(s_{k-len_{p'}-1}=c\)(注意条件不是存在 \(son(p',c)\),可能这个回文串在其他位置出现时两边有 \(c\),如字符串 \(ccbbc\)),然后将 \(fail(q)\) 设为 \(son(p',c)\)。若直到跳到 \(t_0\) 再跳到 \(t_1\) 都没有找到 \(p'\),则将 \(fail(q)\) 设为空串对应的节点 \(t_0\)。

此时将 \(fail(t_1)\) 设为 \(t_0\),将 \(fail(t_0)\) 设为 \(t_1\) 的用途就体现出来了:若插入字符 \(c\) 之前的串形如为 \(c+A+c+B\)(其中 \(B\) 即为 \(p\) 代表的偶回文串,且不包含字符 \(c\)),则最后 \(fail(q)\) 应该为 \(son(t_1,c)\),而 \(p\) 原本以 \(t_0\) 为根节点,若无 \(fail(t_0)=t_1\) 就难以实现较为简单的判别。而若原串为 \(A+c\) 的形式(其中 \(A\) 未出现过 \(c\),\(A+c\) 为奇回文串),则最后我们需要将 \(son(t_0,c)\) 赋为 \(q\),最后一步需要从 \(t_1\) 跳到 \(t_0\)。像这样的特例还有许多,这样做明显更简洁地处理了这些特例。

时间复杂度

考虑 \(len(fail(last))\) 的变化。可以发现每一次跳 fail 指针均会使当前的节点的 \(len\) 减少,而最后 \(len(fail(last))\) 一定小于 \(n\),故时间复杂度均摊下来是 \(O(n)\) 的。和 SAM 的时间复杂度证明很像。

(辟谣:CF 上的 一篇博客 说时间复杂度为 \(O(n\log\Sigma)\),其中 \(\Sigma\) 是字符集大小,因为用到了主席树维护转移边)

模板代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=500010;
int ch,p,q,len,cur,lst,tot,ans; 
char s[maxn];
struct PAM_Node{
	int fail,len,dep;
	int nxt[26];
}N[maxn];
#define len(p) N[p].len
#define dep(p) N[p].dep
#define nxt(p) N[p].nxt
#define fail(p) N[p].fail
int main(){
	N[0].fail=tot=1;
	N[0].len=-1;
	for(;;){
		ch=getchar();
		if(ch<'a') return 0;
		ch=(ch-97+ans)%26;
		s[++len]=ch+97;
		p=lst;
		while(s[len-len(p)-1]!=ch+'a') p=fail(p);
		if(nxt(p)[ch]) lst=nxt(p)[ch];
		else{
			cur=++tot; len(cur)=len(p)+2;
			lst=p;p=fail(p);q=0;
			while((q!=1)&&(s[len-len(p)-1]!=ch+'a')){
				q=p;
				p=fail(p);
			}
			if(!nxt(p)[ch]) fail(cur)=1;
			else fail(cur)=nxt(p)[ch];
			dep(cur)=dep(fail(cur))+1;
			nxt(lst)[ch]=cur; lst=cur;
		}
		ans=dep(lst);
		printf("%d ",ans);
	}
	return 0; 
}

例题

初始有一个空串,利用下面的操作构造给定串 \(S\)。

  1. 串开头或末尾加一个字符
  2. 串开头或末尾加一个该串的逆串

求最小化操作数,\(|S|\le 10^5\),字符集为 \(\{A,T,C,G\}\)。

解法(暂缺)

代码(暂缺)


标签:子串,字符,Manacher,len,fail,自动机,sim,回文
From: https://www.cnblogs.com/FrancaisDrakeCwoi/p/16775627.html

相关文章

  • KMP 和 AC 自动机
    引入:字符串匹配给定字符串\(S\)和\(T\),查询\(T\)在\(S\)中所有出现的位置。(其中\(S\)称为文本串,\(T\)称为模式串)显然暴力匹配的最坏时间复杂度是\(O(|S||T|)\)......
  • leetcode-647. 回文子串
    647.回文子串回文子串是指这个子串正着读反着读读得内容都一样,比如aaa,有以下回文字串a,a,a,aa,,aa,aaa,字符虽然一样但不是同一个字符仍然被看作一个子串我们可以使用双......
  • 量化自动机器人跟单交易系统开发及源码
    交易算法量化交易策略,其运用“双推力”系统根据历史价格构建更新的回溯期,这在理论上使其在任何给定时期内更加稳定。怎么在量化平台上实现此算法。首先,我们要选择所交易......
  • AC自动机+DP luoguP4052 and P3311
    https://www.luogu.com.cn/problem/P4052题意:求长度为m的小写字母组成的字符串ss中包含给定字符串集合S中任意一个为子串的ss个数。思路:经典的在ac自动机上跑dp的套路,......
  • 求最长回文子序列长度问题
    求最长回文子序列长度问题作者:Grey原文地址:博客园:求最长回文子序列长度问题CSDN:求最长回文子序列长度问题题目描述给你一个字符串s,找出其中最长的回文子序列,并返回......
  • AC自动机
    背景给出一个字典,和若干询问:多少个字典串在询问串中出现过。即单串与多串的匹配问题。AC自动机AC自动机基于Trie,将KMP的Border概念推广到多模式串上。AC自动机......
  • 【数据结构和算法】LeetCode,初级算法-16验证回文串
    截止到目前我已经写了600多道算法题,其中部分已经整理成了pdf文档,目前总共有1000多页(并且还会不断的增加),大家可以免费下载下载链接:​​https://pan.baidu.com/s/1hjwK0ZeRxY......
  • #yyds干货盘点# 前端歌谣的刷题之路-第一百零三题-回文字符串
    前言我是歌谣我有个兄弟巅峰的时候排名c站总榜19叫前端小歌谣曾经我花了三年的时间创作了他现在我要用五年的时间超越他今天又是接近兄弟的一天人生难免坎坷大不了从......
  • AC自动机
    AC自动机Aho-Corasickautomaton,该算法在1975年产生于贝尔实验室,是著名的多模匹配算法。要学会AC自动机,我们必须知道什么是Trie,也就是字典树。Trie树,又称单词查找树或键......
  • AC 自动机
    感谢EC姐姐与HMZ姐姐对我的帮助!/qq这是模板题这个现在你就知道AC自动机干嘛用的,是用来对多个模式串匹配一个文本串的。如果是一个模式串匹配一个文本串呢?我们可以......