1. 程式人生 > >LintCode揹包問題總結

LintCode揹包問題總結

揹包問題是動態規劃的一種題型,它的特點如下:

特點: 
1. 用值作為dp維度
2. dp過程就是填寫矩陣
3. 可以用滾動陣列進行優化

有個揹包問題九講的連結推薦:揹包問題九講

Given n items with size Ai, an integer m denotes the size of a backpack. How full you can fill this backpack? 要求返回揹包最多能容納的大小是多大。

state: 
    f[i][j] 前i個物品,取出一些能否組成和為j
function:
    f[i][j] = 
        1. f[i - 1][j - a[i]] // 能放下第i個物品,那麼要看除掉第i 個物品剩下的容量 j - a[i]時候與i - 1個物品的情況
        2. f[i - 1][j] // 若前i-1個物品就能組成大小為j
Intialize:
    f[0][0] = true;
Result:
    檢查所有的f[n][j] (j = 0, 1, 2,..., n)

時間複雜度為O(m * n)

    public int backpack(int capacity, int[] A) {
        // state dp[m][n]: if it can fill the capacity n from the first m items
        boolean dp[][] = new boolean[A.length + 1][capacity + 1];
        // initialize
        for (int i = 0; i <= A.length; i++) {
            for (int j = 0; j <= capacity; j++) {
                dp[i][j] = false;
            }
        }
        dp[0][0] = true;
        // dp function
        for (int i = 1; i <= A.length; i++) {
            for (int j = 0; j <= capacity; j++) {
                dp[i][j] = dp[i - 1][j];
                if (j >= A[i - 1] && dp[i - 1][j - A[i - 1]]) {
                    dp[i][j] = true;
                }
            }
        }
        // result
        for (int i = capacity; i >= 0; i--) {
            if (dp[A.length][i]) {
                return i;
            }
        }
        return 0;
    }
仔細觀察,這道題可以用滾動陣列優化,從而優化了空間複雜度:
    public int backpackWithRollingArray(int capacity, int[] A) {
        // state dp[m][n]: if it can fill the capacity n from the first m items
        boolean dp[][] = new boolean[2][capacity + 1];
        // initialize
        for (int i = 0; i < dp.length; i++) {
            for (int j = 0; j <= capacity; j++) {
                dp[i][j] = false;
            }
        }
        dp[0][0] = true;
        // dp function
        for (int i = 1; i <= A.length; i++) {
            for (int j = 0; j <= capacity; j++) {
                dp[i%2][j] = dp[(i - 1)%2][j];
                if (j >= A[i - 1] && dp[(i - 1)%2][j - A[i - 1]]) {
                    dp[i%2][j] = true;
                }
            }
        }
        // result
        for (int i = capacity; i >= 0; i--) {
            if (dp[A.length%2][i]) {
                return i;
            }
        }
        return 0;
    }

125. Backpack II

物品不僅有大小size,還有價值value,分別有2個數組表示每個物品的大小和價值,然後給一個大小為target的揹包,這個揹包裡能裝下的最大價值是多少。

State:    f[i][j]表示前i個物品當中選出一些物品組成容量為j的最大價值

DP Function:    f[i][j] = Max( f[i-1][j], f[i-1][j-A[i]]+Value[i]);  

Initialize:    f[0][0] = 0;

Result:    f[m][n]

時間複雜度:O(m * n)

    public int backPackII(int n, int[] A, int V[]) {
        int m = A.length;
        // state
        int[][] dp = new int[m + 1][n + 1];
        // initialize
        dp[0][0] = 0;
        // dp function
        for (int i = 1; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                dp[i][j] = Math.max(dp[i][j], dp[i-1][j]);
                if (A[i - 1] <= j) {
                    dp[i][j] = Math.max(dp[i][j], dp[i-1][j-A[i-1]]+V[i-1]);
                }
            }
        }
        // result
        return dp[m][n];
    }
440. Backpack III

這道題是上題Backpack II的變種,區別就每種item可以重複的選擇,基本思路跟上題一樣,只不過由於可以重複,所以在最內層迴圈還要再加一層while迴圈用於遍歷列舉重複的組合。時間複雜度是O(m * n * k)。

    public int backPackIII(int[] A, int[] V, int n) {
        int m = A.length;
        // state
        int[][] dp = new int[m + 1][n + 1];
        // initialize
        dp[0][0] = 0;
        // dp function
        for (int i = 1; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                int k = 0;
                while (A[i - 1] * k <= j) {
                    dp[i][j] = Math.max(dp[i][j], dp[i-1][j-A[i-1]*k]+V[i-1]*k);  
                    k++;
                }
            }
        }
        // result
        int res = 0;
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                res = Math.max(res, dp[i][j]);
            }
        }
        return res;
    }

562. Backpack IV

給定一些物品陣列和一個目標值,問有多少種可以組成目標的組合數,比如給定物品陣列 [2,3,6,7] 和目標值 7, 那麼就有2種可能:[7] 和 [2, 2, 3]。所以返回2。這道題也可以這樣描述:給1,2,5,10硬幣無數多個,請問湊80元的方案總數。

State:
    dp[m][n] 前m種硬幣湊成n元的方案數量
DP Function:
    dp[m][n] = dp[m - 1][n] + dp[m - 1][n - A[m] * 1] + dp[m - 1][n - A[m] * 2] + dp[m - 1][n - A[m] * 3] + ...
Initialize:
    dp[0][0] = 1
result:
    dp[m][n]

