關於揹包問題的一點發散

昨天詳解了一下揹包問題,以後有人問我若是每種元素均可以選擇任意數目那會怎麼樣?這是很常見的揹包問題的變種問題,只須要咱們在原來的算法基礎上作一點小小的改動,咱們一塊兒來看下。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];
    }
複製代碼

發現沒有,這個問題對咱們根本毫無壓力?掌握了昨天的進階文章,咱們甚至還能夠對這個算法再進行優化兩百遍!(其實兩遍)

皮這一下真開心,最後的優化我就不帶你們一塊兒走了,思路都是同樣的,留給你們去思考,你們平時作算法題的時候必定要多思考,盡力把題目轉化成咱們熟悉的題目,轉換成功後那咱們結題呀優化呀一切都遊刃有餘了。

相關文章
相關標籤/搜索