0-1背包問題的學習及LeetCode相關習題練習


0-1背包問題:

n件物品,它們裝入背包所占的容量分別為w1、w2……wn;它們所擁有的價值分別為v1、v2 ……vn;
有一個總容量為C的背包;

在裝滿背包的情況下,如何使得包內的總價值最大?

該問題的特點是:每個物品僅有一個,可以選擇放或者不放,也就是說每個物品只能使用一次。

思路:

1.首先定義一個狀態轉移數組dp,dp[i][j]表示前i件物品放入容量為j的背包中所能得到的最大價值;

2.尋找數組元素之間的關系式,也就是狀態轉移方程,我們將第i件物品是否放入背包中這個子問題拿出來進行分析,首先要明確的是我們的一切目標都是使得在既有的背包容量下,能夠得到最大的價值,

所以對於第i件物品,如果放入能夠使得在現有的容量下背包價值最大,則dp[i][j] = dp[i-1][j-w] + v;如果不能,則就不把第i件物品放入背包,那么在dp[i][j] = dp[i-1][j],即前i件物品放入容量為j的背包中所得到的

最大價值就是前i-1件物品放入容量為j的背包中所得的的最大價值。

  總結一下,狀態轉移方程就是: dp[i][j] = max{dp[i][j] = dp[i-1][j-w] + v,dp[i][j] = dp[i-1][j]}

3.確定初始值,dp[0][0]表示前0件物品放入容量為0的背包中的最大價值,那么就是0,而對於多有i=0和j=0的元素,其值都是0;

模擬過程:

舉一個例子來模擬程序整個的執行過程;

i 1 2 3
w 1 2 3
v 6 9 13

現在有三件物品,這些物品的價值和所占容量如上表所示,有一個容量為5的背包,在裝滿背包的情況下,如何使得背包里的價值最大?

通過一個表格來顯示狀態轉移數組的內部情況:

i\j 0 1 2 3 4 5
0 0 0 0 0 0 0
1 0 6 6 6 6 6
2 0 6 9 15 15 15
3 0 6 9 15 19 22

 

該表格表示dp數組內部的元素值,程序的執行過程如下:

  i = 1     ==> w = 1,v = 6 :

1 dp[1][1] = max(dp[0][1],dp[0][0]+6)
2 dp[1][2] = max(dp[0][2],dp[0][1]+6)
3 dp[1][3] = max(dp[0][3],dp[0][2]+6)
4 dp[1][4] = max(dp[0][4],dp[0][3]+6)
5 dp[1][5] = max(dp[0][5],dp[0][4]+6)

 

 

 

 

 

 

 

  i = 2  ==> w = 2,v=9:

   …………

  以此類推,可以得到狀態轉移表格中的數據。

 

優化使用空間:

通過狀態轉移方程 dp[i][j] = max{dp[i-1][j-w] + v,dp[i-1][j]} 我們可以發現,背包從前i件物品所能得到的最大價值只和前i-1件件物品所能得到的最大價值有關,所以可以將狀態轉移數組簡化成一維數組,只存儲在既有的容量下所能得到的

最大價值,但是內循環的容量變化順序應該翻轉一下,即容量應該從最大的總量依次向下變小,否則在正序計算的時候會發生錯誤計算,也就是會隱形的放大在當前容量下,能夠放入背包的物品的選擇范圍。比如對於優化后的方程dp[j] = max{dp[j-w] + v,dp[j]}, 

dp[j]實際是dp[i][j],而dp[j-w]和dp[j]實際是dp[i-1][j-w]和dp[i-1][j];如果正序計算的話那么,dp[j-w]和dp[j]實際是dp[i][j-w]和dp[i][j],所得出的語義結論就成了前i件物品在既有背包容量j的情況下,如果將第i件物品放入背包,最大價值是當前物品價值加上前i件物品在

容量j-w下的最大價值,這是不正確的;對於不放入背包的情況也是一樣的,第i件物品你都不讓入背包了,實際的最大價值怎么可能還是前i件物品在既有容量j下所得的的最大價值。所以內循環應該倒序計算。


實現的代碼如下:

