Manacher/马拉车算法

原理

计算每个子串是是否回文,看起来是个 O ( n 2 ) O(n^2) O(n2)的问题,比较朴素的做法是区间dp或者中心拓展法。

但是如果换一个表达形式,计算以每个位置为回文中心的最大回文半径 p i p_i pi,就有希望将复杂度降低到 O ( n ) O(n) O(n),只要计算出这个 O ( n ) O(n) O(n)个元素的 p p p数组即可,而这可以借助马拉车做到

马拉车类似 z z z函数,建议先和 z z z函数放一起学习,对于每个位置 i i i,回文半径 p i p_i pi,则表示 [ i − p i , i + p i ] [i-p_i,i+p_i] [i−pi,i+pi]这区间是 i i i为中心的最大回文串,我们把这个区间当成类似 z z z函数里的 z b o x zbox zbox,维护右端点最大的区间。

如果当前 i i i在区间内,则类似 z f u n c zfunc zfunc的计算,我们可以复用前面的计算结果。具体来说就是由于 i i i所在的这个区间是个回文,把 i i i关于区间中心轴对称过去 2 c − i 2c-i 2c−i,那边的字符和这边是一个镜像关系。

2 c − i 2c-i 2c−i这个位置的回文半径,在不超出 b o x box box的范围内,和 i i i是相同的。超出部分,再暴力匹配,可以看到这个思路和 z f u n c zfunc zfunc是完全一样的,只是 z z z函数利用的是后缀和整个串的 l c p lcp lcp是一段相同串,这里利用的是回文串对称位置,是镜像关系。

复杂度分析也类似,只有超出 r r r才会暴力,并把 r r r更新到暴力的结尾,所以 r r r的移动次数等于暴力更新的次数,最多 O ( n ) O(n) O(n)

实现

实现上的重点是,如果想一次求出所有奇数长度和偶数长度的回文串,可以在原串的每个字符两侧都插入一个填充字符,这样以原始字符为回文中心的结果,对应奇回文,以填充为中心的结果,对应偶回文,只是这样最后需要做一些下标转换,才能得到原始串上的回文串。

具体转换就是最后 r e t u r n return return那里,最长回文串的回文中心 m x i mx_i mxi,最大回文半径 m x mx mx,可以计算出这个回文串在原始串中的起始位置 ( m x i − m x ) / 2 (mx_i-mx)/2 (mxi−mx)/2,并且对应地回文串长度就是 m x mx mx

c 复制代码
string manacher(const string& s) {
    string t = "^#";
    for (char c : s) {
        t += c;
        t += "#";
    }
    t += '$';

    int n = t.size();
    vector<int> p(n);
    int r = 0, c = 0;
    int mx = 0, mx_i = 0;

    for (int i = 1; i < n - 1; i++) {
        if (i < r) {
            p[i] = min(r - i, p[2 * c - i]);
        }

        while (t[i + p[i] + 1] == t[i - p[i] - 1]) {
            p[i]++;
        }

        if (i + p[i] > r) {
            c = i;
            r = i + p[i];
        }
        if (p[i] > mx) {
            mx = p[i];
            mx_i = i;
        }
    }
    return s.substr((mx_i - mx) / 2, mx);
}

例题

647. 回文子串

回文串个数,这就体现马拉车的优势了,对每个回文中心,只要知道回文半径 p i p_i pi,就意味着有 p i p_i pi个回文串,注意我这个板子的 p i p_i pi实际上是处理后的串的回文半径,原始串的回文半径需要除二,并且这里的半径不包含 [ i , i ] [i,i] [i,i]这个串,所以如果最大回文串长度是,回文串个数还需要加一,实现上就 ( p i + 1 ) / 2 (p_i+1)/2 (pi+1)/2

214. 最短回文串

在开头添加最少的字符,使得回文。

这题前面做过,kmp做的。既然回文,马拉车是更简单的做法。实际上就是找最长回文前缀,枚举每个回文中心,检查他的最大回文串是不是一个前缀,也就检查最大回文串的左端点是不是 0 0 0,这里的转换就用前面提到的公式, l i = ( i − p i ) / 2 , l_i=(i-p_i)/2, li=(i−pi)/2,如果是,更新答案。

3327. 判断 DFS 字符串是否是回文串

dfs序+马拉车

询问树上每个子树,按后序遍历构造的字符串,是不是回文。

