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\) 时,
-
若 \(id\times 2-mx\)(\(mx\) 关于 \(id\) 对称点)\(>1b-p[1b]\),即以 \(1b\) 为中心点回文子串超出 \(mx\) 范围,则\(p[2b]\) 初始为 \(mx-2b\),剩下只能暴力枚举了
-
若 \(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. 找所有回文串(个数等)
\(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
\(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. 在扩展时计算答案
首先应枚举中间的断点,因为整个串也是回文串,在这个串中找答案
关键在快速枚举左右的中心
在 \(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;
}