Manacher

Manacher

是找出字符串中回文子串很有效的算法

具体而言,是在暴力的基础上一步步优化

首先,最暴力的是 O(n2) 枚举左右端点,再 O(n) 判断

但可以直接枚举中间点,优化到 O(n2)

此时出现了个问题:长度为奇数的回文串中点在中间字母,

但长度为偶数的回文串中间点在空隙处!

所以,我们把空隙处插入'#'的特殊字符(保证与其它字符不同),就更方便枚举空隙(在最前面和最后面也加入)

O(n2)的复杂度也不理想

# a # b # b # a # a # b # b #

这时我们要利用回文串本身的对称性

看第3个#号,易知以它为中心,两段字符对称最长可延伸至开头和第5个#(mx,此时已找出的回文串最大的右边界)处(也就是最长回文子串)

向后移一个看b(记作 p[2b]),此时以第3个 # (id) 为对称点,它与第1个b完全对称,记为 p[1b]p[i] 中存了以 i 为对称中心的极长回文串长度 +1 的值

那么当 1b<=mx 时,

  1. id×2mx(mx 关于 id 对称点)>1bp[1b],即以 1b 为中心点回文子串超出 mx 范围,则p[2b] 初始为 mx2b,剩下只能暴力枚举

  2. id×2mx1bp[1b],则以 1b 为中心点回文子串与以 2b 为中心点回文子串一模一样(对称),即 p[2b]=p[1b]

1b>mx 时,只能枚举了

分析:

时间复杂度:O(n)

循环好理解,但暴力查询为何线性?

因为假如 i+p[i]mx 小,根本不会执行 while ,不用枚举

但即便 i+p[i]mx 大,由于 mx 为已找出的回文串最大的右边界,因此mx递增,自然此时 i+p[i] 要比 mx 大也应递增


应用:

1. 重载比较方式

P3501 [POI2010]ANT-Antisymmetry

solution

直接把比较回文串的相等改为异或为 1 则相等即可

注意此时 #​ 为通配符

代码:

for(int i = 1; i <= n * 2; i += 2)
	{
		p[i] = (mx > i) ? min(mx - i + 1, p[id * 2 - i]) : 1;
		while(i - p[i] >= 1 && i + p[i] <= n * 2 && (t[i - p[i]] != t[i + p[i]] || (t[i - p[i]] == '#')))
			p[i]++;
		sum += p[i] >> 1;
		if(i + p[i] - 1 > mx)   mx = i + p[i] - 1, id = i;
	}

2. 找所有回文串(个数等)

P1659 [国家集训队]拉拉队排练

solution

题目只让我们求串长为奇数的串,且要找前 k 大的串长之积

比较显然的想法是开数组 tong 对每个长度记录个数,但个数太多直接算不行

注意 manacher 求的是极长回文子串,也就是一个长为 x 的包含长为 x2,x4 的回文串

那除去下标为偶数位的,相当于覆盖了数组的前缀

因此,从大到小枚举奇数位到位置 xsumtong[x+2n] 的和,那长为 x 的回文串个数为 tong[x]+sum

或者相当于极长长度为 x 的串把 tong[1x]+1,用差分维护

代码:

	for(int i = 1; i <= n * 2 + 1; i++)
	{
		p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
		while(t[i - p[i]] == t[i + p[i]])	p[i]++;
		if(i + p[i] > mx)	mx = i + p[i], id = i;
		if(i % 2 == 0)	tong[1]++, tong[p[i] / 2 + 1]--;
	}
	for(int i = 1; i <= (n + 1) / 2 + 1; i++)	
	{
		tong[i] += tong[i - 1];
		sum += tong[i];
		if(tong[i])	maxx = max(maxx, (ll)i);
	}
	if(sum < k)	printf("-1");
	else
	{
		ll noww = k;
	    for(int i = maxx; i > 0; i--)
	    {
	    	if(noww - tong[i] < 0 && noww > 0)	
	    	{
	    		ans = ans * qmi((ll)(i * 2 - 1), noww) % mod;
	    		break;
			}
			else if(noww - tong[i] < 0 && noww <= 0)	break;
	    	ll lsh = qmi((ll)(i * 2 - 1), tong[i]);
	    	if(tong[i])	ans = ans * lsh % mod; 
	    	noww -= tong[i];
		}
		printf("%lld", ans);
	}

3. 进行 dp

P4555 [国家集训队]最长双回文串

solution

注意到拼接时两个串不一定都是极长的,因此 manacher 后得额外进行 dp

因为中间没有重复字符,所以想枚举中间的端点,则端点一定为 #​

l[i] 表示以 i 为左端点点的极长回文串,r[i] 为以 i 为右端点的极长回文串

manacher 时对极长的串进行预处理,l[ip[i]+1]=r[i+p[i]1]=p[i]1

然后每向右移两位 l[i]2,每向左移两位 r[i]2

代码:

for(int i = 1; i <= n * 2; i++)
	{
		p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
		while(t[i - p[i]] == t[i + p[i]])	p[i]++;
		if(i + p[i] > mx)	mx = i + p[i], id = i;
		l[i - p[i] + 1] = max(l[i - p[i] + 1], p[i] - 1);
		r[i + p[i] - 1] = max(r[i + p[i] - 1], p[i] - 1);
	}
	for(int i = 1; i <= n * 2; i += 2)	l[i] = max(l[i], l[i - 2] - 2);
	for(int i = n * 2; i > 0; i -= 2)	r[i] = max(r[i], r[i + 2] - 2);
	for(int i = 1; i <= n * 2; i += 2)	
	    if(r[i] && l[i])	ans = max(ans, l[i] + r[i]);

