搞定动态规划系列(四):子序列问题

55 阅读1分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情

总结一下动态规划的解题思路:

  1. 看题目是否能找出重复的子问题
  2. 找出对应的状态转移方程dp和条件
  3. 找出初始状态初始化
  4. 确定求解值

最大子数组和

  • 状态转移方程为dp[i]=Math.max(dp[i], dp[j]+1)
var maxSubArray = function(nums) {
    let n = nums.length
    if (n === 0) {
        return 0
    }
    let max = nums[0]
    let prev = nums[0]
    for (let i = 1; i < n; i++) {
        prev = Math.max(prev+nums[i], nums[i])
        max = Math.max(max, prev)
    }
    return max
};

乘积最大子数组

  • 这题容易被误解,错把状态转移方程式写成dp[i] = Math.max(dp[i-1]* nums[i], nums[i]),是因为只考虑了正数的情况
  • 但是乘法中求最大,有可能是上一次最小值* 负数,那么状态转移方程就变成了
    • lastMax = Math.max(lastMax * nums[i], lastMin * nums[i], nums[i])
    • lastMin = Math.min(lastMax * nums[i], lastMin * nums[i], nums[i])
  • 如果直接写上述方程式在代码中,又会出问题,因为第二行右侧的lastMax已经不是上个循环里的lastMax了,因此需要加2个变量缓存起来

最终代码如下:

var maxProduct = function(nums) {
  let lastMax = nums[0], lastMin = nums[0], target = nums[0]
  for (let i = 1; i < nums.length; i++) {
    let temp1 = lastMax * nums[i]
    let temp2 = lastMin * nums[i]
    lastMax = Math.max(temp1, temp2, nums[i])
    lastMin = Math.min(temp1, temp2, nums[i])
    target = Math.max(target, lastMax)
  }
  return target
};

image.png

最长递增子序列

  • 状态转移方程为dp[i]=Math.max(dp[i], dp[j]+1)
var lengthOfLIS = function(nums) {
  let n = nums.length
  let dp = new Array(n).fill(1)
  let res = 1
  for (let i = 1; i < n; i++) {
      for (let j = 0; j < i; j++) {
          if (nums[i] > nums[j]) {
            dp[i] = Math.max(dp[i], dp[j]+1)
          }
      }
      res = Math.max(res, dp[i])
  }
  return res
};

image.png

看起来时间复杂度和空间复杂度并非最优,那是因为此题还有更优解,二分法+贪心,不过本文主要讲动态规划,暂且不看

摆动序列

  • 本题可以分为两种情况,上升子序列和下降子序列
  • 上升子序列的状态转移方程为:
    • num[i] > nums[i-1]时 up[i] = Math.max(up[i-1], down[i-1]+1)
    • num[i] <= nums[i-1]时 up[i] = up[i-1]
  • 下降子序列的状态转移方程为:
    • num[i] >= nums[i-1]时 down[i] = down[i-1]
    • num[i] < nums[i-1]时 down[i] = Math.max(down[i-1], up[i-1]+1)
var wiggleMaxLength = function(nums) {
    let n = nums.length
    if (n < 1) {
        return 0
    }
    let up = new Array(n).fill(1)
    let down = new Array(n).fill(1)
    for (let i = 1; i < n; i++) {
        if (nums[i] > nums[i-1]) {
            up[i] = Math.max(up[i-1], down[i-1]+1)
            down[i] = down[i-1]
        } else if (nums[i] < nums[i-1]) {
            up[i] = up[i-1]
            down[i] = Math.max(down[i-1], up[i-1]+1)
        } else {
            up[i] = up[i-1]
            down[i] = down[i-1]
        }
    }
    return Math.max(up[n-1], down[n-1])
};

image.png

但这种方案空间复杂度太高,因此需要做状态压缩,压缩后代码为

var wiggleMaxLength = function(nums) {
    let n = nums.length
    if (n < 2) {
        return n
    }
    let up = down = 1
    for (let i = 1; i < n; i++) {
        if (nums[i] > nums[i-1]) {
            up = Math.max(up, down+1)
        } else if (nums[i] < nums[i-1]) {
            down = Math.max(down, up+1)
        }
    }
    return Math.max(up, down)
};