由于后续也是一种dfs序,后序遍历生成的字符串,一个子树对应的串是在一个连续区间内的,只不过由于后续,子树的根是最后访问的,所以对应区间不再是先序的 [ d f n i , d f n i + s z i − 1 ] [dfn_i,dfn_i+sz_i-1] [dfni,dfni+szi−1],而是 [ d f n i − s z i + 1 , d f n i ] [dfn_i-sz_i+1,dfn_i] [dfni−szi+1,dfni]。

然后我们再后序遍历生成的串上跑马拉车,即可 O ( 1 ) O(1) O(1)检查每个区间是否回文。检查时由于我们只知道左右端点,还需要转化成回文中心,并且计算在处理后串(有填充字符)中的下标。

c 复制代码
class Solution {
public:
    vector<bool> findAnswer(vector<int>& parent, string s) {
        int n = s.size();
        vector<bool> ans(n);

        vector<vector<int>> g(n + 1);
        vector<int> dfn(n + 1), sz(n + 1, 1);
        int cnt = 0;

        for (int i = 1; i < n; i++) {
            g[parent[i]].push_back(i);
        }

        for (int i = 0; i < n; i++) {
            ranges::sort(g[i]);
        }
        string ss;
        ss.reserve(n);
        auto&& dfs = [&](auto&& dfs, int u) -> void {
            for (int v : g[u]) {
                dfs(dfs, v);
                sz[u] += sz[v];
            }
            ss.push_back(s[u]);
            dfn[u] = ++cnt;
        };
        dfs(dfs, 0);

        // cout << ss << '\n';
        string t = "^#";
        for (char c : ss) {
            t += c;
            t += "#";
        }
        t += '$';

        int m = t.size();
        vector<int> p(m);
        int r = 0, c = 0;

        for (int i = 1; i < m - 1; i++) {
            if (i < r) {
                p[i] = min(r - i, p[2 * c - i]);
            }

            while (t[i + p[i] + 1] == t[i - p[i] - 1]) {
                p[i]++;
            }

            if (i + p[i] > r) {
                c = i;
                r = i + p[i];
            }
        }

        for (int i = 0; i < n; i++) {
            int r = dfn[i];
            int l = r - sz[i] + 1;
            int len = (r - l + 1);
            int mid;
            if (len % 2) {
                mid = (l + len / 2) * 2;
            } else {
                mid = (l + len / 2) * 2 - 1;
            }
            // cout << l << ' ' << r << ' ' << mid << '\n';
            // cout << p[mid] << ' ' << len << '\n';
            ans[i] = (p[mid] >= len);
        }
        return ans;
    }
};

1745. 分割回文串 IV

回文+划分型dp

问能否划分成三个区间,每个都是回文,划分型dp即可

f ( i ) ∣ = f ( j − 1 ) & & g ( j , i ) f(i)\mid=f(j-1)\&\& g(j,i) f(i)∣=f(j−1)&&g(j,i)

马拉车预处理,然后查询一个区间是否回文 g ( j , i ) g(j,i) g(j,i),可以给马拉车增加一个这个接口,还挺常用的

c 复制代码
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

struct Manacher {
    std::vector<int> p;
    std::string t;

    Manacher(const std::string& s) {
        // 1. 预处理字符串
        t = "^#";
        for (char c : s) {
            t += c;
            t += "#";
        }
        t += '$';

        int n = t.size();
        p.resize(n, 0);
        int r = 0, c = 0;

        for (int i = 1; i < n - 1; i++) {
            if (i < r)
                p[i] = std::min(r - i, p[2 * c - i]);
            while (t[i + p[i] + 1] == t[i - p[i] - 1])
                p[i]++;
            if (i + p[i] > r) {
                c = i;
                r = i + p[i];
            }
        }
    }

    // 新增:询问 s[L...R] (闭区间) 是否为回文
    bool isPalindrome(int L, int R) const {
        if (L < 0 || R < 0 || L > R)
            return false;
        int center = L + R + 2; // 关键映射公式
        int len = R - L + 1;
        return p[center] >= len;
    }

