揹包問題細節探析

引言:

上一週在看《揹包九講》,這周也在練習揹包型動態規劃的題目,在這裏分享幾點學習過程中的體會,涉及到空間優化:二維轉一維的理解以及循環順序的理解,最後會析揹包問題的一些變種問題。爲了敘述方便,這篇博客僅僅談及01背白問題和完全揹包問題,其餘讀者感興趣可以自己查閱《揹包九講》

01揹包

01揹包問題是所有揹包問題的基礎,先來看裸的01揹包問題:
題目:有N件物品和一個容量爲V 的揹包。第i件物品的費用爲c[i],價值爲w[i]。求解將哪些物品放入揹包可以使價值總和最大。
注意:每種物品僅有一件,可以選擇放或者不放。

動態規劃問題最好先在草稿紙上寫下子問題的定義狀態以及方程
子問題定義爲:dp[i][j]表示前i件物品放入容量爲j的揹包中所能獲得的最大價值。
狀態方程爲:
這裏寫圖片描述
這個是二維數組的狀態方程,這個在《揹包九講》的基礎上做了修正,因爲《揹包九講》是要優化到一維,所以沒有注意到這個細節。
狀態方程的意思是1 .如果第i件物品大於揹包容量,那麼問題轉化爲將前i-1件物品放入揹包中所獲得的最大價值。2.如果第i件物品小於於揹包容量,那麼可以考慮a.不放第i件物品,問題轉換爲將前i-1件物品放入揹包中所獲得的最大價值b.放第i件物品,問題轉化爲將前i-1件物品放入揹包j - c[i]中所獲得的最大價值加上第i件物品的價值w[i]..

Code:

public class Solution {
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A & V: Given n items with size A[i] and value V[i]
     * @return: The maximum value
     */
    public int backPackII(int m, int[] A, int V[]) {
        // write your code here
        int[][] dp = new int[A.length][m + 1];
        for (int i = 0; i < A.length; i++) {
            dp[i][0] = 0;
        }
        for (int j = 1; j < m + 1; j++) {
            if (A[0] > j) {
                dp[0][j] = 0;
            } else {
                dp[0][j] = V[0];
            }
            for (int i = 1; i < A.length; i++) {
                if (A[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - A[i]] + V[i]);
                }
            } 
        }
        return dp[A.length - 1][m];
    }
}

二維數組轉一維數組怎麼理解

《揹包九講》中,推薦我們使用一維數組,那樣可以節省空間,僞代碼如下:
這裏寫圖片描述
這個僞代碼第一次看感覺不到什麼,但是寫着寫着就會發現有些地方是沒有想明白的,我通過一番探究,基本上把這個一維的思路想清楚了。
先來看看和二維數組的不同之處,只有對比才能發現問題。
除了一維數組和二維數組的區別外,有兩點不同。
1.循環的次序發生了改變,二維數組爲外層揹包,內層物品。而一維數組是外層物品,內層揹包。
2.內層循環的方向發生了改變,之前是遞增內循環,現在是遞減內循環。
Code:

public class Solution {
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A & V: Given n items with size A[i] and value V[i]
     * @return: The maximum value
     */
    public int backPackII(int m, int[] A, int V[]) {
        // write your code here 
        int[] dp = new int[m + 1];
        dp[0] = 0;
        for(int i = 0; i < A.length; i++) {
            for (int j = m; j >= A[i]; j--) {
                dp[j] = Math.max(dp[j], dp[j - A[i]] + V[i]);
            }
        }
        return dp[m];
    }
}

現在來分析原因,循環次序交換的原因是揹包遞減的判斷的截止條件爲A[i],需要外循環先遍歷物品。
比較不好理解的點是,爲什麼要遞減內循環,這也是01揹包完全揹包的區別點之一。
理論原因解釋是在01揹包問題中,在j - c[i]體積的情況下,裏面不能存在c[i]這個物品,也就是說在求狀態dp[j]的時候,dp[j -c[i]]還不能被更新過,所以dp[j -c[i]]要放在dp[j]後更新,用遞減循環的方式實現這個功能。
舉個例子,揹包中如果有個物品的價值很大,但是費用不高,用順序循環的方式,會造成加了那個物品,下次還會加,就不符合01揹包的限制了。

完全揹包

