動態規劃之揹包問題

前言

此篇博客主要講常見的三種揹包問題,0-1揹包,徹底揹包,多重揹包。爲了加深理解,每一個問題都有leetcode實例。java

1. 0-1揹包問題

1.1 題目

有N件物品和一個容量爲V 的揹包。放入第i件物品耗費的空間是Ci,獲得 的價值是Wi。求解將哪些物品裝入揹包可以使價值總和最大。算法

1.2 基本思路

這是最基礎的揹包問題,特色是:每種物品僅有一件,能夠選擇放或不 放。 用子問題定義狀態:即F[i,v]表示前i件物品恰放入一個容量爲v的揹包能夠 得到的最大價值。則其狀態轉移方程即是:數組

F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi}bash

這個方程很是重要,基本上全部跟揹包相關的問題的方程都是由它衍生 出來的。因此有必要將它詳細解釋一下:「將前i件物品放入容量爲v的揹包中」這個子問題,若只考慮第i件物品的策略(放或不放),那麼就能夠轉化 爲一個只和前i−1件物品相關的問題。若是不放第i件物品,那麼問題就轉化 爲「前i−1件物品放入容量爲v的揹包中」,價值爲F[i−1,v];若是放第i件物 品,那麼問題就轉化爲「前i−1件物品放入剩下的容量爲v −Ci的揹包中」, 此時能得到的最大價值就是F[i−1,v −Ci]再加上經過放入第i件物品得到的價 值Wi。 僞代碼以下:函數

F[0,0..V ] = 0 
   for i = 1 to N 
      for v = Ci to V 
         F[i,v] = max{F[i−1,v],F[i−1,v−Ci] + Wi}

複製代碼

1.3 例題: leetcode 416. 分割等和子集

給定一個只包含正整數的非空數組。是否能夠將這個數組分割成兩個子集,使得兩個子集的元素和相等。
注意:
每一個數組中的元素不會超過 100
數組的大小不會超過 200
示例 1:
輸入: [1, 5, 11, 5]
輸出: true
解釋: 數組能夠分割成 [1, 5, 5] 和 [11].
複製代碼

0-1 揹包問題也是最基礎的揹包問題,它的特色是:待挑選的物品有且僅有一個,能夠選擇也能夠不選擇。下面咱們定義狀態,不妨就用問題的問法定義狀態試試看。學習

dp[i][j]:表示從數組的 [0, i] 這個子區間內挑選一些正整數,每一個數只能用一次,使得這些數的和等於 j。優化

根據咱們學習的 0-1 揹包問題的狀態轉移推導過程,新來一個數,例如是 nums[i],根據這個數可能選擇也可能不被選擇:ui

若是不選擇 nums[i],在 [0, i - 1] 這個子區間內已經有一部分元素,使得它們的和爲 j ,那麼 dp[i][j] = true;spa

若是選擇 nums[i],在 [0, i - 1] 這個子區間內就得找到一部分元素,使得它們的和爲 j - nums[i] ,我既然這樣寫出來了,你就應該知道,這裏討論的前提條件是 nums[i] <= j。 以上兩者成立一條都行。因而獲得狀態轉移方程是:.net

dp[i][j] = dp[i - 1][j] or dp[i - 1][j - nums[i]], (nums[i] <= j)

因而按照 0-1 揹包問題的模板,咱們不難寫出如下代碼。

參考代碼:

public class Solution {
    public boolean canPartition(int[] nums) {
        int size = nums.length;

        int s = 0;
        for (int num : nums) {
            s += num;
        }

        // 特判 2:若是是奇數,就不符合要求
        if ((s & 1) == 1) {
            return false;
        }

        int target = s / 2;

        // 建立二維狀態數組,行:物品索引,列:容量
        boolean[][] dp = new boolean[size][target + 1];
        // 先寫第 1 行
        for (int i = 1; i < target + 1; i++) {
            if (nums[0] == i) {
                dp[0][i] = true;
            }
        }
        for (int i = 1; i < size; i++) {
            for (int j = 0; j < target + 1; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= nums[i]) {
                    dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
                }
            }
        }
        return dp[size - 1][target];
    }
}

複製代碼

這也是0-1揹包問題的常規解法。

1.4 空間複雜度優化

根據咱們學習過的 0-1 揹包問題的經驗,在填 dp 數組的時候,從第 2 行開始,每一行都參考了前一行的值,所以狀態數組從能夠從二維降到一維,從而減小空間複雜度。

注意:從第 2 行開始,每一行都參考了前一行的當前位置的值,而且還參考了前一行的小於當前位置的值。

public class Solution {

    public boolean canPartition(int[] nums) {
        int size = nums.length;
        
        int s = 0;
        for (int num : nums) {
            s += num;
        }
        if ((s & 1) == 1) {
            return false;
        }

        int target = s / 2;

        // 從第 2 行之後,當前行的結果參考了上一行的結果,所以使用一維數組定義狀態就能夠了
        boolean[] dp = new boolean[target + 1];
        // 先寫第 1 行,看看第 1 個數是否是可以恰好填滿容量爲 target
        for (int j = 1; j < target + 1; j++) {
            if (nums[0] == j) {
                dp[j] = true;
                // 若是等於,後面就不用作判斷了,由於 j 會愈來愈大,確定不等於 nums[0]
                break;
            }
        }
        // 注意:由於後面的參考了前面的,咱們從後向前填寫
        for (int i = 1; i < size; i++) {
            // 後面的容量愈來愈小,所以沒有必要再判斷了,退出當前循環
            for (int j = target; j >= 0 && j >= nums[i]; j--) {
                dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}
複製代碼

2. 徹底揹包問題

2.1 題目

有N種物品和一個容量爲V 的揹包,每種物品都有無限件可用。放入第i種 物品的耗費的空間是Ci,獲得的價值是Wi。求解:將哪些物品裝入揹包,可以使 這些物品的耗費的空間總和不超過揹包容量,且價值總和最大。

2.2 基本思路

這個問題很是相似於01揹包問題,所不一樣的是每種物品有無限件。也就是從 每種物品的角度考慮,與它相關的策略已並不是取或不取兩種,而是有取0件、 取1件、取2件……直至取⌊V /Ci⌋件等不少種。若是仍然按照解01揹包時的思路,令F[i,v]表示前i種物品恰放入一個容量 爲v的揹包的最大權值。仍然能夠按照每種物品不一樣的策略寫出狀態轉移方 程,像這樣: F[i,v] = max{F[i−1,v−kCi] + kWi |0 ≤ kCi ≤ v} 這跟01揹包問題同樣有O(V N)個狀態須要求解,但求解每一個狀態的時 間已經不是常數了,求解狀態F[i,v]的時間是O( v Ci ),總的複雜度能夠認爲 是O(NV Σ V Ci ),是比較大的。 將01揹包問題的基本思路加以改進,獲得了這樣一個清晰的方法。這說明01 揹包問題的方程的確是很重要,能夠推及其它類型的揹包問題。但咱們仍是要 試圖改進這個複雜度。

2.3 例題:leetcode 518. 零錢兌換 II

給定不一樣面額的硬幣和一個總金額。寫出函數來計算能夠湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。 
示例 1:
輸入: amount = 5, coins = [1, 2, 5]
輸出: 4
解釋: 有四種方式能夠湊成總金額:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
複製代碼

此題是個典型的徹底揹包問題,思路: dp[i][j] 表示 :用coins的前i個元素,加出j有多少種方法 用coins的前i個元素,加出j有多少種方法

參考代碼:

public int change(int amount, int[] coins) {
        if (amount < 0 || coins == null  || coins.length == 0) {
           return amount == 0 ? 1 : 0;
        }
        //dp[i][j]: [0~i]中加出j,有多少種方法
        int[][] dp = new int[coins.length][amount + 1];
        dp[0][0] = 1;
        for (int j = 1; j < amount + 1; ++j) {
            dp[0][j] = j % coins[0] == 0 ? 1 : 0;
        }
        for (int i = 1; i < coins.length; ++i) {
            dp[i][0] = 1;
            for (int j = 1; j < amount + 1; ++j) {
                dp[i][j] = dp[i - 1][j] + (j - coins[i] >= 0 ? dp[i][j - coins[i]] : 0);
            }
        }
        return dp[coins.length - 1][amount];
    }
複製代碼

2.4 空間複雜度優化

public int change(int amount, int[] coins) {
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int i = 1; i < coins.length + 1; ++i) {
            for (int j = coins[i - 1]; j < amount + 1; ++j) {
                dp[j] += dp[j - coins[i - 1]];
            }
        }
        return dp[amount];
    }
複製代碼

徹底揹包問題的解決方法大致和0-1揹包問題相似。

3. 多重揹包問題

3.1 題目

有N種物品和一個容量爲V 的揹包。第i種物品最多有Mi件可用,每件耗費 的空間是Ci,價值是Wi。求解將哪些物品裝入揹包可以使這些物品的耗費的空間 總和不超過揹包容量,且價值總和最大。

3.2 基本算法

這題目和徹底揹包問題很相似。基本的方程只需將徹底揹包問題的方程略 微一改便可。 由於對於第i種物品有Mi+1種策略:取0件,取1件……取Mi件。令F[i,v]表示前i種物品恰放入一個容量爲v的揹包的最大價值,則有狀態轉移方程: F[i,v] = max{F[i−1,v−k∗Ci] + k∗Wi |0 ≤ k ≤ Mi} 複雜度是O(V ΣMi)。

3.3 轉化爲01揹包問題

另外一種好想好寫的基本方法是轉化爲01揹包求解:把第i種物品換成Mi件01 揹包中的物品,則獲得了物品數爲ΣMi的01揹包問題。直接求解之,複雜度仍 然是O(V ΣMi)。 可是咱們指望將它轉化爲01揹包問題以後,可以像徹底揹包同樣下降複雜 度。 仍然考慮二進制的思想,咱們考慮把第i種物品換成若干件物品,使得原問 題中第i種物品可取的每種策略——取0...Mi件——均能等價於取若干件代換之後的物品。另外,取超過Mi件的策略必不能出現。 方法是:將第i種物品分紅若干件01揹包中的物品,其中每件物品有一個系 數。 這件物品的費用和價值均是原來的費用和價值乘以這個係數。令這些係數 分別爲1,2,22 ...2k−1,Mi −2k + 1,且k是知足Mi −2k + 1 > 0的最大整數。例 如,若是Mi爲13,則相應的k = 3,這種最多取13件的物品應被分紅係數分別 爲1,2,4,6的四件物品。 分紅的這幾件物品的係數和爲Mi,代表不可能取多於Mi件的第i種物品。另 外這種方法也能保證對於0...Mi間的每個整數,都可以用若干個係數的和表 示。這裏算法正確性的證實能夠分0...2k−1和2k ...Mi兩段來分別討論得出, 但願讀者本身思考嘗試一下。 這樣就將第i種物品分紅了O(logMi)種物品,將原問題轉化爲了複雜度 爲O(V ΣlogMi)的01揹包問題,是很大的改進。 下面給出O(logM)時間處理一件多重揹包中物品的過程:

def MultiplePack(F,C,W,M) 
  if C ·M ≥ V 
    CompletePack(F,C,W) 
    return 
  k := 1 
  while k < M 
    ZeroOnePack(kC,kW)
    M := M −k 
    k := 2k 
  ZeroOnePack(C ·M,W ·M) 
複製代碼

但願你仔細體會這個僞代碼,若是不太理解的話,不妨翻譯成程序代碼之後, 單步執行幾回,或者頭腦加紙筆模擬一下,以加深理解。

3.4 例題:leetcode 474. 一和零

在計算機界中,咱們老是追求用有限的資源獲取最大的收益。
如今,假設你分別支配着 m 個 0 和 n 個 1。另外,還有一個僅包含 0 和 1 字符串的數組。
你的任務是使用給定的 m 個 0 和 n 個 1 ,找到能拼出存在於數組中的字符串的最大數量。每一個 0 和 1 至多被使用一次。
注意:
給定 0 和 1 的數量都不會超過 100。
給定字符串數組的長度不會超過 600。
示例 1:
輸入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
輸出: 4
解釋: 總共 4 個字符串能夠經過 5 個 0 和 3 個 1 拼出,即 "10","0001","1","0" 。
複製代碼

參考代碼:

public class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        if(strs.length==0)
            return 0;
        //[0:i-1]的字符串物品中,j個0,k個1最多可以構成字符串數量。字符串爲物品,0,1數量爲揹包限制。
        //dp[i][j]=max(dp[i][j],dp[i-0數量][j-1數量]+1)
        int[][] dp=new int[m+1][n+1];
        for(String str: strs){
            int zeros=0, ones=0;
            //統計該字符串的0,1數量
            for(int i=0; i<str.length(); i++){
                char c=str.charAt(i);
                if( c=='0')
                    zeros++;
                else
                    ones++;
            }
            for(int j=m; j>=zeros; j--)
                for(int k=n; k>=ones; k--)
                    dp[j][k]=Math.max(dp[j][k],1+dp[j-zeros][k-ones]);
        }
        return dp[m][n];
    }
}
複製代碼

4. 小結&參考資料

小結

揹包問題還包括混合三種揹包的問題,分組的揹包問題等。具體均可以由着三種拓展,能夠參考揹包問題九講

參考資料

相關文章
相關標籤/搜索