Manacher算法

Manacher算法

给定一个字符串,要求求出字符串中所有的回文字串。Manacher算法给出了一个线性时间算法。

Manacher算法分为三个步骤:

  1. 寻找回文半径下限
  2. 扩展半径,记录回文子串信息
  3. 更新区间 [ l , r ] [l,r] [l,r]

寻找回文半径下限

回文串分为奇回文串和偶回文串,而奇回文串操作比较优雅。故先介绍奇回文串的Manacher算法。

源字符串为 t [ i ] t[i] t[i],定义数组 p [ i ] p[i] p[i],代表以字符 t [ i ] t[i] t[i]为中心,回文子串的最大半径,例如:

回文半径

Manacher算法其实是一种动态规划算法,核心在于如何利用已经计算出的 p p p值去推导没有计算出的 p p p值。

接下来,我们要维护一段区间,变量 l , r l,r l,r记录了区间的左右边界。其区间的含义为,当前计算出的所有回文子串 [ l , r ] [l,r] [l,r]中最大的 r r r值所代表的区间。

当我们计算新的 p p p值的时候,我们有两种情况:

  1. 当前下标 i i i在区间 [ l , r ] [l,r] [l,r]中。
  2. 当前下标 i i i不在区间 [ l , r ] [l,r] [l,r]中。

当前下标 i i i在区间 [ l , r ] [l,r] [l,r]

如果当前下标 i i i在区间 [ l , r ] [l,r] [l,r]中,那么我们可以利用回文子串的对称性,去利用 p [ r − i + l ] p[r-i+l] p[ri+l]的值,也就是 i i i的对称位置的值。

p [ r − i + l ] p[r-i+l] p[ri+l]的值也有两种情况,第一,半径没有超过区间:

情况1

由图可知,此时 p [ i ] = p [ r − i + l ] p[i]=p[r-i+l] p[i]=p[ri+l]

第二种情况,对称位置的回文半径超过了区间:

超过区间

由对称性可知, p [ i ] p[i] p[i]的值此时不等于 p [ r − i + l ] p[r-i+l] p[ri+l] p [ i ] p[i] p[i]的值应该是 r − i + 1 r - i + 1 ri+1

第三种情况,对称位置的回文半径正好等于区间左端点:

恰好等于

总结一下,当 i ≤ r i \leq r ir的时候, p [ i ] p[i] p[i]的一个下界为 p [ i ] = min ⁡ ( r − i + 1 , p [ r − i + l ] ) p[i]=\min(r-i+1,p[r-i+l]) p[i]=min(ri+1,p[ri+l])

当前下标不在 i i i在区间 [ l , r ] [l,r] [l,r]

如果当前下标不在 i i i在区间 [ l , r ] [l,r] [l,r]中,对称性是未知的,此时 p [ i ] p[i] p[i]的下限为 1 1 1,我们只能使用朴素回文算法,慢慢扩展回文半径。

扩展半径,记录回文子串信息

此时我们找到回文半径的下限之后,我们就需要扩展半径,扩展半径按照朴素回文算法去扩展半径,直到不能再扩展为止,此时的 p [ i ] p[i] p[i]的值就是正确的。

算出 p [ i ] p[i] p[i]之后,我们就可以记录回文子串的信息了。

更新区间 [ l , r ] [l,r] [l,r]

别忘了,如果当前 p [ i ] + i − 1 > r p[i] + i - 1 \gt r p[i]+i1>r,那么就需要更新区间 l = p [ i ] − i + 1 , r = p [ i ] + i − 1 l = p[i] - i + 1,r = p[i] + i - 1 l=p[i]i+1,r=p[i]+i1。保证循环不变式的正确。

偶数情况

为了处理偶数回文的情况,我们有一下两种做法:

插入无关字符

在文本串的开头和末尾,以及每个字符的间隙中插入相同的无关字符,可以让偶回文变成奇回文。此时在按照上述过程运行Manacher算法即可,例如:

T = a b b a , T ′ = # a # b # b # a # T=abba,T'=\#a\#b\#b\#a\# T=abba,T=#a#b#b#a#

此时去处理 T ′ T' T即可。

硬处理偶回文

我们模仿奇回文的Manacher算法,写出偶回文的Manacher算法。

为了计算奇回文半径数组 d 1 [ i ] d_{1}[i] d1[i],我们有:

vector<int> d1(n);
for (int i = 0, l = 0, r = -1; i < n; i++) {
  int k = (i > r) ? 1 : min(d1[l + r - i], r - i + 1);
  while (0 <= i - k && i + k < n && s[i - k] == s[i + k]) {
    k++;
  }
  d1[i] = k;
  if (i + k > r) {
    l = i - k + 1;
    r = i + k - 1;
  }
}

