【code】动态规划

了解动态规划

  • 动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等
  • 求解动态规划的核心问题是穷举。因为要求最值,肯定要把所有可行的答案穷举出来,然后在其中找最值。需要你熟练掌握递归思维,只有列出正确的「状态转移方程」,才能正确地穷举
  • 动态规划常常适用于有重叠子问题和最优子结构性质的问题,并且记录所有子问题的结果
    • 需要判断算法问题是否具备「最优子结构」,是否能够通过子问题的最值得到原问题的最值
    • 动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
      ps: 用空间换时间是降低时间复杂度的不二法门

动态规划的解题方式

  • 动态规划有自底向上和自顶向下两种解决问题的方式。自顶向下即记忆化递归,自底向上就是递推。
  • 使用动态规划解决的问题有个明显的特点,一旦一个子问题的求解得到结果,以后的计算过程就不会修改它,这样的特点叫做无后效性,求解问题的过程形成了一张有向无环图。动态规划只解决每个子问题一次,具有天然剪枝的功能,从而减少计算量
  • 动态规划与状态和选择有关

动态规划的三要素

  • 重叠子问题
  • 最优子结构
  • 状态转移方程

动态规划五部曲

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 举例推导dp数组

debug思路

  1. 做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
  2. 再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样
  3. 如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了
  4. 如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。
这道题目我举例推导状态转移公式了么?
我打印dp数组的日志了么?
打印出来了dp数组和我想的一样么?
-- 问问题是一个专业活,是的,问问题也要体现出专业

动态规划的题目

1. 斐波那契数

斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1
【思路】

  • 确定dp数组以及下标的含义:dp[i]的定义为:第i个数的斐波那契数值是dp[i]
  • 题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];
  • 题目中把如何初始化也直接给我们了,F(0) = 0,F(1) = 1
  • 确定遍历顺序:dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
  • 举例推导dp数组,代码写出来把dp数组打印出来看看和我们推导的数列是不是一致的

2. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
【思路】第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。

  • 确定dp数组以及下标的含义:dp[i]: 爬到第i层楼梯,有dp[i]种方法
  • 递推公式:dp[i - 1] 再一步跳一个台阶就是dp[i];dp[i - 2]再一步跳两个台阶就是dp[i]; ==》dp[i] = dp[i - 1] + dp[i - 2]
  • 初始化:dp[1] = 1,dp[2] = 2
  • 遍历顺序: dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历
  • 举例推导,对比

3.使用最小花费爬楼梯

cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
【思路】

  • dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]。
  • 递推公式:有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]
    dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。
    dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。
    dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
  • 初始化: dp[0] = 0,dp[1] = 0;
  • 遍历顺序: dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了

4. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?
【思路】使用状态递推公式

  • dp数组:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
  • 递推公式:想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。==》,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来
  • 初始化:dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理
  • 遍历顺序:dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了
  • 举例推导

5. 不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。现在考虑网格中有障碍物(网格中的障碍物和空位置分别用 1 和 0 来表示)。问总共有多少条不同的路径?
【思路】我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的dp table(dp数组)保持初始值(0)就可以了

  • dp数组:dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
  • 递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - 1],但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)
if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j]
    dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
  • 初始化: 从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。
  • 遍历顺序: dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了

5. 整数拆分

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
【思路】 dp[i]最大乘积是怎么得到的呢?可以从1遍历j,然后有两种渠道得到dp[i]
一个是j * (i - j) 直接相乘。
一个是j * dp[i - j],相当于是拆分(i - j)

  • dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。
  • 递推公式:dp[i] = max({(i - j) * j, dp[i - j] * j});
  • 初始化:dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。dp[2] = 1
  • 遍历顺序:dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。

6. 不同的二叉搜索树

给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种?

