比KMP简单的Manacher

P3805 【模板】manacher - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

“没时间悼念KMP了,接下来上场的是Manacher!”


什么是Manacher?

历史背景:

1975 年,一个叫 Manacher 的人发明了这个算法,所以叫Manacher 算法(中文名:马拉车算法)

应用背景:

现在有个字符串A,你需要找到该字符串中最长的回文字串,输出其长度。

回文串是正着读反着读都是一样的,例如:101,aba,tnt。

Manacher算法思路:

Manacher第一步修改原字符串。

我们认定一个字符串是否为回文串,都会先找这个字符串的中心,然后向两边扩散进行字符对比。比如aba这个字符串,我们要判断它是否为回文串,就可以先找到中心位置b,然后再向两边扩散比较,b左边的字符是不是等于b右边的字符,如果都一致,那么该字符串就是回文串。

当然,聪明的你一定发现了一个问题,如果该字符串是abba这种偶数长度的字符串呢?这个时候我们就可以把偶数长度的字符串变为奇数长度的字符串,向原字符串插入一个原字符串不会出现的字符,比如,"$"。那么对于abba而言插入后的字符串就变成了$a$b$b$a$。然后我们就可以套用判断奇字符串是否是回文串的思想,来对这个进行判断了。

插入相同字符后的字符串如果是回文串,那么没有插入相同字符的字符串也是回文串,可自行验证。

所以Mannacher的第一步就是在原来的字符串的基础上,插入一个原字符串不会出现的字符。

Manacher第二步回文范围[L,R],回文中心W。

约定p数组放的是回文半径,则p[i]存放的是以i为回文中心的回文半径。

如图,在字符串(黑色框)中有一回文子串(蓝色框),范围为[L, R],其中M为该回文串的中心,现在我们欲求以i为回文中心的回文半径r。就要先分两种大情况。

第一种是i在[L, R]区间里,我们就可以先找i相当于M的对称点K,因为i是在K之后的所以K的回文半径是已知的,那么当K的回文区域不包含L,即K-p[K]+1>L时,p[i] = p[k] =[2*M-i]。当K的回文区域包含L时,即K-p[K]+1<=L时,p[i] = R-i+1。

第二种是i在[L, R]区间外面的,我们就只能暴力向两边扩散比较,求得它的回文半径。

经过如图的数学推导,我们就可以给第一种情况化简为p[i]=min(p[2*M-i], R-i+1),然后再向两边扩散比较字符。

通过以上这种方法,写一个for循环加一层while就能够快捷地得到以每个字符作为回文中心的回文半径了。

得到回文半径有什么用呢?题目不是让我们求字符串里面最长的回文子串有多长嘛?那么最大的回文半径-1,不就是最长的回文子串的长度嘛?也就解开了。

因为原字符串在插入了原字符串没有的字符后,原字符串的长度变成了原来的2倍,所以回文半径-1就是最长的回文子串。可自己写几个试试。

【算法详解】Manacher算法_哔哩哔哩_bilibili 


如何实现Manacher代码?

插入原字符串没有的字符

for (ll i = 0; i < 2 * a.length(); i++)
	{
		if (i & 1)
//如果是奇数
			b[i] = a[i / 2];

		else
			b[i] = '$';
	}
	b[2 * a.length()] = '$';
//a为原字符串
//b为插入后的字符串

求以每个字符作为回文中心的回文半径

ll M = 0, R = 0;
//初始化M和R
	for (ll i = 0; i <= 2 * a.length(); i++)
	{
// for循环遍历字符串里面每个字符
		if (i > R)
			p[i] = 1;
//如果i是在[L, R]区间外面的 
//那么它的回文半径默认从1开始
		else
			p[i] = min(p[2 * M - i], R - i + 1);
//否则回文半径就是min(p[2 * M - i], R - i + 1)
		while (i + p[i]<= 2 * a.length() && i - p[i] >= 0 && b[i - p[i]] == b[i + p[i]])
//while循环就是向两边扩散比较的过程
			p[i]++;
//只有当左右两边的字符一致
//回文半径才+1
		if (i + p[i] - 1 > R)
		{
			M = i;
			R = i + p[i] - 1;
		}
//如果以新的字符为回文中心
//回文半径更大的话
//那么就以新的字符为回文中心
//更新R
	}

全部代码

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long ll;
const ll N = 2e7;
string a;
vector<ll> p(2 * N);
char b[2 * N];

int main()
{
	cin >> a;
	for (ll i = 0; i < 2 * a.length(); i++)
	{
		if (i & 1)
			b[i] = a[i / 2];

		else
			b[i] = '$';
	}
	b[2 * a.length()] = '$';
	// cout << "变形后的:" << b << endl;
	ll M = 0, R = 0;
	for (ll i = 0; i <= 2 * a.length(); i++)
	{
		if (i > R)
			p[i] = 1;
		else
			p[i] = min(p[2 * M - i], R - i + 1);
		while (i + p[i]<= 2 * a.length() && i - p[i] >= 0 && b[i - p[i]] == b[i + p[i]])
			p[i]++;
		if (i + p[i] - 1 > R)
		{
			M = i;
			R = i + p[i] - 1;
		}
	}
	// cout << M << " " << R << endl;

	cout << *max_element(p.begin(), p.end()) - 1;

	return 0;
}

  • 23
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HUTAC

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值