Manacher

Manacher

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

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

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

但可以直接枚举中间点,优化到 \(O(n^2)\)

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

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

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

但 $O(n^2) $的复杂度也不理想

# 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\times 2-mx\)(\(mx\) 关于 \(id\) 对称点)\(>1b-p[1b]\),即以 \(1b\) 为中心点回文子串超出 \(mx\) 范围,则\(p[2b]\) 初始为 \(mx-2b\),剩下只能暴力枚举

  2. \(id\times 2-mx\le 1b-p[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\) 的包含长为 \(x-2,x-4\cdots\) 的回文串

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

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

或者相当于极长长度为 \(x\) 的串把 \(tong[1\sim x]+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[i-p[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 回文匹配

先求出那些左端点后面可以接上 \(s_2\),做前缀和

\(s_1\) 跑 manacher

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

前缀和相减就是 \(qzh_{r-len+1}-qzh_{l-1}\)

但是还有 \([l+1,r-1],[l+2,r-2]\dots\),也需要计算

\(qzh_{r-len+1}-qzh_{l-1}+qzh_{r-len}-qzh_{l-2}\dots\)

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

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

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)\)

因为枚举端点是 \(mx\sim i+p_i\),而下一轮 \(mx\) 即更新为 \(i+p_i\),原理同 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 @ 2024-02-14 21:56  KellyWLJ  阅读(4)  评论(0编辑  收藏  举报