文章目录
- 01背包理论基础(二维)
- 01背包典例(装满背包的最大价值)
- 01背包理论基础(一维)
- 分割等和子集(能不能装满背包)
- 最后一块石头的重量II(背包最多能装多少)
- 目标和(装满背包有多少种方式)
- 一和零(装满背包最多用多少个物品)
01背包理论基础(二维)
01背包代表的就是有多种不同价值、重量的物品,背包承载的物品重量有限,每个物品的数量只有一个,怎么放进背包价值最高。
01背包的dp数组可以是二维的也可以是一维的,先说二维更好理解。
我们从dp四部曲来分析背包问题:
- dp数组及下标的含义:在二维dp数组中,一个维度是物品,另一个维度代表背包的容量。dp[i][j]表示把0 ~ i号物品放到容量为j的背包中最大的价值是多少。
- 递推公式:dp[i][j]可以从dp[i - 1][]推导而来,一种是有物品i,另一种是没有物品i。如果没有物品i,那么可以直接从dp[i - 1][j]推导而来,如果有物品i,那么就需要从dp[i - 1][j - weight[i]] + value[i]推导得到。所以最终两者取最大值就是dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - weight[i]] + value[i])
- 初始化:首先根据含义j等于0的情况背包什么都装不了,所以可以将dp[i][0]全部初始化为0。再根据递推式,我们肯定需要dp[0][j]的数据,那么j < weight[0]的时候dp[0][j]为0,j >= weight[0]的时候dp[0][j] = value[0]。其余位置使用默认值0就可以。
- 遍历顺序:根据递推式,dp[i][j]依赖上方与左上方的数据,先遍历物品,再遍历背包,更好理解。先遍历背包,再遍历物品也可以。
在实际的问题中很少会出现直接的01背包问题,要将问题进行转化,如果能转化到背包的最大价值,那么就可以尝试使用背包问题的解法。
01背包典例(装满背包的最大价值)
题目链接:46. 携带研究材料
代码如下,要注意的是背包的容量是从0开始计算的,所以对应的长度要 + 1;
java
import java.util.*;
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int item = in.nextInt();
int bag = in.nextInt();
int[] space = new int[item];
int[] values = new int[item];
for(int i = 0;i < item;i++) space[i] = in.nextInt();
for(int i = 0;i < item;i++) values[i] = in.nextInt();
//初始化
int[][] dp = new int[item][bag + 1];
for(int i = 1;i <= bag;i++) {
if(i < space[0]) dp[0][i] = 0;
else dp[0][i] = values[0];
}
//遍历
for(int i = 1;i < item;i++) {
for(int j = 1;j <= bag;j++) {
if(j < space[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - space[i]] + values[i]);
}
}
System.out.println(dp[item - 1][bag]);
}
}
01背包理论基础(一维)
通过递推式我们可以知道,当前层的数据只与上一层有关,那么我们其实干脆就可以在上一行的基础上进行更新,而无需新启一行。这就是滚动数组,通过滚动数组我们可以将二维的dp数组优化到一维。
从dp四部曲来分析的话,遍历这一步有很大的变化:
- 只能倒序遍历:这是为了保证物品i只被添加一次。加入我们还是使用正序遍历,那么我们的dp数组随着遍历的进行会出现左边一部分对应的是物品i的情况,而右半部分对应的是物品i - 1的情况,这样我们在进行正序递推的时候可能会出现需要物品i - 1的数据已经被物品i给覆盖了。
- 只能先遍历物品再遍历背包
代码如下:
java
import java.util.*;
public class Main {
public static void main(String[] args){
Scanner in = new Scanner(System.in);
int item = in.nextInt();
int bag = in.nextInt();
int[] space = new int[item];
int[] values = new int[item];
for(int i = 0;i < item;i++) space[i] = in.nextInt();
for(int i = 0;i < item;i++) values[i] = in.nextInt();
//初始化
int[] dp = new int[bag + 1];
//遍历
for(int i = 0;i < item;i++) {
for(int j = bag;j > 0;j--) {
if(j >= space[i]) dp[j] = Math.max(dp[j],dp[j - space[i]] + values[i]);
}
}
System.out.println(dp[bag]);
}
}
分割等和子集(能不能装满背包)
题目链接:416. 分割等和子集
解题思路:
这个问题可以转化为数组中的元素能否构成target,target是数组元素和的一半。因为数组的元素只能使用一次,将数字的值当作重量以及价值,也就是说看容量为target的背包能否装满。再转化一下:也就是看背包的最大价值,如果最大为target,就说明装满了。这个问题可以使用01背包来解决。
解题代码:
java
class Solution {
public boolean canPartition(int[] nums) {
int item = nums.length;
int sum = 0;
for(int num : nums) sum += num;
if(sum % 2 != 0) return false;
int bag = sum / 2;
int[] dp = new int[bag + 1];
for(int i = 0;i < item;i++) {
for(int j = bag;j > 0;j--) {
if(j >= nums[i]) dp[j] = Math.max(dp[j - nums[i]] + nums[i],dp[j]);
}
if(dp[bag] == bag) return true;
}
return false;
}
}
最后一块石头的重量II(背包最多能装多少)
题目链接:1049. 最后一块石头的重量 II
解题思路:
本题乍一看和动态规划没什么联系,我们可以将问题转化一下。如果要让剩下的石头质量尽量地小,那么我们需要将石头分为尽量相等的两堆相撞即可。这就与上一题有很大的相似性,只不过上一题可以严格的相等。
代码如下:
java
class Solution {
public int lastStoneWeightII(int[] stones) {
if(stones.length == 1) return stones[0];
int item = stones.length;
int sum = 0;
for(int stone : stones) sum += stone;
int bag = sum / 2;
int[] dp = new int[bag + 1];
int min = bag;
for(int i = 0;i < item;i++) {
for(int j = bag;j > 0;j--) {
if(j >= stones[i]) dp[j] = Math.max(dp[j - stones[i]] + stones[i],dp[j]);
}
int result = Math.abs(dp[bag] - (sum - dp[bag]));
if(result < min) min = result;
}
return min;
}
}
目标和(装满背包有多少种方式)
题目链接:494. 目标和
解题逻辑:
将下面两个式子联立,得到正数 = (target + sum)/ 2
- 正数 - 负数 = target
- 正数 + 负数 = sum
也就是说我们我们只用选取数组中的正数,正好达到(target + sum)/ 2即可。这也就转化成了想要将(target + sum)/ 2 容量的背包正好装满有多少种方法?
从dp四部曲来看:
- dp数组及下标的含义:dp[j]表示能将j容量的背包装满的方式
- 递推关系式:dp[j] = dp[j] + dp[j - nums[i]]。不放物品与放物品两种情况,不放物品就有dp[j]方式,放物品就有dp[j - nums[i]]方式
- 初始化方式:i = 0的时候,将dp[0]初始化为1,代表不放物品装满。dp[nums[0]]++,表示放当前物品装满;
- 遍历方式:从后往前遍历
代码如下:
java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num : nums) sum += num;
if((sum + target) % 2 != 0) return 0;
int bag = (sum + target) / 2;
if(bag < 0) return 0;
int[] dp = new int[bag + 1];
//初始化
dp[0] = 1;
if(nums[0] <= bag) dp[nums[0]]++;
//遍历
for(int i = 1;i < nums.length;i++) {
for(int j = bag;j >= 0;j--) {
if(nums[i] <= j) dp[j] = dp[j] + dp[j - nums[i]];
}
}
return dp[bag];
}
}
一和零(装满背包最多用多少个物品)
题目链接:474. 一和零
解题逻辑:
本题的一大特点就是背包具有多个维度对0的个数,1的个数都有限制。那么我们的dp数组就需要用二维的。
接下来从dp四部曲分析:
- dp数组以及下标的含义:dp[i][j]表示该背包最多允许i个0,j个1时最多能装几个元素
- 递推公式:dp[i][j] = max(dp[i - m][j - n] + 1,dp[i][j])
- 初始化:全部初始化为0
- 遍历:从下往上,从右往左。防止当前字符串被重复添加。
解题代码:
java
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp = new int[m + 1][n + 1];
for(String str : strs) {
int num0 = 0;
int num1 = 0;
for(char c : str.toCharArray()) {
if(c == '0') num0++;
else num1++;
}
for(int i = m;i >= 0;i--) {
for(int j = n;j >= 0;j--) {
if(i - num0 >= 0 && j - num1 >= 0) {
dp[i][j] = Math.max(dp[i - num0][j - num1] + 1,dp[i][j]);
}
}
}
}
return dp[m][n];
}
}