1. 关于动规
- 01背包
- 完全背包
- 正序遍历背包 for (int j=target; j>=weight[i]; j++)
- 先背包or先物品?
- 先物品后背包:适用于求组合
- 先背包后物品:适用于求排列(不去重组合)
- 联动排列组合专题:(待施工)
- 算排列/组合数量:动规377 518;
- 列出所有排列/组合方法:回溯39 40
2. 例题
lc377 组合总和Ⅳ
思路
-
组合vs 排列:
- 组合:先遍历物品i=0并递增,故一定先放物品1,再放物品2,如[1,2],不会出现[2,1]
- 排列:先遍历背包i=0并递增,则在背包容量固定的情况下,计算所有的排列方法,既有[1,2]也有[2,1]
- 注意:此时的遍历起始顺序,背包和物品都从0开始,但是在执行正文里需要判定背包容量是否可以容纳物品重量if(j>=weight[i])
-
将问题转化为背包问题:
- step1,
- 要素察觉无限数字,满足固定总和=>完全背包
- step2,
- 排除情况n/a
- step3,
- 背包概念化
- 背包:总和target
- 物品:数字们nums。物品重量nums[i],物品价值nums[i],物品特性可以无限重复使用
- 完全背包情况2:用无限用品完全装满背包有几种排列方法(先遍历背包,再遍历物品)
- 对应公式:dp[i]+=dp[i-nums[j]]
- step1,
-
正式代码:
- 创建dp:dp[i],当要求总和为i时,可以组合的方法数量为dp[i]
- 初始化:dp[0]=1
- 递推公式:dp[i]+=dp[i-nums[j]]
- 遍历顺序:不去重的所有组合(排列问题),故先背包再物品,全都顺序遍历
易错点
注意不去重:顺序不同的序列可以重复组合!
代码实现
class Solution {
public int combinationSum4(int[] nums, int target) {
//创建dp数组
int[] dp = new int[target+1];
//初始化
dp[0]=1;
//递推公式
for(int i=0;i<=target;i++){ //背包容量
for(int j=0;j<nums.length;j++){ //物品:这里只能从物品0开始遍历,不能像再遍历背包时那样从nums[i]开始数,故判定条件if要加在里面
if(i>=nums[j])
dp[i]+=dp[i-nums[j]]; //注意因为要确保i-nums[j]有意义,故要加if(i>=nums[j])的判定
}
System.out.println(dp[i]);
}
return dp[target];
}
}
**
lc322 零钱兑换
思路
-
将问题转化为背包问题:
背包概念化
- 背包容量:amount总金额
- 物品:硬币们coins。物品价值/重量coins[i],物品特性无限使用+组合需去重,物品数量dp[j]
- 完全背包情况3:完全装满背包时,物品的最小数量 Math.min(dp[j],dp[j-coins[i]]+1) -
正式代码:
- dp数组:dp[j]。当总金额为j时,完全满足总金额j所需要的最小物品数量
- 初始化:先预设最大,然后用min慢慢削小 Arrays.fill(dp,n+1)+ dp[0]=0
- 排除例外:如果凑不到,则return -1
- 递推公式:Math.min(dp[j],dp[j-coins[i]]+1)
- 遍历顺序:先背包,再物品,**注意背包容量不足物品重量的数组边界问题
前置知识
填充数组中的所有值:Arrays.fill(nums,?)
易错点
因为是求min,所以dp的初始值必须最大化,而不能只用dp[0]=0
代码实现
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
//例外:amount=0
if(amount==0) return 0;
//初始化
//易错:因为是求min,所以dp的初始值必须最大化,而不能只用dp[0]=0
Arrays.fill(dp,amount+1);
dp[0]=0;
for(int i=0;i<coins.length;i++){ //物品
for(int j=coins[i];j<=amount;j++){ //背包
dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
}
}
//排除例外:凑不到(dp[]仍然是初始值amount+1)要return-1
if(dp[amount]>amount){
return -1;
}else{
return dp[amount];
}
}
}
**
lc279 完全平方数
思路
遍历n里的所有数字,挑出完全平方数数组nums
遍历nums,完全背包情况3:完全背包-完全装满-求物品最小数量
前置知识
链表转为数组:链表.stream().mapToInt(Integer::intValue).toArray();
待优化
类似322,多了一个提取完全平方数的步骤
注意有一段代码写完的高效版本,待优化
代码实现
class Solution {
public int numSquares(int n) {
int[] nums = perfectSquare(n);
int[] dp=new int[n+1];
//初始化:先预设最大,然后慢慢削小
Arrays.fill(dp,n+1);
dp[0]=0;
//递推
for(int i=0;i<nums.length;i++){ //物品
for(int j=nums[i];j<=n;j++){ //背包
dp[j]=Math.min(dp[j],dp[j-nums[i]]+1);
}
}
return dp[n];
}
public int[] perfectSquare(int n){
LinkedList<Integer> perfectsquare = new LinkedList<>();
for(int i=1;i<=n;i++){
double squareroot = Math.sqrt(i);
if(squareroot == (int)squareroot){
perfectsquare.add(i);
}
}
return perfectsquare.stream().mapToInt(Integer::intValue).toArray();
}
}
**
lc139 单词拆分
思路
基于原始字符串,比较s-correctWord的长度里,是否跟word匹配,而不用额外创建字符串链表并add(word in wordDict),更加高效
-
将问题转化为背包问题:
背包概念化
- 背包:字符串
- 物品:单词们wordDict,物品价值单词是否和s中对应字段匹配,物品重量单词长度,物品特性每个物品可重复使用且组合可重复——排列
- 完全背包情况3-boolean:求排列情况并返回这种排列是否可行
- 对应递推公式:dp[i-b’s_Length]==true && a’s_slice.equals(b) -
正式代码:
- dp数组:当背包容量为i/字符串长度为i时,dp[i]表示是否能够凑成单词s(boolean)
- 初始化:dp[0]=true
- 递推公示:排列方法:s.substring(i-j.length()).equals(word)
- 遍历顺序:先背包,再物品
前置知识
- 看字符串中某一段是否与单词匹配:substring。s.substring(i-j.length()).equals(word)
- 善用for(string num: nums)
代码实现
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp=new boolean[wordDict.size()];
//初始化
dp[0]=true;
//递推
//遍历背包(字段)本身,一旦有符合true的,就可以略过这段不提
for(int i=1;i<s.length();i++){
//遍历物品,看这个单词是否可以跟背包字符串中的特定字段匹配
for(String word:wordDict){
int len=word.length();
if(i>=len && dp[i-len]==true && s.substring(i-len,i).equals(word)){ //先从s的字符串中留出当前word的长度,然后把word填进去看看是否符合
return true;
break;
}
}
}
return false;
}
}