你有一个背包,地上一堆物品,挑选一些物品放入背包中,最大能够挑选出来的价值是多少
背包可以装满,背包也是可以不必都装满
一)01背包问题
【模板】01背包_牛客题霸_牛客网 (nowcoder.com)
1)求这个背包最多可以装多大价值的物品?
那么这意味着这个背包可以不用完全装满,那么所能够装的最大价值就是10+4=14,此时装的容量是3,剩余背包容量体积是2
2)若背包恰好装满,求至多可以装多大价值的物品?
如果这个背包恰好装满,那么只能选择2物品和3号物品进行装上,此时装的物品的价值是9,背包剩余容量是0,如果无法将背包装满,就比如说在示例2中,那么返回0
第一问:
一)确定一个状态表示:经验+题目要求
1)背包问题本质上是一个线性的dp问题,当我们在挑选物品的时候,可以从左向右顺序进行挑选,考虑每一个物品是否挑选,从而放到背包里面
dp[i]表示从第一个物品开始进行挑选,挑选到第i个物品的时候所有的选择方法中最大的价值
2)在进行挑选dp[i]的时候是很有可能会使用到之前的状态的,我在进行填写dp[i]的时候,会进行考虑这个i号物品是否进行选择,必须得知道背包容量,不知道背包容量,所以无法确定是否可以把物品放进背包;
3)dp[i][j]表示从前i个物品进行挑选,此时体积不超过j的(可以等于j也可以小于j),在这些选法中,所能进行挑选的最大价值,可以等于j也可以小于j
二)根据状态表示推导状态转移方程:根据最后一个位置的状况来进行划分问题:
1)要么我选择第i号位置的物品,要么我不进行选择第i号位置的物品
不选择i物品:dp[i][j]=dp[i-1][j],我们直接在1到i-1物品中找到最大价值,并且体积不超过j
选择i物品:dp[i][j]=w[i]+dp[i-1][j-v[i]]
2)在两种情况中选择一个一个最大值即可,但是此时还需要考虑一个特殊情况,那么有可能是j-v[i]不存在,如果说背包的体积就是10,但是当前物品的体积是11,整个背包都装不下,所以一定要满足j-v[i]是大于等于0的;
3)假设j==v[i]那么dp[i][j]=w[i]
三:初始化+填表顺序+返回值:
1)下标从1开始进行计数,那么原始的dp表就多了一行和多了一列,第一行是0表示没有商品(有包没有能力装任何商品),第一列表示背包的容量是0(商品都装不进去),所以第一行和第一列都是0
2)从上向下填写,每一行从左向右进行填写
3)返回值:返回的是从N个物品中选择,总体积不超过V的商品所有选法中的最大价值
第二问:
一)定义一个状态标识:必须要装满
dp[i][j]表示从前i个物品中进行挑选,总体积此时正好等于j,所有选法中,所能进行挑选物品的最大价值
二)根据状态标识推导状态转移方程:
1)如果选择i位置的商品,那么就去1-i-1中去选择商品组合,选择商品总容量的总体积要等于j-v[i],如果当前礼物的价值已经超过了j(v[i]>j]那么一定不能选择当前商品,如果j==v[i],那么礼物的最大价值就是dp[i][j]=dp[i-1][j-v[i](j-v[i]>=0)
2)如果不选择i位置的商品,那么就去1-i-1中去寻找商品组合,dp[i][j]=dp[i-1][j],但是这个选择方法是不一定存在的,我们可能永远也无法选取到总体积等于j的商品组合,如果dp[i][j]=-1的时候无法选择,总体积是无法凑到j的,但是这个dp[i-1][j]一定存在吗,我的意思是从前i-1个位置的物品进行挑选,选择体积恰好等于j的商品的最大价值,但是有可能选择不到,没有任何商品的组合体积的和等于j,如果选择不到,那么就让dp[i-1][j]==-1
3)那么为什么不选择dp[i][j]=0来表示从前i个物品中进行挑选,总体积凑不到j的这种情况呢
dp[0][j]表示没有物品进行选择总体积等于j,此时能够选择到的商品的最大价值,其实是可以选到的,不过里面的价值是等于0;
4)对于第一种情况来说,我不进行选择i位置的元素,dp[i-1][j]如果等于-1
那么dp[i][j]=dp[i-1][j]也是可以的
5)对于第二种情况来说,如果选择i位置的商品,必须要进行判断dp[i-1][j-v[i]]不等于-1,如果在i-1区间内找不到礼物价值是j-v[i]的礼物组合,那么dp[i][j]也是-1
三)初始化:
dp[0][0]=0,dp[0][i]=-1,dp[j][0]=0(啥都不选就可以了)
import java.util.Scanner; import java.util.Arrays; // 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main { public static void main(String[] args) { Scanner scanner=new Scanner(System.in); int n=scanner.nextInt();//物品的个数 int V=scanner.nextInt();//背包的体积 int[] v=new int[n+1];//存放物品的体积 int[] w=new int[n+1];//存放物品的价值 //让数组从第一个位置开始进行输入,到时候下标就不用-1了 for(int i=1;i<=n;i++){ v[i]=scanner.nextInt(); w[i]=scanner.nextInt(); } int[][] dp1=new int[n+1][V+1]; for(int i=1;i<=n;i++){ for(int j=1;j<=V;j++){ if(j-v[i]>=0) dp1[i][j]=dp1[i-1][j-v[i]]+w[i]; //如果不进行选择当前i位置的物品放入到背包里面 dp1[i][j]=Math.max(dp1[i][j],dp1[i-1][j]); //如果选择当前i位置的物品放入到背包里面 } } int[][] dp2=new int[n+1][V+1]; dp2[0][0]=0; for(int i=1;i<=V;i++) dp2[0][i]=-1; //此时一件商品都没有,是无法找到一个商品使背包的体积变成j的 //for(int i=1;i<=V;i++) dp2[i][0]=0;//此时啥也不选,就能使背包的体积变成0 for(int i=1;i<=n;i++){ for(int j=1;j<=V;j++){ if(j-v[i]>=0&&(dp2[i-1][j-v[i]]!=-1)) dp2[i][j]=dp2[i-1][j-v[i]]+w[i]; else dp2[i][j]=-1; //如果不进行选择当前i位置的物品放入到背包里面 dp2[i][j]=Math.max(dp2[i][j],dp2[i-1][j]); //如果选择当前i位置的物品放入到背包里面 } } System.out.println(dp1[n][V]); System.out.println(dp2[n][V]==-1?0:dp2[n][V]); //如果最后无法凑出体积是V的情况,那么dp[n][V]是等于-1,那么dp[n][V]=0 } }
四)利用滚动数组做空间上面的优化可能是常数级别+直接在原始的代码上稍加修改即可
dp[i][j]所依赖的是两部分的值dp[i][j]所进行依赖的是dp[i-1][j]和dp[i][j-v[i]],当我们在进行填写dp[i][j]的时候,只是需要dp[i-1][j]和dp[i-1][j-v[i]]的值即可,至于其他的值是不会关心的
1)当我们进行填写某一行的时候只是依赖于上一行的值,况且当前行填写完成之后,上一行就不需要再进行使用了
2)但是又仔细想了一下,开辟两个数组空间开销还是有点大,那么我们此时使用的方法就是使用一个数组来完成优化操作
3)只需要把所有的横坐标全部干掉,然后再改变填表顺序
import java.util.Scanner; import java.util.Arrays; // 注意类名必须为 Main, 不要有任何 package xxx 信息 public class Main { public static void main(String[] args) { Scanner scanner=new Scanner(System.in); int n=scanner.nextInt(); int V=scanner.nextInt(); int[] dp1=new int[V+1];//这一行既充当当前要进行填写的行的上一行也充当当前行 int[] v=new int[n+1];//存放物品的体积 int[] w=new int[n+1];//存放物品的价值 //让数组从第一个位置开始进行输入,到时候下标就不用-1了 for(int i=1;i<=n;i++){ v[i]=scanner.nextInt(); w[i]=scanner.nextInt(); } for(int i=1;i<=n;i++){ for(int j=V;j>=1;j--){//从后向前进行填表 if(j-v[i]>=0){ dp1[j]=Math.max(dp1[j-v[i]]+w[i],dp1[j](当dp[i]还没有更新之前上一行的状态)); } } } int[] dp2=new int[V+1]; dp2[0]=0; for(int i=1;i<=V;i++){ dp2[i]=-1; } for(int i=0;i<=n;i++){ for(int j=V;j>=1;j--){ if(j-v[i]>=0&&dp2[j-v[i]]!=-1) dp2[j]=Math.max(dp2[j-v[i]]+w[i],dp2[j]); } } System.out.println(dp1[V]); System.out.println(dp2[V]==-1?0:dp2[V]); } }
注意事项:
1)这道题是模板题,这个题的分析思路是可能会用到很多题里面
2)不要去强行解释优化的状态表示以及状态转移方程
二)分割等和子集
转换:在数组中选择一些数出来,让这些数的和等于sum/2即可
每一个数都面临着选或者是不选的情况,像遇到这样的问题就是0 1背包问题,相当于是数组下标是物品的编号,数组的数是物品的体积,背包的容量是sum/2
一)定义一个状态表示:
dp[i][j]表示在0-i区间内,是否能够找到和等于j的情况
dp[i][j]表示从前i个数中选择所有的选法中,看看和能否凑成j这个数,如果能够凑成这个数,那么最终返回的值就是true,否则返回的值就是false
二)根据状态表示推导状态转移方程:根据最后一步的状态来划分问题
1)如果选择i位置的值,那么dp[i][j]=dp[i-1][j-nums[i]],但是是有可能出现一种情况的,如果当前这个数的值已经大于了j,那么j-nums[i]是负数,那么此时这个值就不应该选了,所以说此时要满足条件j-nums[i]>=0;
2)如果不选择i位置的值,那么dp[i][j]=dp[i-1][j];
三)初始化:仍然新增加一行新增加一列,注意下表的映射关系和新增加的列要保证后续填表是正确的
dp[0][0]=true,dp[0][i]=false,dp[j][0]=true(什么都不选)
四)填表顺序:从上向下填写每一行
五)返回值:dp[nums.length][sum/2]
注意:将数组分成两部分,前提是能够保证sum/2是一个偶数,如果发现sum是一个奇数直接返回false
class Solution { public boolean canPartition(int[] nums) { //dp[i][j]表示是从0-i区间内选择一些数看看他们的和是否等于j int sum=0; for(int i=0;i
=0) dp[i][j]=dp[i-1][j-nums[i-1]]||dp[i][j]; } } return dp[nums.length][sum/2]; } } 六)空间优化
当我们进行填写当前行的时候,只是需要依赖于上一行的值,所以可以采取滚动数组的方式来做空间优化:
1)将所有的行去掉
2)修改遍历顺序
class Solution { public boolean canPartition(int[] nums) { //dp[i][j]表示是从0-i区间内选择一些数看看他们的和是否等于j int sum=0; for(int i=0;i
=1;j--){ if(j-nums[i-1]>=0) dp[j]=dp[j-nums[i-1]]||dp[j]; } } return dp[sum/2]; } }
三)目标和
494. 目标和 - 力扣(LeetCode)
1)我们可以将整个数组种的数填上正数,填上负数,划分成两部分
我们设所有正数的和是a,所有的负数的绝对值得和是b(b>0),那么此时a-b=target,问的是一共有多少种分法?
a-b=target,a+b=sum,所以a=(target+sum)/2;
2)现在我们就可以将问题转化成在数组中挑选出一些数,让这些数的和等于(target+sum)/2即可,问:一共有多少种挑法?
3)我们可以在数组中从左向右挑选出来一些数,每一个都是可以挑,或者是不挑的,也就是说数组的这个是可以选择的,当然也是可以不需要进行选择的,每一个数都会面临着选或者是不选,让所有元素的和等于a
4)现在数组的下标是每一个物品的编号,数组下标对应的值是物品的体积,背包的容量是a
一)定义一个状态表示:
dp[i][j]表示从数组中的0-i区间内选择一些数,让他们的和恰好等于j的选择方法的个数
背包问题都是根据最后一个位置是否选择来进行划分问题
二)根据状态标识推到状态转移方程
1)如果选择最后一个位置的元素,那么dp[i][j]=dp[i-1][j-nums[i]]
1.1)这个状态转移方程后面是否需要加nums[i]或者是1呢?
1.2)如果当前nums[i]的值太大导致已经超过了j,那么当前位置是不可以进行选择的,所以要加上一个限制条件j-nums[i]>=0,我们当前进行考虑的是一共有多少种选择方法,当前我们只是需要找到有多少种数组合的和等于j-nums[i]即可,后面再跟上一个数nums[i]即可,多的仅仅是每一种选法后面多了一个元素;
2)如果不选择最后一个位置的元素,那么dp[i][j]=dp[i-1][j]
3)dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]]
三)初始化
1)注意下标的映射关系
2)仍然和之前一样是多加一行多加一列
dp[0][0]=1(啥也不选,就是1种方法)
dp[0][i]=0(连商品都没有无法选择)
dp[i][0]=从0-i区间内选,找到目标和为0的组合方法,但是翻一翻leetcode的nums[i]的题目的给定输入范围,nums[i]是可以等于0的,所以dp[i][0]是不一定等于0的,所以初始化时不方便的,但是我们不需要进行初始化,第一列除了(0,0)位置不用初始化
3)原因就是j-nums[i]>=0,因为j此时等于0,那么nums[i]要满足等于0的时候才能进入到状态转移方程里面dp[i][j]=dp[i-1][0],所以我们不需要初始化第一列的值,只需要放到填表逻辑里面就可以了,因为数组根本不会出现越界访问的情况,只会使用到上一行的值
四)填表顺序:从上向下
五)返回值:返回值dp[n][a]
class Solution { public int findTargetSumWays(int[] nums, int target) { int sum=0; for(int i=0;i
=0) dp[i][j]=dp[i-1][j-nums[i-1]]; dp[i][j]=dp[i][j]+dp[i-1][j]; } } return dp[nums.length][a]; } }
四)最后一块石头的重量
1049. 最后一块石头的重量 II - 力扣(LeetCode)
下面是最后一块石头的重量1
class Solution { public int lastStoneWeight(int[] stones) { //1.首先建立一个大堆 PriorityQueue
queue= new PriorityQueue<>(3, new Comparator () { @Override public int compare(Integer o1, Integer o2) { return o2-o1; } }); //2.将所有数字存放进去 for(int i=0;i 1){ a=queue.poll(); b=queue.poll(); result=Math.abs(a-b); if(result==0) continue; queue.offer(result); } return queue.isEmpty()?0:queue.poll(); } }
1)可以将石头分成两堆,一堆石头是正数,另一堆石头是负数,所有正数的和是a,所有负数的绝对值的和是b,所以我们进行寻找的是a-b的绝对值之和最小,将这堆数分成两部分,一部分添加正号,一部份数添加负号,最后求出这两堆数的最小值,a+b=sum
2)我们假设a>b,为什么可以假设a>b呢?因为如果a但是我们可以把这堆负数加上-号扔到正数位置,把正数加上一个-号扔到负数位置
3)这个题就转化成了选一堆数是正数,选一堆数做负数,是正数和-负数和的绝对值之差最小
小学的时候做过一些题,将两个数进行拆分,使拆分之后两个数的差的绝对值最小
4)从上面我们就可以看出,当两个数的差最接近9的一半的时候,两个数的绝对值之差最小,在转化:数组中选择一些数,让这些数的和尽可能接近总和的一半,这个题从本质上来说才是一个0 1背包问题,在这些所有的数中,每一个数都可以是选或者是不选
5)我们将数组的下标当作使物品的编号,把数组下标对应的值当成是物品的体积,物品的价值是nums[i],sum/2就是背包的容量,就是选一些物品,使得总体积不超过sum/2,尽可能地将背包装满,选一些数作为商品放到背包里面,背包是有容量的,容量是sum/2,在不超过sum/2的情况下,里面的最大价值是多少,背包装满的同时,体积和价值都在变大
定义一个状态表示+根据状态标识推导状态状态转移方程
dp[i][j]表示从前i个数中进行选择,总和不超过j,此时的最大和
class Solution { public int lastStoneWeightII(int[] stones) { //dp[i][j]表示从前i个数中进行挑选,找到和为j的最大和 int sum=0; for(int i=0;i
=0) dp[i][j]=dp[i-1][j-stones[i-1]]+stones[i-1]; dp[i][j]=Math.max(dp[i][j],dp[i-1][j]); } } System.out.println(dp[stones.length][sum/2]); return sum-2*dp[stones.length][sum/2]; } }