浅析马拉车Manacher算法(附Codeforces 17E Palisection)

引入

在字符串问题处理中,常常会遇见处理回文串的问题。那么首先就要介绍一下什么是回文串——回文串就是对于一个字符串,自左向右和自右向左所得的字符串是相同的。例如    a b b a    \;abba\; abba    x y x    \;xyx\; xyx    a    \;a\; a等都是回文串,而    a b c d    \;abcd\; abcd    a b b c    \;abbc\; abbc    a b c    \;abc\; abc等则不是回文串。

实现

那么如何来实现求解一个字符串中所有回文串问题呢?先考虑最朴素的实现方法,即枚举字符串的每一个位置,然后不断向两边扩展,直到二者不同,则得到了以该位置为中心的回文串的长度,显然每个位置的回文串长度至少为    1    \;1\; 1。特别地,对于回文串而言,还有特别考虑长度的奇偶问题,因为对于奇长度的回文串其中心位置是一个字符,而偶长度的回文串则不是,但无论如何该算法的复杂度都达到了    O ( n 2 )    \;O(n^2)\; O(n2),往往是不足以接受的。

那么 M a n a c h e r Manacher Manacher算法是如何将复杂度降到    O ( n )    \;O(n)\; O(n)的呢,其设计思想类似于Z函数(戳我一键直达),考虑到回文串的对称性,观察下图:

当我们需要求解以    i    i \;i\;i ii为中心的回文串长度时,我们已经得知了在    i    \;i\; i之前的一个位置    m i d    \;mid\; mid,以它为中心的回文串的包含了    i    \;i\; i位置的字符,那么根据回文串的对称性,我们可以得到对称点    j    \;j\; j的信息,而该位置的回文串长度是已经求解过的,那么就在以    m i d    \;mid\; mid为中心的回文半径中(即图中蓝色部分),我们可以得到    i    \;i\; i的回文半径的长度和    j    \;j\; j的回文半径存在关联,这种关联可以极大地减少回溯次数,从而降低了复杂度。其分为下述两种情况:

1.以    j    \;j\; j为中心的回文半径未超出以    m i d    \;mid\; mid为中心的回文半径

对于这种情况,我们就可以    O ( 1 )    \;O(1)\; O(1)地得到以    i    \;i\; i为中心的回文半径,利用反证法,如果以    i    \;i\; i为中心的回文半径还要更长,因为此时这两个位置的回文边界都处于    m i d    \;mid\; mid的回文串之中,所以二者是严格对称的,即回文半径相同。

2.以    j    \;j\; j为中心的回文半径未超出以    m i d    \;mid\; mid为中心的回文半径

对于这种情况,因为已经超出了以    m i d    \;mid\; mid为中心的回文串范围,所以利用已有的对称性我们只能保证在    i ~ r    \;i~r\; ir这段区间的长度至少是以    i    \;i\; i为中心的回文半径,而对于    r    \;r\; r之后的位置,则需要枚举去对比。

那么问题来了,上文提到过,回文串的长度是分奇偶的,刚刚的论述过程似乎只适用于奇长度的回文串,那偶长度该如何处理呢?其实方法类似,只需要修改一下临界位置的条件即可,这里不给出实现代码给出了也没人用,毕竟可以二合一谁还那么麻烦去分类讨论 。为了避免繁琐的分类导致出现差错,在这里有一个实现技巧,即在字符串中每个字符之间插入一个特殊字符,例如将    a b b a b b a    \;abbabba\; abbabba改写为    # a # b # b # a # b # b # a #    \;\#a\#b\#b\#a\#b\#b\#a\#\; #a#b#b#a#b#b#a#,这样不论是奇长度还是偶长度就可以统一处理了。另外需要注意的是,因为引入了特殊字符,导致字符串长度会增加    2    \;2\; 2倍,实现是要注意空间大小以防溢出,另外就是得到每个位置的回文半径因为有特殊字符的影响,所以实际长度=所求长度/2。

实现代码如下:

void read()
{
	char ch = getchar();
	len = 1;
	while (ch < 'a' || ch>'z')
	{
		ch = getchar();
	}
	while (ch >= 'a' && ch <= 'z')
	{
		s[len++] = '#';
		s[len++] = ch;
		ch = getchar();
	}
	s[len] = '#';
}
void manacher()
{
	for (int i = 1, r = 0, mid = 0; i <= len; i++)
	{
		if (i <= r)
		{
			p[i] = min(p[(mid << 1) - i], r - i + 1);
		}
		while (s[i - p[i]] == s[i + p[i]])
		{
			p[i]++;
		}
		if (p[i] + i > r)
		{
			r = p[i] + i - 1;
			mid = i;
		}
	}
}

例题解析: C o d e f o r c e s   17 E — — P a l i s e c t i o n Codeforces\ 17E——Palisection Codeforces 17EPalisection
在这里插入图片描述
题目大意:找出一个字符串中所有相交的回文串的个数。

题意分析:考虑到求解相交回文串并不容易,所以逆向考虑,即总共的回文串数量减去互不相交的回文串的个数即为所求答案,另外需要注意的是这道题也运用到了差分的知识

Solution:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 4e6 + 16;
const ll mod = 51123987ll;
char s[maxn];
int n, len, p[maxn];
int l[maxn], r[maxn];
ll ans;
inline void read()
{
	char ch = getchar();
	s[0] = '$';
	while (ch < 'a' || ch > 'z')
	{
		ch = getchar();
	}
	while (ch >= 'a' && ch <= 'z')
	{
		s[++len] = '#';
		s[++len] = ch;
		ch = getchar();
	}
	s[++len] = '#';
}
inline void manacher()
{
	for (int i = 1, r = 0, mid = 0; i <= len; i++)
	{
		if (i < r)
		{
			p[i] = min(p[(mid << 1) - i], r - i + 1);
		}
		else
		{
			p[i] = 1;
		}
		while (s[i - p[i]] == s[i + p[i]])
		{
			p[i]++;
		}
		if (p[i] + i > r)
		{
			r = p[i] + i - 1;
			mid = i;
		}
	}
}
inline void solve()
{
	for (int i = 1; i <= len; i++)
	{
		//利用差分处理,l[i]表示以i为起点的回文串,r[i]表示以i为终点的回文串
		l[i - p[i] + 1]++;
		l[i + 1]--;
		r[i]++;
		r[i + p[i]]--;
		ans = (ans + ((ll)p[i] / 2)) % mod;		//统计所有回文串的数量
	}
	ans = ans * (ans - 1) / 2 % mod;	//假设回文串之间都能相交
	ll s = 0;
	for (int i = 1; i <= len; i++)
	{
		//利用差分求出原始序列
		l[i] += l[i - 1];
		r[i] += r[i - 1];
		if (!(i & 1))		//偶数位置即原字符串中的字符
		{
			//s表示之前结束的回文串,与当前的l[i]相乘就是不相交的个数
			ans = (ans - s *(ll) l[i] % mod) % mod;
			s = (s +(ll) r[i]) % mod;
		}
	}
	cout << ((ans + mod) % mod) << endl;
}
int main()
{
	cin >> n;
	read();
	manacher();
	solve();
	return 0;
}

完结撒花!!!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值