中心拓展法解决回文问题

原理

一般回文有这么几个解决思路:马拉车,区间dp,中心拓展。

前者是最强大的, O ( n ) O(n) O(n),后两者都是 O ( n 2 ) O(n^2) O(n2)的,但中心拓展比区间dp常数小(只用o1空间,访存更快),更好写,并且很多时候,中心拓展是一个很重要思想(后面会举例),所以还是很有学习价值。

大致思想就是枚举回文中心,然后往两侧拓展,直到遇到不相等的,就停止,这样也能计算出每个回文中心的最大回文半径,这类似马拉车。

实现

基于回文中心的写法也会遇到奇偶回文串的问题,想要一次循环解决这两类回文串,可以这样写。

这样可以交替枚举起始位置 l = r , l + 1 = r l=r,l+1=r l=r,l+1=r的情况,也就是枚举奇偶回文串。

c 复制代码
class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        int L = 0, R = 0;

        for (int i = 0; i < 2 * n - 1; i++) {
            int l = i / 2, r = (i + 1) / 2;
            while (l >= 0 && r < n && s[l] == s[r]) {
                l--;
                r++;
            }
            if (r - l - 1 > R - L + 1) {
                L = l + 1, R = r - 1;
            }
        }

        return s.substr(L, R - L + 1);
    }
};

例题

2472. 不重叠回文子字符串的最大数目

划分型dp

每段长度不小于k,都是回文,问最大划分段数。

可以区间dp或者中心拓展预处理每个区间的回文情况。

但这样写,仍然需要 O ( n 2 ) O(n^2) O(n2)空间,没有发挥中心拓展的 O ( 1 ) O(1) O(1)空间优势,被拉到和区间dp一个水平线了

c 复制代码
class Solution {
public:
    int maxPalindromes(string s, int k) {
        int n = s.size();
        vector<vector<int>> f(n + 1, vector<int>(n + 1));

        for (int i = 0; i < 2 * n - 1; i++) {
            int l = i / 2, r = (i + 1) / 2;
            while (l >= 0 && r < n && s[l] == s[r]) {
                f[l + 1][r + 1] = 1;
                l--;
                r++;
            }
        }
        vector<int> g(n + 1);
        for (int i = 1; i <= n; i++) {
            g[i] = g[i - 1];
            for (int j = 1; i - j + 1 >= k; j++) {
                if (f[j][i]) {
                    g[i] = max(g[j - 1] + 1, g[i]);
                }
            }
        }
        return g[n];
    }
};

所以更好的写法是,在中心拓展的过程中,每次拓展一步,意味着找到一个回文,就转移一次,这样空间 O ( 1 ) O(1) O(1)了,一般来讲这种 O ( 1 ) O(1) O(1)空间相比于 O ( n 2 ) O(n^2) O(n2)空间,即使时间复杂度相同,常数也会差十倍左右,这就是访存开销带来的,所以优化内存访问也是很重要的一环。

然后这里的转移就是,每新增一个点,可以不选它,那么答案直接继承上一轮的。这是查表,后面中心拓展里的转移时刷表。这里查表刷表混用,但由于我们是从左到右枚举回文中心的,不会有问题。

最后还有个小优化,如果中心拓展长度不小于k了,这个回文中心的转移就可以结束了,因为后面即使有更长的回文串,对答案的贡献同样是1,并且占用的区间更大,不会更优。加上这个优化常数又变小十倍。

c 复制代码
class Solution {
public:
    int maxPalindromes(string s, int k) {
        int n = s.size();
        vector<int> f(n + 1);
        for (int i = 0; i < 2 * n - 1; i++) {
            int l = i / 2, r = (i + 1) / 2;
            f[l + 1] = max(f[l + 1], f[l]);
            while (l >= 0 && r < n && s[l] == s[r]) {
                if (r - l + 1 >= k) {
                    f[r + 1] = max(f[r + 1], f[l] + 1);
                    break;
                }
                l--;
                r++;
            }
        }

        return f[n];
    }
};

附运行时间截图

3615. 图中的最长回文路径

在图上找一个最长路径,形成的字符串是个回文串。

图不大,记搜可过。比较朴素的方法是从一个点出发,每次把一个点加入路径,但这样的问题是,想判断回文需要到最后才能判断,也就是我们要把目前的串作为参数传递下去,而串的个数是不同路径条数, O ( n ! ) O(n!) O(n!)的,无法记忆化。

