【算法训练营Day27】动态规划part3
文章目录
- 完全背包理论基础
- 完全背包典例
- 零钱兑换II
- 组合总和 Ⅳ
- 爬楼梯(进阶版)
- 零钱兑换
- 完全平方数
- 单词拆分
- 背包问题总结
完全背包理论基础
完全背包与01背包的唯一区别就在于完全背包的每件物品有无数个,可以放入背包多次。
我们从dp四部曲来分析一下完全背包问题:
- dp数组及其下标:dp[i][j]表示物品0 ~ i可多次放入到容量为j的背包中最大价值是多少?
- 递推关系:我们从01背包的递推对比来看:
- 01背包的递推关系:关键点在于物品只能用一次,所以如果要放物品,那么肯定是没有该物品,然后把该物品的位置空出来再进行推导。所以dp[i][j]是两种情况取较大值:
- 放物品i:dp[i - 1][j - weight[i]] + value[i]
- 不放物品i:dp[i - 1][j]
- 完全背包的递推关系:关键点在于物品可以使用多次,所以如果要放物品,此时是可以有该物品的,只需要把物品的位置空出来即可。所以dp[i][j]是两种情况取较大值:
- 放物品i:dp[i][j - weight[i]] + value[i]
- 不放物品i:dp[i - 1][j]
- 01背包的递推关系:关键点在于物品只能用一次,所以如果要放物品,那么肯定是没有该物品,然后把该物品的位置空出来再进行推导。所以dp[i][j]是两种情况取较大值:
- 初始化:根据推导公式以及dp含义,我们可以将容量为0的那一列先初始化为0,注意到递推式中dp[i][j]依赖左边以及上边,所以我们一定要初始化第一行,也就是只放物体0的情况。因为可以多次放物品0,所以dp[0][j] = dp[0][j - weight[0]] + value[0]
- 遍历顺序:在01背包中是从后往前,这是因为一个物品只能取一次,而在完全背包中是从前往后,因为每个物品可以取多次。
完全背包典例
题目链接:52. 携带研究材料
import java.util.*;public class Main {public static void main(String[] args) {Scanner in = new Scanner(System.in);int n = in.nextInt();int bag = in.nextInt();int[] weight = new int[n];int[] values = new int[n];for(int i = 0;i < n;i++) {weight[i] = in.nextInt();values[i] = in.nextInt();}int[][] dp = new int[n][bag + 1];//初始化for(int j = 1;j <= bag;j++) if(j - weight[0] >= 0) dp[0][j] = dp[0][j - weight[0]] + values[0];//遍历for(int i = 1;i < n;i++) {for(int j = 1;j <= bag;j++) {if(j - weight[i] >= 0) dp[i][j] = Math.max(dp[i][j - weight[i]] + values[i],dp[i - 1][j]);else dp[i][j] = dp[i - 1][j];}}System.out.println(dp[n - 1][bag]);}
}
零钱兑换II
题目链接:518. 零钱兑换 II
解题逻辑:
这个题就类似于我们在01背包中做过的,把背包装满有多少种情况。所以他的递推式是相加,而不是取较大值。
dp的四部曲分析和上面的典例基本一样,就不在此赘述。
class Solution {public int change(int amount, int[] coins) {int n = coins.length;int[][] dp = new int[n][amount + 1];//初始化for(int i = 0;i < n;i++) dp[i][0] = 1;for(int j = 1;j <= amount;j++) if(j - coins[0] >= 0) dp[0][j] = dp[0][j - coins[0]];//遍历for(int i = 1;i < n;i++) {for(int j = 1;j <= amount;j++) {if(j - coins[i] >= 0) dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]];else dp[i][j] = dp[i - 1][j];}}return dp[n - 1][amount];}
}
一维dp数组:
class Solution {public int change(int amount, int[] coins) {int[] dp = new int[amount + 1];//初始化dp[0] = 1;//遍历for(int i = 0;i < coins.length;i++) {for(int j = 1;j <= amount;j++) {if(j - coins[i] >= 0) dp[j] = dp[j] + dp[j - coins[i]];}}return dp[amount];}
}
组合总和 Ⅳ
题目链接:377. 组合总和 Ⅳ
这一题和上面的题很相似,也是求把背包塞满有多少种情况。区别在于上一题是求组合数(也就是不考虑顺序),而本题是求排列数(也就是要考虑顺序),两者的处理区别在于:
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。
为什么?
- 先遍历物品,再遍历背包。相当于固定了物品之后,对全容量进行递推,每次外循环新增一个后面的元素,所以dp数组里存的是组合的情况。
- 先遍历背包,再遍历物品。相当于固定了容量,对物品进行递推,当前容量可以由任意物品组成,进入下一轮容量固定之后,其依赖上一轮的结果,并且此轮也可以由任意物品组成,所以dp数组里面存的是排列的情况。
解题代码如下:
class Solution {public int combinationSum4(int[] nums, int target) {int n = nums.length;int[] dp = new int[target + 1];//递推式:一维dp :dp[j] = dp[j] + dp[j - nums[i]]//初始化dp[0] = 1;//遍历for(int j = 1;j <= target;j++) {for(int i = 0;i < n;i++) {if(j - nums[i] >= 0) dp[j] = dp[j] + dp[j - nums[i]];}}return dp[target];}
}
爬楼梯(进阶版)
题目链接:57. 爬楼梯
解题思路同上。
解题代码:
import java.util.*;public class Main{public static void main(String[] args) {Scanner in = new Scanner(System.in);int bag = in.nextInt();int m = in.nextInt();int[] dp = new int[bag + 1];//递推 dp[j] = dp[j] + dp[j - value[i]]dp[0] = 1;for(int j = 1;j <= bag;j++) {for(int i = 0;i < m;i++) {if(j >= i + 1) {dp[j] = dp[j] + dp[j - (i + 1)];}}}System.out.println(dp[bag]);}
}
零钱兑换
题目链接:322. 零钱兑换
解题逻辑:
从dp四部曲分析:
- dp数组及下标含义:dp[j]表示装满容量为j的背包最少的硬币个数
- 递推式:dp[i][j] = min(dp[i - 1][j],dp[i][j - values[i]] + 1),简化为一维dp,则dp[j] = min(dp[j],dp[j - values[i]] + 1)
- 初始化:dp[0] = 0,其余全部初始化为Integer.MAX_VALUE - 1,减1是因为防止递推的时候 + 1导致变为负数从而取最小的时候误取。
- 遍历顺序:先遍历物品,再遍历背包,使用物品的组合可以解决问题。
代码如下:
class Solution {public int coinChange(int[] coins, int amount) {int n = coins.length;int[] dp = new int[amount + 1];//初始化for(int i = 1;i <= amount;i++) dp[i] = Integer.MAX_VALUE - 1;//遍历for(int i = 0;i < n;i++) {for(int j = coins[i];j <= amount;j++) {dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);}}return dp[amount] == Integer.MAX_VALUE - 1 || dp[amount] < 0 ? -1 : dp[amount];}
}
或者当遍历到Integer.MAX_VALUE直接跳过,也就是如果取当前元素,那么dp[j - coins[i]]需要先被取到:
class Solution {public int coinChange(int[] coins, int amount) {int n = coins.length;int[] dp = new int[amount + 1];//初始化for(int i = 1;i <= amount;i++) dp[i] = Integer.MAX_VALUE;//遍历for(int i = 0;i < n;i++) {for(int j = coins[i];j <= amount;j++) {if(dp[j - coins[i]] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);}}return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];}
}
完全平方数
题目链接:279. 完全平方数
解题逻辑和上一题一样,代码如下:
class Solution {public int numSquares(int n) {int[] dp = new int[n + 1];for(int i = 1;i <= n;i++) dp[i] = Integer.MAX_VALUE;for(int i = 1;i <= 100;i++) {for(int j = i * i;j <= n;j++) {if(dp[j - i * i] != Integer.MAX_VALUE) dp[j] = Math.min(dp[j],dp[j - i * i] + 1);}}return dp[n];}
}
单词拆分
题目链接:139. 单词拆分
解题逻辑:
从dp四部曲分析:
- dp数组及下标含义:dp[j]表示s的前j个元素能否被拼出
- 递推式:也是分为取和不取两种情况。dp[j] = dp[j] || (wordDict.get(i).equals(s.substring(j - wordDict.get(i).length(),j)) && dp[j - wordDict.get(i).length()])
- 初始化:dp[0] = true
- 遍历顺序:先遍历背包,再遍历物品,使用物品的排列可以解决问题。
代码逻辑:
class Solution {public boolean wordBreak(String s, List<String> wordDict) {int n = wordDict.size();int bag = s.length();boolean[] dp = new boolean[bag + 1];//初始化dp[0] = true;for(int j = 1;j <= bag;j++) {for(int i = 0;i < n;i++) {if(wordDict.get(i).length() <= j) {dp[j] = dp[j] || (wordDict.get(i).equals(s.substring(j - wordDict.get(i).length(),j)) && dp[j - wordDict.get(i).length()]);}}}return dp[bag];}
}
背包问题总结
关于(一维dp数组)常用的递推公式:
- 问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
- 问装满背包有几种方法:dp[j] += dp[j - nums[i]]
- 问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
- 问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j])
关于遍历顺序:
- 01背包
- 二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
- 一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
- 完全背包
- 一维dp数组实现,先遍历物品还是先遍历背包需要分情况,且第二层for循环是从小到大遍历。
- 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
- 如果求排列数就是外层for遍历背包,内层for循环遍历物品。