0-1背包问题

分数背包问题可以用贪心算法来求解,而0-1背包问题则需要用动态规划方法求解。

问题描述:

    假设我们有n件物品,分别编号为1, 2...n。其中编号为i的物品价值为vi,它的重量为wi。为了简化问题,假定价值和重量都是整数值。现在,假设我们有一个背包,它能够承载的重量是W。现在,我们希望往包里装这些物品,使得包里装的物品价值最大化,那么我们该如何来选择装的东西呢?

问题解答:

    我们需要选择n个元素中的若干个来形成最优解,假定为k个。那么对于这k个元素a1, a2, ...ak来说,它们组成的物品组合必然满足总重量<=背包重量限制,而且它们的价值必然是最大的。因为它们是我们假定的最优选择嘛,肯定价值应该是最大的。假定ak是我们按照前面顺序放入的最后一个物品。它的重量为wk,它的价值为vk。既然我们前面选择的这k个元素构成了最优选择,如果我们把这个ak物品拿走,对应于k-1个物品来说,它们所涵盖的重量范围为0-(W-wk)。假定W为背包允许承重的量。假定最终的价值是V,剩下的物品所构成的价值为V-vk。这剩下的k-1个元素是不是构成了一个这种W-wk的最优解呢?

    我们可以用反证法来推导。假定拿走ak这个物品后,剩下的这些物品没有构成W-wk重量范围的最佳价值选择。那么我们肯定有另外k-1个元素,他们在W-wk重量范围内构成的价值更大。如果这样的话,我们用这k-1个物品再加上第k个,他们构成的最终W重量范围内的价值就是最优的。这岂不是和我们前面假设的k个元素构成最佳矛盾了吗?所以我们可以肯定,在这k个元素里拿掉最后那个元素,前面剩下的元素依然构成一个最佳解。

    现在我们经过前面的推理已经得到了一个基本的递推关系,就是一个最优解的子解集也是最优的。可是,我们该怎么来求得这个最优解呢?我们这样来看。假定我们定义一个函数c[i, w]表示到第i个元素为止,在限制总重量为w的情况下我们所能选择到的最优解。那么这个最优解要么包含有i这个物品,要么不包含,肯定是这两种情况中的一种。如果我们选择了第i个物品,那么实际上这个最优解是c[i - 1, w-wi] + vi。而如果我们没有选择第i个物品,这个最优解是c[i-1, w]。这样,实际上对于到底要不要取第i个物品,我们只要比较这两种情况,哪个的结果值更大不就是最优的么?

    在前面讨论的关系里,还有一个情况我们需要考虑的就是,我们这个最优解是基于选择物品i时总重量还是在w范围内的,如果超出了呢?我们肯定不能选择它,这就和c[i-1, w]一样。

    另外,对于初始的情况呢?很明显c[0, w]里不管w是多少,肯定为0。因为它表示我们一个物品都不选择的情况。c[i, 0]也一样,当我们总重量限制为0时,肯定价值为0。

    这样,基于我们前面讨论的这3个部分,我们可以得到一个如下的递推公式:

    有了这个关系,我们可以更进一步的来考虑代码实现了。我们有这么一个递归的关系,其中,后面的函数结果其实是依赖于前面的结果的。我们只要按照前面求出来最基础的最优条件,然后往后面一步步递推,就可以找到结果了。

    我们再来考虑一下具体实现的细节。这一组物品分别有价值和重量,我们可以定义两个数组int[] v, int[] w。v[i]表示第i个物品的价值,w[i]表示第i个物品的重量。为了表示c[i, w],我们可以使用一个int[i][w]的矩阵。其中i的最大值为物品的数量,而w表示最大的重量限制。按照前面的递推关系,c[i][0]和c[0][w]都是0。而我们所要求的最终结果是c[n][w]。所以我们实际中创建的矩阵是(n + 1) x (w + 1)的规格。下面是该过程的一个代码参考实现:

