【轉】徹底理解0-1揹包問題

轉載自:https://blog.csdn.net/chanmufeng/article/details/82955730

0-1揹包問題

在這裏插入圖片描述0-1揹包問題指的是每個物品只能使用一次

遞歸方法

在這裏插入圖片描述

public class KnapSack01 {
    /**
     * 解決揹包問題的遞歸函數
     *
     * @param w        物品的重量數組
     * @param v        物品的價值數組
     * @param index    當前待選擇的物品索引
     * @param capacity 當前揹包有效容量
     * @return 最大價值
     */
    private static int solveKS(int[] w, int[] v, int index, int capacity) {
        //基準條件:如果索引無效或者容量不足,直接返回當前價值0
        if (index < 0 || capacity <= 0)
            return 0;

        //不放第index個物品所得價值
        int res = solveKS(w, v, index - 1, capacity);
        //放第index個物品所得價值(前提是:第index個物品可以放得下)
        if (w[index] <= capacity) {
            res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index]));
        }
        return res;
    }

    public static int knapSack(int[] w, int[] v, int C) {
        int size = w.length;
        return solveKS(w, v, size - 1, C);
    }

    public static void main(String[] args){
        int[] w = {2,1,3,2};
        int[] v = {12,10,20,15};
        System.out.println(knapSack(w,v,5));
    }
}

記憶化搜索

我們用遞歸方法可以很簡單的實現以上代碼,但是有個嚴重的問題就是,直接採用自頂向下的遞歸算法會導致要不止一次的解決公共子問題,因此效率是相當低下的。
我們可以將已經求得的子問題的結果保存下來,這樣對子問題只會求解一次,這便是記憶化搜索。
下面在上述代碼的基礎上加上記憶化搜索

public class KnapSack01 {
    private static int[][] memo;

    /**
     * 解決揹包問題的遞歸函數
     *
     * @param w        物品的重量數組
     * @param v        物品的價值數組
     * @param index    當前待選擇的物品索引
     * @param capacity 當前揹包有效容量
     * @return 最大價值
     */
    private static int solveKS(int[] w, int[] v, int index, int capacity) {
        //基準條件:如果索引無效或者容量不足,直接返回當前價值0
        if (index < 0 || capacity <= 0)
            return 0;

        //如果此子問題已經求解過,則直接返回上次求解的結果
        if (memo[index][capacity] != 0) {
            return memo[index][capacity];
        }

        //不放第index個物品所得價值
        int res = solveKS(w, v, index - 1, capacity);
        //放第index個物品所得價值(前提是:第index個物品可以放得下)
        if (w[index] <= capacity) {
            res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index]));
        }
        //添加子問題的解,便於下次直接使用
        memo[index][capacity] = res;
        return res;
    }

    public static int knapSack(int[] w, int[] v, int C) {
        int size = w.length;
        memo = new int[size][C + 1];
        return solveKS(w, v, size - 1, C);
    }

    public static void main(String[] args) {
        int[] w = {2, 1, 3, 2};
        int[] v = {12, 10, 20, 15};
        System.out.println(knapSack(w, v, 5));
    }
}

動態規劃算法

在這裏插入圖片描述
在這裏插入圖片描述

public class KnapSack01 {
    public static int knapSack(int[] w, int[] v, int C) {
        int size = w.length;
        if (size == 0) {
            return 0;
        }

		//dp[i][j] 代表存放前i個物品容量爲j能夠存放的最大值
        int[][] dp = new int[size][C + 1];
        //初始化第一行
        //僅考慮容量爲C的揹包放第0個物品的情況
        for (int i = 0; i <= C; i++) {
            dp[0][i] = w[0] <= i ? v[0] : 0;
        }
		//填充其他行和列
        for (int i = 1; i < size; i++) {
            for (int j = 0; j <= C; j++) {
                dp[i][j] = dp[i - 1][j]; //不放第i件物品
                if (w[i] <= j) { //可以放得下第i件物品,並且把第i件物品放進去
                    dp[i][j] = Math.max(dp[i][j], v[i] + dp[i - 1][j - w[i]]);
                }
            }
        }
        return dp[size - 1][C];
    }

    public static void main(String[] args) {
        int[] w = {2, 1, 3, 2};
        int[] v = {12, 10, 20, 15};
        System.out.println(knapSack(w, v, 5));
    }
}

空間複雜度的極致優化

上面的動態規劃算法使用了O(n*C)的空間複雜度(因爲我們使用了二維數組來記錄子問題的解),其實我們完全可以只使用一維數組來存放結果,但同時我們需要注意的是,爲了防止計算結果被覆蓋,我們必須從後向前分別進行計算
在這裏插入圖片描述
我們仍然假設揹包空間爲5,根據
F(i,C)=max(F(i−1,C),v(i)+F(i−1,C−w(i))) F(i,C)=max(F(i-1,C),v(i)+F(i-1,C-w(i)))
F(i,C)=max(F(i−1,C),v(i)+F(i−1,C−w(i)))
我們可以知道,當我們利用一維數組進行記憶化的時候,我們只需要使用到當前位置的值和該位置之前的值,舉個例子
假設我們要計算F(i,4) F(i,4)F(i,4),我們需要用到的值爲F(i−1,4) F(i-1,4)F(i−1,4)和F(i−1,4−w(i)) F(i-1,4-w(i))F(i−1,4−w(i)),因此爲了防止結果被覆蓋,我們需要從後向前依次計算結果
最終的動態規劃代碼如下


public class KnapSack01 {
    public static int knapSack(int[] w, int[] v, int C) {
        int size = w.length;
        if (size == 0) {
            return 0;
        }

        int[] dp = new int[C + 1];
        //初始化第一行
        //僅考慮容量爲C的揹包放第0個物品的情況
        for (int i = 0; i <= C; i++) {
            dp[i] = w[0] <= i ? v[0] : 0;
        }

        for (int i = 1; i < size; i++) {
            for (int j = C; j >= w[i]; j--) {
                dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
            }
        }
        return dp[C];
    }

    public static void main(String[] args) {
        int[] w = {2, 1, 3, 2};
        int[] v = {12, 10, 20, 15};
        System.out.println(knapSack(w, v, 5));
    }
}

利用揹包問題的思想解決問題

leetcode 416 Partition Equal Subset Sum
給定一個僅包含正整數的非空數組,確定該數組是否可以分成兩部分,要求兩部分的和相等

問題分析

該問題我們可以利用揹包問題的思想進行求解。

假設給定元素個數爲n nn的數組arr,數組元素的和爲sum,對應於揹包問題,等價於有n nn個物品,每個物品的重量和價值均爲爲arr[i],揹包的限重爲sum/2,求解揹包中的物品最大價值爲多少?

class Solution {
    private boolean knapSack(int[] nums,int sum){
        int size = nums.length;
        
        boolean[] dp = new boolean[sum + 1];
        for (int i = 0;i <= sum;i ++){
           dp[i] = i == nums[0];
        }
        
        for (int i = 1;i < size;i++){
            for (int j = sum;j >= nums[i];j--){
                dp[j] = dp[j] || dp[j-nums[i]];
            }
        }
        return dp[sum];
    }
    
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for (int item : nums){
            sum += item;
        }
        
        //如果數組元素和不是2的倍數,直接返回false
        if (sum % 2 != 0)
            return false;
        
        return knapSack(nums,sum/2);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章