【字符串题目讲解】一文理解 Manacher Algoirth(马拉车算法)——以洛谷 P3805 和 P5446 为例

M a n a c h e r   A l g o r i t h m \mathrm{Manacher\ Algorithm} Manacher Algorithm

Manacher 算法主要是解决怎样的问题呢,其实是求解最长的回文串,但是只能找到长度为奇数的回文串,不过可以通过转化使得能够求解任意长度的回文串。

例题:P3805 【模板】manacher

A l g o r i t h m   F l o w \mathrm{Algorithm\ Flow} Algorithm Flow

Manacher 的算法是在 O ( n ) O(n) O(n) 的时间复杂度内,查找对于每一个 i i i,最大的回文半径 p i p_i pi 是多少?

从左往右递推做,在递推的过程中,每一次维护右端点最靠右的回文串的中心位置 m i d mid mid,以及该回文串最靠右的位置 m r mr mr(通常不包含该回文串最靠右的位置,即为该位置 + 1 +1 +1)。(如图)

那么,对于求解 i i i p i p_i pi,可以分情况讨论:

  • i i i m r mr mr 的左边,即在 m i d mid mid 所在回文串的内部。那么,充分利用已经算过的信息,考虑 i i i 在该回文串内的对应点 j j j,其中 j = m i d × 2 − i j=mid\times 2 - i j=mid×2i
  • j j j 所在的最大回文串仍然在 m i d mid mid 所在回文串的范围内,则 p i ≥ p j p_i\ge p_j pipj。(何时取 ≥ \ge ,当且仅当 j j j 的左端点恰好为 m i d mid mid 所在回文串的左端点时)
  • j j j 所在的最大回文串超出了 m i d mid mid 所在回文串的范围,则 p i = m r − i p_i=mr-i pi=mri

综上所述, p i ≥ min ⁡ ( p j , m r − i ) p_i\ge \min(p_j,mr-i) pimin(pj,mri)

  • i i i m r mr mr 的右边,即在 m i d mid mid 所在回文串的外部。那么,此时 p i ≥ 1 p_i\ge 1 pi1

最后,暴力的向外扩展即可:

while (S[i - p[i]] == S[i + p[i]]) p[i] ++;

判断当前的 p i + i p_i+i pi+i 是否超出 m r mr mr,若超出,则更新 m r mr mr m i d mid mid 即可。

A n a l y s i s   o f   t h e   T i m e   C o m p l e x i t y \mathrm{Analysis\ of\ the\ Time\ Complexity} Analysis of the Time Complexity

其实,时间复杂度就是 while 循环的执行次数(即暴力扩展的次数)。

只要执行 while 那么一定会更新 m r mr mr,且 while 执行的次数与 m r mr mr 向右扩展的数量相同。

所以 m r mr mr 总是线性向右递推的,所以总的时间复杂度为 O ( n ) O(n) O(n)

T r a n s f o r m \mathrm{Transform} Transform

刚才的算法只是解决一个字符串中奇数长度的回文串,偶数的是找不出来的。

不过,通过加一些字符可以转化为能够求出任意长度的最长回文串,具体如下:

在任意 2 2 2 个字符之间添加 1 1 1#,首尾也要加入 #,再在首尾分别加入 $^ 以防出界,因为你会发现 while 循环是没有边界的。

具体形式: $ # a 1 # a 2 # a 3 # … a n # ∧ \$ \# a_1\# a_2\# a_3\#\dots a_n\#\wedge $#a1#a2#a3#an#

这样对于新串的最长回文半径 − 1 -1 1 即为旧串最长回文串长度。

A c c e p t e d   C o d e \mathrm{Accepted\ Code} Accepted Code
#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef pair<int, int> PII;
typedef long long LL;

const int SIZE = 4e7 + 10;

int N;
string A, S;
int P[SIZE];

