【背包问题】01背包问题

一、01背包问题

1.1 题目

N N N件物品和一个容量为 V V V的背包,放入第 i i i件物品耗费的费用是 C i C_i Ci,得到的价值是 W i W_i Wi。求解将哪些物品装入背包可使得价值总和最大。

1.2 基本思路

01背包是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或者不放。
用子问题定义状态:即 F [ i , v ] F[i,v] F[i,v] 表示前 i i i 件物品恰放入一个容量为 v v v 的背包可以获得
的最大价值。则其状态转移方程便是:
F [ i , v ] = m a x { F [ i − 1 , v ] , f [ i − 1 , v − C i ] + W i } F[i,v]=max\{F[i-1,v],f[i-1,v-C_i]+W_i\} F[i,v]=max{F[i1,v],f[i1,vCi]+Wi}
对于第 i i i件物品来说,它只有放入背包和不放入背包两种选择。其中, m a x { } max\{\} max{}中的两个元素表示的含义分别为:

  • F [ i − 1 , v ] F[i-1,v] F[i1,v]:第 i i i件物品不放入背包
  • f [ i − 1 , v − C i ] + W i f[i-1,v-C_i]+W_i f[i1,vCi]+Wi:第 i i i件物品放入背包

如果对这个状态方程还不是很理解,那我们可以举个例子画图说明。假设背包容量为10,物品数量为4,每种物品的花费与价值如下:

花费/重量价值
21
33
45
79

这里用 w [ i ] w[i] w[i]来表示第 i i i 件物品的重量, v [ i ] v[i] v[i]表示第 i i i 件物品的价值。
那么我们首先建立一个二维的dp数组用来保存状态信息,然后依次枚举所有状态数据:
在这里插入图片描述
注意我涂了颜色的格子:

  • 红色格子:此时背包容量 j = 2 j=2 j=2,此时只有1号物品可以选择,该物品重量为2,价值为1,刚好可以放入该容量为2的背包,因此 d p [ 1 ] [ 2 ] = 1 dp[1][2]=1 dp[1][2]=1。这一行后面的数据自然也为1,因为背包容量一直在增加,而可选择的物品还是只有一个。
  • 橙色格子:此时背包容量为3,前两件物品中,只有将2号物品放入背包时价值最大,故 d p [ 2 ] [ 3 ] = 3 dp[2][3]=3 dp[2][3]=3
  • 绿色格子:此时背包容量变为5,刚好可以容纳前两件物品,此时可以将前两件物品同时放入背包,获得最大价值4,即 d p [ 2 ] [ 5 ] = 4 dp[2][5]=4 dp[2][5]=4

根据以上分析,我们可以得到以下结论:
在这里插入图片描述

有了状态方程,接下来我们用代码实现一下:

public class ZeroOnePack {
    public static int method(int V, int N, int[] weight, int[] value){
        //这里的dp数组就是对应于上面定义的F
        int[][] dp = new int[N + 1][V + 1];
        //这里索引从1开始取,即dp[1][v]表示的是将第1个物品放入容量为v的背包的最大价值
        for (int i = 1; i < N + 1; i++){
            for (int j = 1; j < V + 1; j++){
                //如果第i件物品的花费大于背包容量j,则不装入背包
                //由于weight和value数组下标都是从0开始,故第i个物品的花费为weight[i-1],价值为value[i-1]
                if (weight[i - 1] > j){
                    dp[i][j] = dp[i - 1][j];
                }else{
                    //上文提出的状态转移方程
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                }
            }
        }
        return dp[N][V];
    }
 }

我们以华为的笔试题作为例子来测试一下结果,题目如下:


小朋友考试得到第一名可以得到奖励零食,现有价格 A , B , C , D , E . . . A,B,C,D,E... A,B,C,D,E...元商品各 A 1 , B 1 , C 1 , D 1 , E 1... A1,B1,C1,D1,E1... A1,B1,C1,D1,E1...个,小朋友的喜爱度依次为 A 2 , B 2 , C 2 , D 2 , E 2 , . . . . . A2,B2,C2,D2,E2,..... A2,B2,C2,D2,E2,.....请返回选取 X X X元零食可达到的最大喜爱度。
第一行输入为 X X X N N N X X X为可使用 钱的总额, N N N为零食种类。
第二行开始为零食属性,每行为3个整型数,分别代表零食价格,数量以及喜爱度。
输入

钱总额零食种类
67
价格数量喜爱度
318
412
311
917
411
418
414

输出:9
解释:6元可以分别选取价格3元喜爱度为8的商品1件以及3元喜爱度为1的商品一件,喜爱度总和为9。


对于这个问题,我们暂时不考虑数量的限制,去掉数量限制后,这题就是一个01背包问题。那么我们直接调用上面写的方法即可解决该问题,代码如下:

public class Main {
    public static void main(String[] args) {
        int[] weight = {3,4,3,9,4,4,4};
        int[] value = {8,2,1,7,1,8,4};
        int V = 6;
        int N = 7;
        System.out.println(ZeroOnePack.method(V, N, weight, value));
    }
}

输出:9

以上实现01背包问题的算法其实还可以优化空间复杂度。请注意我们在推导dp数组中每一行的值时,其实只用到了dp数组中上一行的值,那么我们没有必要将所有的状态值都保存起来,我们只需用一个一维数组保存上一行的状态值即可。如下图:
在这里插入图片描述
我们用一维数组 d p [ j ] dp[j] dp[j]来表示背包容量为 j j j时的最大价值。此时
在这里插入图片描述
上述代码变为:
在这里插入图片描述
从状态转移方程中可以看出, d p [ j ] dp[j] dp[j]是由 d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]推导出来的,准确来说,这里的 d p [ j ] dp[j] dp[j]对应于二维数组中第 i i i行第 j j j列的数据,而 d p [ j − w [ i ] ] dp[j-w[i]] dp[jw[i]]对应于第 i − 1 i-1 i1行第 j − w [ i ] j-w[i] jw[i]的数据。而一维数组 d p [ j ] dp[j] dp[j]在推导第 i i i行状态数据之前的初始值就是第 i − 1 i-1 i1行的数据,因此我们在写代码时,遍历一维数组 d p [ j ] dp[j] dp[j]时,必须要从后往前遍历

	// 01背包的优化
	public static int method2(int V,int N,int[] weight,int[] value){
		int[] dp = new int[V+1];
		for(int i=1;i<N+1;i++){
			//遍历dp[]数组时,从后往前遍历,并顺便过滤掉容量不足的情况
			for(int j=V;j>=weight[i-1];j--){
				dp[j] = Math.max(dp[j-weight[i-1]]+value[i-1],dp[j]);
			}
		}
		return dp[V];		
	}

参考文献:
[1] https://github.com/tianyicui/pack

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值