回文子串与子序列dp全解析

目录

简介

回文子串

最长回文子串

分隔回文串

分隔回文串Ⅱ

最长回文子序列

总结


简介

回文子串是算法刷题的高频考点,核心特征是:字符串中连续的子串,正读和反读完全相同(如 "aba"、"aa"、"a")。与学的 “子序列(非连续)” 不同,回文子串强调「连续」,解题思路也更聚焦于 “中心扩展”“动态规划” 等经典方法。

概念定义例子(s = "abcba")关键区别
回文子串连续、正读 = 反读"a"(5 个)、"b"(2 个)、"c"、"bcb"、"abcba" → 共 7 个连续
回文子序列非连续、正读 = 反读"aba"、"aca"、"abcba" 等 → 远多于子串非连续

这就有点像子数组和子序列

核心思想

定义状态 dp[i][j]:表示字符串 s[i...j](从 i 到 j 的连续子串)是否为回文子串。

  • 边界条件:
    • i == j(单个字符):dp[i][j] = true(所有单个字符都是回文);
    • j - i == 1(两个字符):dp[i][j] = (s[i] == s[j])
  • 转移方程:当 j - i > 1 时,dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1](两端字符相等,且中间子串也是回文)。

注意:相比于动态规划,更优的算法是中心扩展算法和马拉车算法

马拉车只能解决回文子串、对于难题来说动态规划更好,动态规划能够将回文信息全部存入表中

关于面试,可以去了解一下中心扩展和马拉车,有时候面试官会要求优化

回文子串

647. 回文子串 - 力扣(LeetCode)

比如这道题,我们是否可以枚举每个子串

当然,以i位置为起始,j位置为结束

则dp[i][j]:表示以i位置起始,j位置为结束的子串是否为回文子串

也就是我们需要填写一个二维dp表,并且dp表只要填写对角线和上三角就行

为什么说动态规划更适合解难题,因为你把回文子串的信息都保留在dp表当中

那这道题让你找到回文子串的个数,只需要返回表中为true的个数就行

初始化:无需初始化,因为不会越界

填表顺序:从下往上填写

返回值:true的个数

class Solution {
public:
    int countSubstrings(string s) {
        int m = s.size();
        vector<vector<bool>> dp(m, vector<bool>(m, false));
        int ret = 0;
        for (int i = m - 1; i >= 0; i--) {
            for (int j = i; j < m; j++) {
                if (s[i] == s[j]) {
                    if (i == j || i + 1 == j) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                } else {
                    dp[i][j] = false;
                }
                if (dp[i][j]) {
                    ret++;
                }
            }
        }
        return ret;
    }
};

最长回文子串

5. 最长回文子串 - 力扣(LeetCode)

因为有了第一题的铺垫,所以我们仅仅需要在dp表当中找到为true且为最长的即可

采用ret去记录长度,然后最长的时候记录begin,采用substr找到子串

s.substr(begin,ret);

class Solution {
public:
    string longestPalindrome(string s) {
        int m = s.size();
        vector<vector<bool>> dp(m, vector<bool>(m, false));
        int ret = 0;
        int begin;
        for (int i = m - 1; i >= 0; i--) {
            for (int j = i; j < m; j++) {
                if (s[i] == s[j]) {
                    if (i == j || i + 1 == j) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                } else {
                    dp[i][j] = false;
                }
                if (dp[i][j]) {
                    if (ret < j - i + 1) {
                        ret = j - i + 1;
                        begin = i;
                    }
                }
            }
        }
        return s.substr(begin, ret);
    }
};

分隔回文串

1745. 分割回文串 IV - 力扣(LeetCode)

所以说为什么要学动态规划,因为这些题采用动态规划,可以把所有的回文信息填入dp表中

那你所有的题都是去dp表中寻找信息

那这道题就是分隔成三个回文串

在dp表当中判断这三个子串同时为回文串才行

枚举的时候只要想中间子串的起始和结束即可,也就能快速判断出i和j的范围

那中间子串前面肯定要有字符,则i起始肯定为1,j起始为i

那后面也要预留字符,则为m-2

class Solution {
public:
    bool checkPartitioning(string s) {
        int m = s.size();
        vector<vector<bool>> dp(m, vector<bool>(m, false));
        int ret = false;
        //填写dp表
        for (int i = m - 1; i >= 0; i--) {
            for (int j = i; j < m; j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = i + 1 < j ? dp[i+1][j-1] : true;
                }
            }
        }
        //寻找结果[0,i-1][i,j][j+1,m-1]
        for(int i=1;i<m-1;i++){
            for(int j=i;j<m-1;j++){
                if(dp[0][i-1]&&dp[i][j]&&dp[j+1][m-1]){
                    ret=true;
                }
            }
        }
        return ret;
    }
};