時間複雜度是O(m * n * k)

    public int backpack(int [] nums, int target) {
        // state dp[m][n]: the number of combinations that first m kinds of items form the target n
        int m = nums.length, n = target;
        int dp[][] = new int[m + 1][n + 1];
        dp[0][0] = 1;
        // dp function
        for (int i = 1; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                int k = 0;
                while (k * nums[i - 1] <= j) {
                    dp[i][j] += dp[i - 1][j - k * nums[i - 1]];
                    k++;
                }
            }
        }
        // result
        return dp[m][n];
    }

這道題是Backpack IV那道湊硬幣題目的變種,唯一的區別就是現在每種型別的硬幣是不可以重複的選擇。每個硬幣只能出現一次。

State:
    dp[m][n] 前m種硬幣湊成n元的方案數量
DP Function:
    dp[m][n] = dp[m - 1][n] + dp[m - 1][n - A[m] ];
Initialize:
    dp[0][0] = 1
result:
    dp[m][n]

時間複雜度是O(m * n)

    public int backPackV(int[] nums, int target) {
        // state dp[m][n]: the number of combinations that first m kinds of items form the target n  
        int m = nums.length, n = target;  
        int dp[][] = new int[m + 1][n + 1];  
        dp[0][0] = 1;  
        // dp function  
        for (int i = 1; i <= m; i++) {  
            for (int j = 0; j <= n; j++) {
                dp[i][j] = dp[i - 1][j];
                if (nums[i - 1] <= j) {
                    dp[i][j] += dp[i - 1][j - nums[i - 1]];
                }
            }  
        }  
        // result  
        return dp[m][n]; 
    }

給定一個包含了一些數字的陣列,和一個目標值,從數組裡面取數做排列,使得排列的數字的和等於target。問有多少種排列方法。數字是闊以重複取出來的。比如給定陣列[1, 2, 4]和target值4。那麼能得到如下的組合。總共有6種,則返回6.

[1, 1, 1, 1]
[1, 1, 2]
[1, 2, 1]
[2, 1, 1]
[2, 2]
[4]
用dp[i]表示target為i的排列數有多少種。比如以上面那個例子為例,我們可以畫出如下的dp搜尋圖:


從而就不難寫出如下程式碼了:

    public int backPackVI(int[] nums, int target) {
        // state
        int[] dp = new int[target + 1];
        // initialize
        dp[0] = 1;
        // dp function
        for (int i = 1; i <= target; i++) {
            for (int num: nums) {
                if (i >= num) {
                    dp[i] += dp[i - num];
                }
            }
        }
        // result
        return dp[target];
    }

從一個數組中取k個數的和為target,求有多少種組合。假如陣列是[1,2,3,4], k = 2, target = 5。那麼有2種解:[1,4] 和 [2,3]。

state:
    f[i][j][t]前 i 個數中取 j 個數出來組成和為 t 的組合數目
function: 
    f[i][j][t] = f[i - 1][j][t] + f[i - 1][j - 1][t - a[i - 1]] (不包括第i 個數的時候組成t的情況 + 包括第i個數的時候組成t的情況)
initialize:
    f[i][0][0] = 1
result:
    f[n][k][target]

時間複雜度:O(n * k * target)

    public int kSum(int A[], int k, int target) {
        int n = A.length;
        // state
        int[][][] dp = new int[n + 1][k + 1][target + 1];
        // initialize
        for (int i = 0; i <= n; i++) {
            dp[i][0][0] = 1;
        }
        // dp function
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= k; j++) {
                for (int l = 0; l <= target; l++) {
                    dp[i][j][l] = dp[i-1][j][l];
                    if (l >= A[i-1]) {
                       dp[i][j][l] += dp[i-1][j-1][l-A[i-1]];
                    }
                }
            }
        }
        // result
        return dp[n][k][target];
    }

91. Minimum Adjustment Cost

題意有點複雜,給定一個整型陣列,調整這個陣列使得每兩個數之間的差值不超過給定target值。問你調整這個陣列所需要的最小開銷是多少。注意陣列中的每個數不會超過100,這是一個非常關鍵的條件。因為這樣的話,當前可取的值是1-100,並且與上一個值是在target的差值以內。那我們可以轉換成揹包問題:

State:    f[i][v] 前i個數,第i個數調整為v,滿足相鄰兩數<=target,所需要的最小代價 
Function:    f[i][v] = min(f[i-1][v’] + |A[i]-v|, |v-v’| <= target)
Answer:    f[n][a[n]-target~a[n]+target]
時間複雜度:    O(n * A * T)

其實很簡單,就是當前index為v時,我們把上一個index從1-100全部過一次,取其中的最小值(判斷一下前一個跟當前的是不是abs <= target)

    public int MinAdjustmentCost(ArrayList<Integer> A, int target) {
        if (A == null || A.size() == 0) {
            return 0;
        }
        // state
        int[][] dp = new int[A.size() + 1][101];
        // dp function
        for (int i = 1; i <= A.size(); i++) {
            for (int j = 1; j <= 100; j++) {
                dp[i][j] = Integer.MAX_VALUE;
                for (int k = 1; k <= 100; k++) {
                    if (Math.abs(k - j) > target) {
                        continue;
                    }
                    int diff = Math.abs(j - A.get(i - 1)) + dp[i-1][k];
                    dp[i][j] = Math.min(dp[i][j], diff);
                }
            }
        }
        // result
        int res = Integer.MAX_VALUE;
        for (int i = 1; i <= 100; i++) {
            res = Math.min(res, dp[A.size()][i]);
        }
        return res;
    }