注意,如果使用此代码去处理插入无关字符的方法,那么原来的回文长度为 d 1 [ i ] − 1 d_{1}[i] - 1 d1[i]1

为了计算偶回文半径数组 d 2 [ i ] d_{2}[i] d2[i],我们改写一下代码即可:

注意,在这种情况下,每个回文子串都存在最小回文中心 p [ i ] p[i] p[i] p [ i − 1 ] p[i-1] p[i1]

vector<int> d2(n);
for (int i = 0, l = 0, r = -1; i < n; i++) {
  int k = (i > r) ? 0 : min(d2[l + r - i], r - i + 1);
  while (0 <= i - k - 1 && i + k < n && s[i - k - 1] == s[i + k]) {
    k++;
  }
  d2[i] = k;
  if (i + k > r) {
    l = i - k;
    r = i + k - 1;
  }
}

模板

P3805

#include <bits/stdc++.h>

using namespace std;

#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)

typedef long long ll;

int dp[22000005];

int main()
{
    string s;
    cin >> s;
    string text;
    text.push_back('#');
    for (int i = 0; i < s.size(); i++)
    {
        text.push_back(s[i]);
        text.push_back('#');
    }
    int l = 0;
    int r = -1;
    int ans = 1;
    for (int i = 0; i < text.size(); i++)
    {
        int f = 1;
        if (i <= r)
            f = min(dp[r - i + l], r - i + 1);

        while (i + f < text.size() && i - f >= 0 && text[i + f] == text[i - f])
            f++;

        if (i + f - 1 > r)
        {
            l = i - f + 1;
            r = i + f - 1;
        }

        dp[i] = f;
        ans = max(ans, f - 1);
    }

    cout << ans;
    return 0;
}

##例题

不重叠,两个回文,枚举分割点:

LeetCode 5220

class Solution
{
public:
    long long maxProduct(string s)
    {
        vector<int> pr(s.size());

        vector<int> psum(s.size());
        vector<int> ssum(s.size());
        int l = -1, r = -1;
        for (int i = 0; i < s.size(); i++)
        {
            int k = i <= r ? min(r - i + 1, pr[l + r - i]) : 1;

            while (i + k < s.size() && i - k >= 0 && s[i + k] == s[i - k])
                k++;

            pr[i] = k;

            if (i + k - 1 > r)
            {
                r = i + k - 1;
                l = i - k + 1;
            }
        }
        int c = 0;
        for (int i = 0; i < s.size(); i++)
        {
            while (c + pr[c] - 1 < i)
                c++;
            psum[i] = max(2 * (i - c) + 1, i == 0 ? 0 : psum[i - 1]);
        }

        c = s.size() - 1;
        for (int i = s.size() - 1; i >= 0; i--)
        {
            while (c - pr[c] + 1 > i)
                c--;
            ssum[i] = max(2 * (c - i) + 1, i == s.size() - 1 ? 0 : ssum[i + 1]);
        }

        long long ans = 0;
        for (int i = 0; i < s.size() - 1; i++)
        {
            ans = max(ans, ((long long)psum[i]) * ssum[i + 1]);
        }

        return ans;
    }
};


P4555

#include <bits/stdc++.h>
#define FR freopen("in.txt", "r", stdin)
#define FW freopen("out.txt", "w", stdout)

using namespace std;

typedef long long ll;

int pr[500005];
int dp[500005];

int main()
{
    string prestr;
    cin >> prestr;
    string str;
    for (int i = 0; i < prestr.size(); i++)
    {
        str.push_back('#');
        str.push_back(prestr[i]);
    }
    str.push_back('#');
    // manarcher
    int l = 0, r = -1;
    for (int i = 0; i < str.size(); i++)
    {
        int k = i > r ? 1 : min(r - i + 1, pr[l + r - i]);

        while (i + k < str.size() && i - k >= 0 && str[i + k] == str[i - k])
            k++;

        pr[i] = k;
        if (i + k - 1 > r)
        {
            r = i + k - 1;
            l = i - k + 1;
        }
    }
    int c = 0;
    for (int i = 0; i < str.size(); i++)
    {
        while (i > c + pr[c] - 1)
        {
            c++;
        }

        dp[i] = (i - c) * 2 + 1;
    }
    int ans = 0;
    for (int i = 0; i < str.size() - 1; i++)
    {
        // ans = max(ans, (pr[i] - 1) * 2 + 1 + (i - pr[i] - 1 >= 0 ? dp[i - pr[i] - 1] : 0));
        ans = max(ans, (pr[i] - 1) + (i - pr[i] >= 0 ? (dp[i - pr[i]] + 1) / 2 : INT_MIN));
    }
    cout << ans;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值