动态规划

141 阅读2分钟

dfs(i)→dfs(i+1)

  • 当前操作是什么?当前操作使从子问题i转化成i+1
  • 子问题:>=i
  • 下一个子问题 >= i+1

与回溯三问不同的是,这里的dfs(i)表示i的结果,而回溯里表示的是遍历到第i个。

打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

解法分析:从最后一个房子开始考虑,

  • 选择最后一个房子,下一个子问题为:从1到倒数第三个房子的最大值 + 最后一个房子的值
  • 不选最后一个房子,下一个子问题为:从1到倒数第二个房子的最大值

递推表达式:dfs(i)=max(dfs(i1),dfs(i2)+nums[i])dfs(i) = max(dfs(i-1), dfs(i-2)+nums[i])

时间复杂度:O(2n)O(2^n)

// 使用递归函数的写法会超时
var rob = function(nums) {
  // 表示从1到i个房子的最高金额
  const dfs = i => {
    if (i < 0) return 0
    return Math.max(dfs(i - 1), dfs(i - 2) + nums[i])
  } 
  return dfs(nums.length - 1)
};

时间复杂度优化:递归函数的写法,会导致很多子问题的重复计算,dfs(4)和dfs(3)均需要计算dfs(2),此时可以使用数组或对象将之前计算结果中保存下来,避免重复计算,称为递推。 在动态规划中,一般使用memo数组保存。

时间复杂度:O(n)O(n),共有n个节点,每个节点的计算时间为O(1)

空间复杂度:O(n)O(n)

var rob = function(nums) {
  // 避免i-2下标小于0
  // memo数组初始值为0
  const memo = new Array(nums.length + 2).fill(0)
  for (let i = 0; i < nums.length; i++) {
    memo[i + 2] = Math.max(memo[i+1], memo[i] + nums[i])
  }
  return memo[memo.length - 1]
};

空间复杂度优化:i只依赖i-1和i-2的状态,可以只使用两个变量保存即可。

空间复杂度:O(1)

var rob = function(nums) {
  let f0 = 0, f1 = 0
  for (let i = 0; i < nums.length; i++) {
    let newf = Math.max(f1, f0 + nums[i])
    f0 = f1
    f1 = newf
  }
  return f1
};

0-1背包

有n个物品,第i个物品的体积为w(i),价值为v[i],每个物品至多选1个,求体积不超过capicity时的最大价值和。

dfs(i,c)=max(dfs(i1,c),dfs(i1,cw[i])+v[i])dfs(i,c) = max(dfs(i-1,c), dfs(i-1,c-w[i])+v[i])

  • 当前操作:枚举第i个物品选或不选;不选,剩余容量不变;选,剩余容量减少w[i]
  • 子问题:在剩余容量为c时,从前i个物品中得到的最大价值和
  • 下一个子问题
    • 不选,在剩余容量为c时,从前i-1个物品中得到的最大价值和
    • 选,在剩余容量为c-w[i]时,从前i-1个物品中得到的最大价值和

常见变形

  • 至多装capacity, 求方案数/最大价值和
  • 恰好装capacity, 求方案数/最大价值和
  • 至少装capacity, 求方案数/最大价值和

目标和

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :

  • 例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。

返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

递归写法

// s: nums所有元素和,p:加正号元素和
// p - (s - p) = target ==> p = (s + target)/2
var findTargetSumWays = function(nums, target) {
  const s = nums.reduce((prev, cur) => prev + cur, 0)
  if ((s + target) % 2 === 1) return 0 
  const n = nums.length
  // dfs表示从前i个数中,选择和为t的方案个数
  const dfs = (i, t) => {
    if(i < 0) {
      return t === 0 ? 1 : 0
    }
    // 不选择第i个数,或选择第i个数
    return dfs(i-1, t) + dfs(i-1, t-nums[i])
  }
  return dfs(n-1, (s + target) / 2)
};

递推写法

// s: nums所有元素和,p:加正号元素和
// p - (s - p) = target ==> p = (s + target)/2
var findTargetSumWays = function (nums, target) {
  target += nums.reduce((prev, cur) => prev + cur, 0)
  if (target % 2 === 1 || target < 0) return 0
  target /= 2
  const n = nums.length
  let memo = Array.from({ length: n + 1 }, () => Array(target + 1).fill(0))
  memo[0][0] = 1
  for (let i = 0; i < nums.length; i++) {
    for (let j = 0; j < memo[i].length; j++) {
      if (nums[i] > j) {
        memo[i + 1][j] = memo[i][j]
      } else {
        memo[i + 1][j] = memo[i][j] + memo[i][j - nums[i]]
      }
    }
  }
  return memo[n][target]
}

滚动数组写法

每次状态转移,只涉及i行和i-1行。 注意j要倒序迭代,不然在计算时会使用到新计算出来的新值

// s: nums所有元素和,p:加正号元素和
// p - (s - p) = target ==> p = (s + target)/2
var findTargetSumWays = function (nums, target) {
  target += nums.reduce((prev, cur) => prev + cur, 0)
  if (target % 2 === 1 || target < 0) return 0
  target /= 2
  const n = nums.length
  let memo = new Array(target + 1).fill(0)
  memo[0] = 1
  for (let i = 0; i < nums.length; i++) {
    // j要倒序迭代
    for (let j = memo.length - 1; j >= 0 ; j--) {
      if (nums[i] <= j) {
        memo[j] = memo[j] + memo[j - nums[i]]
      }
    }
  }
  return memo[target]
}

完全背包

有n个物品,第i个物品的体积为w(i),价值为v[i],每个物品无限次重复选,求体积不超过capicity时的最大价值和。

dfs(i,c)=max(dfs(i1,c),dfs(i,cw[i])+v[i])dfs(i,c) = max(dfs(i-1,c), dfs(i,c-w[i])+v[i])

  • 当前操作:枚举第i个物品选或不选;不选,剩余容量不变;选,剩余容量减少w[i]
  • 子问题:在剩余容量为c时,从前i个物品中得到的最大价值和
  • 下一个子问题
    • 不选,在剩余容量为c时,从前i-1个物品中得到的最大价值和
    • 选,在剩余容量为c-w[i]时,从前i个物品中得到的最大价值和

零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的

递推写法

// 将物品的价值定义为1的话,最少硬币数可以转换为:恰好amount时,获取的最小价值
var coinChange = function (coins, amount) {
  const n = coins.length
  const memo = Array.from({ length: n + 1 }, () => Array(amount + 1).fill(Infinity))
  memo[0][0] = 0
  for (let i = 0; i < coins.length; i++) {
    for (let k = 0; k < amount + 1; k++) {
      if (coins[i] > k) {
        memo[i + 1][k] = memo[i][k]
      } else {
        memo[i + 1][k] = Math.min(memo[i][k], memo[i + 1][k - coins[i]] + 1)
      }
    }
  }
  const res = memo[n][amount]
  return res < Infinity ? res : -1
}

滚动数组写法

// 将物品的价值定义为1的话,最少硬币数可以转换为:恰好amount时,获取的最小价值
var coinChange = function (coins, amount) {
  const n = coins.length
  const memo = Array(amount + 1).fill(Infinity)
  memo[0] = 0
  for (let i = 0; i < coins.length; i++) {
    // 可以正序遍历
    for (let k = 0; k < amount + 1; k++) {
      if (coins[i] <= k) {
        memo[k] = Math.min(memo[k], memo[k - coins[i]] + 1)
      }
    }
  }
  const res = memo[amount]
  return res < Infinity ? res : -1
}