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的最優解呢?this

    咱們能夠用反證法來推導。假定拿走ak這個物品後,剩下的這些物品沒有構成W-wk重量範圍的最佳價值選擇。那麼咱們確定有另外k-1個元素,他們在W-wk重量範圍內構成的價值更大。若是這樣的話,咱們用這k-1個物品再加上第k個,他們構成的最終W重量範圍內的價值就是最優的。這豈不是和咱們前面假設的k個元素構成最佳矛盾了嗎?因此咱們能夠確定,在這k個元素裏拿掉最後那個元素,前面剩下的元素依然構成一個最佳解。spa

    如今咱們通過前面的推理已經獲得了一個基本的遞推關係,就是一個最優解的子解集也是最優的。但是,咱們該怎麼來求得這個最優解呢?咱們這樣來看。假定咱們定義一個函數c[i, w]表示到第i個元素爲止,在限制總重量爲w的狀況下咱們所能選擇到的最優解。那麼這個最優解要麼包含有i這個物品,要麼不包含,確定是這兩種狀況中的一種。若是咱們選擇了第i個物品,那麼實際上這個最優解是c[i - 1, w-wi] + vi。而若是咱們沒有選擇第i個物品,這個最優解是c[i-1, w]。這樣,實際上對於到底要不要取第i個物品,咱們只要比較這兩種狀況,哪一個的結果值更大不就是最優的麼?code

    在前面討論的關係裏,還有一個狀況咱們須要考慮的就是,咱們這個最優解是基於選擇物品i時總重量仍是在w範圍內的,若是超出了呢?咱們確定不能選擇它,這就和c[i-1, w]同樣。blog

    另外,對於初始的狀況呢?很明顯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運算會比上述解法採用數學運算速度快一些。

相關文章
相關標籤/搜索