算法---- 01背包问题和完全背包问题LeetCode系列问题题解
Posted TheWhc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法---- 01背包问题和完全背包问题LeetCode系列问题题解相关的知识,希望对你有一定的参考价值。
背包问题
背包问题:
- 背包:最大容量v
- 物品:
- 物品价值w
- 物品体积v
- 每个物品的数量
- 只有一个(01背包)
- 无数个(完全背包)
例子:
有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
1、01背包入门
1.1 二维背包
/**
* 背包最大重量为4
*
* 重量 价值
* 物品0 1 15
* 物品1 3 20
* 物品2 4 30
*
*
*/
/**
* 思路: 二维dp数组01背包
*
* 1. 确定dp数组以及下标含义
* dp[i][j]: 表示从下标为0~i的物品任意取,放进容量为j的背包,价值总和最大是多少
*
* 2. 确定递推公式
* if(j < weight[i]) {
* dp[i][j] = dp[i-1][j];
* } else {
* dp[i][j] = Math.max(dp[i-1][j-weight[i]] + value[i], dp[i-1][j])
* 放入物品i 不放入物品i
* }
*
* 3. 初始化
* 背包容量为0时, 即j = 0 时, dp[i][0] = 0
*
* i = 0时
* for(int j = weight[0]; j <= bagweight; j++) {
* dp[0][j] = value[0];
* }
*
* 4. 确定遍历顺序
* 先遍历物品,再遍历背包容量; 反之也可以
* 从上到下,从左往右
*
* 5. 举例推导dp数组
* 重量
* 0 1 2 3 4
* 物品0 0 15 15 15 15
* 物品1 0 15 15 20 35
* 物品2 0 15 15 20 35
*
*
* 时间: O(n*m)
* 空间: O(n*m)
* n为物品,m为重量
*/
static void wei_bag_problem() {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagWeight = 4;
// 1. 确定dp数组以及下标含义
// 二维数组
// dp[i][j] 代表下标[0-i]物品里任意取,放进容量为j的背包,价值总和最大是多少
int[][] dp = new int[weight.length][bagWeight + 1];
// 2. 初始化
// 背包重量为0的 dp[i][0]价值为0
for (int i = 0; i < weight.length; i++) {
dp[i][0] = 0;
}
// 存放编号为0的物品的时候, 各个容量的背包所能存放的最大价值
// 倒叙遍历
/*for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}*/
// 正序遍历
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = value[0];
}
// 3. 遍历顺序
for (int i = 1; i < weight.length; i++) { // 遍历物品
for (int j = 1; j <= bagWeight; j++) { // 遍历背包容量
if(j < weight[i]) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
}
}
}
System.out.println(dp[weight.length-1][bagWeight]);
}
1.2 一维背包
public class _01_背包_一维 {
/**
* 背包最大重量为4
*
* 重量 价值
* 物品0 1 15
* 物品1 3 20
* 物品2 4 30
*
*
*/
/**
* 思路: 一维数组01背包
*
* 1. 确定dp数组以及下标含义
* dp[j]: 表示容量为j的背包的最大价值
*
* 2. 确定递推公式
* dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i])
* 不放入当前物品 放入当前物品
*
* 3. 初始化
* dp[0] = 0
*
* 4. 确定遍历顺序
* 先遍历物品,再遍历背包容量
* 遍历背包容量的时候,应该是倒叙遍历
*
* 如果是正序遍历的话
* dp[1] = dp[1-weight[0]] + value[0] = 15
* dp[2] = dp[2-weight[0]] + value[0] = 15 + 15 = 30
* 重复放了2次
*
* 如果是倒叙遍历的话
* dp[2] = dp[2-weight[0]] + value[0] = 15
* dp[1] = dp[1-weight[0]] + value[0] = 15
*
* 5. 举例推导递推公式
* 背包容量: 0 1 2 3 4
* 用物品0,遍历背包: 0 15 15 15 15
* 用物品1,遍历背包: 0 15 15 20 35
* 用物品2,遍历背包: 0 15 15 20 35
*/
public static void wei_bag_problem() {
int[] weight = {1,3,4};
int[] value = {15, 20, 30};
int bagweight = 4;
// 1. 确定dp数组及下标含义
// dp[j] 表示容量为j的背包,价值总和最大是多少
int[] dp = new int[bagweight+1];
// 2. 确定递推公式
// dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]) 不放入物品, 放入物品
// 所以一维数组时,递推公式如下
// dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i])
// 3. 初始化
// 要和dp数组的定义吻合
// 若价值都为正整数,则全部初始化为0, 如果出现负整数, 那么价值要初始化为最小值(负无穷),防止比较大小的时候出错
// j = 0时,背包容量为0
// 4. 确定遍历顺序
// 先遍历物品,再遍历背包重量(反之不行)
for (int i = 0; i < weight.length; i++) {
// 倒叙遍历
// 从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
// 背包容量从大到小,与二维dp写法不同(二维正序倒叙都可),倒叙遍历目的是使得物品i只被放入一次
for (int j = bagweight; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i]);
}
}
// (错误写法)先背包容量,后物品情况
// 那么背包里只会放入了一个物品
// 背包容量要倒叙遍历,防止出现重复放入物品
/*for (int j = bagweight; j >= 0; j--) {
for (int i = 0; i < weight.length; i++) {
if(j >= weight[i]) {
dp[j] = Math.max(dp[j], dp[j-weight[i]] + value[i]);
}
}
}*/
}
}
2、01背包问题
2.1 背包装最多问题
416. 分割等和子集
/**
* 思路: _01背包
*
* 如何转化为01背包?
* - 背包体积(重量)为sum/2
* - 背包要放入的商品(集合里的元素)重量为元素的数值,价值也为元素的数值
* - 背包如何正好装满,说明找到了总和为sum/2的子集
* - 背包中里的每一个元素都是不可重复放入的
*
* 1. 确定dp数组以及下标含义
* dp[j]: 表示背包容量为j,所背的物品价值可以最大为dp[j]
*
* 2. 确定递推公式
* dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
*
* 3. 初始化
* dp[0] = 0
*
* 4. 确定遍历顺序
* 先遍历物品,再遍历背包容量
* 注意遍历背包容量的时候应该是逆序遍历,防止重复放入
*
* 5. 举例推导dp数组
* 比如nums=[1,5,11,5]
*
* 背包容量为11
* 背包容量 0 1 2 3 4 5 6 7 8 9 10 11
* 元素0 0 1 1 1 1 1 1 1 1 1 1 1
* 元素1 0 1 1 1 1 1 6 6 6 6 6 6
* 元素2 0 1 1 1 1 1 6 6 6 6 6 11
* 元素3 0 1 1 1 1 5 6 6 6 6 10 11
*/
public static boolean canPartition(int[] nums) {
int sum = 0;
int maxNum = 0;
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
if(sum % 2 == 1 || maxNum > sum / 2) {
return false;
}
// 1.dp数组
int target = sum / 2;
int[] dp = new int[target + 1];
// 2.确定递推公式
// 3.初始化
// 4.确定遍历顺序
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 dp[target] == target;
}
1049. 最后一块石头的重量II
// 求背包最多能装多少
/**
* 思路: 01背包 动态规划
* 将石头尽量拆分成重量相同的两堆,所以最后返回结果sum - dp[重量/2] - dp[重量/2]
*
* 1. 确定dp数组以及下标含义
* dp[j]表示背包容量为j的最大所背重量
*
* 2. 确定递推公式
* dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i])
* 不放入当前石头 放入当前石头
*
* 3. 初始化
* dp[0] = 0
*
* 4. 确定遍历顺序
* 01背包,先遍历物品,再遍历背包容量,背包容量应该逆序遍历
*
* 5. 举例推导dp数组
* 输入[2,4,1,1]
* 下标 0 1 2 3 4
* 石头0 0 0 2 2 2
* 石头1 0 0 2 2 4
* 石头2 0 1 2 3 4
* 石头3 0 1 2 3 4
*
* dp[4] = 4
* res = 8 - 4 - 4 = 0
*
* 时间: O(n * m) n为石头块数
* 空间: O(m) m为石头总重量的一半
*/
public int lastStoneWeightII(int[] stones) {
// 1 <= stones.length <= 30
// 1 <= stones[i] <= 100
// 最大重量的一半 = 30 * 100 / 2 = 1500
int[] dp = new int[1501];
int sum = 0;
for (int stone : stones) {
sum += stone;
}
int target = sum / 2;
for (int i = 0; i < stones.length; i++) {
for (int j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i]);
}
}
return sum - dp[target] - dp[target];
}
2.2 凑满背包方法数问题
494. 目标和
// 凑满背包方法数问题
/**
* 思路: 01背包动态规划
*
* 如何转化为01背包?
* 假设加法总和为x,那么减法对应的总和为sum - x
* 所以 x - (sum - x) = target
* x = (target + sum) / 2
* 所以只需要求凑满x有多少种方案就可以了
*
* 注意这里的除2问题,如果不能整除,那么直接返回0,没有方案
* 还有一个问题这里是两个数相加,可能出现整数溢出,但本题不用担心溢出问题
*
* 1. 确定dp数组
* dp[j]:表示凑满j有多少种方案,注意是凑满,之前都是求容量为j时,最多能装多少
*
* 2. 确定递推公式
* dp[j] += dp[j - nums[i]]
* 只需要搞到nums[i],凑成dp[j]就有dp[j-nums[i]]方法
*
* 3. 初始化
* dp[0] = 1 : 表示凑满背包容量为0,有1种方案,dp[0]也是一切递推结果的起源
*
* 4. 确定遍历顺序
* 先遍历物品,再遍历背包容量,注意背包容量逆序遍历
*
* 5. 举例推导数组
* 例子: [1,1,1,1,1]
* 下标 0 1 2 3 4
* 物品0 1 1 0 0 0
* 物品1 1 2 1 0 0
* 物品2 1 3 3 1 0
* 物品3 1 4 6 4 1
* 物品4 1 5 10 10 5
*
* 时间: O(n * m) n为nums数组长度,m为背包容量
* 空间: O(m)
*
*/
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int bagSize = (target + sum) / 2;
if((target + sum) % 2 == 1) {
return 0;
}
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];
}
2.3 双重维度问题
474. 一和零
/**
* 思路: 01背包问题
*
* 如何转化为01背包?
* m和n相当于是一个背包,两个维度的背包
*
* 一个维度是m,一个维度是n,不同长度的字符串就是不同大小的待装物品
*
* 1. 确定dp数组以及下标含义
* dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]
*
* 2. 确定递推公式
* dp[i][j] = Math.max(dp[i][j], dp[i-zeroNum][j-oneNum] + 1)
* 不放入当前字符串 放入当前字符串(由前一个str里的字符串推出来,所以减去当前字符串的0和1的个数得到前一个字符串的最大子集再加1)
*
* 3. 初始化
* 初始化都为0
*
* 4. 确定遍历顺序
* 先遍历物品(strs)
* 再遍历背包
* 能不能先遍历背包,再遍历物品?
* 因为这里物品只能存放一次,所以背包容量应该是倒叙遍历,防止重复放置
* 并且应该是先遍历物品,再遍历背包容量,如果是先遍历背包容量,后遍历物品时,那么就会出现背包只会存放一个元素的情况
*
*/
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m+1][n+1];
// 外层遍历物品
for (String str : strs) {
int zeroSum = 0;
int oneSum = 0;
for (int i = 0; i < str.length(); i++) {
if(str.charAt(i) == '0') {
zeroSum++;
} else {
oneSum++;
}
}
// 内层遍历背包容量,逆序遍历
for (int i = m; i >= zeroSum; i--) {
for (int j = n; j >= oneSum; j--) {
dp[i][j] = Math.max(dp[i][j], dp[i-zeroSum][j-oneSum] + 1);
}
}
}
return dp[m][n];
}
3、完全背包问题
3.1 凑满组合问题
518. 零钱兑换II
// 完全背包 + 凑满背包问题 + 组合数,求凑满背包的个数
/**
* 思路: 完全背包
* 1. 确定dp数组以及下标含义
* dp[j]: 凑成总金额为j的货币组合数为dp[j]
*
* 2. 确定递推公式
* dp[j](考虑coins[i]的组合总和),就是所有的dp[j-coins[i]]相加
*
* dp[j] += dp[j-coins[i]]
*
* 3. 初始化
* dp[0] = 1是一切递推的基础
*
* 4. 确定遍历顺序
* 外层遍历物品,内层遍历背包容量(正序遍历)
*
* 能否先遍历背包容量(正序遍历),再遍历物品?
* 答案是不行的,因为本题是求组合问题,不是求排列问题,排列问题才需要考虑元素之间的顺序问题
*
* 比如先遍历物品,再遍历背包容量,假设coins[0]=1,coins[1]=5
* 得到的方法数量只有{1,5},不会出现{5,1}情况
*
* 但是如果先遍历背包容量,再遍历物品,假设coins[0]=1,coins[1]=5
* 得到的方法数量既{1,5},也有{5,1}情况
*
* 5. 举例推导dp数组
* amount = 5, coins = [1, 2, 5]
* 背包容量 0 1 2 3 4 5
* 物品0 1 1 1 1 1 1
* 物品1 1 1 2 2 3 3
* 物品2 1 1 2 2 3 4
*/
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];
}
3.2 凑满排列问题
377. 组合总和IV
// 完全背包 + 凑满问题 + 排列数, 求组成target元素组合的个数
/**
* 思路: 完全背包
*
* 1. 确定dp数组以及下标含义
* dp[j]: 凑成目标数为j的排列数为dp[j]
*
* 2. 确定递推公式
* dp[j] += dp[j-nums[i]]
* 求凑满元素的个数问题,都是这样的公式
*
* 3. 初始化
* dp[0] = 1,是一切递推的前提
*
* 4. 确定遍历顺序
* 求排列数,先遍历背包容量,再遍历物品
* 因为是完全背包,所以背包容量应该正序遍历
* 因为是排列问题,所以外层是背包容量
*
* 5. 举例推导递推公式
* nums = [1,2,3], target = 4
* 背包容量 0 1 2 3 4
* 物品0 1 1 1 2 4
* 物品1 1 1 2 3 6
* 物品2 1 1 2 4 7
*/
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target+1];
dp[0] = 1;
// 凑满排列问题,外层先遍历背包容量
for (int j = 1; j <= target; j++) {
// 内存再遍历物品
for (int i = 0; i < nums.length; i++) {
if(j >= nums[i]) {
dp[j] += dp[j-nums[i]];
}
}
}
return dp[target];
}
JZ9. 跳台阶扩展问题
/**
* 思路: 完全背包问题
*
* 如何转换?
* 比如跳到阶梯3时,凑成3时, 可以是1、2步,也可以是2、1步,这两种方法不同, 即求的是排列数
*
* 1. 确定dp数组
* dp[j]: 凑成目标target的排列数为dp[j]
*
* 2. 确定递推公式
* dp[j] += dp[j-nums[i]]
*
* nums[i]的取值范围为[1,target]
*
* 3. 初始化
* dp[0] = 1
*
* 4. 确定遍历顺序
* 凑满排列问题,先遍历背包容量,再遍历物品
* 因为是元素可以重复选,所以背包容量正序遍历
*
* 5. 举例推导dp数组
* 输入3
* 0 1 2 3
* 物品0 1 1 1 2
* 物品1 1 1 2 3
* 物品2 1 1 2 4
*/
public int jumpFloorII(int target) {
int[] dp = new int[target+1];
dp[0] = 1;
// 外层遍历背包容量
for (int j = 1; j <= target; j++) {
// 内层遍历物品,即阶梯[1,target]
for (int i = 1; i <= target; i++) {
if(j >= i)
dp[j] += dp[j-i];
}
}
return dp[target];
}
3.3 凑满最少问题
322. 零钱兑换
// 求凑成总金额所需的最少的硬币个数
// 完全背包 + 凑满问题 + 凑满最少问题
/**
* 思路: 完全背包
* 1. 确定dp数组以及下标含义
* dp[j]: 凑成金额为amount的最少硬币数为dp[j]
*
* 2. 确定递推公式
* dp[j] = Math.min(dp[j], dp[j-coins[i]] + 1)
* 不放入当前硬币 放入当前硬币
*
* 3. 初始化
* dp[0] = 0
* 凑成金额为0的硬币数一定为0
* 其它dp应该初始化为最大值,这样才能保证不被覆盖
*
* 4. 确定遍历顺序
* 先遍历物品,再遍历背包容量
* 或者先遍历背包容量,再遍历物品都可以(因为是最少硬币数,不是求排列数)
*
* 由于是求凑满问题,所以背包容量依然是正序遍历,保证元素可以被重复选取
*
* 5. 举例推导dp数组
* coins = [1, 2, 5], amount = 5
* 背包容量 0 1 2 3 4 5
* 物品0 0 1 2 3 4 5
* 物品1 0 1 1 2 2 3
* 物品2 0 1 1 2 2 1
*
* 最少硬币数为1
*
*/
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
for (int i = 1; i <= amount; i++) {
dp[i] = Integer.MAX_VALUE;
}
// 金额为0时需要的硬币数目为0
dp[0] = 0;
// 外层遍历物品
for (int i = 0; i < coins.length; i++) {
// 内层遍历背包容量
// 正序遍历,保证物品可以重复放入
for (int j = coins[i]; j <= amount; j++) {
// 注意这里只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要,否则会发现整数溢出
// 因为有可能出现无法用给定的硬币凑成目标数的情况,比如{2},amount=3
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 ? dp[amount] : -1;
}
279. 完全平方数
// 完全背包 + 凑满问题 + 凑满最少问题
/**
* 思路: 完全背包问题
*
* 如何转化为背包问题?
* 因为求的是凑成完全平方数的问题,所以凑成目标数当作背包容量,而凑成的完全平方数字看成物品,无限使用
*
* 1. 确定dp数组以及下标含义
* dp[j]: 表示凑成和为j的完全平方数的最少数量dp[j]
*
* 2. 确定递推公式
* dp[j] = Math.min(dp[j], dp[j-i*i] + 1)
*
* if(j >= i * i) {
* dp[j] = Math.min(dp[j], dp[j-i*i] + 1)
* }
* 背包j的取值范围为[0,n]
* 物品i的取值范围为[1, i*i<=n]
*
* 3. 初始化
* dp[0] = 0
* 其余都初始化为最大值,防止被覆盖
*
* 4. 确定遍历顺序
* 先遍历物品,再遍历背包容量(正序遍历,物品可以重复放入)
* 或者先遍历背包容量,再遍历物品都以上是关于算法---- 01背包问题和完全背包问题LeetCode系列问题题解的主要内容,如果未能解决你的问题,请参考以下文章
算法---- 01背包问题和完全背包问题LeetCode系列问题题解
算法---- 01背包问题和完全背包问题LeetCode系列问题题解