目录
【139.单词拆分】
方法一 暴力回溯
关键:
1、如果子节点不包含在单词字典中,直接跳过,不遍历该子节点。
2、如果遍历过程中,发现以start开头的所有子节点都返回false,则标记start位置开头的字符串都不能匹配,即mem[start] = -1,后序再遍历到start开头,直接返回false,不再进入for循环。(通过的关键)
3、如果有一个分支能到达最后,则一路往上返回true。
注意:
这道题不需要返回拆分的路径,因此也不需要记录路径和恢复现场。
class Solution {
Set<String> set;
int[] mem;
public boolean wordBreak(String s, List<String> wordDict) {
set = new HashSet<>(wordDict); // 将list转换为set,加快contains查找速度
mem = new int[s.length()]; // 记录以start~s.length()都不能匹配的情况
return backtracking(s, 0); // 回溯
}
public boolean backtracking(String s, int start){
// 1、递归结束条件
if (start == s.length()) return true;
if (mem[start] == -1) return false; // 之前的搜索已经发现从start开始后面的字符串无法匹配
// 2、单层递归逻辑-遍历子节点
for (int i = start + 1; i <= s.length(); i++){ // i表示结束位置索引,subString左闭右开
String subStr = s.substring(start, i);
if (set.contains(subStr)) {
if (backtracking(s, i)) return true;
}
}
mem[start] = -1; // 标记以start开始后面的字符串都没办法匹配
return false;
}
}
- 时间复杂度:O(2^n),n是字符串长度,因为每一个字符都有两个状态,切割和不切割
- 空间复杂度:O(n),算法递归系统调用栈的空间,最长和字符串长度相等
方法二 完全背包做法(很抽象,不如回溯思路清晰)
分析:
1、单词(物品)可无限次取,属于完全背包问题,正序遍历背包容量。
2、s中单词的顺序固定,但是不一定是按wordDict的顺序排,此处对应的是排列,外层for遍历背包容量。
3、初始化,默认初始化除dp[0]外的所有值为false,dp[0]=true,后面才有可能为true,否则全都是false。
4、dp[i]的含义,i 代表背包的容量,i 前面的子串能否能够被字典中的单词拼接出来,能设为true,否保持默认值false。
5、递推关系,j 代表的是指向s的字符索引,如果dp[j]为true,说明 j 前面的子串能够被拼接,从 j 开始到 i 前面的子串如果也包含在字典中,则 i 前面的字符串都能被拼接,dp[i] 设为true。
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> set = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1]; // dp[0]对应空字符串,默认初始化所有值为false
dp[0] = true; // dp[0]=true,后面才有可能为true,否则全都是false
// s中单词的顺序固定,但是不一定是按wordDict的顺序,所以这里是排列数
for (int i = 1; i < s.length() + 1; i++){ // 外层for遍历背包容量
for (int j = 0; j < i; j++){
if (dp[j] == true && set.contains(s.substring(j, i))){ // i=字符对应索引+1
dp[i] = true;
}
}
}
return dp[s.length()];
}
}
- 时间复杂度:O(n^3),两个for循环,加上s.substring(j, i)的复杂度与子串长度有关,最坏为n
- 空间复杂度:O(n),n为字符串的长度
【多重背包基础】
多重背包和01背包的关系:
多重背包每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
多重背包:
转化为01背包:
【56. 携带矿石资源(第八期模拟笔试)】
关键:在最内层增加一层for遍历,将多重背包转化为01背包。
判断条件要加上j >= k*weights[i],只有容量足够容纳k个相同物品,才考虑放。
import java.util.*;
public class Main{
public static void main (String[] args) {
// 读取数据
Scanner sc = new Scanner(System.in);
int C = sc.nextInt();
int N = sc.nextInt();
int[] weights = new int[N];
int[] values = new int[N];
int[] nums = new int[N];
for (int i = 0; i < N; i++) weights[i] = sc.nextInt();
for (int i = 0; i < N; i++) values[i] = sc.nextInt();
for (int i = 0; i < N; i++) nums[i] = sc.nextInt();
int[] dp = new int[C+1];
for (int i = 0; i < N; i++){
for (int j = C; j >= weights[i]; j--){
// 在最内层增加一层for遍历,将多重背包转化为01背包
for (int k = 1; k <= nums[i] && j >= k*weights[i]; k++){
dp[j] = Math.max(dp[j], dp[j - k * weights[i]] + k * values[i]);
}
}
}
System.out.println(dp[C]);
}
}
- 时间复杂度:O(N × C × k),N为物品种类个数,C背包容量,k单类物品数量
- 空间复杂度:O(C),C为背包容量
【背包问题总结篇】(参考代码随想录-背包问题总结)
1、背包问题判断
01背包问题:每种物品最多只能取1次
完全背包问题:每种物品可以取无限次
多重背包问题:每种物品可以取的次数有规定
2、背包递推公式
问背包装满最大价值:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
问能否能装满背包(或者最多装多少):
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下:
问装满背包有几种方法:
dp[j] += dp[j - nums[i]] ,对应题目如下:
- 动态规划:494.目标和(opens new window)
- 动态规划:518. 零钱兑换 II(opens new window)
- 动态规划:377.组合总和Ⅳ(opens new window)
- 动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
问装满背包所有物品的最小个数:
dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
3、求排列数 or 组合数
求组合数:结果按物品排序固定顺序来
外层for循环遍历物品,内层for遍历背包。
动态规划:518.零钱兑换II(opens new window)
求排列数:结果不按物品排序固定顺序来
外层for遍历背包,内层for循环遍历物品。