目录
算法题
Leetcode 139.单词拆分
个人思路
解法
动态规划
回溯法
多重背包理论基础
背包问题总结篇
解题思路
背包递推公式
遍历顺序
01背包
完全背包
算法题
Leetcode 139.单词拆分
题目链接:139.单词拆分
大佬视频讲解:单词拆分视频讲解
个人思路
这道题有点绕,思路没打开...
解法
这道题目的时候,可以使用回溯法解决,和之前做的一题很像分割回文串 ,是枚举分割后的所有子串,判断是否回文。而这里是判断是否在字典出现。除开回溯法,动态规划也能解决这道题。
动态规划
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。拆分时可以重复使用字典中的单词,说明就是一个完全背包!
动规五部曲:
1.确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词
2.确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true(j < i )
所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true
3.dp数组如何初始化
从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。
下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。
4.确定遍历顺序
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
本题求的是排列数: 拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。
"apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。
"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,所以强调物品之间顺序。
所以说,本题一定是 先遍历 背包,再遍历物品。
5.举例推导dp[i]
以输入: s = "leetcode", wordDict = ["leet", "code"]为例,dp状态如图:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
HashSet<String> set = new HashSet<>(wordDict);
boolean[] valid = new boolean[s.length() + 1];
valid[0] = true;//初始化
for (int i = 1; i <= s.length(); i++) {//遍历背包(字符串s)
for (int j = 0; j < i && !valid[i]; j++) {//遍历物品
if (set.contains(s.substring(j, i)) && valid[j]) {
valid[i] = true;
}
}
}
return valid[s.length()];
}
}
时间复杂度:O(n^2);(嵌套for循环)
空间复杂度:O( n);(存储一个长度为n+1的dp数组)
回溯法
class Solution {
private Set<String> set;
private int[] memo;
public boolean wordBreak(String s, List<String> wordDict) {
memo = new int[s.length()];
set = new HashSet<>(wordDict);
return backtracking(s, 0);
}
public boolean backtracking(String s, int startIndex) {
// System.out.println(startIndex);
if (startIndex == s.length()) {
return true;
}
if (memo[startIndex] == -1) {
return false;
}
for (int i = startIndex; i < s.length(); i++) {
String sub = s.substring(startIndex, i + 1);
// 拆分出来的单词无法匹配
if (!set.contains(sub)) {
continue;
}
boolean res = backtracking(s, i + 1);
if (res) return true;
}
// 这里是关键,找遍了startIndex~s.length()也没能完全匹配,标记从startIndex开始不能找到
memo[startIndex] = -1;
return false;
}
}
时间复杂度:O(n);(其中n是字符串
s
的长度)空间复杂度:O( n+m);(其中n是字符串
s
的长度,m是字典中词的数量)
多重背包理论基础
题目
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
多重背包和01背包是非常像的,因为这里的 每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
例如:
背包最大重量为10。
物品为:
重量 | 价值 | 数量 | |
---|---|---|---|
物品0 | 1 | 15 | 2 |
物品1 | 3 | 20 | 3 |
物品2 | 4 | 30 | 2 |
问背包能背的物品最大价值是多少?
和如下情况是一样的;这就转成了一个01背包问题了,且每个物品只用一次。
重量 | 价值 | 数量 | |
---|---|---|---|
物品0 | 1 | 15 | 1 |
物品0 | 1 | 15 | 1 |
物品1 | 3 | 20 | 1 |
物品1 | 3 | 20 | 1 |
物品1 | 3 | 20 | 1 |
物品2 | 4 | 30 | 1 |
物品2 | 4 | 30 | 1 |
说明
多重背包相当于是一种01背包,用01背包的基础上写出对应代码就可以了。
解决代码
import java.util.Scanner;
class multi_pack{
public static void main(String [] args) {
Scanner sc = new Scanner(System.in);
int bagWeight, n;//bagWeight背包容量 ;n:物品种类
bagWeight = sc.nextInt();//获取用户输入数据,中间用空格隔开,回车键换行
n = sc.nextInt();
int[] weight = new int[n];
int[] value = new int[n];
int[] nums = new int[n];
for (int i = 0; i < n; i++) weight[i] = sc.nextInt();
for (int i = 0; i < n; i++) value[i] = sc.nextInt();
for (int i = 0; i < n; i++) nums[i] = sc.nextInt();
int[] dp = new int[bagWeight + 1];//初始化dp数组
for (int i = 0; i < n; i++) { //先遍历物品再遍历背包,作为01背包处理
for (int j = bagWeight; j >= weight[i]; j--) {
//遍历每种物品的个数
for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) {
dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]);
}
}
}
System.out.println(dp[bagWeight]);
}
}
时间复杂度:O(n^2);(嵌套循环for 其中
W
是背包的最大容量)空间复杂度:O( n);(dp数组大小)
背包问题总结篇
解题思路
卡哥的五部曲十分好用,以后都这样来逐步分析.
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性
背包递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
对应题目有:力扣:416.分割等和子集,1049.最后一块石头的重量 II
问装满背包有几种方法:dp[j] += dp[j - nums[i]]
对应题目有:力扣 494.目标和,518. 零钱兑换 II,377.组合总和Ⅳ
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
对应题目有:力扣 474.一和零
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
对应题目有:力扣 322.零钱兑换,279.完全平方数
遍历顺序
01背包
二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历(倒序)。
完全背包
纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关力扣题目如下:
- 求组合数:518.零钱兑换II
- 求排列数:377. 组合总和 Ⅳ
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
- 求最小数:322. 零钱兑换、279.完全平方数
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。
以上是个人的思考反思与总结,若只想根据系列题刷,参考卡哥的网址代码随想录算法官网