动态规划
01背包问题
01背包经典问题 一定容量装入的最大价值问题
二维解法
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
//递推公式
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
public class BagProblem {
public static void main(String[] args) {
//初始化物品数组
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
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 先初始化第一排 物品0的数据
for (int j = weight[0]; j <= bagSize; j++) {
dp[0][j] = value[0];
}
//从物品1开始遍历
for (int i = 1; i < weight.length; i++) {
//j表示背包的大小
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]);
}
}
}
// 打印dp数组
for (int i = 0; i < goods; i++) {
for (int j = 0; j <= bagSize; j++) {
System.out.print(dp[i][j] + "\t");
}
System.out.println("\n");
}
}
}
一维数组优化解法(滚动数组)
对于背包问题其实状态都是可以压缩的。
在使用二维数组的时候,
//递推公式
dp[i][j] = Math.max(dp[i-1][j] , dp[i-1][j-weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:
//递推公式:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用一维数组dp[j]来表示
这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
//这里注意 使用一维数组解决01背包问题 背包大小需要倒序遍历以避免重复获取物品
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
经典例题
力扣416.分割等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
实现代码:
class Solution {
public boolean canPartition(int[] nums) {
int sum = 0;
int result = 0;
for(int num : nums){
sum += num;
}
if(sum%2!=0){
return false;
}else{
result = sum/2;
}
//定义一维dp数组 用于记录背包容量为能否装下
int[] dp = new int[result+1];
//先遍历物品
for(int i=0;i<nums.length;i++){
//在遍历数组长度
for(int j=result;j>=nums[i];j--){
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
if(dp[result]==result){
return true;
}
return false;
}
}
力扣1049题.最后一块石头的重量 II
有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
示例 1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
示例 2:
输入:stones = [31,26,33,21,40]
输出:5
实现代码:
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for(int num : stones){
sum += num;
}
int target = sum/2;
//力扣题目描述1 <= stones.length <= 30 1 <= stones[i] <= 100
//所以target最大值为1500 我们定义一个1501长度的数组足以
int[] dp = new int[1501];
//先遍历物品后遍历重量
for(int i=0;i<stones.length;i++){
for(int j=target;j>=stones[i];j--){
//不放入stones[i]的重量为dp[j]
//放入stones[i]的重量为 dp[j-stones[i]]+stones[i]
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
int minNum = dp[target];
int maxNum = sum-minNum;
int result = maxNum-minNum;
return result;
}
}
给你一个非负整数数组 nums
和一个整数 target
。
向数组中的每个整数前添加 '+'
或 '-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。
返回可以通过上述方法构造的、运算结果等于 target
的不同 表达式 的数目。
力扣494.目标和
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
实现代码:
class Solution {
int count = 0;
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num : nums){
sum += num;
}
if ( target < 0 && sum < -target) return 0;
if ((target + sum) % 2 != 0) return 0;
int right;//正数集合
int left;//负数集合
//right + left = sum; //正数集合加负数集合等于sum
//right - left = target; //正数集合减去负数集合等于target
//sum + target = 2right;
right = (sum + target)/2; //算出正数集合right
//即求有多少种方法能装满背包容量为right的背包
//先确定dp数组含义 dp[i] 表示 装满容量为i的背包有dp[i]种方法
//初始化dp数组 dp[0] = 1
//解释一下 当已装背包容量为1时 还要需要装入容量为4的物品 因此需要dp[4]种方法
// 已装背包容量 则有多少种方法
// 1 dp[5-1]
// 2 dp[5-2]
// 3 dp[5-3]
// 4 dp[5-4]
// 5 dp[5-5]
int[] dp = new int[right+1];
dp[0] = 1;
//先遍历物品
for(int i=0;i<nums.length;i++){
//在遍历背包容量 j>=nums[i]表示背包容量要大于物品容量才能放得下
for(int j=right;j>=nums[i];j--){
dp[j] += dp[j-nums[i]];
}
}
//最后返回装满right数组所需要的次数
return dp[right];
}
}
再贴一个用暴力递归枚举的代码
class Solution {
int count = 0;
public int findTargetSumWays(int[] nums, int target) {
dfs(nums,target,0,0);
return count;
}
public void dfs(int[] nums,int target,int sum,int startIndex){
if(startIndex==nums.length){
if(sum==target){
count++;
}
}else{
//可以选择加或减 对两种方式分别递归
dfs(nums,target,sum+nums[startIndex],startIndex+1);
dfs(nums,target,sum-nums[startIndex],startIndex+1);
}
}
}
总结
这三道题都用到了01背包的相关知识。
**分割等和子集:**题意是给定一个数组,其中元素要等分为两个数组 ,那么我们就可以认为是要让我们装满一个容量为数组一半背包,物品的价值就是原数组中的值,假如最后我们发现当背包容量装满时,他的值没有达到原数组和的一半时,则说明该数组不可能均分为两份以此来解决该题。
**最后一块石头的重量:**这道题与上道题目的不同点是分割等和子集是要求分割的两块值必须相等,而这道题则是要求出近似相等的两块石头他们的差值是多少。
**目标和:**这道题解题难点在于如何想到将+1 -1想象成正数集合和负数集合
同时要满足正数集合+负数集合=sum 并且正数集合-负数集合=target
最后推出正数集合的值,这样我们就可以将其看做为01背包问题
即用质量为nums[i]的物品装满 一个背包容量为正数集合值的背包
力扣474.一和零
给你一个二进制字符串数组 strs
和两个整数 m
和 n
。
请你找出并返回 strs
的最大子集的长度,该子集中 最多 有 m
个 0
和 n
个 1
。
如果 x
的所有元素也是 y
的元素,集合 x
是集合 y
的 子集 。
示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。
其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = ["10", "0", "1"], m = 1, n = 1
输出:2
解释:最大的子集是 {"0", "1"} ,所以答案是 2 。
实现代码:
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//定义dp数组
int[][] dp = new int[m+1][n+1];
//初始化 默认为0
for(String str : strs){
int zero = 0;//记录每个子串中0的数量
int one = 0;//记录每个子串中1的数量
char[] ch = str.toCharArray();
for(char c : ch){
if(c=='0'){
zero++;
}else{
one++;
}
}
//算出每个子串中的0 1个数后就可以填充dp数组了
//i>=zero 是因为背包要能装下zero个0
for(int i=m;i>=zero;i--){
for(int j=n;j>=one;j--){
//递推公式表示 dp[i][j]为上一次遍历的值
//dp[i-zero][j-one]+1 表示本次装入子串char后 背包中还能装下的0 1的空间
dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one]+1);
}
}
}
return dp[m][n];
}
}
总结
一和零:本题与前面几题的不同在于 这次的物品变为二维数组,要求出最多的子串并同时满足0和1的个数,
因此需要用到三维数组来解决
dp[i][j][k]其中i表示数组中的每个子串即物品 j表示0的个数 k表示1的个数即背包能装下不同物品的容量
//通过滚动数组的优化 则可以将三维数组优化为二维数组
dp[i][j] i表示0的个数 j表示1的个数即背包能装下不同物品的容量
//关于dp关系表达式的推导
//基础01背包问题的表达式如下
dp[j]=Math.max(dp[j],dp[j-weight(i)]+value[i])
//其中dp[j]表示为上一次遍历时背包容量为j时的情况 那么在本题则对应dp[i][j]
//dp[j-weight(i)]+value[i]则表示装入i物品时 能装下的最大价值
//因为本题我们不考虑价值所以每次装入新的子串时让其+1,并且dp[j-weight(i)]表示装入物品i后所能装下的最大价值
//那么在本题中dp[i-0的数量][j-1的数量]则表示装入子串后能装下的最大子串数,由此可得本题的递推关系式:
dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one]+1);
量
//通过滚动数组的优化 则可以将三维数组优化为二维数组
dp[i][j] i表示0的个数 j表示1的个数即背包能装下不同物品的容量
//关于dp关系表达式的推导
//基础01背包问题的表达式如下
dp[j]=Math.max(dp[j],dp[j-weight(i)]+value[i])
//其中dp[j]表示为上一次遍历时背包容量为j时的情况 那么在本题则对应dp[i][j]
//dp[j-weight(i)]+value[i]则表示装入i物品时 能装下的最大价值
//因为本题我们不考虑价值所以每次装入新的子串时让其+1,并且dp[j-weight(i)]表示装入物品i后所能装下的最大价值
//那么在本题中dp[i-0的数量][j-1的数量]则表示装入子串后能装下的最大子串数,由此可得本题的递推关系式:
dp[i][j] = Math.max(dp[i][j],dp[i-zero][j-one]+1);