void Manacher()
{
	int mid, mr = 1;
	for (int i = 1; i <= N; i ++)
	{
		if (i < mr) P[i] = min(P[mid * 2 - i], mr - i);
		else P[i] = 1;
		while (S[i - P[i]] == S[i + P[i]]) P[i] ++;
		if (i + P[i] > mr)
		{
			mr = i + P[i];
			mid = i;
		}
	}
}

signed main()
{
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);

	cin >> A;
	N = A.size(), A = ' ' + A;

	S += "$#";
	for (int i = 1; i <= N; i ++)
		S += A[i], S += '#';
	S += '^';
	N = S.size(), S = ' ' + S;

	Manacher();

	int Result = 0;
	for (int i = 1; i <= N; i ++)
		Result = max(Result, P[i] - 1);

	cout << Result << endl;

	return 0;
}

练习题:P5446 【THUPC2018】绿绿和串串

D e s c r i p t i o n \mathrm{Description} Description

定义翻转的操作:把一个串以最后一个字符作对称轴进行翻转复制。形式化地描述就是,如果他翻转的串为 R R R,那么他会将前 R − 1 R-1 R1 个字符倒序排列后,插入到串的最后

现给出字符串 S S S,询问有哪些串长度不超过 ∣ S ∣ |S| S 的串 R R R 经过若干次翻转操作后得到的串作为 T T T 的前缀。

数据范围: ∑ ∣ S ∣ ≤ 5 × 1 0 6 \sum|S|\le 5\times 10^6 S5×106

S o l u t i o n \mathrm{Solution} Solution

考虑怎样的串翻转若干次之后前缀包含 S S S,其一定是 S S S 的一个前缀,这样翻转后 S S S 才会是它的前缀。

那么,对于 S S S 中每一个位置 i i i 分情况讨论:

  • 1 ∼ i 1\sim i 1i 的串翻转 1 1 1 次之后长度超过 ∣ S ∣ |S| S,则要求 i + 1 ∼ ∣ S ∣ i+1\sim |S| i+1S 的串是 1 ∼ i − 1 1\sim i-1 1i1 的串取反后的前缀,即以 i i i 为中心的回文串最大右端点为 ∣ S ∣ |S| S。标记 i i i 位置为可行位置。
  • 1 ∼ i 1\sim i 1i 的串翻转 1 1 1 次之后长度小于 ∣ S ∣ |S| S,则要求 1 ∼ 2 i − 1 1\sim 2i-1 12i1 是回文串,且右端点为可行位置,也就是能够经过多次翻转使 S S S 为其的前缀。

A c c e p t e d   C o d e \mathrm{Accepted\ Code} Accepted Code

#include <bits/stdc++.h>
#define int long long

using namespace std;

typedef pair<int, int> PII;
typedef long long LL;

const int SIZE = 2e6 + 10;

int N;
string S;
int P[SIZE], Vis[SIZE];

void Manacher()
{
	int mr = 1, mid;
	for (int i = 1; i <= N; i ++)
	{
		if (i < mr) P[i] = min(P[mid * 2 - i], mr - i);
		else P[i] = 1;
		while (S[i - P[i]] == S[i + P[i]]) P[i] ++;
		if (P[i] + i > mr)
			mr = P[i] + i, mid = i;
	}
}

void solve()
{
	cin >> S;
	N = S.size(), S = ' ' + S, S += '^';

	Manacher();

	for (int i = N; i >= 1; i --)
		if (i + P[i] - 1 >= N) Vis[i] = 1;
		else if (Vis[i + P[i] - 1] && i - P[i] + 1 == 1) Vis[i] = 1;

	for (int i = 1; i <= N; i ++)
		if (Vis[i])
			cout << i << " ", Vis[i] = 0;
	cout << endl;
}

signed main()
{
	cin.tie(0);
	cout.tie(0);
	ios::sync_with_stdio(0);

	int Data;

	cin >> Data;

	while (Data --)
		solve();

	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值