再学串串(五):谁会不喜欢可爱的小马(拉车)呢?

· · 算法·理论

因为是赛马娘拉车,所以先于 SA 出来了——绝对不是因为不会 SA 什么的(大嘘)。

可能有点水()五一集训去了加之回来五一假回来要期中考,再加上最近状态不太好去看心理医生了所以这次含水量可能有点高。

过去的章节

  1. 再学串串(一):我真的学会 KMP 了吗?评「clx201022 式的傲慢」
  2. 再学串串(二):AC 自动机不是自动 AC 机
  3. 再学串串(三):被构造自动机上计数 DP 题吓晕
  4. 再学串串(四):后缀是后缀的后缀是后缀的后缀

目前已同步在博客园更新。

第五站:Manacher

为什么叫马拉车呢?因为发明者就是 Manacher,跟 Tarjan 算法[^Tarjan]是 Tarjan 提出的一个道理。

例题:P1723 高手过愚人节。

高手觉得回文串很美观,但回文串美观在哪里呢?

回文基本性质

有一个很简单的定义:设一个字符串 S 的翻转为 S_R[^fanzhaun],那么满足 S = S_R 的串就是一个回文串。

对于回文串,有以下几个美观的性质:

  1. 一个串是回文串当且仅当它的翻转是它本身,且如果一个串的翻转是一个回文串,那么它自身也是一个回文串。

很显然的基本事实。

  1. 一个回文串在两端分别加入一个相同的字符后,仍是一个回文串。

显然,在两端分别加入一个字符对 ss_R 是对称的。由此也能轻松推出它的逆命题:

  1. 一个两端分别加入一个相同的字符后是一个回文串的串,是一个回文串。

还有:

  1. 在一个回文串 S 中,其对于任意合法的 S[l,r],设 l 的对称点 l'=n-lrr'=n-r',则总有 S[l,r]_R=S[r',l']

然后你就可以自己推出 Manacher 算法了。

不信?那就来试试看吧!

Re:从零开始的马拉车推导

目标是求主串 T 中最长的回文串长度,当然能求出所有回文串更好。

抄袭借鉴 KMP 算法(虽然历史上马拉车提出的比 KMP 早),想到从前往后推。因为回文串知道中心的情况下比较好推导,所以存下每一个位置的最长回文子串半径——这样也等价于存下了所有的回文串,因为主串 T 是固定的,同一中心已知最长回文串,其他回文串必然是其子串——设其为 d

奇数串和偶数串的本质是一样的,偶数串当作回文中心在中间两个字符中间即可。有些时候会在每两个字符间填充特殊字符以显式地表现这个中心,但这不是必要的。

设当前已求得 d_1\sim d_{i-1},要求 d_i

考虑当前所有已求得的回文串中蕴含的信息对求 d_i 可能的贡献。

设任意一个已知的 T 的回文串 S 的中心为 pi'iS 的对称点,知道中心的情况下可以方便求为 i'=2p-i

S 范围内任意以 i 为中心的子串 X 都有一个以 i'(1\le i'<p) 为中心的子串 X_R。此时我们想要 X 为一个回文串,由性质 0 可得,这要求 X_R 也必须是一个回文串。而此时我们已经求出以 i' 为中心的回文串,可以方便地求出一个尽可能大的 X

注意到在 S 范围外我们就管不了了,尽可能选取右端点最靠右的回文串可以减少不能确定的情况发生。

:::warning

单纯地选取右端点最靠右而不考虑其他元素并不一定是最优的,只是有一个时间复杂度上界保证。

https://www.luogu.com.cn/record/276793903

    for(int p = 0, i = 1; i <= n; i++)
    {
        if(p + odd[p] > i) // 至少要能够到吧
        {
            int ici = 2 * p - i; // 对称过去
            odd[i] = min(odd[ici], p + odd[p] - i);
        }
        while(i - odd[i] >= 1 && i + odd[i] <= n && s[i - odd[i]] == s[i + odd[i]]) odd[i]++;
        if(!p || i + odd[i] > p + odd[p]) p = i;
        ans = max(ans, odd[i] * 2 - 1);
    }
    for(int p = 0, i = 1; i <= n; i++)
    {
        if(p + even[p] > i) // 至少要能够到吧
        {
            int ici = 2 * p - i; // 对称过去
            even[i] = min(even[ici], p + even[p] - i);
        }
        while(i - even[i] - 1 >= 1 && i + even[i] <= n && s[i - even[i] - 1] == s[i + even[i]]) even[i]++;
        if(!p || i + even[i] > p + even[p]) p = i;
        ans = max(ans, even[i] * 2);
    }

