DAY45

51 阅读3分钟

第九章 动态规划part13

今天 我们就要结束动态规划章节了,大家激不激动!!!

详细布置

647. 回文子串

动态规划解决的经典题目,如果没接触过的话,别硬想 直接看题解。

programmercarl.com/0647.%E5%9B…

DP 数组 dp[i][j] 的含义

  • dp[i][j] 表示字符串 s 中从索引 ij 的子串 s[i...j] 是否是一个回文子串。
    • 如果 dp[i][j]true,则子串 s[i...j] 是回文;
    • 如果 dp[i][j]false,则子串 s[i...j] 不是回文。

递推公式

要判断 dp[i][j] 是否为 true,需要满足以下条件:

  1. 如果 s[i] === s[j] 且子串长度 <= 2(即 j - i <= 1

    • ij 相等时,意味着这是一个单字符子串,单字符必然是回文;
    • j - i == 1 时,表示子串长度为 2,只要两个字符相等,即是回文。 递推条件为: [ dp[i][j] = \text{true} \quad \text{if } s[i] === s[j] \text{ and } j - i <= 1 ]
  2. 如果 s[i] === s[j]dp[i + 1][j - 1]true

    • 说明 s[i + 1 ... j - 1] 子串已经是回文,并且 s[i] === s[j],因此整个 s[i ... j] 也是回文。 递推条件为: [ dp[i][j] = \text{true} \quad \text{if } s[i] === s[j] \text{ and } dp[i + 1][j - 1] = \text{true} ]

遍历顺序

  • 需要从右往左遍历索引 i,因为 dp[i][j] 依赖于 dp[i + 1][j - 1] 的结果,所以要确保在计算 dp[i][j] 时,dp[i + 1][j - 1] 已经被计算过。
/**
 * @param {string} s
 * @return {number}
 */
var countSubstrings = function (s) {
    let dp = Array.from(Array(s.length), () => Array(s.length).fill(false));
    let res = 0;
    
    for (let i = s.length - 1; i >= 0; i--) {
        for (let j = i; j < s.length; j++) {  // j 从 i 开始
            if (s[i] === s[j]) {
                if (j - i <= 1) {
                    dp[i][j] = true;
                    res++;
                } else if (dp[i + 1][j - 1]) {
                    dp[i][j] = true;
                    res++;
                }
            }
        }
    }
    
    console.log(dp);
    return res;
};

516.最长回文子序列

  1. 回文子串,求的是回文子串,而本题要求的是回文子序列, 大家要搞清楚两者之间的区别。

programmercarl.com/0516.%E6%9C…

DP 数组的定义

  • dp[i][j] 表示字符串 s 中从索引 ij 的子串的最长回文子序列的长度。

递推公式

  1. 如果 i === j,表示子串长度为 1,单个字符本身是回文,因此 dp[i][j] = 1
  2. 如果 s[i] === s[j],那么最长回文子序列长度可以通过 dp[i + 1][j - 1] + 2 得到,意思是去掉 ij 两端字符后,剩下子串的最长回文子序列长度加上这两个匹配字符的长度 2。
  3. 如果 s[i] !== s[j],则 dp[i][j] 等于 dp[i][j - 1]dp[i + 1][j] 的较大值,表示在这两个字符不相等的情况下,只能考虑去掉其中一个字符后的最长回文子序列长度。

遍历顺序

  • 外层循环从字符串的末尾向前遍历 i,内层循环从 i + 1 开始向后遍历 j,保证每次更新 dp[i][j] 时,依赖的 dp[i + 1][j - 1] 已经计算出来。

代码如下:

/**
 * @param {string} s
 * @return {number}
 */
var longestPalindromeSubseq = function (s) {
    let dp = Array.from(Array(s.length), () => Array(s.length).fill(0));
    
    // 每个单独的字符都是回文,初始化 dp[i][i] 为 1
    for (let i = 0; i < s.length; i++) {
        dp[i][i] = 1;
    }

    // 从后向前遍历字符串的每个字符 i
    for (let i = s.length - 1; i >= 0; i--) {
        // 遍历比 i 大的 j
        for (let j = i + 1; j < s.length; j++) {
            if (s[i] === s[j]) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i][j - 1], dp[i + 1][j]);
            }
        }
    }

    console.log(dp);
    return dp[0][s.length - 1];
};

示例

假设输入 s = "bbbab",输出将是 4,因为最长回文子序列为 "bbbb"

总结

  • dp[i][j] 记录子串 s[i...j] 的最长回文子序列长度。
  • s[i] === s[j] 时,子序列长度为 dp[i + 1][j - 1] + 2
  • s[i] !== s[j] 时,子序列长度为 dp[i][j - 1]dp[i + 1][j] 的最大值。

动态规划总结篇

programmercarl.com/%E5%8A%A8%E…