回文子序列问题解题模板

回文子序列问题解题模板及本质


回文子序列问题的本质:

  • 回文子序列是一个动态规划相关的经典问题,其目标是寻找一个序列中的最长回文子序列。
  • 回文子序列的本质特点:
    • 回文的定义:从左到右读与从右到左读一样。
    • 子序列的定义:不用连续,但保持相对位置。
  • 核心思想:
    • 通过状态转移和子问题划分解决问题。
    • 回文的性质通常反映在递归和动态规划的左右扩展关系中。

经典问题及解题模板

1. Leetcode 516. 最长回文子序列

问题描述:

给定一个字符串 s,寻找其中最长的回文子序列的长度。

解法特点:

  • 使用动态规划解决问题,通过判断两端字符是否相等来决定状态转移。
  • 经典的二维动态规划问题。

解题模板:动态规划
核心步骤:
  1. 定义状态:
    • 定义 dp[i][j] 表示字符串 s[i...j] 的最长回文子序列长度。
    • i 表示起始位置,j 表示结束位置。
  2. 状态转移方程:
    • 如果 s[i] == s[j]

      dp\[i\]\[j\] = dp\[i+1\]\[j-1\] + 2

      两端字符可以构成回文的一部分。
    • 如果 s[i] != s[j]

      dp\[i\]\[j\] = \\max(dp\[i+1\]\[j\], dp\[i\]\[j-1\])

      两端字符无法同时参与回文,则选择舍弃一侧。
  3. 初始化:
    • 对于单字符回文:dp[i][i] = 1
    • 对于空区间:dp[i][j] = 0j < i)。
  4. 结果:
    • 答案是 dp[0][n-1],即从字符串的起点到终点的最长回文子序列长度。

代码模板:动态规划
java 复制代码
class Solution {
    public int longestPalindromeSubseq(String s) {
        int n = s.length();
        // 初始化 DP 数组
        int[][] dp = new int[n][n];
        
        // 初始化单字符的情况
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1; // 每个单字符自身构成回文
        }
        
        // 从短区间向长区间递推
        for (int len = 2; len <= n; len++) { // 子区间长度
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1; // 计算右边界
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2; // 两端相等
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]); // 两端不相等
                }
            }
        }
        
        return dp[0][n - 1]; // 全区间最长回文长度
    }
}

复杂度分析

  1. 时间复杂度:

    • 外层子区间循环需要进行 (O(n^2)) 计算。
    • 每次计算时,只需根据公式访问相关状态。
    • 总时间复杂度为 (O(n^2))。
  2. 空间复杂度:

    • 二维 DP 数组占用 (O(n^2))。

本问题扩展:逻辑与技巧

2. Leetcode 1312. 让字符串成为回文的最少插入次数

问题描述:

给定一个字符串 s,返回将其转化为回文字符串的最少插入次数。

本质和解法:

  • 本质:求一个字符串的"最少插入次数",实际上可以转化为寻找"最长回文子序列"的补集长度。
  • 公式关系:
    最少插入次数 = 字符串长度 - 最长回文子序列的长度。

代码模板:动态规划
java 复制代码
class Solution {
    public int minInsertions(String s) {
        int n = s.length();
        // 这里复用最长回文子序列的 DP 定义
        int[][] dp = new int[n][n];
        
        // 初始化单字符情况
        for (int i = 0; i < n; i++) {
            dp[i][i] = 1;
        }
        
        // 从短区间向长区间递推
        for (int len = 2; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1;
                if (s.charAt(i) == s.charAt(j)) {
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                } else {
                    dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
                }
            }
        }
        
        return n - dp[0][n - 1]; // 补集长度
    }
}

3. Leetcode 647. 回文子串

问题描述:

给定一个字符串 s,计算该字符串中有多少个回文子串


代码模板:动态规划解决回文子串问题
  • 核心思路:
    通过动态规划记录每个子区间是否是回文。
java 复制代码
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        boolean[][] dp = new boolean[n][n]; // 定义 DP 表:dp[i][j] 表示 s[i...j] 是否是回文
        int count = 0; // 记录回文子串的数量
        
        // 遍历子区间长度
        for (int len = 1; len <= n; len++) {
            for (int i = 0; i <= n - len; i++) {
                int j = i + len - 1; // 计算右边界
                
                if (len == 1) { // 单字符回文
                    dp[i][j] = true;
                } else if (len == 2) { // 两个字符,仅限相等情况
                    dp[i][j] = s.charAt(i) == s.charAt(j);
                } else { // 多字符的情况
                    dp[i][j] = s.charAt(i) == s.charAt(j) && dp[i + 1][j - 1];
                }
                
                if (dp[i][j]) {
                    count++; // 如果是回文子串,计数加一
                }
            }
        }
        
        return count; // 返回总数
    }
}

如何快速写出 AC 代码

  1. 明确子问题的递归关系:右边界和子问题依赖;
  2. 初始化边界条件,例如单字符、空区间;
  3. 状态转移的方向是从短区间到长区间,逐步递推;
  4. 边界处理清晰,提升鲁棒性。

典型例题总结

  1. 最长回文子序列问题: Leetcode 516
  2. 最少插入次数: Leetcode 1312
  3. 回文子串数量: Leetcode 647
  4. 问题转化扩展: 通过 DP 模板处理多种回文字符串问题,灵活应用递归关系。

通过动态规划模板可以快速实现并解决回文相关问题,适配多种场景,非常适合面试。

相关推荐
董董灿是个攻城狮1 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
AI软著研究员8 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish9 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱10 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx1 天前
CART决策树基本原理
算法·机器学习
Wect1 天前
LeetCode 210. 课程表 II 题解:Kahn算法+DFS 双解法精讲
前端·算法·typescript