139.单词拆分
动规五部曲
1.确定valid数组以及下标的含义
valid[i] : 字符串长度为i的话,valid[i]为true,表示可以拆分为一个或多个在字典中出现的单词。
2.valid初始化
valid[0]一定要为true,否则递推下去后面都都是false了
3.递推公式
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && valid[j]是true) 那么 valid[i] = true。
4.遍历顺序
关于求组合还是排列还是最小数做一个总结:
求组合数:动态规划:518.零钱兑换II (opens new window)
求排列数:动态规划:377. 组合总和 Ⅳ (opens new window),动态规划:70. 爬楼梯进阶版(完全背包) (opens new window)
求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数
而本题其实我们求的是排列数,为什么呢。 拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。
"apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。
"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。
所以说,本题一定是 先遍历 背包,再遍历物品。
5.推导
代码实现
class Solution {public boolean wordBreak(String s, List<String> wordDict) {//完全背包//valid[i]表示字符串下标为i时,可以被字符串列表中的字符拼接出来HashSet<String> set=new HashSet<>(wordDict);//,将字典 wordDict 转化为一个 HashSet 集合 setboolean[] valid=new boolean[s.length()+1];//valid[i] 表示从字符串 s 的第 0 个字符到第 i 个字符(不包括第 i 个字符)所组成的子串是否可以被字典中的单词拆分。valid[0]=true;for(int i=1;i<=s.length();i++){for(int j=0;j<i&&!valid[i];j++){if(set.contains(s.substring(j,i))&&valid[j]){//i表示截取的结束位置,j表示截取的起始位置//对于每个子串,检查它是否存在字典中valid[i]=true;//表示从0到i的子串可以被拆分}}}return valid[s.length()];//表示整个字符串s是否可以被拆分成字典中的单词}
}
多重背包
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
其实多重背包和01背包很想,01背包每个物品只有1个,多重背包只是每个物品有多个
ublic 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) { // 把物品展开为iweight.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));}
}
背包问题总结
核心五步
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
01背包
01背包的一维和二维两种实现方式的差异体现在:
一维:倒序遍历(如果正序遍历背包的话一个物品会被放置多次),且必须先遍历物品再遍历背包(防止每个背包只放入一个物品)
二维:正序遍历,先遍历背包还是先遍历物品都可以
416. 分割等和子集:
01背包问题,先遍历物品再倒序遍历背包,要判断给定的数组能不能被分割成和相等的两组,首先对整个数组求和sum,然后得到target=sum/2,确定dp[i]表示容量为i背包的最大价值,确定递推公式为dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
1049. 最后一块石头的重量 II
01背包问题,跟416 分割等和子集很像,先遍历物品再倒序遍历背包
494.目标和
01背包问题,先遍历物品再倒序遍历背包,首先公式推导得出left=(sum+target)/2,问题转换成在集合nums中找出和为left的组合。求装满背包有几种方法的情况下,递推公式一般为:dp[j]+=dp[j-nums[i]];
474.一和零
找出并返回 strs 的最大子集的长度,找出的该子集中最多有m个0和n个1,dp[i][j]表示i个0和j个1时的最大子集大小,首先要遍历字符串数组的每个字符串,统计每个字符串的0的个数和1的个数,然后倒序遍历zeroNum和倒序遍历oneNum
完全背包
完全背包相较于01背包就是物品可以取无限次
377. 组合总和 Ⅳ
这道题是排列问题不是组合,dp[i]: 凑成目标正整数为i的排列个数为dp[i],遍历顺序:target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历,在遍历过程中需要判断容量j是否大于等于nums[i],只有满足这个条件才可以执行递推公式
518.零钱兑换II
求的是组合数,dp[i]表示凑成总金额为i的货币组合数,要判断coins[i]与我们当前遍历的背包的容量j的大小关系,现遍历背包还是先遍历物品都可以
有一个结论:求组合数先遍历物品再遍历背包;求排列数先遍历背包再遍历物品
70. 爬楼梯
之前没学背包问题前用普通动规分析就可以做,现在用完全背包的思路,dp[i]表示到达第i阶的方法数,要判断i与我们当前遍历的背包的容量j的大小关系,dp[j]+=dp[j-i];
322. 零钱兑换
dp[j]:凑足金额为j所需钱币的最少个数,这里也是求的组合问题。下标为0初始化为0,非0下标初始化为最大值,需要进行判断防止int类型溢出:
if (dp[j - coins[i]] != Integer.MAX_VALUE) {
//如果是dp[j - coins[i]等于Integer.MAX_VALUE,那么+1后溢出,变成-2147483648
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
先遍历背包还是物品无所谓,求的是组合数的最小数,所以不影响,如果要求我们把所有组合情况列出来,那么我们就需要回溯法了
279.完全平方数
这道题和322. 零钱兑换思路基本一致,也是求组合数的最小数,dp[i]表示组成和为i的最少完全平方数个数,递推公式:dp[j]=Math.min(dp[j],dp[j-i*i]+1);先遍历背包还是物品无所谓
多重背包
了解即可,因为多重背包问题可以被拆成01背包看