补充: 二叉搜索树
二叉搜索树是一个有序树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉搜索树
    【思路】n为1的时候有一棵树,n为2有两棵树,这个是很直观的。

    来看看n为3的时候,当1为头结点的时候,其右子树有两个节点,看这两个节点的布局,是不是和 n 为2的时候两棵树的布局是一样的啊!
    当3为头结点的时候,其左子树有两个节点,看这两个节点的布局,是不是和n为2的时候两棵树的布局也是一样的啊!
    当2为头结点的时候,其左右子树都只有一个节点,布局是不是和n为1的时候只有一棵树的布局也是一样的啊!

    发现到这里,其实我们就找到了重叠子问题了,其实也就是发现可以通过dp[1] 和 dp[2] 来推导出来dp[3]的某种方式。
    dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
  • dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]。
  • 递推公式:dp[i] += dp[j - 1] * dp[i - j]; (dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]) (j相当于是头结点的元素,从1遍历到i为止)
  • 初始化:从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。所以初始化dp[0] = 1
  • 遍历顺序: 节点数为i的状态是依靠 i之前节点数的状态,遍历i里面每一个数作为头结点的状态,用j来遍历。

7.1. 0-1背包问题

有n件物品,第i件物品的重量是weight[i],得到的价值是value[i]
有一个最多能背重量为w 的背包,每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大?

  • dp数组:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
  • 递推公式: 有两个方向来推出dp[i][j]
    • 不放物品i:由dp[i - 1][j]推出,dp[i][j]=dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同)
    • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i][j]=dp[i - 1][j - weight[i]] + value[i]
      综上,递推公式为 dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
  • 初始化:
    • 如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0
    • dp[0][j]: 当 j < weight[0]的时候,dp[0][j] 应该是 0,j >= weight[0]时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品
  • 遍历顺序: 有两个遍历的维度,物品与背包重量。递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的,两者都在dp[i][j]的左上角方向(包括正上方向),所以顺序都可以,因为需要的数据都在左上角,不影响公式的推导,

7.2. 0-1背包问题,滚动数组(状态压缩)

分析上题目的递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]):
如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
降维之后为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

  • dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]
  • 递推公式:dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取dp[j - weight[i]] + value[i],即放物品i,指定是取最大的,毕竟是求最大价值,dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
  • 初始化:关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱
    • 容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
    • dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
  • 遍历顺序: 从压缩原理来说,i是覆盖i-1的,所以要从后往前遍历

8. 分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
【思路】只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。

  • dp[j] 表示: 容量为j的背包,所背的物品价值最大可以为dp[j]。
  • 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
  • 从dp[j]的定义来看,首先dp[0]一定是0。

9. 最后一块石头的重量II

有一堆石头,每块石头的重量都是正整数。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
【思路】本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

10. 目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
【思路】对非负整数组分组,+号为一组,-号为一组,根据题意可以得出两个数学等式(left前为+,right前为-)
left + right = sum
left - right = target
推导出 left = (target + sum)/2 ,从而题意分解为:在集合nums中找出和为left的组合,也就转变为了背包问题:装满容量为x的背包,有几种方法

  • dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
  • 递推公式:只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法。推导出来:求组合类问题的公式,都是类似这种dp[j] += dp[j - nums[i]]
  • 初始化:如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。
  • 遍历顺序: 涉及到了状态压缩,都应该倒序遍历

11. 一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3
【思路】数组中的二进制字符串含有0和1,对应背包的两个维度m和n,字符串为待装品,m和n是两个背包

  • dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
  • 递推公式:dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。在遍历的过程中,取dp[i][j]的最大值,dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
  • 初始化,01背包的dp数组初始化为0,因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
  • 01背包,从后往前

12. 完全背包

01背包:每个物品只能使用一次,往包中装
完全背包:每个物品可以使用无数次,往包中装。
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

13. 零钱兑换II

给定不同面额的硬币和一个总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。假设每一种面额的硬币有无限个。
【思路】一看到钱币数量不限,就知道这是一个完全背包,纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!
组合不强调元素之间的顺序,排列强调元素之间的顺序。

  • dp[j]:凑成总金额j的货币组合数为dp[j]
  • 递推公式:dp[j] 就是所有的dp[j - coins[i]](考虑coins[i]的情况)相加 ==》dp[j] += dp[j - coins[i]];
  • 初始化:dp[0]一定要为1,dp[0] = 1是 递归公式的基础
  • 遍历顺序:
    • 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况:这种遍历顺序中dp[j]里计算的是组合数
    • 外层for遍历背包(金钱总额),内层for循环遍历物品(钱币):此时dp[j]里算出来的就是排列数

14. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。请注意,顺序不同的序列被视作不同的组合。
【思路】本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。

  • dp[i]: 凑成目标正整数为i的排列个数为dp[i]
  • 递推公式: dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。==》dp[i] += dp[i - nums[j]];
  • dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
  • 遍历顺序:如果求排列数就是外层for遍历背包,内层for循环遍历物品。

15. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?注意:给定 n 是一个正整数。
【思路】升级版改为:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?
1阶,2阶,.... m阶就是物品,楼顶就是背包。每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。问跳到楼顶有几种方法其实就是问装满背包有几种方法。
所以变体这是一个完全背包问题

  • dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法。
  • 求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]];
    本题dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j] ==》递推公式为:dp[i] += dp[i - j]
  • 初始化:递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果
  • 遍历顺序:
    这是背包里求排列问题,即:1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样! ==》需将target放在外循环,将nums放在内循环
    每一步可以走多次,这是完全背包,内循环需要从前向后遍历。

16. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
【思路】题目中说每种硬币的数量是无限的,可以看出是典型的完全背包问题。

  • dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
  • 递推公式分析
    • 凑足总额为j - coins[i]的最少个数为dp[j - coins[i]]
    • 只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])
    • dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的
      递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
  • 初始化:
    • 凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;
    • dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。标非0的元素都是应该是最大值。
  • 遍历顺序
    • 本题求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数
    • 本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序

17. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
【思路】完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

  • dp[j]:和为j的完全平方数的最少数量为dp[j]
  • dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j],要选择最小的dp[j] ==》dp[j] = min(dp[j - i * i] + 1, dp[j]);
  • 初始化:从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。题目描述中可没说要从0开始
  • 遍历顺序: 本题是完全背包,由于求最小值就无须考虑是排列和组合,所以本题外层for遍历背包,内层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的

18.单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。拆分时可以重复使用字典中的单词,可以假设字典中没有重复的单词。
【思路】单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满
拆分时可以重复使用字典中的单词,是完全背包类型

  • dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词
  • 递推公式:如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )
    if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true
  • 初始化:dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了
    下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
  • 遍历顺序:本题字符串是有序的,所有是排列,故先遍历背包,再遍历物品

19. 多重背包

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
这里和01背包的区别:

  • 相同点: 都是二个维度,空间和价值,空间有限,价值最大化
  • 不同点: 01背包中每个物品只能使用一次,而多重每个包,每个物品有对应的数量Mi
    【思路】 把Mi件摊开为1件,m个,其实就是一个01背包问题

20. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
【思路】当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了,当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。

  • dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]。
  • 递推公式:决定dp[i]的因素就是第i房间偷还是不偷。
    • 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i]
    • 如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房
      然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
  • 初始化:从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);
  • 遍历顺序: dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历!

21. 打家劫舍II

在打家劫舍的基础上增加了一个条件:这个地方所有的房屋都 围成一圈
【思路】将其转化为打家劫舍,考虑两种情况:

  • 考虑包含首元素,不包含尾元素
  • 考虑包含尾元素,不包含首元素
  • 取两种情况下的最大值
posted @ 2022-11-16 09:33  xiaoyu_jane  阅读(28)  评论(0编辑  收藏  举报