背包问题和01背包问题,贪心和动态规划

背包问题:

让我们来计算一下每件物品的性价比,其结果如下:在这里插入图片描述
毫无疑问,此时性价比最高的是物品4,我们把物品4放入背包当中,背包剩余的容量是8:在这里插入图片描述
我们选择物品1放入背包,背包剩余的容量是4:在这里插入图片描述
于是,我们选择0.8份的物品5放入背包,背包剩余的容量为0:在这里插入图片描述
代码实现

   public static void main(String[] args) {
        int capacity = 10;
        int[] weights = {4,6,3,2,5};
        int[] values = {9,3,1,6,5};
        System.out.println("背包最大价值:" + getHighestValue(capacity, weights, values));
    }

    public static double getHighestValue(int capacity, int[] weights,int[] values){

        //创建物品列表并按照性价比倒序
        List<Item> itemList = new ArrayList<>();
        for(int i=0;i<weights.length;i++){
            itemList.add(new Item(weights[i], values[i]));
        }
        itemList = itemList.stream().sorted(Comparator.comparing(Item::getRatio).reversed()).collect(Collectors.toList());

        //背包剩余容量
        int restCapacity = capacity;
        //当前背包物品的最大价值
        double highestValue = 0;

        //按照性价比从高到低选择物品
        for(Item item : itemList){
            if(item.weight <= restCapacity){
                highestValue += item.value;
                restCapacity -= item.weight;
            }else{
                //背包装不下完整物品时,选择该件物品的一部分
                highestValue += (double)restCapacity/(double)item.weight * item.value;
                break;
            }
        }

        return highestValue;
    }

    static class Item {
        private int weight;
        private int value;
        //物品的性价比
        private double ratio;

        public Item (int weight, int value){
            this.weight = weight;
            this.value = value;
            this.ratio = (double)value / (double)weight;
        }

        public double getRatio() {
            return ratio;
        }
    }

01背包问题:

仍然给定一个容量是10的背包,有如下三个物品可供选择:
在这里插入图片描述
这一次我们有个条件限制:只允许选择整个物品,不能选择物品的一部分。

如果按照贪心算法的思路,首先选择的是性价比最高的物品1,那么背包剩余容量是4,再也装不下其他物品,而此时的总价值是6:
在这里插入图片描述
但这样的选择,真的能让总价值最大化吗?如果我们不选择物品1,选择物品2和物品3的话,剩余容量是0,总价值是7:在这里插入图片描述
显然,7>6,依靠贪心算法得出的结果,未必是全局最优解。

使用动态规划代码实现如下:

public class BeiBao01 {    
    public int maxValue(int[] weight, int[] value, int W) {
      //这里假定传入的weight和values数组长度总是一致的
        int n = weight.length;
        if (n == 0) return 0;int[][] dp = new int[n + 1][W + 1];
        for (int i = 1; i <= n; i++) {
            for (int k = 1; k <= W; k++) {
               // 存放 i 号物品(前提是放得下这件物品)
               int valueWith_i = (k-weight[i-1] >= 0) ? (value[i-1]+dp[i-1][k-weight[i-1]]) : 0;
               // 不存放 i 号物品
               int valueWithout_i = dp[i - 1][k];
               dp[i][k] = Math.max(valueWith_i, valueWithout_i);
            }
        }return dp[n][W];
    }
  
    public static void main(String[] args) {
        BeiBao01 obj = new BeiBao01();
        int[] w = {1, 4, 3};
        int[] v = {15, 30, 20};
        int W = 4;
        System.out.println(obj.maxValue(w, v, W));
    }
}

下面实现的版本稍有不同:

    public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;int[][] dp = new int[n][W + 1];
        // 先初始化第 0 行,也就是尝试把 0 号物品放入容量为 k 的背包中
        for (int k = 1; k <= W; k++) {
            if (k >= weight[0]) dp[0][k] = value[0];
            else dp[0][k] = 0; // 这一步其实没必要写,因为dp[][]数组默认就是0
        }for (int i = 1; i < n; i++) {
            for (int k = 1; k <= W; k++) {
                // 存放 i 号物品(前提是放得下这件物品)
                int valueWith_i = (k-weight[i] >= 0) ? (value[i] + dp[i-1][k-weight[i]]) : 0;
                // 不存放 i 号物品
                int valueWithout_i = dp[i-1][k];
                dp[i][k] = Math.max(valueWith_i, valueWithout_i);
            }
        }return dp[n-1][W];
    }

在这里插入图片描述
(个人更喜欢第二种实现方式,感觉理解起来更友好)

时间复杂度:O(nW);空间复杂度:O(nW)

动态规划+压缩空间
观察上面的代码,会发现,当更新dp[i][…]时,只与dp[i-1][…]有关,也就是说,我们没有必要使用O(n*W)的空间,而是只使用O(W)的空间即可。下面先给出代码,再结合图例进行说明。

 public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;
        // 辅助空间只需要O(W)即可
        int[] dp = new int[W + 1];
        for (int i = 0; i < n; i++) {
          // 注意这里必须从后向前!!!
            for (int k = W; k >= 1; k--) {
                int valueWith_i = (k - weight[i] >= 0) ? (dp[k - weight[i]] + value[i]) : 0;
                int valueWithout_i = dp[k];
                dp[k] = Math.max(valueWith_i, valueWithout_i);
            }
        }
        return dp[W];
    }

这里的状态转移方程变成了:dp[k](新值) = max(value[i]+dp[k-weight[i]](旧值), dp[k](旧值))

为什么说这里必须反向遍历来更新dp[]数组的值呢?
原因是索引较小的元素可能会被覆盖。我们来看例子,假设我们已经遍历完了第 i=1 个元素(即weight=3,value=30),如下图所示:在这里插入图片描述
现在要更新第 i=2 个元素(即weight=1, value=20),由于我们只申请了一维空间的数组,因此对dp[]数组的修改会覆盖上一轮dp[]数组的值,这里用浅色代表上一轮的值,深色代表当前这一轮的值。在这里插入图片描述
鉴于上面出现的问题,因此必须采用反向遍历来回避这个问题。仍然假设第 i=1 个元素已经更新完毕,现在更新第 i=2 个元素。示意图如下:在这里插入图片描述
可以看到,反向遍历就可以避免这个问题了!

事实上,我们还可以进一步简化上面的代码,如下:

public int maxValue(int[] weight, int[] value, int W) {
        int n = weight.length;
        if (n == 0) return 0;int[] dp = new int[W + 1];
        for (int i = 0; i < n; i++) {
          //只要确保 k>=weight[i] 即可,而不是 k>=1,从而减少遍历的次数
            for (int k = W; k >= weight[i]; k--) {
                dp[k] = Math.max(dp[k - weight[i]] + value[i], dp[k]);
            }
        }
        return dp[W];
    }

为什么可以这样简化呢?我们重新看一下这段代码:

for (int k = W; k >= 1; k--) {
    int valueWith_i = (k - weight[i] >= 0) ? (dp[k - weight[i]] + value[i]) : 0;
    int valueWithout_i = dp[k];
    dp[k] = Math.max(valueWith_i, valueWithout_i);
}

如果k>=weight[i] 不成立,则valueWith_i 的值为0,那么显然有:dp[k] = Math.max(valueWith_i, valueWithout_i) = max(0, dp[k]) = dp[k]
也就是dp[k]没有更新过,它的值还是上一轮的值,因此就没必要执行了,可以提前退出循环!

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值