0-1背包问题

本文详细介绍了经典的01背包问题,通过动态规划解决,包括状态转移方程和Java实现。讨论了空间复杂度的优化,以及如何处理要求背包恰好装满的变种问题。此外,还提及了华为机试中的购物车问题,展示了解题思路。
摘要由CSDN通过智能技术生成

背包问题题目描述

有 N 件物品和一个容量为 V 的背包,第 i 件物品的体积是c[i],价值是w[i],忽略物体几何体积,认为只要背包剩余容量大于物品体积就能放进背包,求将哪些物品装入背包可使价值总和最大。

问题解析

非常常见的动态规划问题,对于所有的动态规划问题,核心就是状态转移方程。

第一步都是确定状态。定义状态 dp[i][j] 是表示目前正在枚举第 i 个物品,目前已取的总体积为 j ,最大价值为 dp[i][j]

第二步是找状态转移方程。物品只有拿和不拿两种选择
(1):假如不拿这个物品,那么 dp[i][j] 肯定是能从上一个物品,同样体积转移过来的,所以 dp[i][j] = dp[i−1][j]
(2):假如我们取这个物品,那么这个物品上一个就是 i - 1 ,上一个物品的体积为 j − c[i] ,对 dp[i][j] 来说,它就从 *dp[i − 1][j − c [i]]*转移而来,由于拿了这个物品,所以还要加上这个物品的价值 w[i]w[i]

所以最终01背包的状态转移方程如下:
(1)j >= c[i]
j>=c[i]时,dp[i][j]=max(dp[i−1][j],dp[i−1][j−c[i]]+w[i])
(1)j < c [i]
dp[i][j] = dp[i−1][j]

思路:

for (int i = 1; i <= N; i++){
	for (int j = V; j >= 0; j--){
		if (j >= w[i]){//如果背包装得下当前的物体
			dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
		}
		else{//如果背包装不下当前物体
			dp[i][j] = dp[i-1][j];
		}
	}
}

Java实现

import java.util.Scanner;
 
/**
 * 使用二维数组非递归的方法求解0/1背包问题
 */
public class ZeroOnePack {
    // N表示物体的个数,V表示背包的载重
    int N,V;
    //用于存储每个物体的重量,下标从1开始
    private int[] weight;
    //存储每个物体的收益,下标从1开始
    private int[] value;
    //二维数组,用来保存每种状态下的最大收益
    private int[][] F;
 
    /**
     * 使用非递归方式,求解F[0 .. N][0 .. V],即for循环从下至上求解
     */
    public void ZeroOnePackNonRecursive() {
        //对二维数组F进行初始化
        for(int j = 0; j <= V; j++) {
            F[0][j] = 0;
        }
 
        //注意边界问题,i是从1开始的,j是从0开始的
        //因为F[i - 1][j]中i要减1
        for(int i = 1; i <= N; i++) {
            for(int j = 0; j <= V; j++) {
                //如果容量为j的背包放得下第i个物体
                if(j >= weight[i]) {
                    F[i][j] = Math.max(F[i - 1][j - weight[i]] + value[i], F[i - 1][j]);
                }else {
                    //放不下,只能选择不放第i个物体
                    F[i][j] = F[i - 1][j];
                }
            }
        }
 
        //打印所有结果,我们要求的是F[N][V]
        for(int i = 0; i <= N; i++) {
            for(int j = 0; j <= V; j++) {
                System.out.print(F[i][j] + " ");
            }
            System.out.println();
        }
    }
 
 
    /**
     * 求解F[n][m]这个最优值具体选取哪几样物品能获得最大价值,但只会输出一种情况
     * @param n     表示前n个物体,n <= N
     * @param v     表示背包的容量,v <= V
     */
    public void printResult(int n, int v) {
        boolean[] isAdd = new boolean[n + 1];
 
        for(int i = n; i >= 1; i--) {
            if(F[i][v] == F[i-1][v])
                isAdd[i] = false;
            else {
                isAdd[i] = true;
                v -= weight[i];
            }
        }
 
        for(int i = 1; i <= n; i++) {
            System.out.print(isAdd[i] + " ");
        }
        System.out.println();
    }
 
    /**
     * 输入格式:
     5 10
     2 2 6 5 4
     6 3 5 4 6
     * result:15
     * 第一行是物体个数、背包总空间;
     * 第二行是每个物体的空间;
     * 第三行是每个物体的收益。
     */
    public void init() {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        V = sc.nextInt();
 
        //下标从1开始,表示第1个物品
        weight = new int[N + 1];
        value = new int[N + 1];
        F= new int[N + 1][V + 1];//注意是 N + 1,因为需要一个初始状态F[0][0],表示前0个物品放进空间为0的背包的最大收益
 
        for(int i = 1; i <= N; i++) {
            weight[i] = sc.nextInt();
        }
 
        for(int i = 1; i <= N; i++) {
            value[i] = sc.nextInt();
        }
    }
 
    public static void main(String[] args) {
        ZeroOnePack zop = new ZeroOnePack();
        zop.init();
        zop.ZeroOnePackNonRecursive();
        zop.printResult(zop.N,zop.V);
    }
}

时间、空间复杂度

时间复杂度:O(N*V)

空间复杂度:O((N+1)*(V+1))

变化1:空间优化

从1到 i 个物品,每次用到的只有 i - 1 行数据。

如果只定义一个数组 F[v] 来表示当前占用体积 V 下的最大价值。每次我们刚要进行第 i 个物品的遍历时,此时存的就是第 i - 1 物品的遍历结果,我们只需利用这些数据获得第 i 个物品的遍历结果然后覆盖即可。

并且,由于是一行数据,我们每一行只需要遍历 [ c[i] , V ]即可,因为此时数据存的就是 i - 1 个物品的数据。

但是,从遍历来看,我们必须采用逆序遍历,因为我们必须保证,我们在运算过程中,取到的一定是第 i - 1 个物品的数据。

如果正序遍历:假设第 i 个物品A价值100,体积为2,正在进行第 i 次外层循环,假设第i-1次的F[v]全为0,按照[0 … V]的顺序遍历,那么v = 2时,F[2] = max(F[2], F[0] + 1000) = 1000;当j = 4时,F[4] = max(F[4], F[2] + 1000) = 2000,此时max用到的F[2]是已经更新过的,所以正序遍历会造成错误的结果。
如果是逆序遍历的话,上述情况不会发送,因为我们每次用到的都是比V小的下标。

缺点:空间优化的方法只能知道最大价值是多少,而不知道是哪些物品组成的。

思路:

F[0..V] = 0;
for i = 1 to N
	for j = V to c[i]
		F[j] = max{F[j],F[j-c[i]] + w[i]}

Java实现:

import java.util.Scanner;
/**
 * 0/1背包的降维,将空间复杂度降为O(V)
 */
public class ZeroOnePackExtend1 {
    // N表示物体的个数,V表示背包的载重
    int N,V;
    //用于存储每个物体的重量,下标从1开始
    private int[] weight;
    //存储每个物体的收益,下标从1开始
    private int[] value;
    //降成一维数组,用来保存每种状态下的最大收益
    private int[] F;
 
