代码随想录算法训练营第四十六天 | LeetCode 139. 单词拆分、多重背包、背包总结
文章链接:单词拆分 多重背包 背包总结
视频链接:单词拆分
1. LeetCode 139. 单词拆分
1.1 思路
- 本题的那些单词就是物品,字符串就是背包,问用这些物品能否装满这个背包,每个物品能使用多次,因此是完全背包
- dp 数组及其下标含义:dp[i] 长度为 i 的字符串能被所给的单词组成则 dp[i] 为 true。因此最后 return 的是 dp[s.length()]
- 递推公式:假设在位置 i 到这个位置的前面的位置 j,如果这一段是字典里的单词,并且 dp[j]=true 那么 dp[i] 就为 true,因为需要保证这个单词前面的单词已经是在字符串 s 中,避免出现前面的单词都不对,但最后一个对了的情况
- dp 数组的初始化:dp[0]=true,根据递推公式,dp[0] 一定为 true,不然后面就无法递推,由于题目给的描述里没有字符串长度为 0 的情况,因此这个初始化就是为了递推公式服务的,非 0 下标就全为 false,因为不知道这些位置能否被字典里的单词组成
- 遍历顺序:在纯完全背包中两层 for 循环是可以颠倒的,如果求装满背包有多少种方法就要区分组合数还是排列数,求组合数是先物品再背包,求排列数是先背包再物品。本题是排列数,因为 applepenapple 和 penappleapple 是不一样的两个单词字符串。因此是先背包再物品,因为背包是非空的,所以 i 从 1 开始 for(int i=1;i<=s.length();i++)for(String word:wordDict)if(i>=word.length()&&dp[i-word.length()]&&word.equals(s.substring(i-word.length(),i))dp[i]=true。最后一个条件就是看单词是否在字符串的这一段子串中,倒数第二个条件就是上面的 dp[j]
- 打印 dp 数组:用于 debug
1.2 代码
// 另一种思路的背包算法
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (String word : wordDict) {
int len = word.length();
if (i >= len && dp[i - len] && word.equals(s.substring(i - len, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];
}
}
2. 多重背包
2.1 介绍
有N种物品和一个容量为V的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。
多重背包和01背包是非常像的, 为什么和01背包像呢?
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
例如:
背包最大重量为10。
物品为:
这两种情况是不是一样呢?因此就转换成01背包了,且每个物品只用一次
2.2 代码
public void testMultiPack1(){
// 版本一:改变物品数量为01背包格式
List<Integer> weight = new ArrayList<>(Arrays.asList(1, 3, 4));
List<Integer> value = new ArrayList<>(Arrays.asList(15, 20, 30));
List<Integer> nums = new ArrayList<>(Arrays.asList(2, 3, 2));
int bagWeight = 10;
for (int i = 0; i < nums.size(); i++) {
while (nums.get(i) > 1) { // 把物品展开为i
weight.add(weight.get(i));
value.add(value.get(i));
nums.set(i, nums.get(i) - 1);
}
}
int[] dp = new int[bagWeight + 1];
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight.get(i); j--) { // 遍历背包容量
dp[j] = Math.max(dp[j], dp[j - weight.get(i)] + value.get(i));
}
System.out.println(Arrays.toString(dp));
}
}
3. 背包总结
3.1 简介
背包问题是动态规划里的非常重要的一部分,单独总结一下。以下是几种常见的背包:
在背包问题中,我们都是按照如下五部来逐步分析。
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结。
3.2 背包递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
416.分割等和子集
1049.最后一块石头的重量 II
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
494.目标和
518. 零钱兑换 II
377.组合总和Ⅳ
70. 爬楼梯进阶版(完全背包)
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
474.一和零
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
322.零钱兑换
279.完全平方数
3.3 遍历顺序
01背包
在01背包二维数组中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
和在01背包一维数组中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
完全背包
说完01背包,再看看完全背包。
在纯完全背包中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关题目如下:
求组合数:518. 零钱兑换 II
求排列数:377.组合总和Ⅳ 70. 爬楼梯进阶版(完全背包)
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
求最小数:322.零钱兑换 279.完全平方数
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。