首页 > 其他分享 >马拉车(manacher) & 回文自动机(PAM)

马拉车(manacher) & 回文自动机(PAM)

时间:2023-04-22 20:12:37浏览次数:40  
标签:int manacher 半径 端点 Fail 自动机 PAM 回文

读了徐安矣2023年集训队论文写的,对于差分性质和习题,我会在理解清楚之后再补充。本篇博客仅讨论前两种算法。

首先,马拉车和回文自动机都是处理回文串问题的。但在此之前,学习一些更加简单的回文算法。

小 trick:把给定串的两头和缝隙插入相同字符,且在边界处用不同字符标记,使得长度为偶数的回文串和长度为奇数的回文串可以用同一种方式算出来。

例子:

ABBABAB \(\to\) @#A#B#B#A#B#A#B#%

其中的 ABBA \(\to\) #A#B#B#A#,回文中心为中间的 #。ABA \(\to\) #B#A#B#,回文中心为 A。

下文的 \(N\) 表示字符串长度。除了回文自动机使用原串外,其它算法都需要使用在缝隙添加相同字符的新字符串。

  • 暴力

对于一个回文中心,枚举回文半径。时间复杂度为 \(O(n^2)\)。

  • 哈希

预处理前缀哈希和后缀哈希,可以对于一个回文中心,二分回文半径。具体原理就是如果二分到 Mid,i 左右的长度为 Mid 的段的哈希值相等,那就是回文的,反之则不回文,有二分性。时间复杂度,预处理 \(O(n)\),求单个点的回文半径 \(O(\log n)\)。

  • 马拉车

记录当前右端点最大的回文串的回文中心的右端点。初始都是 \(0\)。枚举回文中心,从 \(1\) 到 \(N\)。

如果当前回文中心在右端点之后,那根据每个回文串右端点至少为其本身的性质,当前最右右端点一定为 \(i-1\),则直接暴力更新这个点为回文中心的回文串的右端点,顺带可以算出这个点的回文半径。

否则,根据回文的性质,设当前最右端点为 \(MaxR\),其回文中心为 \(MaxI\),则 \(i\) 可以通过 \(MaxI\) 映射到 \(2MaxI-i\) 这个点,因为这是回文的。之前已经计算过 \(2MaxI-i\) 的回文半径了,但由于在 \(MaxI\) 为回文半径的回文串左端点之前的值与 \(MaxR\) 之后的值不能映射(因为不回文了),若 \(R_i\) 表示 \(i\) 的回文半径,则 \(i\) 点的初始回文半径为 \(\min(R_{2MaxI-i},MaxR-i+1)\)。

定义一下回文半径,若 \(i\) 的回文半径为 \(R_i\),则以 \(i\) 为回文中心的极长回文串为 \([i-R_i+1,r+R_i-1]\),极长的意思就是再长就不行了。

上文的初始回文半径可能并不是最终回文半径,所以暴力拓展,在拓展的同时,顺带更新 \(MaxR\) 即可。因为想要暴力拓展,当且仅当 \(i\) 点的初始回文半径为 \(MaxR-i+1\),此时当前回文串的右端点即为 \(MaxR\),所以 \(MaxR\) 可以跟 \(R_i\) 同时扩展。

考虑时间复杂度,枚举 \(i\) 这部分的时间复杂度为 \(O(N)\),\(MaxR\) 最多只会增加到 \(N\),所以均摊时间复杂度也是 \(O(N)\)。总时间复杂度为 \(O(N)\)。是最快的处理每个点的回文半径的算法。

  • 回文自动机

说到这个,我以后还会写 AC 自动机和后缀自动机,优先度很高。如果还有多余的时间,可以写子序列自动机。

回文自动机可以理解成,既是一个 DAG,又是一棵树,其中每个点表示一个回文串。其中 DAG 每条边最多有字符集大小条出边,每条出边表示一个字符。走一条边相当于把一个回文串两边同时加上这条边对应的字符。回文自动机上的每个点的回文串都是原字符串的本质不同回文子串。

为了方便表示,设字符集大小为 \(|S|\)。树上节点 \(u\) 的父亲用 \(Fail_u\) 表示。

