目录
理论基础
一、基础题目
LeetCode509:斐波那契数
LeetCode70:爬楼梯
LeetCode746:使用最小花费爬楼梯
LeetCode62:不同路径
LeetCode63:不同路径ii
LeetCode343:整数拆分
LeetCode96:不同的二叉搜索树
二、背包问题
2.1 01背包
01背包理论基础1:二维dp数组
01背包理论基础2:滚动/一维dp数组
LeetCode416:分割等和子集(求是否能正好装满)
LeetCode1049:最后一块石头的重量ii(最多装多少)
LeetCode493:目标和(求装满背包有多少种方法)
LeetCode474:一和零(求装满背包最多有多少个物品)
2.2 完全背包
理论基础
LeetCode518:零钱兑换ii(求组合数)
LeetCode377:组合总和iv(求排列)
LeetCode70:爬楼梯(求排列数)
LeetCode322:零钱兑换(求最小数)
LeetCode279:完全平方数(求最小数)
LeetCode139:单词拆分
2.3 多重背包
理论基础
理论基础
动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的;
举例:
有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。
这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。
- 状态转移公式(递推公式)在动态规划中相当重要
- 动态规划五部曲如下:
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化(很多时候递推公式决定了dp数组要如何初始化)
- 确定遍历顺序
- 举例推导dp数组
在debug时把dp数组打印出来,看看究竟是不是按照自己思路推导的
一、基础题目
LeetCode509:斐波那契数
思路:每一个数字等于前两项之和,给定n求F(n)。动规五部曲——
①dp数组和下标的含义:dp[i]即第i个数的斐波那契数
②递归公式:题干中就给出了,dp[i] = dp[i - 1] + dp[i - 2];
③初始化:题干中也给出了,dp[0] = 0,dp[1] = 1;
④遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的
⑤举例推导:按照递推公式,n为10时,dp数组应该是如下的:
0 1 1 2 3 5 8 13 21 34 55(如果结果不对,就打印dp数组)
class Solution {
public int fib(int n) {
if(n<=1) return n;
int[] dp = new int[n+1];
dp[0] = 0;
dp[1] = 1;
for(int i = 2; i<=n; i++){
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
}
LeetCode70:爬楼梯
思路:每次你可以爬 1 或 2 个台阶
①dp数组和下标的含义:爬到第i阶,有dp[i]种方法
②递归公式:爬到i-1阶有dp[i-1]种方法,然后再爬一阶就是dp[i];同理,爬到i-2阶有dp[i-2]种方法,然后再爬两阶就是dp[i]。所以dp[i] = dp[i - 1] + dp[i - 2];
③初始化:可以推测出,dp[0] = 0(不重要),dp[1] = 1,dp[2] = 2;
④遍历顺序:从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出遍历的顺序一定是从前到后遍历的
⑤举例推导:按照递推公式,n为5时,dp数组应该是如下的:
0 1 2 3 5 8
发现不就是斐波那契数吗?!同样的代码一点毛病没有
public int climbStairs(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
也可以使用变量优化一下空间复杂度
class Solution {
public int climbStairs(int n) {
if(n <= 2) return n;
int a = 1, b = 2, sum = 0;
for(int i = 3; i <= n; i++){
sum = a + b; // f(i - 1) + f(i - 2)
a = b; // 记录f(i - 1),即下一轮的f(i - 2)
b = sum; // 记录f(i),即下一轮的f(i - 1)
}
return b;
}
}
LeetCode746:使用最小花费爬楼梯
思路:可以选择从下标0或者1开始爬,爬1-2阶
①dp数组和下标的含义:爬到第i阶,所花费的最少体力是dp[i]
②递归公式:要算得dp[i]有两种方法,一个是从dp[i-1]花cost[i-1]向上爬一阶,一个是行dp[i-2]花cost[i-2]向上爬两阶,最终使用哪种是最少话费呢?
那就是dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
③初始化:可以推测出,dp[0] = 0,dp[1] = 0;
④遍历顺序:从递归公式中可以看出遍历的顺序一定是从前到后遍历的
⑤举例推导
class Solution {
public int minCostClimbingStairs(int[] cost) {
int len = cost.length;
int[] dp = new int[len + 1];
// 从下标为 0 或下标为 1 的台阶开始,因此支付费用为0
dp[0] = 0;
dp[1] = 0;
// 计算到达每一层台阶的最小费用
for (int i = 2; i <= len; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[len];
}
}
LeetCode62:不同路径
思路:如果采用深度搜索,则这棵树的深度为m+n-1,而深搜代码的时间复杂度为O(2^(m + n - 1) - 1),这是非常大的。那么采用动态规划,从(0,0)出发,到(m,n):
①dp数组和下标的含义:dp[i][j]表示从(0,0)出发,到达(i,j)有多少种路径;
②递归公式:要算得dp[i][j]有两种方法,一个是从dp[i-1][j] 向下移动一格,一个是从dp[i][j-1]向右移动一格。dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
③初始化:首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理
for (int i = 0; i < m; i++) dp[i][0] = 1; for (int j = 0; j < n; j++) dp[0][j] = 1;
④遍历顺序:从递归公式中可以看出遍历的顺序是从左到右一层一层遍历的
⑤举例推导
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i=0;i<m;i++){
dp[i][0] = 1;
}
for(int i=0;i<n;i++){
dp[0][i] = 1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
LeetCode63:不同路径ii
思路:相比1,给出了障碍物,路径需要绕开障碍物。与1的区别仅仅在于:
1. 初始化时如果障碍在边上,则仅初始化障碍前的dp值为1,障碍后仍为0(无法到达)
2. 在遍历时跳过给有障碍的dp赋值
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for(int i=0;i<m && obstacleGrid[i][0] == 0;i++){
dp[i][0] = 1;
}
for(int i=0;i<n && obstacleGrid[0][i] == 0;i++){
dp[0][i] = 1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
LeetCode343:整数拆分
思路:给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。
①dp数组和下标的含义:分拆数字i,所得到的最大乘积是dp[i]
②递归公式:要算得dp[i]有两种方法,从1遍历j,一个是j*(i-j),一个是j*dp[i-j]。可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。
那就是dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});
在取最大值的时候,为什么还要比较dp[i]呢?因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。
③初始化:可以推测出,dp[2] = 1,dp[3] = 2;(n从3开始拆)
④遍历顺序:从递归公式中可以看出遍历的顺序一定是从前到后遍历的,且先有dp[i - j]再有dp[i]。i从3-n,j从1到i-1(不包括i-1),更优化的遍历方法是:j直接从1到i/2,这是因为一定是拆分成m个近似相同的子数相乘才是最大的
⑤举例推导
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n+1];
dp[2] = 1;
for(int i = 3;i<=n;i++){
for(int j = 1;j<=i/2;j++){
dp[i] = Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
}
}
return dp[n];
}
}
LeetCode96:不同的二叉搜索树
思路:重点是推导公式,容易看出
dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量,
元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量
元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量
元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量
即dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]
① dp的概念:dp[i]就是以1到i为节点构成的二叉搜索树的个数
②递归公式:i从2开始遍历到n,j从1开始遍历到i,dp[i] += dp[j-1]*dp[i-j];
③初始化:dp[0]=1; dp[1] = 1;
④遍历顺序和举例推导
class Solution {
public int numTrees(int n) {
//初始化 dp 数组
int[] dp = new int[n + 1];
//初始化0个节点和1个节点的情况
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i; j++) {
//对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
//一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}
二、背包问题
2.1 01背包
01背包理论基础1:二维dp数组
(物品下标-背包容量)
背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。
一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维数组 01背包:
- dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 递归公式:①不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同)。②放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。
- dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- 初始化:关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。根据递推公式可以看出需要得知dp[i][0]和dp[0][j]的初始值,才能开始递推。可以一开始就统一把dp数组统一初始为0。当背包容量为0时,无论怎么选取物品,都无法放入背包中,即价值为0,dp[i][0] = 0;当放入序号为0的物品时且背包容量为0-j时,所能放下物品的最大价值:当j<weight[0]时,背包里放不下物品0,dp[0][j]应该是0;当j>=weight[0]时,背包里可以放下编号0物品,dp[0][j]应该是value[0]。
- 遍历顺序:问题来了,先遍历物品还是先遍历背包重量呢?其实都可以!! 但是先遍历物品更好理解。
- 举例推导:自己推导一下
例题:卡码网46 携带研究材料(给定背包容量,求装满的最大价值)
public class Main {
public static void main(String[] args) {
int[] weight = {2,2,3,1,5,2};
int[] value = {2,3,1,5,4,3};
int bagSize = 1;
testWeightBagProblem(weight,value,bagSize);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods][bagSize + 1];
// 初始化dp数组
// 创建数组后,其中默认的值就是0
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}
// 填充dp数组
for (int i = 1; i < weight.length; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i]) {
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i-1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
}
}
}
System.out.println(dp[goods-1][bagSize]);
}
}
01背包理论基础2:滚动/一维dp数组
对于背包问题其实状态都是可以压缩的。
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:
dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
即重复利用上一层,与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
- 在一维数组中,dp[j]代表:容量为j的背包,所背的物品价值可以最大为dp[j];
- 递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);仍然相当于其中一层的不放物品i和放物品i
- 初始化:看一下递推公式,可知只需初始化dp[0]]=0即可;
- 遍历顺序:重点是倒序遍历,是为了确保物品i只被放入一次。二维的就不用倒序,因为二维dp[i][j]计算是通过上一层dp[i - 1][j]得来的,不会覆盖,而由于一维数组用的是同一个dp[j],所以会从前向后覆盖,影响dp值的计算,所以为了避免重复需要倒序。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
public class Main {
public static void main(String[] args) {
int[] weight = {2,2,3,1,5,2};
int[] value = {2,3,1,5,4,3};
int bagSize = 1;
testWeightBagProblem(weight,value,bagSize);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[] dp = new int[bagSize + 1];
// 初始化dp数组,直接默认全为0
// 填充dp数组,放不下的即j < weight[i]就默认不改变,一维数组直接继承上一层无需赋值,简化了代码
for (int i = 0; i < goods; i++) {
for (int j = bagSize; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j] , dp[j-weight[i]] + value[i]);
}
}
System.out.println(dp[bagSize]);
}
}
总结:
1. 为什么二维背包两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。
答:两个for循环的顺序无非是先遍历物品还是先遍历背包容量,在二维数组中其实都行得通,但是遍历物品更好理解。根据递推公式,每一个dp[i][j]其实都是由上一个dp[i-1][j]或者上一个dp[i - 1][j - weight[i]] 推导出来的,如果以表格的形式来看,位于dp[i][j]的左上角,如果先遍历物品,就是在每一个物品都遍历一遍不同的背包重量,从左到右,从上到下;而如果先遍历背包容量就是先从上到下,在从左到右,由于数据位于左上角,所以两个for的顺序都不影响dp[i][j]的推导。而这种推导方式显然要得到所有的dp[0][j]和dp[i][0]公式才能接着往下,往右推导。
2. 改成一维以后,两个for循环的顺序反过来写行不行?为什么?
答:不可以,改成一维之后只能先遍历物品再遍历背包容量,这是由于一维dp的写法中背包容量一定是要倒序遍历,其本质上还是对二维数组的遍历,右下角的值依赖上一层左上角的值,因此需要保证左边的值仍然是上一层的,从右向左覆盖,保证每个物品只添加一次。如果非要换顺序,倒序遍历的作用便失去了。
LeetCode416:分割等和子集(求是否能正好装满)
思路:是否可以将数组分割成两个子集,使得子集和相等。
怎么转换成背包问题呢?
首先,每个元素我们只用一次,所以是01背包问题。背包的最大容量是target = sum/2,放入n个元素,每个元素的值既是他的value,也是他的weight;dp[j]的定义是容量为j的背包里能装的最大重量,如果正好装满,即dp[j] = j。
其次,01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);对于本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);01背包的初始化:dp[0] = 0即可。
首先得到背包,最终做判断如果dp[target] = target,就说明可以分割等和子集。
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
for(int i= 0;i<nums.length;i++){
sum += nums[i];
}
if(sum % 2 == 1) return false;
int target = sum/2;
int[] dp = new int[target+1];
for(int i = 0;i<nums.length;i++){
for(int j = target;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[target] == target){
return true;
}
return false;
}
}
LeetCode1049:最后一块石头的重量ii(最多装多少)
思路:其实本质上就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了,同样是每个物品重量和价值都为stones[i]。target = sum/2。
得到背包后,不用完全相等,一堆石头是dp[target],另一堆是sum - dp[target]。由于target = sum/2是向下取整,所以sum - dp[target] > dp[target],
所以最终的最小石头重量为 (sum - dp[target]) - dp[target]。
class Solution {
public int lastStoneWeightII(int[] nums) {
int sum = 0;
for(int i= 0;i<nums.length;i++){
sum += nums[i];
}
int target = sum/2;
int[] dp = new int[target+1];
for(int i = 0;i<nums.length;i++){
for(int j = target;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return sum - 2*dp[target];
}
}
LeetCode493:目标和(求装满背包有多少种方法)
思路:如何转化成01背包问题——
一部分left为+,一部分right为-,left-right = target,left+rght = sum,已知sum和target,就可以求出 left = (target + sum)/2 ,此时问题就变成了在集合中找bagSize = left的组合。
之前的几道题都是求容量为j的背包,最多能装多少,本题则是说装满有几种方法,是组合问题。
* 首先排除两种情况,即①target的绝对值>sum,以及②(target+sum)除以2的余数不为0
1. dp[j]:容量为j的背包能有dp[j]种方法,
2. 递推公式:求方法的总和——dp[j] += dp[j - nums[i]]
3. 初始化:dp[0] = 1,别的初始化为0;背包容量为0的话,[0]可以有一种取法,[0,0,0,0,0]有32种取法,也是dp[0] = 1叠加起来的
4. 遍历顺序就是01背包的先遍历物品,再倒序遍历背包容量
5. 举例推导:
这道题也可以用回溯来做,甚至可以列出所有组合,dp仅仅可以求个数
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum =0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
if(Math.abs(target) > sum) return 0;
if((target+sum)%2 == 1) return 0;
int bagSize = (target+sum)/2;
int[] dp = new int[bagSize+1];
dp[0] = 1;
for(int i=0;i<nums.length;i++){
for(int j=bagSize;j>=nums[i];j--){
dp[j] += dp[j-nums[i]];
}
}
return dp[bagSize];
}
}
LeetCode474:一和零(求装满背包最多有多少个物品)
思路:strs数组里的元素就相当于是物品,包含zeroNum个0,oneNum个1,这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。
dp[i][j]就相当于最多有i个0和j个1的最大子集大小,递推通过加一来实现即可。字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。
相当于还是先遍历物品,在内循环里通过构建zeroNum和oneNum两个双循环来遍历背包容量。
01背包递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
该二维weight递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
其实就是把dp变成二维的,本质还是一样的
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m+1][n+1];
for(String str: strs){ //遍历物品
int zeroNum = 0;
int oneNum = 0;
//得到每个物品的weight,分为0和1两个维度
for(char ch: str.toCharArray()){
if(ch == '0'){
zeroNum++;
}else{
oneNum++;
}
}
for(int i = m; i>=zeroNum; i--){
for(int j = n; j>=oneNum; j--){
dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
}
2.2 完全背包
理论基础
区别于01背包:每件物品都可以放入背包无限次,可以重复放入
首先再回顾一下01背包的核心代码
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
且在纯完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的!
例题:携带研究材料(完全背包)
public class Main {
public static void main(String[] args) {
int[] weight = {1,2,3,4};
int[] value = {2,4,4,5};
int bagSize = 5;
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[] dp = new int[bagSize + 1];
// 初始化dp数组,直接默认全为0
// 填充dp数组,放不下的即j < weight[i]就默认不改变,一维数组直接继承上一层无需赋值,简化了代码
for (int i = 0; i < goods; i++) {
for (int j = weight[i]; j <= bagSize; j++) {
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
System.out.println(dp[bagSize]);
}
}
LeetCode518:零钱兑换ii(求组合数)
不同面额的硬币(可以重复使用)和一个总金额,求组合数
一个关键的知识点:
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
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=coins[i];j<=amount;j++){
dp[j] += dp[j-coins[i]];
}
}
return dp[amount];
}
}
LeetCode377:组合总和iv(求排列)
思路:同上,只是调一个for的顺序,顺序不同的序列被视作不同的组合所以是求排列
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
for(int i=0;i<= target;i++){
for(int j=0;j<nums.length;j++){
if(i >= nums[j]) dp[i] += dp[i-nums[j]];
}
}
return dp[target];
}
}
LeetCode70:爬楼梯(求排列数)
思路:这其实就是完全背包的求排列数,秒啦
import java.util.Scanner;
public class Main{
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
while(sc.hasNextInt()){
int n = sc.nextInt();
int m = sc.nextInt();
int[] dp = new int[n+1];
dp[0] = 1;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
if(i>=j) dp[i] += dp[i-j];
}
}
System.out.println(dp[n]);
}
}
}
LeetCode322:零钱兑换(求最小数)
凑成目标的最小硬币数量
思路:相比于LeetCode474:一和零是01背包求最大个数(二维weight)
这道题只不过改成了是完全背包求最小个数(weight就是数值)
这个初始化dp[0]=0,且由于求的是最小值,所以需要把所有dp一开始初始化为max才能进行比较
不多说了上代码!
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int[] dp = new int[amount+1];
//初始化dp数组为最大值
for (int j = 0; j < dp.length; j++) {
dp[j] = max;
}
dp[0] = 0;
for(int i=0;i<coins.length;i++){
for(int j=coins[i];j<=amount;j++){
//只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
if(dp[j-coins[i]]!= max) dp[j] = Math.min(dp[j],dp[j-coins[i]]+1);
}
}
return dp[amount]==max?-1:dp[amount];
}
}
LeetCode279:完全平方数(求最小数)
思路:其实就是元素1-10的平方,给定target,上道题稍微改动一下就能过
class Solution {
public int numSquares(int n) {
int max = Integer.MAX_VALUE;
int[] dp = new int[n+1];
//初始化dp数组为最大值
for (int j = 0; j < dp.length; j++) {
dp[j] = max;
}
dp[0] = 0;
for(int i=1;i<=10;i++){
for(int j=i*i;j<=n;j++){
//这个if是为了防止凑不成,而因为有1所以一定可以凑成,所以if可以略去
if(dp[j-i*i]!= max) dp[j] = Math.min(dp[j],dp[j-i*i]+1);
}
}
return dp[n]==max?-1:dp[n];
}
}
LeetCode139:单词拆分
判断单词是否可以被空格拆分为字典里的词
思路:关于单词拆分,回溯法也能做(这个再说);如何转换成背包呢?单词即物品,字符串即背包,这道题就转换成了物品是否能正好把背包装满。
1. dp[i]代表i前面的子串是否能由字典构成,true/false
2. 递推:如果dp[j]==true && [j,i]子串也属于字典则dp[i]==true(i<j)
3. 初始化:dp[0] = true
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] dp = new boolean[s.length()+1];
HashSet<String> set = new HashSet<>(wordDict);
dp[0] = true;
for(int i=1;i<=s.length();i++){
for(int j=0;j<i;j++){
if(dp[j] == true && set.contains(s.substring(j,i))){
//本身子字符串就不包含i,所以i就是下一个单词开始的第一个字符
dp[i] = true;
}
}
}
return dp[s.length()];
}
}
2.3 多重背包
理论基础
相当于n种物品和一个容量为v的背包,每种物品只有mi件,耗费空间ci,价值是wi
多重背包摊开可以变成01背包
例题:卡码网56 携带矿石资源
import java.util.Scanner;
class multi_pack{
public static void main(String [] args) {
Scanner sc = new Scanner(System.in);
/**
* bagWeight:背包容量
* n:物品种类
*/
int 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];
//先遍历物品再遍历背包,作为01背包处理
for (int i = 0; i < n; i++) {
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]);
}
}