[带自己学/复习算法] 2.Manacher

Manacher

Manacher算法主要与回文串问题相关,能够求得以 i i i 为下标的回文串半径 d p [ i ] dp[i] dp[i],它相对于普通暴力算法的最关键点就是对 回文半径的初始化(重点)不仅仅是一开始为1,而是可能从之前算出来的结果中转移,而本文也将对其介绍。

1 算法的重点 - 半径初始化 及 分类讨论

Manacher算法示意图
首先解释一下上面的各个变量含义

i : 当前遍历到的下标 i,由于是从左往右遍历的,此时 [0, i - 1] 的回文串都已经求出了。
R : 当前最右边界,这一项的设置是为了保证算法的O(n)的复杂度,代码中会有解释。
	图中有两个R是可能出现的两种位置:R 在当前下标 i 的左边 / 右边
idx : 当前达到最右边界的回文串中心下标
i_mirror :当前 i 关于 idx 的镜像对称位置 i_mirror,这是为了设置当前遍历下标 i 的回文串初始化长度,
可能由此镜像位置的状态进行转移 

以上图为例,Manacher算法的主要思路中运用到了 d p ( 动 态 规 划 ) dp(动态规划) dp状态转移思想和分类讨论。它通过对初始化半径长度的状态转移,以及通过设置最右边界对后续可增加长度的最多增加限制,保证了整个算法是以 O ( n ) O(n) O(n) 的复杂度不断进行(代码中会较为详细的指出)。
状态转移:以 i i i 为下标的回文串初始大小不是一定从1开始,而是可能继承自 i_mirror 等长度,从而减少了复杂度。
最右边界:到当前下标 i i i 为止,回文串的最右端延伸到的最远距离 R R R
分类讨论:最终总共可分为3种不同的情况。我们需要从头开始讨论:
在这里插入图片描述

为了方便对照,此处再放一次实例图片

  1. 首先当前遍历到的下标 i 是确定了

  2. 然后 idx 和 i 的关系也能确定下来(idx < i ,因为当前达到最右边界的回文串一定是在之前求出来的,所以一定在 i 左侧)

  3. 再讨论 R ,会发现 R 是无法确定的(既可能半径较小,在 i 左侧;也可能半径较大,在 i 右侧;也可能刚好在 i,此时可以归为在左侧那一类 )于是我们一一往下讨论:在这里插入图片描述

    3.1. R 在 i 的左侧或刚好为i,此时如上图所示,没有太多的其他信息,我们找不到当前以 i 为下标的回文串初始化长度 init_length 和之前状态的关系,只能将其初始化为 0,即以 i 为下标的回文串半径初始化为他自己, d p [ i ] = 0 , w h e n   R ≤ i dp[i] = 0,when \ R \leq i dp[i]=0,when Ri
    在这里插入图片描述

    3.2 R 在 i 的右侧,此时如上图所示,说明 [ i d x , R ] [idx,R] [idx,R] 这右边半段一定和左半段是以 i d x idx idx 为中心对称的回文串,那么此时就可以看出
    在这里插入图片描述

    如上图所示,以 i i i 为中心的回文串一定和以 i m i r r o r i_{mirror} imirror 为中心的回文串(上图绿色部分)大小相同(真的一定相同吗?),即 d p [ i ] = d p [ i m i r r o r ] dp[i] = dp[i_{mirror}] dp[i]=dp[imirror]
    在这里插入图片描述

    显然有一个反例,如上图所示,可以看到此时以 i m i r r o r i_{mirror} imirror 为中心的回文串左侧已经超出了红色边界 R ′ R' R,而我们只保证了 [ R ′ , i d x ] [R',idx] [R,idx] [ i d x , R ] [idx,R] [idx,R] (下图绿色部分)一定是对称的( [ R ′ , R ] [R',R] [R,R]是回文串),我们无法保证剩下部分(下图紫色部分)是对称的,如下图所示
    在这里插入图片描述

    换句话说,我们只能确保以 i i i 为中心的回文串半径初始化长度在 R − i R - i Ri,或是 d p [ i m i r r o r ] dp[i_{mirror}] dp[imirror],那么为了确保一定正确,我们只需要让以 i i i 为中心的回文串初始化长度为两者的 m i n min min 即可,即 d p [ i ] = m i n ( R − i , d p [ i m i r r o r ] ) , w h e n   R > i dp[i] = min(R-i,dp[i_{mirror}]), when \ R>i dp[i]=min(Ri,dp[imirror]),when R>i,再结合前面的 d p [ i ] = 0 , w h e n   R ≤ i dp[i] = 0, when\ R \leq i dp[i]=0,when Ri,可以得出一个总体的初始化长度:

    d p [ i ] = R > i ? m i n ( R − i , d p [ i m i r r o r ] ) : 0 dp[i ] = R>i ? min(R-i,dp[i_{mirror}]) : 0 dp[i]=R>i?min(Ri,dp[imirror]):0

    这是一个C语言的三目表达式 A ? B : C A?B:C A?B:C,其中 A = { R > i } , B = { m i n ( R − i , d p [ i m i r r o r ] ) } , C = { 0 } A = \{R>i\},B =\{min(R-i,dp[i_{mirror}])\},C = \{0\} A={R>i},B={min(Ri,dp[imirror])},C={0}翻译出来就是 R > i R>i R>i吗,如果是,则表达式的值为 B B B的值,否则为 C C C的值

2 算法的一些另外的小trick - 填充字符串

好处:对字符串进行填充,能够使奇数/偶数长度的回文串统一变成奇数长度的回文串,更为简单且减少了对边界的判断。
填充方法:abba => @#a#b#b#a#

对每个字符前面添加 # 填充,首尾再加两个不一样的字符来填充

当然填充完后,对填充后的字符串进行Manacher,相应求出的半径是带有 # 的需要转换。此时 d p [ i ] dp[i] dp[i] 的含义不变,但在数值上就等于以 i i i 为中心的回文串的长度+1,

3 代码解释 及 模板

string change(string s){
	string str = "@";
	for(char c : s){
		str += '#';
		str += c;
	}
	str += '#';
	return str;
}
string palindrome(string s_){
	s = change(s_);
	int n = s.length();
	vector<int>dp(n);//# 保存回文半径的长度,但经过change变换后,dp[i]其实是回文串长度+1
	int R = 0, idx = 0;
	for(int i = 1; i < n; i++){
		//# 初始化
		int i_mirror = 2 * idx - i ;
		dp[i] = R > i ? min(R - i, dp[i_mirror]) : 1;
		//# 有限制的暴力
		while( s[i + dp[i]] == s[i - dp[i]] ) dp[i]++;
		//# 对最右边界进行更新
		if(i + dp[i] > R){
			R = i + dp[i];
			idx = i;
		} 
	}
	int maid = 1; //# 找出最长回文串的中心
	for(int i = 1; i < n; i++){
		maid = dp[i] > dp[maid] ? i : maid;
	}
	int center = (maid - 1) / 2;// # 最长回文子串的中心点
	int r = (dp[maid] - 1) / 2; //# 最长回文子串的半径
	return s_.substr(center - r , dp[maid] - 1);
}
	
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值