    // 原有的功能:获取最长回文子串
    std::string getLongest(const std::string& s) const {
        auto it = std::max_element(p.begin(), p.end());
        int mx = *it;
        int mx_i = std::distance(p.begin(), it);
        return s.substr((mx_i - mx) / 2, mx);
    }
};
class Solution {
public:
    bool checkPartitioning(string s) {
        int n = s.size();
        s = ' ' + s;

        bool dp[n + 1][4];
        memset(dp, 0, sizeof dp);
        Manacher m(s);
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= 3; j++) {
                for (int k = 1; k <= i; k++) {
                    if (m.isPalindrome(k, i)) {
                        dp[i][j] |= dp[k - 1][j - 1];
                    }
                }
                // cout<<dp[i][j]<<' ';
            }
            // cout<<'\n';
        }
        return dp[n][3];
    }
};

1960. 两个回文子字符串长度的最大乘积

前后缀分解 回文

问两个不重叠的奇回文串,长度乘积的最大值?

就俩串,还不重叠,肯定可以找到一个分界点,一个串在左边,一个在右边,那么可以枚举这个分界点,也就是前后缀分解。这需要预处理前缀,后缀的最长的奇回文串。

难点在这个预处理,因为对于一个回文中心,他不是只有 a [ i + p i ] = 2 p i + 1 a[i+p_i]=2p_i+1 a[i+pi]=2pi+1这一个值的,实际上在半径内的每个位置,都有一个赋值,还都不同。这种情况下,想做前缀和,直接 a [ i + p i ] = 2 p i + 1 a[i+p_i]=2p_i+1 a[i+pi]=2pi+1然后扫一遍前缀,是不对的。

可以利用一个性质:如果 i i i是一个长度 l e n len len的回文串右端点,那么 i − 1 i-1 i−1一定是一个长度 l e n − 2 len-2 len−2的回文串的右端点,所以先做一次正常的扫描前缀,再扫描后缀,执行这里提到的这个更新,计算出的就是最优的前缀。

后缀同理,反过来就行

c 复制代码
class Solution {
public:
    long long maxProduct(string s) {
        int n = s.size();
        vector<int> p(n);

        int r = 0, c = 0;
        for (int i = 0; i < n; i++) {
            if (i < r) {
                p[i] = min(r - i, p[2 * c - i]);
            }
            while (i + p[i] + 1 < n && i - p[i] - 1 >= 0 && s[i + p[i] + 1] == s[i - p[i] - 1]) {
                p[i]++;
            }

            if (i + p[i] > r) {
                r = i + p[i];
                c = i;
            }
        }

        vector<int> pre(n), suf(n);
        for (int i = 0; i < n; i++) {
            int l = i - p[i] , r = i + p[i] ;
            pre[r] = max(pre[r], 2 * p[i] +1);
            suf[l] = max(suf[l], 2 * p[i] +1);
        }

        for (int i = 1; i < n; i++) {
            pre[i] = max(pre[i], pre[i - 1]);
        }
        for (int i = n - 2; i >= 0; i--) {
            pre[i] = max(pre[i], pre[i + 1] - 2);
        }

        for (int i = n - 2; i >= 0; i--) {
            suf[i] = max(suf[i], suf[i + 1]);
        }

        for (int i = 1; i < n; i++) {
            suf[i] = max(suf[i], suf[i - 1] - 2);
        }

        long long ans = 0;
        for (int i = 0; i < n - 1; i++) {
            ans = max(ans, 1ll * pre[i] * suf[i + 1]);
        }

        return ans;
    }
};
相关推荐
phoenix@Capricornus1 小时前
初等数学中点到直线的距离
人工智能·算法·机器学习
田里的水稻2 小时前
FA_规划和控制(PC)-快速探索随机树(RRT)
人工智能·算法·数学建模·机器人·自动驾驶
jaysee-sjc2 小时前
十三、Java入门进阶:异常、泛型、集合与 Stream 流
java·开发语言·算法
元亓亓亓2 小时前
LeetCode热题100--41. 缺失的第一个正数--困难
数据结构·算法·leetcode
码云数智-大飞2 小时前
.NET 10 & C# 14 新特性详解:扩展成员 (Extension Members) 全面指南
java·数据库·算法
weixin_477271692 小时前
狗象:与强大的一方建立联系,并控制调用对方的力量。)马王堆帛书《周易》原文及甲骨文还原周朝生活现象《函谷门
算法·图搜索算法
小妖6662 小时前
js 实现归并排序算法
算法·排序算法
fu的博客3 小时前
【数据结构7】链式栈实现
数据结构·算法
xiaoye-duck3 小时前
《算法题讲解指南:优选算法-双指针》--01移动零,02复写零
c++·算法