首先抛出来一个非常厉害的结论,若在一个串最后添加一个字符,这个串最多只会新增一个回文串。因为如果新增 \(\geq 2\) 个回文串,右端点肯定都是新增那个字符。然后这些回文串的回文中心一定有先后关系,最前面的那个回文串(即最长后缀回文串)可以把后面的回文串给映射到前面去。既然后面是回文的,所以映射到前面去跟后面是相同的,所以后面的回文串一定在前面出现过。所以没出现过的新增回文串只有可能有一个,且为最长后缀回文串。

根据上面这个性质,可以知道,每个字符串的本质不同回文子串的数量级是 \(O(n)\) 的,所以回文自动机的点数和边数有保证。\(Fail_u\) 的实际意义则为:\(u\) 这个回文串的最长回文后缀(除去自己以外的)。

考虑增量法构造回文自动机。已经构造好当前字符串的回文自动机了,新增一个字符 \(c\),然后变成新的回文自动机。开一个数组 \(Length\),其中 \(Length_u\) 表示 \(u\) 这个回文串的长度。方便更新回文自动机。设边的集合为 \(Next\),\(Next_{u,i}\) 表示点 \(u\) 的字符为 \(i\) 的出边连向的点。

初始有两个点,\(0\) 号点,\(Length=0\),表示空回文串,\(1\) 号点,\(Length=-1\),表示左右各添加一个相同字符后成为一个长度为 \(1\) 的回文串。\(Fail_0=1\),方便写代码。用全局变量 \(End\) 表示当前字符串的最长回文后缀,初始为 \(0\)。

新增的点,从最长回文后缀开始跳 \(Fail\),直到遇到一个回文串(设为 \(u\))满足原串中这个回文串左边的第一个字符为 \(c\),这样就满足加入 \(c\) 后,最长回文后缀为那个回文串左右各添加一个 \(c\)。若那个字符串已经有一条 \(c\) 的边了,说明当前没有新增回文串,则 \(End=Next_{u,c}\),没了。否则就新建一个点,再连上这条边。再从 \(Fail_u\) 开始找最长回文后缀满足在原串上左边为 \(c\) 的回文串,设为 \(v\)。则 \(Fail_{new}=Next_{v,c}\)。根据上面的结论,这条边 \(Next_{v,c}\) 一定是存在的。

注意特殊情况,即 \(u\) 为 \(1\) 时,即最长回文后缀就是这个字符本身,那么 \(Fail_{new}=0\)。在写代码时,可以通过全局数组初始值全为 \(0\) 的特性来让代码更简洁,而不需要特判,具体见代码。

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e6+50;
char s[MAXN],a[MAXN];
int N;
int tot;
int Next[MAXN][26],Fail[MAXN],Length[MAXN];
int End;
long long Cnt[MAXN];
int GetFail(int u,int Id)
{
	while(a[Id-Length[u]-1]!=a[Id])
	u=Fail[u];
	return u;
}
int main()
{
	a[0]=-1;
	Length[1]=-1;
	Fail[0]=1;
	tot=1;
	scanf("%s",&s[1]);
	N=strlen(s+1);
	for(int i=1;i<=N;i++)
	{
		a[i]=s[i]-'a';
		int Last=GetFail(End,i);
		if(Next[Last][a[i]]==0)
		{
			tot++;
			Fail[tot]=Next[GetFail(Fail[Last],i)][a[i]];
			Length[tot]=Length[Last]+2;
			Next[Last][a[i]]=tot; 
		}
		End=Next[Last][a[i]];
		Cnt[End]++;
	}
	long long Max=0;
	for(int i=tot;i>=2;i--)
	{
		Max=max(Max,1ll*Length[i]*Cnt[i]);
		Cnt[Fail[i]]+=Cnt[i];
	}
	printf("%lld",Max);
}

时间复杂度为 \(O(N)\),因为跳 \(Fail\) 可以看做,每次使得深度加一,然后再使得深度变浅很多,所以也是 \(O(N)\) 级别的。

