关于背包问题的一点发散

昨天详解了一下背包问题,以后有人问我若是每种元素均可以选择任意数目那会怎么样?这是很常见的背包问题的变种问题,只须要咱们在原来的算法基础上作一点小小的改动,咱们一块儿来看下。java

照例来看下题目定义:给定N种水果的重量跟收益,咱们须要把它们放进一个可容重量为C的背包里,使得包里的水果在总重量不超过C的同时拥有最高的收益,假设水果数量无限,一种可选多个c++

此次咱们还要去卖水果,在携带量有限的状况下得到最大的收益。假设档状况是: 水果: { 苹果, 橙子, 西瓜 } 重量: { 1, 2, 3 } 收益: { 15, 20, 50 } 可容重量: 5算法

咱们也一样先来稍稍列举下可能的状况: 5 苹果 (总重 5) => 75 收益 1 苹果 + 2 橙子 (总重 5) => 55 收益 2 苹果 + 1 西瓜 (总重 5) => 80 收益 1 橙子 + 1 西瓜 (总重5) => 70 收益数组

咱们能够看到两个苹果跟西瓜是绝配,载重量有限的状况下咱们得到了最大收益。关键是咱们得把这个过程经过代码表达出来,咱们来分析一下,对于每种水果,咱们能够选择放进去而后进行下一轮选择,或者不放进去直接进行下一轮选择,在每次放进去一种水果A以后,咱们还要选择要不要把A再放进去,知道超出背包的载重量,而后在这个过程当中咱们要选出两种选择中带来最大收益的那个。缓存

也照旧,咱们先用递归来把算法实现出来,后期再慢慢优化。上面已经描述得很清楚了,咱们能够直接写出来:post

private int knapsackRecursive(int[] profits, int[] weights, int capacity, int currentIndex) {
        if (capacity <= 0 || profits.length == 0 || weights.length != profits.length ||
                currentIndex >= profits.length)
            return 0;

        // 选择了当前元素以后继续循环处理,要注意这里选择结束后并无把索引+1
        int profit1 = 0;
        if (weights[currentIndex] <= capacity)
            profit1 = profits[currentIndex]
                    + knapsackRecursive(profits, weights, capacity - weights[currentIndex], currentIndex);

        // 跳过当前元素而后继续作选择
        int profit2 = knapsackRecursive(profits, weights, capacity, currentIndex + 1);

        return Math.max(profit1, profit2);
    }
复制代码

想必你们都看的出来,咱们的算法跟昨天的很类似,除了一些条件的变化。要注意的是这里的时间复杂度变成了O(2^(N+C)),N是元素元素数量,C是背包最大载重,由于咱们能够重复选择某一元素。优化

如今遇到这种问题,写出了暴力递归的作法,你们确定都能条件反射般地用缓存来优化算法了。这边已经不须要我卖关子了,我们直接上代码:spa

private int knapsackRecursive(Integer[][] dp, int[] profits, int[] weights, int capacity, int currentIndex) {

        if (capacity <= 0 || profits.length == 0 || weights.length != profits.length ||
                currentIndex >= profits.length)
            return 0;

        // 检查咱们以前有木有遇到过一样的子问题,有就直接返回结果
        if (dp[currentIndex][capacity] == null) {
            // 作完选择以后继续递归处理,注意选择后咱们还能够继续选择当前元素
            int profit1 = 0;
            if (weights[currentIndex] <= capacity)
                profit1 = profits[currentIndex] + knapsackRecursive(dp, profits, weights,
                        capacity - weights[currentIndex], currentIndex);

            // 跳过当前元素直接进行下一次递归
            int profit2 = knapsackRecursive(dp, profits, weights, capacity, currentIndex + 1);

            dp[currentIndex][capacity] = Math.max(profit1, profit2);
        }
        return dp[currentIndex][capacity];
    }
复制代码

这时候由于咱们把子问题的结果都缓存在二维数组中,因此咱们最多进行了NC次计算,因此咱们的时间复杂度降低到了O(NC),可是如今想必你们也都能发现都发觉了一般光缓存是达不到最优的,那咱们再来试试从另外一个方向,采用自下而上的方式来思考这个问题。(又到了激动人心的环节了!)code

本质上,咱们仍是想在上面的递归过程当中,对于每个索引,每个剩余的可容重量,咱们都想在这一步得到能够的最大收益。咱们仍是面临两个选择,递归

  1. 跳过当前元素,那么咱们这时候的最大收益确定跟前面一个元素的最大收益相同,即dp[index-1][c]
  2. 选择当前元素,那么咱们的最大收益就是当前元素的收益加上剩余载重量可得的最大收益,即profit[index] + dp[index][c-weight[index]]

最后咱们获得了想得到最大收益的公式:dp[index][c] = max (dp[index-1][c], profit[index] + dp[index][c-weight[index]])。跟咱们昨天的思路简直一毛同样!

刚看完昨天文章的你们确定明白是怎么回事了,我也很少说了,直接把代码贴出来供你们观赏:

public int solveKnapsack(int[] profits, int[] weights, int capacity) {
        if (capacity <= 0 || profits.length == 0 || weights.length != profits.length)
            return 0;

        int n = profits.length;
        int[][] dp = new int[n][capacity + 1];

        // 0载重量0收益
        for (int i = 0; i < n; i++)
            dp[i][0] = 0;

        // 循环处理全部元素全部重量
        for (int i = 0; i < n; i++) {
            for (int c = 1; c <= capacity; c++) {
                int profit1 = 0, profit2 = 0;
                if (weights[i] <= c)
                    profit1 = profits[i] + dp[i][c - weights[i]];
                if (i > 0)
                    profit2 = dp[i - 1][c];
                dp[i][c] = profit1 > profit2 ? profit1 : profit2;
            }
        }

        // 最大收益确定出如今最右下角
        return dp[n - 1][capacity];
    }
复制代码

发现没有,这个问题对咱们根本毫无压力?掌握了昨天的进阶文章,咱们甚至还能够对这个算法再进行优化两百遍!(其实两遍)

皮这一下真开心,最后的优化我就不带你们一块儿走了,思路都是同样的,留给你们去思考,你们平时作算法题的时候必定要多思考,尽力把题目转化成咱们熟悉的题目,转换成功后那咱们结题呀优化呀一切都游刃有余了。

相关文章
相关标签/搜索