分隔回文串Ⅱ

132. 分割回文串 II - 力扣(LeetCode)

本题也是思考如何在原来的dp表信息当中寻找最优解

有点感觉在dp表的基础上还要一个动态规划

可以回看单词拆分,思想一样的

这个回文串优化点就会我们提前把回文信息存储到dp表当中

单词拆分存储到哈希表当中

class Solution {
public:
    int minCut(string s) {
        int m = s.size();
        vector<vector<bool>> dp(m, vector<bool>(m, false));
        // 填写dp表
        for (int i = m - 1; i >= 0; i--) {
            for (int j = i; j < m; j++) {
                if (s[i] == s[j]) {
                    dp[i][j] = i + 1 < j ? dp[i + 1][j - 1] : true;
                }
            }
        }
        // 寻找最少分隔次数
        vector<int> dp1(m, 0x3f3f3f3f);
        for (int i = 0; i < m; i++) {
            if (dp[0][i]) {
                // 如果是回文串
                dp1[i] = 0;
            } else {
                // 如果不是则往前遍历
                for (int j = 1; j <= i; j++) {
                    if (dp[j][i]) {
                        dp1[i] = min(dp1[i], dp1[j - 1] + 1);
                    }
                }
            }
        }
        return dp1[m - 1];
    }
};

最长回文子序列

516. 最长回文子序列 - 力扣(LeetCode)

以i位置为结尾推导的时候压根推不出来,因为你无法根据前面的状态推出dp[i]的状态

因为你不知道前面的子序列长啥样,你没有信息

那应该思考一下换别的状态,跟第一题一样,以区间[i,j]为状态,此时可以推出状态转移方程

核心就是:新加的字符s[i]和s[j]的关系

填表顺序:从下往上,从左往右

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        vector<vector<int>> dp(n, vector<int>(n));
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (s[i] == s[j]) {
                    if (i == j)
                        dp[i][j] = 1;
                    else if (i + 1 == j)
                        dp[i][j] = 2;
                    else
                        dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[0][n - 1];
    }
};

关于状态表示,做了这么多道题,发现以区间[i,j]分析基本都是对的,尝试一下

初始化,因为i+1==j和i+1<j可以合并填写(画图发现,i+1==j用到的dp[i+1][j-1]为0),初始化的时候全部初始为0即可

然后会用到左边和下边的值,那填表的时候从左往右,从下往上即可

返回值:dp[0,m-1]

class Solution {
public:
    int minInsertions(string s) {
        int m = s.size();
        vector<vector<int>> dp(m, vector<int>(m));
        for (int i = m - 1; i >= 0; i--) {
            for (int j = i; j < m; j++) {
                // 从下往上,从左往右
                if (s[i] == s[j]) {
                    if (i + 1 == j || i + 1 < j) {
                        dp[i][j]=dp[i+1][j-1];
                    }
                }
                else{
                    //不相等时取小
                    dp[i][j]=min(dp[i+1][j],dp[i][j-1])+1;
                }
            }
        }
        return dp[0][m-1];
    }
};

总结

回文类题型是字符串算法的核心考点,其中回文子串(连续)回文子序列(非连续) 是两类高频问题,解题思路既有共性(利用回文对称性),也有显著差异(连续 vs 非连续)。以下从「核心区别、通用解法、经典题型、避坑要点」四个维度做系统总结

维度回文子串回文子序列
连续性必须连续(如 s="abcba" 的 "bcb")无需连续(如 s="abcba" 的 "aca")
解题核心利用「中心对称性」或「区间 DP」仅能通过「区间 DP」(非连续无中心)
时间复杂度最优 O (n²)(中心扩展 / DP)最优 O (n²)(区间 DP)
空间复杂度中心扩展 O (1) / DP O (n²)区间 DP O (n²)(无法优化到 O (1))
核心特征数量少(O (n²) 级)数量多(O (2ⁿ) 级,需 DP 避免枚举)
  • 回文子串(连续)
    • 核心是「对称性」,最优解为中心扩展法(O (n²) 时间 + O (1) 空间);
    • 分割类问题需先预处理回文 DP 表,再用 DP 求分割数。
  • 回文子序列(非连续)
    • 无中心可扩展,仅能通过区间 DP 解决(O (n²) 时间 + O (n²) 空间);
    • 状态转移核心是「两端字符是否相等」,遍历顺序必须从短区间到长区间。
  • 通用逻辑
    • 所有回文类问题都依赖「区间 DP」或「中心扩展」,暴力枚举(O (n³))仅用于理解概念;
    • 预处理回文状态是进阶题的关键(如分割、计数),避免重复判断回文。

核心记住区间[i,j]的状态表示方式

dp表当中记录了回文子串的信息

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值