https://www.luogu.com.cn/record/276824692

    for(int p = 0, i = 1; i <= n; i++)
    {
        if(p + odd[p] > i) // 至少要能够到吧
        {
            int ici = 2 * p - i; // 对称过去
            odd[i] = min(odd[ici], p + odd[p] - i);
        }
        while(i - odd[i] >= 1 && i + odd[i] <= n && s[i - odd[i]] == s[i + odd[i]]) odd[i]++;
        if(!p || i + odd[i] >= p + odd[p]) p = i;
        ans = max(ans, odd[i] * 2 - 1);
    }
    for(int p = 0, i = 1; i <= n; i++)
    {
        if(p + even[p] > i) // 至少要能够到吧
        {
            int ici = 2 * p - i; // 对称过去
            even[i] = min(even[ici], p + even[p] - i);
        }
        while(i - even[i] - 1 >= 1 && i + even[i] <= n && s[i - even[i] - 1] == s[i + even[i]]) even[i]++;
        if(!p || i + even[i] >= p + even[p]) p = i;
        ans = max(ans, even[i] * 2);
    }

都符合条件,但经实测,前者总是比后者更优。

:::

把已知信息都用完之后就只能根据性质 1 暴力推了。不过有一个好消息是每次成功扩展都必然会将区间最右往右推一步,所以总时间复杂度是 O(n) 的。

:::info[证明]

:::

d_1 的边界情况是容易处理的。记得对奇数长度和偶数长度的回文串分别处理。

:::success[马拉车到~~~]

#include<bits/stdc++.h>
using namespace std;
const int maxlen = 1e7;
string s;
int odd[maxlen + 1], even[maxlen + 1];
// odd 回文串半径(向上取整)
void solve()
{
    cin >> s;
    int n = s.size();
    int ans = 0;
    s = " " + s;
    memset(odd, 0, sizeof odd);
    memset(even, 0, sizeof even);
    for(int p = 0, i = 1; i <= n; i++)
    {
        if(p + odd[p] > i) // 至少要能够到吧
        {
            int ici = 2 * p - i; // 对称过去
            odd[i] = min(odd[ici], p + odd[p] - i);
        }
        while(i - odd[i] >= 1 && i + odd[i] <= n && s[i - odd[i]] == s[i + odd[i]]) odd[i]++;
        if(!p || i + odd[i] > p + odd[p]) p = i;
        ans = max(ans, odd[i] * 2 - 1);
    }
    for(int p = 0, i = 1; i <= n; i++)
    {
        if(p + even[p] > i) // 至少要能够到吧
        {
            int ici = 2 * p - i; // 对称过去
            even[i] = min(even[ici], p + even[p] - i);
        }
        while(i - even[i] - 1 >= 1 && i + even[i] <= n && s[i - even[i] - 1] == s[i + even[i]]) even[i]++;
        if(!p || i + even[i] > p + even[p]) p = i;
        ans = max(ans, even[i] * 2);
    }
    cout << ans << endl;
}
int main()
{
    int n;
    cin >> n;
    while(n--) solve();
    return 0;
}

:::

友链

创作声明

本文遵循 CC BY-NC-SA 4.0 协议。

保证本文未使用任何 AI 工具辅助创作。

转载例如下:

[原文](https://www.luogu.com.cn/article/8bh9pjuk)作者为 [clx201022](https://www.luogu.com.cn/user/552688),转载人保证会遵循 [CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/deed.zh-hans) 协议:以适当的方式署名,不以盈利为目的进行转载,并以相同方式共享此文。

[^Tarjan]: 你还记得有多少算法叫 Tarjan 算法吗?

[^fanzhaun]: 设 S=S_1S_2S_3\dots S_{n-1}S_n,则 S_R=S_nS_{n-1}\dots S_3S_2S_1。当然如果你想要用 0-index 也可以,那样就是 S=S_0S_1S_2\dots S_{n-2}S_{n-1}S_R=S_{n-1}S_{n-2}\dots S_2S_1S_0