这就是前面说的,有时候中心拓展法是一个重要的思想。考虑回文串的构造过程,处理每次在末尾添加一个字符,然后保存已构成的字符串,判断是否回文。

另一种方式是中心拓展的思想,每次在两侧加两个相同字符,这样是一个最优子结构,我们无需关心回文串内部长什么样子,只要知道两端是哪两个点,就能拓展。虽然由于是简单路径,每个点只能用一次,还需要一个mask记录哪些点用了,但这样的状态复杂度只有 O ( n 2 2 n ) O(n^22^n) O(n22n),相比原来的 O ( n ! ) O(n!) O(n!)已经是很大进步了。

搜索内部,枚举当前回文串两个端点的邻居对,要求点不重复,点上字符相同,这是一个 O ( n 2 ) O(n^2) O(n2)的枚举,整体复杂度 O ( n 4 2 n ) O(n^42^n) O(n42n), n = 14 n=14 n=14,计算量约为 6 e 8 6e8 6e8,可过。

由于lc的整体+部分计时法,还需要卡常一下。

  • 完全图可以特判掉,完全图所有路径都是可得到的,转化成贪心问题。
  • 可以假设这个图是个完全图,算出来答案上界,某次循环后达到上界直接返回
  • 枚举点对时保证 x < y x<y x<y,减少一半的枚举常数。
c 复制代码
class Solution {
public:
    int maxLen(int n, vector<vector<int>>& e, string s) {
        int cnt[26]{};
        for (char c : s) {
            cnt[c - 'a']++;
        }
        int odd = 0;
        for (int c : cnt) {
            odd += c % 2;
        }
        int mx = n - max(odd - 1, 0);
        if (e.size() == n * (n - 1) / 2) {
            return mx;
        }
        vector<vector<int>> g(n);

        for (auto& t : e) {
            int u = t[0], v = t[1];
            g[u].push_back(v);
            g[v].push_back(u);
        }

        vector<int> f(n * n * (1 << n), -1);
        auto&& dfs = [&](this auto self, int u, int v, int mask) -> int {
            int cur = u + v * n + mask * n * n;
            if (f[cur] != -1)
                return f[cur];
            int res = 0;
            for (int x : g[u]) {
                if (mask >> x & 1)
                    continue;
                for (int y : g[v]) {
                    if (mask >> y & 1)
                        continue;
                    if (x == y || s[x] != s[y])
                        continue;
                    res = max(res, self(x, y, mask | (1 << x) | (1 << y)) + 2);
                }
            }
            return f[cur] = res;
        };

        int ans = 0;
        for (int i = 0; i < n; i++) {
            ans = max(ans, dfs(i, i, 1 << i) + 1);
            if (ans == mx)
                return mx;
        }
        for (int i = 0; i < n; i++) {
            for (int j : g[i]) {
                if (i <= j && s[i] == s[j]) {
                    ans = max(ans, dfs(i, j, (1 << i) | (1 << j)) + 2);
                    if (ans == mx)
                        return ans;
                }
            }
        }

        return ans;
    }
};
相关推荐
1 小时前
2.19列阵,私聊调配,求小数位数个数
算法
weixin_477271692 小时前
马王堆帛书《周易》六十四貞如何读象(《函谷门》原创)
算法·图搜索算法
追随者永远是胜利者10 小时前
(LeetCode-Hot100)53. 最大子数组和
java·算法·leetcode·职场和发展·go
生成论实验室10 小时前
即事经:一种基于生成论的宇宙、生命与文明新范式
人工智能·科技·神经网络·算法·信息与通信
王老师青少年编程11 小时前
csp信奥赛c++高频考点假期集训(分模块进阶)
数据结构·c++·算法·csp·高频考点·信奥赛·集训
癫狂的兔子12 小时前
【Python】【机器学习】K-MEANS算法
算法·机器学习·kmeans
Bear on Toilet13 小时前
递归_二叉树_50 . 从前序与中序遍历序列构造二叉树
数据结构·算法·leetcode·深度优先·递归
plus4s13 小时前
2月18日(82-84题)
c++·算法·动态规划
艾醒14 小时前
打破信息差——2026年2月19日AI热点新闻速览
算法