4. P6216 回文匹配

先求出那些左端点后面可以接上 s2,做前缀和

s1 跑 manacher

对于一个回文中心有极长回文串 [l,r],那么 [l,rlen+1] 中的左端点就能产生贡献

前缀和相减就是 qzhrlen+1qzhl1

但是还有 [l+1,r1],[l+2,r2],也需要计算

qzhrlen+1qzhl1+qzhrlenqzhl2

发现加的前缀和是一个区间,减的前缀和也是一个区间

再次前缀和,注意边界的计算

int main()
{
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> m >> (s + 1) >> (t + 1);
	lens = strlen(s + 1), lent = strlen(t + 1);
	s[0] = '~', s[lens + 1] = '#', pw[0] = 1; 
	for(ui i = 1; i <= lens; ++i)	
		hsh[i] = hsh[i - 1] * P + s[i], pw[i] = pw[i - 1] * P;
	for(ui i = 1; i <= lent; ++i)	hah = hah * P + t[i];
	for(ui i = 1; i + lent - 1 <= lens; ++i)
		if(gethsh(i, i + lent - 1) == hah)	++num[i];
	for(ui i = 1; i <= lens; ++i)	tot[i] = tot[i - 1] + num[i];
	for(ui i = 1; i <= lens; ++i)	sum[i] = sum[i - 1] + tot[i];
	for(ui i = 1; i <= lens; ++i)
	{
		p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
		while(s[i - p[i]] == s[i + p[i]])	++p[i];
		if(i + p[i] > mx)	mx = i + p[i], id = i;
		if(i + p[i] - 1 - (i - p[i] + 1) + 1 < lent || i <= lent / 2)	continue;
		ui r = i + (lent / 2), l = i - (lent / 2);
		if(i - p[i])	ans += sum[i + p[i] - lent] - sum[r - lent] - (sum[l - 1] - sum[i - p[i] - 1]);
		else	ans += sum[i + p[i] - lent] - sum[r - lent] - (sum[l - 1] - sum[i - p[i]]);
	}
	cout << ans;
	return 0;
}

5. 在扩展时计算答案

P4287 [SHOI2011] 双倍回文

首先应枚举中间的断点,因为整个串也是回文串,在这个串中找答案

关键在快速枚举左右的中心

mx 被更新时,可以枚举串的右端点,则中间和右端点的中点则是右半部分的中心

关于中间对称的位置应该是左半部分的中心,判断它是否满足条件

Q1:为什么这样不会漏掉答案呢?

因为如果没发生拓展,则在另一边对称的有完全相同的回文串,没必要枚举了

Q2:为什么时间复杂度是 O(n)

因为枚举端点是 mxi+pi,而下一轮 mx 即更新为 i+pi,原理同 manacher 复杂度分析

所以,一个串中有多少个本质不同的回文串?

O(n) 个!

#include<bits/stdc++.h>
using namespace std;

const int N = 1000010, inf = 1e9;
int n, p[N], id, mx, ans, q[N], hh = 1, tt, tree[N << 2];
char t[N], s[N];
int main()
{
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	cin >> n >> (t + 1);
	s[0] = '~', s[n * 2 + 1] = '#', s[n * 2 + 2] = '$';
	for(int i = 1; i <= n; ++i)	s[i * 2] = t[i], s[i * 2 - 1] = '#';
	for(int i = 1; i <= n * 2; ++i)
	{
		p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
		while(s[i - p[i]] == s[i + p[i]])	++p[i];
		if(i + p[i] > mx)	
		{
			if(i & 1)
				for(int j = max(i + 4, mx); j < i + p[i]; ++j)
					if((j - i) % 4 == 0 && p[i * 2 - (i + j) / 2] + i * 2 - (i + j) / 2 >= i)	
						ans = max(ans, j - i);
			mx = i + p[i], id = i;
		}
	}
	cout << ans;
	return 0;
}

6. P5446 [THUPC2018] 绿绿和串串

发现复制的本质是把串变成以末尾字符为中心的回文串

然后原串一定是 S 的前缀

S 跑 manacher

如果只复制一次,那么可以通过判断以当前点为中心回文的部分能否覆盖到末尾判断

复制多次看起来有点麻烦

但如果复制一次,得到的串可以再经过一系列复制得到 S,就判断这个结尾可以

倒着处理,类似 DP 的思想

int main()
{
	ios::sync_with_stdio(false), cin.tie(0);
	cin >> T;
	while(T--)
	{
		cin >> s, n = s.length();
		t.clear();	t.pb('~'), t += s;
		t.pb('$'), mx = id = las = 0;
		for(int i = 1; i <= n; ++i)
		{
			book[i] = 0, p[i] = (mx > i) ? min(p[id * 2 - i], mx - i) : 1;
			while(t[i - p[i]] == t[i + p[i]])	++p[i];
			if(i + p[i] > mx)	mx = i + p[i], id = i; 
		}
		for(int i = n; i > n / 2; --i)
			if(i + p[i] - 1 >= n)	book[i] = 1, las = i;
		for(int i = n / 2; i > 0; --i)
			if(i == p[i])
			{
				if(book[i + p[i] - 1])	las = i, book[i] = 1;
			}
		for(int i = 1; i <= n; ++i)
			if(book[i])	print(i), putchar(' ');
		putchar('\n');
	}
	return 0;
}
posted @   KellyWLJ  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示