Day44-动态规划,leetcode1143,1035,53,392

  1. 最长公共子序列
  • 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
  • 一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
  • 例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
    两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

  • 思路
  • 不连续
  • 1.确定dp数组定义及下标的含义:dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
  • 2.确定递推公式:
  • 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
  • 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
  • 3.dp数组如何初始化:text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;同理dp[0][j]也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
  • 4.确定遍历顺序:从前向后,从上到下
  • 5.举例推导dp数组,打印dp数组:
// dp[i][j] 表示:text1 的前 i 个字符和 text2 的前 j 个字符的最长公共子序列长度。
const longestCommonSubsequence = (text1, text2) => {
    // 创建一个 (text1.length+1) 行 (text2.length+1) 列的二维数组,全部初始化为0。
    // 多加一行一列是为了处理空串的情况,方便状态转移。
    let dp = Array.from(Array(text1.length+1), () => Array(text2.length+1).fill(0));
    /**
    * 外层循环遍历 text1,内层循环遍历 text2。
    * 如果当前字符相等,说明找到了一个公共子序列,长度加1。
    * 如果不相等,取去掉当前 text1 或 text2 字符后的最大公共子序列长度。
    * 
    * 下标从 1 开始,是为了让 dp[0][] 和 dp[][0] 代表空串,便于初始化和转移。
    * 可以到 =text1.length,这样 dp[text1.length][text2.length] 就是最终答案。
     */
    for(let i = 1; i <= text1.length; i++) {
        for(let j = 1; j <= text2.length; j++) {
            if(text1[i-1] === text2[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;;
            } else {
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
            }
        }
    }

    return dp[text1.length][text2.length];
};


  1. 不相交的线
  • 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足:nums1[i] == nums2[j],且绘制的直线不与任何其他连线(非水平线)相交。请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。以这种方法绘制线条,并返回可以绘制的最大连线数。

  • 思路
  • 直线不能相交,这就是说明在字符串nums1中 找到一个与字符串nums2相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。其实求绘制的最大连线数,就是求两个字符串的最长公共子序列的长度。
  • 1.确定dp数组定义及下标的含义:dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
  • 2.确定递推公式:
  • nums1[i - 1] 与 nums2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1;
  • 如果nums1[i - 1] 与 nums2[j - 1]不相同,那就看看nums1[0, i - 2]与nums2[0, j - 1]的最长公共子序列 和 nums1[0, i - 1]与nums2[0, j - 2]的最长公共子序列,取最大的。 即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
  • 3.dp数组如何初始化:nums1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0;同理dp[0][j]也是0。其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。
  • 4.确定遍历顺序:从前向后,从上到下
  • 5.举例推导dp数组,打印dp数组:
// dp[i][j] 表示:nums1 的前 i 个字符和 nums2 的前 j 个字符的最长公共子序列长度。
const longestCommonSubsequence = (nums1, nums2) => {
    // 创建一个 (nums1.length+1) 行 (nums2.length+1) 列的二维数组,全部初始化为0。
    // 多加一行一列是为了处理空串的情况,方便状态转移。
    let dp = Array.from(Array(nums1.length+1), () => Array(nums2.length+1).fill(0));
    /**
    * 外层循环遍历 nums1,内层循环遍历 nums2。
    * 如果当前字符相等,说明找到了一个公共子序列,长度加1。
    * 如果不相等,取去掉当前 nums1 或 nums2 字符后的最大公共子序列长度。
    * 
    * 下标从 1 开始,是为了让 dp[0][] 和 dp[][0] 代表空串,便于初始化和转移。
    * 可以到 = nums1.length,这样 dp[nums1.length][nums2.length] 就是最终答案。
     */
    for(let i = 1; i <= nums1.length; i++) {
        for(let j = 1; j <= nums2.length; j++) {
            if(nums1[i-1] === nums2[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;;
            } else {
                dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])
            }
        }
    }

    return dp[nums1.length][nums2.length];
};


  1. 最大子数组和
  • 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。

  • 思路
  • 1.确定dp数组定义及下标的含义:dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i],要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。所以在递推公式的时候,可以直接选出最大的dp[i]
  • 2.确定递推公式:dp[i]只有两个方向可以推出来:(1)dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和;(2)nums[i],即:从头开始计算当前连续子序列和;一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]);
  • 3.dp数组如何初始化:dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。根据dp[i]的定义,dp[0]应为nums[0]即dp[0] = nums[0]
  • 4.确定遍历顺序:递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历
  • 5.举例推导dp数组,打印dp数组:
/** 用动态规划,逐步计算以每个元素结尾的最大连续子数组和,并实时更新全局最大值,最终返回最大和。
 * 1. dp数组定义
    dp[i] 表示:以 nums[i] 结尾的最大连续子数组和。
 * 2. 初始化
    dp[0] 初始化为 nums[0],即第一个元素本身。
    max 用于记录全局最大子数组和,初始为 dp[0]。
 * 3. 状态转移
    对于每个位置 i,有两种选择:
        继续累加前面的最大和:dp[i - 1] + nums[i]
        重新开始一个新的子数组:nums[i]
    取两者最大值作为以 i 结尾的最大连续子数组和。
    每次更新全局最大值 max。
 * 4. 返回结果
    返回 max,即整个数组的最大连续子数组和。
 */

const maxSubArray = nums => {
    // 数组长度,dp初始化
    const len = nums.length;
    let dp = new Array(len).fill(0);
    dp[0] = nums[0];
    // 最大值初始化为dp[0]
    let max = dp[0];
    for (let i = 1; i < len; i++) {
        dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]);
        // 更新最大值
        max = Math.max(max, dp[i]);
    }
    return max;
};


  1. 判断子序列
  • 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
  • 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。
  • 进阶:如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

  • 思路
  • 子序列的定义:s 是 t 的子序列,s 的所有字符都能在 t 中按顺序找到(可以不连续),但不能多也不能少。比如 s = "abc",t = "aebdc",s 是 t 的子序列。
  • s是否是t的子序列,其实求s和t的最长公共子序列,如果s和t的最长公共子序列的长度等于s的长度,说明s是t的子序列。dp[i][j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[s.length][t.length] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。
  • 1.确定dp数组定义及下标的含义:dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]。注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。
  • 2.确定递推公式:
  • if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1; 因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1
  • if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];
  • 3.dp数组如何初始化:dp[i][0]=0;dp[0][j]=0
  • 4.确定遍历顺序:遍历顺序,从上到下,从左到右
  • 5.举例推导dp数组,打印dp数组:
/**
 * 1. dp数组定义
    dp[i][j] 表示:s 的前 i 个字符和 t 的前 j 个字符的最长公共子序列长度。
 * 2. 初始化
    创建一个 (m+1) 行 (n+1) 列的二维数组,全部初始化为0。
    多加一行一列是为了处理空串的情况,方便状态转移。
 * 3. 状态转移
    外层循环遍历 s,内层循环遍历 t。
    如果当前字符相等,说明找到了一个公共字符,最长公共子序列长度加1。
    如果不相等,说明 t 需要删除当前字符,继续匹配,取 dp[i][j-1]。
 * 4. 判断结果
    如果 s 和 t 的最长公共子序列长度等于 s 的长度,说明 s 是 t 的子序列,返回 true;否则返回 false。

 * 用动态规划求 s 和 t 的最长公共子序列长度,判断是否等于 s 的长度,从而判断 s 是否为 t 的子序列。
 */
const isSubsequence = (s, t) => {
    // s、t的长度
    const [m, n] = [s.length, t.length];
    // dp全初始化为0
    const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0));
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            // 更新dp[i][j],两种情况
            if (s[i - 1] === t[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = dp[i][j - 1];
            }
        }
    }
    // 遍历结束,判断dp右下角的数是否等于s的长度
    return dp[m][n] === m ? true : false;
};



参考&感谢各路大神

posted @ 2025-07-10 09:54  安静的嘶吼  阅读(4)  评论(0)    收藏  举报