可以快速得到的东西很多,比如想要知道当前字符串的本质不同回文子串数量,则为除了 \(0\) 和 \(1\) 这两个点之外的点数。遍历一遍即可得到所有回文串,得到回文中心和半径也就很简单了。

标签:int,manacher,半径,端点,Fail,自动机,PAM,回文
From: https://www.cnblogs.com/0htoAi/p/17340598.html

相关文章

  • 力扣——5.最长回文子串(c语言)
    题目描述:给定一个字符串s,找到s中最长的回文子串。你可以假设s的最大长度为1000。示例1:输入:"babad"输出:"bab"注意:"aba"也是一个有效答案。示例2:输入:"cbbd"输出:"bb"1、思路1:动态规划对于一个子串而言,如果它是回文子串,并且长度大于2,那么将它首尾两个字......
  • 用C#写一个上传下载文件至OSS后返回文件路径用DES加密解密
    废话不多说,直接上代码:usingAliyun.OSS;//引入阿里云OSSSDKusingSystem;usingSystem.IO;usingSystem.Security.Cryptography;//引入.NETFramework中的加密库usingSystem.Text;publicclassOSSHelper{///<summary>///将文件上传至OSS,并使用D......
  • leetcode-234回文链表
    回文链表方法一:借助数组进行判断把节点的值复制到一个数组中再利用数组进行判断,但是这样需要占用额外的空间/***Definitionforsingly-linkedlist.*structListNode{*intval;*ListNode*next;*ListNode():val(0),next(nullptr){}*......
  • 4/20 C语言判断字符串是否为回文,字符串中可以包含中文和英文
    //已知中文字符占用两个字节#include<stdio.h>#include<string.h>booljudge(char*a,int&i,int&k);intmain(){inti,k;chara[100];while(scanf("%s",a)!=EOF){i=0;k=strlen(a)-1;while......
  • LeetCode Top100:回文链表 (python)
    LeetCodeTop100:回文链表给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。示例1:输入:head=[1,2,2,1]输出:true示例2:输入:head=[1,2]输出:false提示:链表中节点数目在范围[1,105] 内0<=Node.val<=9 ......
  • 【DP】LeetCode 132. 分割回文串 II
    题目链接132.分割回文串II思路分析动态规划题目的时候只需要考虑最后一个阶段,因为所有的阶段转化都是相同的,考虑最后一个阶段容易发现规律在数组的动态规划问题中,一般dp[i]都是表示以nums[i]为结尾的状态;dp[i][j]分别表示以nums1[i]和nums2[j]为结尾的状态,以此类......
  • 寻找回文数
    一、问题描述:寻找并输出11~999的数m,它满足m、m2和m3均为回文数。回文数所谓回文数是指其各位数字左右对称的整数。例如:121、676、94249等。满足上述条件的数如m=11,m2=121,m3=1331.二、设计思路:从11~999遍历每个数;判断是否为回文数,用除以10取余的方法从最低位开始取出各位......
  • papamelon 302. 碰撞游戏 Stripies(挑战程序设计竞赛)
    地址https://www.papamelon.com/problem/302http://poj.org/problem?id=1862解答自取了几个样例从大到小和从小到大进行模拟发现最大的数最先碰撞则开方的次数最多,所以需要结果最小,则优先去最大的数进行合并。代码如下#include<iostream>#include<algorithm>#includ......
  • hdoj 素数回文 1431 (模拟)
    素数回文TimeLimit:2000/1000MS(Java/Others)    MemoryLimit:65536/32768K(Java/Others)TotalSubmission(s):16372    AcceptedSubmission(s):3621ProblemDescriptionxiaoou33对既是素数又是回文的数特别感兴趣。比如......
  • 超级码力初赛第二场 五字回文 题解
    题目描述小栖最近很喜欢回文串,由于小栖的幸运数字是5,他想知道形似“abcba"的回文串在他给定的字符串中的数量s.length<=10^6字符串s只包含小写字母示例示例1:输入:s="abcba"输出:1示例2:输入:s="abcbabcccb"输出:2解释:形似”abcba“的字符串有”abcba“和”cbab......