public class DynamicKnapSack {  
    private int[] v;  
    private int[] w;  
    private int[][] c;  
    private int weight;  
  
    public DynamicKnapSack(int length, int weight, int[] vin, int[] win) {  
        v = new int[length + 1];  
        w = new int[length + 1];  
        c = new int[length + 1][weight + 1];  
        this.weight = weight;  
        for(int i = 0; i < length + 1; i++) {  
            v[i] = vin[i];  
            w[i] = win[i];  
        }  
    }  
  
    public void solve() {  
       for(int i = 1; i < v.length; i++) {  
            for(int k = 1; k <= weight; k++) {  
                if(w[i] <= k) {  
                    if(v[i] + c[i - 1][k - w[i]] > c[i - 1][k])  
                        c[i][k] = v[i] + c[i - 1][k - w[i]];  
                    else  
                        c[i][k] = c[i - 1][k];  
                } else  
                    c[i][k] = c[i - 1][k];  
            }  
        }  
    }  
  
    public void printResult() {  
        for(int i = 0; i < v. length; i++) {  
            for(int j = 0; j <= weight; j++)  
                System.out.print(c[i][j] + " ");  
            System.out.println();  
        }  
    }  
      
    public static void main(String[] args) {  
        int[] v = {0, 60, 100, 120};  
        int[] w = {0, 10, 20, 30};  
        int weight = 50;  
        DynamicKnapSack knapsack = new DynamicKnapSack(3, weight, v, w);  
        knapsack.solve();  
        knapsack.printResult();  
    }  
}  

 

类似地,leetcode上第416题Partition Equal Subset Sum可以用0-1背包的思想来解决。

问题描述:

Given a non-empty array containing only positive integers, find if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Note:

  1. Each of the array element will not exceed 100.
  2. The array size will not exceed 200.

 

Example 1:

Input: [1, 5, 11, 5]

Output: true

Explanation: The array can be partitioned as [1, 5, 5] and [11].

 

Example 2:

Input: [1, 2, 3, 5]

Output: false

Explanation: The array cannot be partitioned into equal sum subsets.

 问题解答:

本题与0-1背包问题类似。用数组dp[i][j]来表示体积不超过j时的前i个元素的最大和。可得出递推公式为dp[i][j] = Math.max(dp[i - 1][j - nums[i]] + nums[i], dp[i -1][j]), 其中j >= nums[i].当dp[i][sum] = sum,即背包装满时,则返回true。

仔细分析发现,在本题情况中,每次循环dp[i][j]只与上一层循环得到的dp[i -1][x]有关,故只需要一维数组,每次更新数组值即可。具体代码如下。注意第二层循环中j要从大到小遍历,原因是在更新数组的过程中避免使用到当次更新的数组元素。

public class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        if (sum % 2 != 0) return false; 
        sum /= 2;
        int[] dp = new int[sum + 1];
        for (int i = 0; i < nums.length; i++) {
            for (int j = sum; j >= nums[i]; j--) {
                dp[j] = Math.max(dp[j - nums[i]] + nums[i], dp[j]);
            }
        }
        return dp[sum] == sum;
    }
}

问题优化:

本问题只需要知道是否有元素和为sum,因此可以采用boolean数组dp[j]来记录是否有和为sum的情况存在。可得到递推公式为dp[j] = dp[j] || dp[j - nums[i]], 其中j >= nums[i].

public class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
        if (sum % 2 != 0) return false; 
        sum /= 2;
        boolean[] dp = new boolean[sum + 1];
        dp[0] = true;
        for (int i = 0; i < nums.length; i++) {
            for (int j = sum; j >= nums[i]; j--) {
                dp[j] = dp[j] || dp[j - nums[i]];
                if (dp[sum]) return true;
            }
        }
        return false;
    }
}

这种情况下采取boolean运算会比上述解法采用数学运算速度快一些。

posted on 2016-11-03 18:34  ShinningWu  阅读(1622)  评论(0编辑  收藏  举报

导航