01背包问题------动态规划

你有一个背包,地上一堆物品,挑选一些物品放入背包中,最大能够挑选出来的价值是多少

背包可以装满,背包也是可以不必都装满

01背包问题------动态规划_第1张图片

 一)01背包问题

【模板】01背包_牛客题霸_牛客网 (nowcoder.com)

01背包问题------动态规划_第2张图片

01背包问题------动态规划_第3张图片

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]

01背包问题------动态规划_第4张图片

01背包问题------动态规划_第5张图片

三:初始化+填表顺序+返回值:

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

01背包问题------动态规划_第6张图片

01背包问题------动态规划_第7张图片 三)初始化:

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]]的值即可,至于其他的值是不会关心的

01背包问题------动态规划_第8张图片

01背包问题------动态规划_第9张图片

 01背包问题------动态规划_第10张图片

01背包问题------动态规划_第11张图片

1)当我们进行填写某一行的时候只是依赖于上一行的值,况且当前行填写完成之后,上一行就不需要再进行使用了

2)但是又仔细想了一下,开辟两个数组空间开销还是有点大,那么我们此时使用的方法就是使用一个数组来完成优化操作

3)只需要把所有的横坐标全部干掉,然后再改变填表顺序

01背包问题------动态规划_第12张图片

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;i1){
      a=queue.poll();
      b=queue.poll();
      result=Math.abs(a-b);
      if(result==0) continue;
      queue.offer(result);
  }
 return queue.isEmpty()?0:queue.poll();
    }
}

01背包问题------动态规划_第13张图片

01背包问题------动态规划_第14张图片

1)可以将石头分成两堆,一堆石头是正数,另一堆石头是负数,所有正数的和是a,所有负数的绝对值的和是b,所以我们进行寻找的是a-b的绝对值之和最小,将这堆数分成两部分,一部分添加正号,一部份数添加负号,最后求出这两堆数的最小值,a+b=sum

2)我们假设a>b,为什么可以假设a>b呢?因为如果a但是我们可以把这堆负数加上-号扔到正数位置,把正数加上一个-号扔到负数位置

3)这个题就转化成了选一堆数是正数,选一堆数做负数,是正数和-负数和的绝对值之差最小

小学的时候做过一些题,将两个数进行拆分,使拆分之后两个数的差的绝对值最小

01背包问题------动态规划_第15张图片

4)从上面我们就可以看出,当两个数的差最接近9的一半的时候,两个数的绝对值之差最小,在转化:数组中选择一些数,让这些数的和尽可能接近总和的一半,这个题从本质上来说才是一个0 1背包问题,在这些所有的数中,每一个数都可以是选或者是不选

01背包问题------动态规划_第16张图片

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];
    }
}

你可能感兴趣的:(动态规划,算法)