public int knapsacks(int W,int N,int[] weights,int[] values){
            int[][] dp = new int[N+1][W+1];
        /*
            i: 代表當前的物品總數量
            j: 代表當前的背包總體積
         */
        dp[0][0] = 0;
        dp[0][1] = 0;
        dp[1][0] = 0;
        for (int i = 1; i <= N; i++) {
            /*
                w: 代表第i個物品的體積
                v: 代表第i個物品的價值
             */
            int w = weights[i-1];
            int v = values[i-1];
            for (int j = 1; j <= W; j++) {
                if (j>=w){
                    dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-w]+v);
                }else {
                    // 如果當前這個第i個物品的體積比當前背包的總體積要大,那說明不能放入背包,
                    // 直接就是考慮前i-1個物品放入背包,所能得到的最大價值
                    dp[i][j] = dp[i-1][j];
                }
            }
        }
        return dp[N][W];
    }

優化空間后的代碼:

    public int knapsacks2(int W,int N,int[] weights,int[] values){
        int[] dp = new int[W + 1];
        for (int i = 1; i <= N; i++) {
            int w = weights[i - 1], v = values[i - 1];
            for (int j = W; j >= 1; j--) {
                if (j >= w) {
                    dp[j] = Math.max(dp[j], dp[j - w] + v);
                }
            }
        }
        return dp[W];
    

 

LeetCode練習:

第416題:

給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。

注意:

每個數組中的元素不會超過 100
數組的大小不會超過 200

思路:

對於此題,如果能夠分割成兩個子集,同時這兩個子集的元素和相等,那么前提是這個數組的所有元素的和是一個偶數,否則一定不能分割成兩個元素和相等的子集;

如果所有的元素和是一個偶數,那么符合要求,我們接下來的目標就是尋找一個子集,這個子集的元素和應該是數組所有元素和的一半,就只要能夠找到一個子集合它的所有元素和是 sum / 2,那么返回true,暫且把sum/2命名為target;

經過分析可以直到,這是一個典型的0-1背包問題,target相等於背包的容量,只不過我們在這里不是如何使得背包裝滿的情況下,使得其價值最大,而只要能夠裝滿即可,所以采用動態規划的解決方案如下:

  1.定義一個狀態轉移數組dp,dp[i][j]表示前i個元素中能否找到和為j的元素的子集,dp數組的類型是布爾類型

  2.尋找狀態轉移方程:對於第i個元素,如果其放入“背包”中,那么dp[i][j]值應該是dp[i][j] = dp[i-1][j-nums[i]];如果不放入“背包”中,那么dp[i][j] = dp[i-1][j];

  3.確定初始值,dp[0][0] = true,因為對於前0個元素,其和就是0,所以是能夠找到和為0的子集合的。

代碼如下:

public boolean canPartition(int[] nums) {
        if (nums.length == 1 && nums[0] != 0)
            return false;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 !=0)
            return false;
        int target = sum / 2;
        boolean[][] dp = new boolean[nums.length+1][target+1];
        dp[0][0] = true;
     // 元素從第1個到第n個
     // 容量從0到target
for (int i = 1; i <= nums.length; i++) { for (int j = 0; j <= target; j++) { if (j >= nums[i-1]) dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i-1]]; else dp[i][j] = dp[i-1][j]; } } return dp[nums.length][target]; }

優化空間后的代碼:

public boolean canPartition2(int[] nums) {
        if (nums.length == 1 && nums[0] != 0)
            return false;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 !=0)
            return false;
        int target = sum / 2;
        boolean[] dp = new boolean[target+1];
        dp[0] = true;
        for (int i = 0; i < nums.length; i++) {
            for (int j = target; j >= nums[i]; j--) {
                if (j >= nums[i])
                    dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[target];
    }

 繼續優化,通過分析狀態轉移方程我們可以直到,我們要的最后的結果是dp[target],而最終的dp[target]是從dp[target - nums[nums.length-1]]得到,以此類推,對於dp[i]我們知道,推導出它的公式是 dp[i] = dp[target - sum(nums[i..nums.length-1])],所以我們可以對內循環的邊界進行進一步的優化,從而減少循環的次數,優化的邊界值如下:

  bound = Math.max(nums[i],target - sumarray(nums,i))

修改后的程序如下所示:

    public boolean canPartition3(int[] nums) {
        if (nums.length == 1)
            return false;
        int sum = sumarray(nums,0);
        if (sum % 2 !=0)
            return false;
        int target = sum / 2;
        boolean[] dp = new boolean[target+1];
        dp[0] = true;
        for (int i = 0; i < nums.length; i++) {
            int bound = Math.max(nums[i],target - sum);
            for (int j = target; j >= bound; j--) {
                dp[j] = dp[j] || dp[j - nums[i]];
            }
            sum = sum - nums[i];
        }
        return dp[target];
    }
    private int sumarray(int[] nums,int index){
        int res = 0;
        for (int i = index; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM