Day46-动态规划,leetcode647,516,5

  1. 回文子串
  • 给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
  • 回文字符串 是正着读和倒过来读一样的字符串。
  • 子字符串 是字符串中的由连续字符组成的一个序列。

  • 思路
  • 1.确定dp数组定义及下标的含义:dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
  • 2.确定递推公式:
  • 当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。
  • 当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况
  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
  • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
  • 3.dp数组如何初始化:dp[i][j]初始化为false
  • 4.确定遍历顺序:从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
  • 5.举例推导dp数组,打印dp数组:
/**
 * 1. dp数组定义
    dp[i][j] 表示:区间 [i, j] 的子串是否是回文子串(true/false)。
 * 2. 初始化
    所有区间初始都不是回文子串。
 * 3. 状态转移
    外层循环枚举右端点 j,内层循环枚举左端点 i。
    如果 s[i] === s[j],分三种情况:
        i==j:单个字符,一定是回文。
        j-i==1:两个相同字符,比如 "aa",也是回文。
        j-i>1:看去掉两端后的子串 dp[i+1][j-1] 是否是回文。
    每次发现回文子串,计数加一。
 * 4. 返回结果
    返回所有回文子串的数量 numOfPalindromicStr。

 * 用动态规划,枚举所有区间,判断是否为回文并计数,最终返回回文子串总数。
 */

const countSubstrings = (s) => {
    const strLen = s.length;
    let numOfPalindromicStr = 0;
    let dp = Array.from(Array(strLen), () => Array(strLen).fill(false));

    for(let j = 0; j < strLen; j++) {
        for(let i = 0; i <= j; i++) {
            if(s[i] === s[j]) {
                if((j - i) < 2) {
                    dp[i][j] = true;
                } else {
                    dp[i][j] = dp[i+1][j-1];
                }
                numOfPalindromicStr += dp[i][j] ? 1 : 0;
            }
        }
    }

    return numOfPalindromicStr;
}

  • 双指针法:首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以一个元素可以作为中心点,两个元素也可以作为中心点。
/**
 * 1. 思路
    回文子串的中心可以是一个字符(奇数长度),也可以是两个字符之间(偶数长度)。
    枚举所有可能的中心点,然后向两边扩展,统计所有回文子串。
 * 2. 代码流程
    i 从 0 到 2 * strLen - 2,枚举所有中心点(包括字符和字符间隙)。
    left 和 right 分别表示当前中心的左右指针。
        如果 i 是偶数,中心是单个字符。
        如果 i 是奇数,中心是两个字符之间。
    while 循环向两边扩展,只要 s[left] === s[right] 就说明是回文子串,计数加一。
    每次扩展后,left--,right++,继续判断更大的区间。
 * 3. 返回结果
    返回所有回文子串的数量 numOfPalindromicStr。

 * 通过枚举所有中心点,向两边扩展,统计所有回文子串,时间复杂度 O(n²),空间复杂度 O(1)。
 */
const countSubstrings = (s) => {
    const strLen = s.length;
    let numOfPalindromicStr = 0;
    /**
     * 对于长度为 strLen 的字符串:
        单字符中心有 strLen 个(每个字符都可以作为中心)
        两字符之间的中心有 strLen - 1 个(每两个字符之间都可以作为中心)
        总共中心点数量是 strLen + (strLen - 1) = 2 * strLen - 1。

        遍历 i < 2 * strLen - 1,可以枚举所有可能的中心点,保证所有回文子串都能被统计到。
        如果只遍历到 strLen,就只能统计奇数长度的回文子串,偶数长度的就漏掉了。
     */
    for(let i = 0; i < 2 * strLen - 1; i++) {
        let left = Math.floor(i/2);
        let right = left + i % 2;

        while(left >= 0 && right < strLen && s[left] === s[right]){
            numOfPalindromicStr++;
            left--;
            right++;
        }
    }

    return numOfPalindromicStr;
}


  1. 最长回文子序列
  • 给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
  • 子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

  • 思路
  • 回文子串是要连续的,回文子序列可不是连续的!
  • 1.确定dp数组定义及下标的含义:
  • dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
  • 如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
  • 加入s[j]的回文子序列长度为dp[i + 1][j]。
  • 加入s[i]的回文子序列长度为dp[i][j - 1]。
  • 那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
  • 2.确定递推公式:如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
  • 3.dp数组如何初始化:当i与j相同,dp[i][j] = 1 ,即:一个字符的回文子序列长度就是1。其他情况dp[i][j]初始为0就行
  • 4.确定遍历顺序:dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的。j的话,可以正常从左向右遍历。
  • 5.举例推导dp数组,打印dp数组:
/**
 * 1. dp数组定义
    dp[i][j] 表示:字符串 s 在区间 [i, j] 范围内最长回文子序列的长度。
 * 2. 初始化
    单个字符本身就是回文子序列,长度为 1。
 * 3. 状态转移
    从下到上遍历 i,从左到右遍历 j,保证依赖的状态已计算。
    如果 s[i] === s[j],则最长回文子序列长度为去掉两端后区间的长度加 2。
    如果不相等,则取去掉左端或右端后的最大值。
 * 4. 返回结果
    dp[0][strLen - 1] 就是整个字符串的最长回文子序列长度。

 * 用动态规划,枚举所有区间,递推最长回文子序列长度,最终返回全局最优解。
 */
const longestPalindromeSubseq = (s) => {
    const strLen = s.length;
    let dp = Array.from(Array(strLen), () => Array(strLen).fill(0));

    for(let i = 0; i < strLen; i++) {
        dp[i][i] = 1;
    }

    for(let i = strLen - 1; i >= 0; i--) {
        for(let j = i + 1; j < strLen; j++) {
            if(s[i] === s[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][strLen - 1];
};


  1. 最长回文子串
  • 给你一个字符串 s,找到 s 中最长的 回文 子串。

  • 思路
  • 1.确定dp数组定义及下标的含义:dp[i][j]:表示区间范围[i,j] 的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
  • 2.确定递推公式:如果 s[i] === s[j],且 j-i < 2 或 dp[i+1][j-1] 为 true,则 dp[i][j] = true。
  • 当s[i]与s[j]不相等,dp[i][j]一定是false。
  • 当s[i]与s[j]相等时,有如下三种情况
  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是文子串
  • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
  • 3.dp数组如何初始化:所有 dp[i][j] 默认为 false,单个字符一定是回文
  • 4.确定遍历顺序:情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。从下到上,从左到右遍历。
  • 5.举例推导dp数组,打印dp数组:
// 动态规划
var longestPalindrome = function(s) {
    const len = s.length;
    // 布尔类型的dp[i][j]:表示区间范围[i,j] 的子串是否是回文子串,如果是dp[i][j]为true,否则为false
    let dp = new Array(len).fill(false).map(() => new Array(len).fill(false));
    // left起始位置  maxlenth回文串长度
    let left = 0, maxlenth = 0;
    for(let i = len - 1; i >= 0; i--){
        for(let j = i; j < len; j++){
            // 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串 j - i == 0
            // 情况二:下标i 与 j相差为1,例如aa,也是文子串 j - i == 1
            // 情况一和情况二 可以合并为 j - i <= 1
            // 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]===true
            if(s[i] === s[j] && (j - i <= 1 || dp[i + 1][j - 1])){
                dp[i][j] = true;
            }
            // 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置
            if(dp[i][j] && j - i + 1 > maxlenth) {
                maxlenth = j - i + 1; // 回文串长度
                left = i; // 起始位置
            }
        }
    }
    return s.substr(left, maxlenth); // 找到子串
};

  • 双指针法:首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。在遍历中心点的时候,要注意中心点有两种情况。一个元素可以作为中心点,两个元素也可以作为中心点。
var longestPalindrome = function(s) {
    let left = 0, right = 0, maxLength = 0;
    const extend = (s, i, j, n) => {// s为字符串 i,j为双指针 n为字符串长度
        while(i >= 0 && j < n && s[i] === s[j]){
            if(j - i + 1 > maxLength){
                left = i; // 更新开始位置
                right = j; // 更新结尾位置
                maxLength = j - i + 1; // 更新子串最大长度
            }
            // 指针移动
            i--;
            j++;
        }
    }
    for(let i = 0; i < s.length; i++){
        extend(s, i, i, s.length); // 以i为中心
        extend(s, i, i + 1, s.length); // 以i和i+1为中心
    }
    return s.substr(left, maxLength);
};



参考&感谢各路大神

posted @ 2025-07-12 00:29  安静的嘶吼  阅读(6)  评论(0)    收藏  举报