动态规划思路(js实现)

35 阅读5分钟

一.需求

  1. 需要实现输出数组中的最大递增子序列个数,子序列不可以不相邻;
  2. 输出最大递增子序列个数下的详细子序列明细

二. 需要实现输出数组中的最大递增子序列个数,子序列不可以不相邻

实现思路

  1. 初始化dp 数组:用于记录以每个元素结尾的最长递增子序列的长度。初始时,每个元素自身构成一个长度为1的子序列,所以 dp 数组初始化为全1。 • nums 数组:输入的数组。

  2. 动态规划 • 外层循环:从第二个元素开始遍历数组,逐个检查每个元素 nums[i]。 • 内层循环:对于每个 nums[i],再遍历它之前的所有元素 nums[j](其中 j < i)。 • 条件判断:如果 nums[i] > nums[j] 并且 dp[j] + 1 > dp[i],说明 nums[i] 可以接在 nums[j] 后面形成一个更长的递增子序列。 ◦ 更新 dp[i]dp[j] + 1,表示以 nums[i] 结尾的最长递增子序列长度增加了1。

  3. 结果 • 遍历 dp 数组,找到其中的最大值,这个最大值就是最长递增子序列的长度。

代码笔记

function lengthOfLIS(nums) {
  if (nums.length === 0) return 0; // 如果数组为空,直接返回 0
  
  // 初始化 dp 数组,每个元素先都设为 1
  const dp = new Array(nums.length).fill(1);
  
  // 外层循环:从第二个元素开始遍历数组
  for (let i = 1; i < nums.length; i++) {
    // 内层循环:对于当前元素 i,再遍历它之前的所有元素 j
    for (let j = 0; j < i; j++) {
      // 条件判断:如果 nums[i] > nums[j] 并且 dp[j] + 1 > dp[i]
      if (nums[i] > nums[j] && dp[j] + 1 > dp[i]) {
        // 更新 dp[i] 为 dp[j] + 1
        dp[i] = dp[j] + 1;
      }
    }
  }
  
  // 返回 dp 数组中的最大值,就是最长递增子序列的长度
  return Math.max(...dp);
}

// 测试用例
console.log(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18])); // 输出: 4

关键点总结

  1. 动态规划数组 dp: • dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。 • 初始时,每个元素自身构成一个长度为1的子序列。

  2. 双重循环: • 外层循环遍历每个元素 nums[i]。 • 内层循环遍历 nums[i] 之前的所有元素 nums[j],检查是否可以形成更长的递增子序列。

  3. 条件判断: • nums[i] > nums[j]:确保 nums[i] 可以接在 nums[j] 后面。 • dp[j] + 1 > dp[i]:确保通过 nums[j] 形成的子序列比当前记录的更长。

  4. 结果计算: • 遍历 dp 数组,找到最大值,即为最长递增子序列的长度。

复杂度分析

时间复杂度:O(n^2),其中 n 是数组的长度。双重循环导致时间复杂度为平方级别。 • 空间复杂度:O(n),需要额外的 dp 数组存储中间结果。

三.输出最大递增子序列个数下的详细子序列明细

实现思路

  1. 初始化dp 数组:用于记录以每个元素结尾的最长递增子序列的长度。初始时,每个元素自身构成一个长度为1的子序列,所以 dp 数组初始化为全1。 • prev 数组:用于记录每个元素的前驱元素的索引。初始时,所有元素都没有前驱元素,所以 prev 数组初始化为全-1。 • nums 数组:输入的数组。

  2. 动态规划 • 外层循环:从第二个元素开始遍历数组,逐个检查每个元素 nums[i]。 • 内层循环:对于每个 nums[i],再遍历它之前的所有元素 nums[j](其中 j < i)。 • 条件判断:如果 nums[i] > nums[j] 并且 dp[j] + 1 > dp[i],说明 nums[i] 可以接在 nums[j] 后面形成一个更长的递增子序列。 ◦ 更新 dp[i]dp[j] + 1,表示以 nums[i] 结尾的最长递增子序列长度增加了1。 ◦ 更新 prev[i]j,记录 nums[i] 的前驱元素的索引。

  3. 找到最长递增子序列的长度和索引 • 遍历 dp 数组,找到其中的最大值及其对应的索引 maxIndex

  4. 重建最长递增子序列 • 从 maxIndex 开始,通过 prev 数组回溯,逐步找到所有前驱元素的索引。 • 将这些元素按顺序添加到 sequence 数组中,最终得到最长递增子序列。

代码笔记

function lengthOfLISWithSequence(nums) {
  if (nums.length === 0) return { length: 0, sequence: [] };
  
  // 初始化 dp 数组和 prev 数组
  const dp = new Array(nums.length).fill(1);
  const prev = new Array(nums.length).fill(-1);
  
  // 动态规划:填充 dp 和 prev 数组
  for (let i = 1; i < nums.length; i++) {
    for (let j = 0; j < i; j++) {
      if (nums[i] > nums[j] && dp[j] + 1 > dp[i]) {
        dp[i] = dp[j] + 1;
        prev[i] = j;
      }
    }
  }
  
  // 找到 dp 数组中的最大值及其索引
  let maxLength = 0;
  let maxIndex = -1;
  for (let i = 0; i < dp.length; i++) {
    if (dp[i] > maxLength) {
      maxLength = dp[i];
      maxIndex = i;
    }
  }
  
  // 重建最长递增子序列
  const sequence = [];
  while (maxIndex !== -1) {
    sequence.unshift(nums[maxIndex]);
    maxIndex = prev[maxIndex];
  }
  
  return { length: maxLength, sequence };
}

// 测试用例
const result = lengthOfLISWithSequence([10, 9, 2, 5, 3, 7, 101, 18]);
console.log(result); // 输出: { length: 4, sequence: [2, 3, 7, 101] }

关键点总结

  1. 动态规划数组 dpprev: • dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度。 • prev[i] 记录 nums[i] 的前驱元素的索引。

  2. 双重循环: • 外层循环遍历每个元素 nums[i]。 • 内层循环遍历 nums[i] 之前的所有元素 nums[j],检查是否可以形成更长的递增子序列。

  3. 条件判断: • nums[i] > nums[j]:确保 nums[i] 可以接在 nums[j] 后面。 • dp[j] + 1 > dp[i]:确保通过 nums[j] 形成的子序列比当前记录的更长。

  4. 重建子序列: • 从 dp 数组中找到最大值及其索引。 • 通过 prev 数组回溯,逐步找到所有前驱元素的索引,构建最长递增子序列。

复杂度分析

时间复杂度:O(n^2),其中 n 是数组的长度。双重循环导致时间复杂度为平方级别。 • 空间复杂度:O(n),需要额外的 dpprev 数组存储中间结果。