完全揹包可以由01揹包推導而來,對應物品可以取0件,取1件,取2件…….
這裏寫圖片描述
這個方程式不要小看,這個也是混合揹包的基本思想。即將一種物品拆成多件物品
下面給出完全揹包的狀態方程分析
這裏寫圖片描述
這個式子中注意由原先的01揹包的f[i - 1][v - c[i]] + w[i]變爲完全揹包的f[i][v - c[i]] + w[i]。這個式子的意思是,在已經取用第i個物品的情況下,我再取用第i個物品的情況。
僞代碼如下:
這裏寫圖片描述
這個地方的內循環爲遞增,之前不滿足01揹包的情況現在是滿足完全揹包的。由於沒有找到相應的裸完全揹包,這裏不給代碼,相信讀者閱讀了前面的01揹包理解的話,可以自己實現。

揹包問題的變種

揹包問題本來是動態規劃中比較適合研究的點,關於這些問題的變種有很多,但是核心思想是不會變的,這裏根據作者最近做的題,做下簡單分析和代碼參考。
1.最多可以裝滿多少的揹包空間:https://www.lintcode.com/zh-cn/problem/backpack/#,每個物品的大小爲A[i], 默認01揹包
分析:設dp[i][j]爲前i件物品放入容量大小爲j的揹包中,最多能夠裝滿的空間。
這裏寫圖片描述
2.求方案總數:https://www.lintcode.com/zh-cn/problem/backpack-vi/#
一個數可以在組合中出現多次(完全揹包), 數的順序不同認爲是不同的組合(這個限制有點奇怪)
Code:

public class Solution {
    /**
     * @param nums an integer array and all positive numbers, no duplicates
     * @param target an integer
     * @return an integer
     */
    public int backPackVI(int[] nums, int target) {
        // Write your code here
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int j = 1; j < target + 1; j++) {
            for (int i = 0; i < nums.length; i++) {
                if (j >= nums[i]) {
                    dp[j] += dp[j - nums[i]];
                }
            }
        }
        return dp[target];
    }
}

這個問題的奇怪之處在於我的加粗的地方,已經是變了樣的完全揹包,所以雖然是一維數組,但是外層循環不是物品而是揹包容量,初始化的意思是,大小爲0的揹包有一種組合方案(都不取)。
3.Partition Equal Subset Sum:https://www.lintcode.com/zh-cn/problem/partition-equal-subset-sum/#01揹包
分析:這個問題的難點在於分析這是個01揹包問題,揹包容量爲sum / 2…..
dp[i][j]表示前i件物品放入容量大小爲j的揹包中的可行性(true or false )。
這裏寫圖片描述
Code:

public class Solution {
    /**
     * @param nums a non-empty array only positive integers
     * @return return true if can partition or false
     */
    public boolean canPartition(int[] nums) {
        // Write your code here
        if (nums == null || nums.length == 0) {
            return true;
        }
        int len = nums.length;
        int sum = 0;
        for (int i = 0; i < len; i++) {
            sum += nums[i];
        }
        if (sum % 2 == 1) {
            return false;
        }
        sum /= 2;

        boolean[][] dp = new boolean[len][sum + 1];
        for (int i = 0; i < nums.length; i++) {
            dp[i][0] = true;
        }

        for (int j = 1; j < sum + 1; j++) {
            if (nums[0] == j) {
                dp[0][j] = true;
            } else {
                dp[0][j] = false;
            }
            for (int i = 1; i < nums.length; i++) {
                if (nums[i] > j) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        return dp[len - 1][sum];

    }
}

循環次序辨析補充

在求方案總數中,有兩類問法,一種是認爲不同的組合爲一種方案(LeetCode上的Coin Change2),另外一種認爲不同的組合爲不同的方案(LintCode上的揹包問題6)。我們先來看代碼:

Coin Change2:

public class Solution {
    public int change(int amount, int[] coins) {
        if (coins == null) {
            return 0;
        }
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int i = 0; i < coins.length; i++) {
            for (int j = coins[i]; j < amount + 1; j++) {
                dp[j] += dp[j - coins[i]];
            }
        }
        return dp[amount];
    }
}

揹包問題6:

public class Solution {
    /**
     * @param nums an integer array and all positive numbers, no duplicates
     * @param target an integer
     * @return an integer
     */
    public int backPackVI(int[] nums, int target) {
        // Write your code here
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int j = 1; j < target + 1; j++) {
            for (int i = 0; i < nums.length; i++) {
                if (j >= nums[i]) {
                    dp[j] += dp[j - nums[i]];
                }
            }
        }
        return dp[target];
    }
}

分析:

可以看到最大的不同是,考慮組合次序不同是不同的方案的話是先迭代揹包容量(空間),而考慮組合次序不同是同一種方案先迭代物品。
這是因爲比如容量大小爲4的揹包,[1, 1,2]和[2, 1, 1]是不同的方案,容量都爲4,所以要先迭代容量,找出所有容量爲4的物品組合。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章