    /**
     * 使用非递归方式
     */
    public void ZeroOnePackNonRecursive() {
        //对一维数组F进行初始化
        for(int i = 0; i <= V; i++) {
            F[i] = 0;
        }
 
        //注意边界问题,i是从1开始的
        for(int i = 1; i <= N; i++) {
            for(int j = V; j >= 0; j--) {
                //降序遍历
                if(j >= weight[i]) {
                    F[j] = Math.max(F[j - weight[i]] + value[i], F[j]);
                }else {
                    //可以省略
                    F[j]= F[j];
                }
            }
        }
 
        //打印所有结果,我们要求的是F[V]
        for(int i = 0; i <= V; i++) {
            System.out.print(F[i] + " ");
        }
        System.out.println();
    }
 
 
    /**
     * 输入格式:
     5 10
     2 2 6 5 4
     6 3 5 4 6
     * result:15
     * 第一行是物体个数、背包总空间;
     * 第二行是每个物体的空间;
     * 第三行是每个物体的收益。
     */
    public void init() {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        V = sc.nextInt();
 
        //下标从1开始,表示第1个物品
        weight = new int[N + 1];
        value = new int[N + 1];
        F= new int[V + 1];//注意是 V + 1
 
        for(int i = 1; i <= N; i++) {
            weight[i] = sc.nextInt();
        }
 
        for(int i = 1; i <= N; i++) {
            value[i] = sc.nextInt();
        }
    }
 
    public static void main(String[] args) {
        ZeroOnePackExtend1 zope1 = new ZeroOnePackExtend1();
        zope1.init();
        zope1.ZeroOnePackNonRecursive();
    }
}

变化2:是否要求全部装满

如果是不要求全部装满,那么则是上述思路。

如果要求全部装满,不同点在于初始化的时候把第 0 个物品的F[V] =[0,-INF,-INF,…]
原来的下标表示物品用了多少体积,此时我们可以把下标理解成此时背包恰巧有这么多体积。当第0个物品时,只有背包体积为0符合条件“恰好装满”,其余均不符合,为负无穷,这样遍历下去,只要F[V]不为负无穷,那么它代表的值一定是装满该下标所代表体积的情况下的。

Java实现:

import java.util.Scanner;
/**
 * 0/1背包问题变种:求恰好装满背包的最优解
 */
public class ZeroOnePackExtend2 {
    // N表示物体的个数,V表示背包的载重
    int N,V;
    //用于存储每个物体的重量,下标从1开始
    private int[] weight;
    //存储每个物体的收益,下标从1开始
    private int[] value;
    //降成一维数组,用来保存每种状态下的最大收益
    private int[] F;
    int INF = 9999;
    
    /**
     * 使用非递归方式
     */
    public void ZeroOnePackNonRecursive() {
        //仅在初始化有区别
        F[0] = 0;
        for(int i = 1; i <= V; i++) {
            F[i] = -INF;
        }
 
        //注意边界问题,i是从1开始的
        for(int i = 1; i <= N; i++) {
            for(int j = V; j >= 0; j--) {
                //降序遍历
                if(j >= weight[i]) {
                    F[j] = Math.max(F[j - weight[i]] + value[i], F[j]);
                }else {
                    //可以省略
                    F[j]= F[j];
                }
            }
        }
 
        //打印所有结果,我们要求的是F[V]
        for(int i = 0; i <= V; i++) {
            System.out.print(F[i] + " ");
        }
        System.out.println();
    }
 
    /**
     * 输入格式:
     5 10
     2 2 6 5 4
     6 3 5 4 6
     * result:14
     * 第一行是物体个数、背包总空间;
     * 第二行是每个物体的空间;
     * 第三行是每个物体的收益。
     */
    public void init() {
        Scanner sc = new Scanner(System.in);
        N = sc.nextInt();
        V = sc.nextInt();
 
        //下标从1开始,表示第1个物品
        weight = new int[N + 1];
        value = new int[N + 1];
        F= new int[V + 1];//注意是 V + 1
 
        for(int i = 1; i <= N; i++) {
            weight[i] = sc.nextInt();
        }
 
        for(int i = 1; i <= N; i++) {
            value[i] = sc.nextInt();
        }
    }
 
    public static void main(String[] args) {
        ZeroOnePackExtend2 zope2 = new ZeroOnePackExtend2();
        zope2.init();
        zope2.ZeroOnePackNonRecursive();
    }
}

其他变化:放入物品有限制条件

华为机试:购物车问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值