【算法學習】01揹包問題

來源是慕課網的實戰算法課程——動態規劃

01揹包問題

01揹包
相當於還是求n個物品的組合!
暴力解法:
每一件物品,都可以放進揹包,也可以不放進。複雜度是O( (2^n) * n ), 對於每一個組合,還要看看對應的總重是多少,看看是不是超過了容量C,從而看價值。

組合方式都可以用遞歸的方式來求解。只是是能不能找到重疊子問題、最優子結構,從而轉換爲DP。

設計:
狀態:之前狀態都使用一個參數就解決了問題,通常問題中參數的個數意味着要解決問題需要滿足的約束條件。
這個問題2個約束條件,首先要從n個物品裏選,然後容量要滿足≤C,因此:狀態(函數)是
F(n,C)考慮將n個物品放入容量爲C的揹包,使得價值最大。

狀態轉移:F( i,c) = F(i-1,c)
= v(i) + F( i-1, c-w(i) ) 放入後,考慮前面i-1個物品
對於第i個物品到底要放入or不放入揹包,在二者中選一個max
狀態轉移方程:F(i,c) = max( F(i-1,c), v(i) + F( i-1, c-w(i) ) )
很多動態規劃問題都可以使用類似上面的思想!

寫代碼:依舊先使用自頂向下,再改造爲自底向上
遞歸方式:

class 
Knapsack01 {
private:
    // 用[0,...index]的物品,來填充容積爲c的揹包的最大值
    int bestValue(const vector<int> &w, const vector<int> &v, int index, int c){

        // 無法選物品 or 裝不下物品,都無價值
        if(index < 0 || c<=0)  
            return 0;

        int res = bestValue( w, v, index-1, c);
        // 檢查一下容積C
        if( c >= w[index])
            res = max(res, v[index] + bestValue(w,v,index-1, c-w[index]) );

        return res;
    }

public:
    int knapsack01(const vector<int> &w, const vector<int> &v, int C ){

        int n = w.size();

        return bestValue(w, v, n-1, C); //考慮從0-n-1的n個物品,裝入C
    }
    
};

用記憶化搜索+遞歸:
格式
重疊子問題:index 和c構成了數據對,這樣的數據對在求解過程中同樣的數據對可能求解了多次,爲此可以使用記憶化搜索方式。
由於揹包問題有兩個約束條件,每一個狀態中被2個變量定義,所以開闢記憶化空間是二維數組!—— vector<vector> memo(n, vector(C+1, -1));
memo有n行,每一行都是一個vector,一共有C+1列

private:
    vector<vector<int>> memo;
    // 用[0,...index]的物品,來填充容積爲c的揹包的最大值
    int bestValue(const vector<int> &w, const vector<int> &v, int index, int c){

        // 先判斷有無值
        if(memo[index][c] != -1)
            return memo[index][c];

        // 無法選物品 or 裝不下物品,都無價值
        if(index < 0 || c<=0)  
            return 0;

        for(int i=, i>=0; --i)
            for( int j=1; j<c; ++j ){
                memo[i] + ;
            }

        int res = bestValue( w, v, index-1, c);
        if( c >= w[index])
            res = max(res, v[index] + bestValue(w,v,index-1, c-w[index]) ); // 這裏又重疊子問題了
        
        memo[index][c] = res;
        return res;
    }

動態規劃:

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

int knapsack01(const vector<int> &w, const vector<int> &v, int C ){

        assert(v.zise() == w.size()); // 判斷一下
        int n = w.size();
        if(n==0)
            return 0;

        memo = vector<vector<int>>(n, vector<int>(C+1, -1)); //memo有n行,每一行都是一個vector<int>,一共有C+1列

        for(int j=0; j<=C; ++j)
            memo[0][j] = ( j >= w[0] ? v[0] : 0 );  //j是此時揹包的容量
        
        for(int i=1; i<n; ++i)
            for(int j=0; j<=C; ++j){  

                memo[i][j] = memo[i-1][j];
                if( w[i] <= j )  // 可用容量大
                    memo[i][j] = max(memo[i][j], v[i]+memo[i-1][j-w[i] ] );
            }

        return memo[n-1][C];
    }
    

01揹包複雜度:
時間複雜度:O(nC)
空間複雜度:O(n
C)
優化空間:時間上很小,空間上有很大的優化空間~

01揹包問題的優化:
狀態轉移方程:F(i,c) = max( F(i-1,c), v(i) + F( i-1, c-w(i) ) )
第i行元素只依賴於第i-1行元素。理論上,只需要保持兩行元素。因此空間複雜度變爲:O(2*C) ≈O( C )
在這裏插入圖片描述
上面行處理偶數,下面行處理奇數。

int knapsack01(const vector<int> &w, const vector<int> &v, int C ){

        assert(v.zise() == w.size()); // 判斷一下
        int n = w.size();
        if(n==0)
            return 0;

        memo = vector<vector<int>>(2, vector<int>(C+1, -1)); //memo有n行,每一行都是一個vector<int>,一共有C+1列

        for(int j=0; j<=C; ++j)
            memo[0][j] = ( j >= w[0] ? v[0] : 0 );  //j是此時揹包的容量
        
        for(int i=1; i<n; ++i)
            for(int j=0; j<=C; ++j){  

                memo[i%2][j] = memo[(i-1)%2][j];
                if( w[i] <= j )  // 可用容量大
                    memo[i%2][j] = max(memo[i%2][j], v[i]+memo[(i-1)%2][j-w[i] ] );
            }

        return memo[(n-1)%2][C];
    }
    

進一步優化!
使得只用一行大小爲C的數組來完成DP
在這裏插入圖片描述
對於上一行:永遠只使用上邊和左邊的元素,而不會去碰右邊的元素!
因此,如果只有一行的話,只看當前值和之前的內容~ 從而只需要從右向左的來刷新內容~ 即從:C=5,4,…1,0
如果有一個格子,容量不足以裝下當前的物品,則這個容量到之前的物品都不能放置了!都要使用原先的物品來放。從而這樣算法還能提前終止一些,空間時間複雜度都可以降低一些~

int knapsack01(const vector<int> &w, const vector<int> &v, int C ){

        assert(v.zise() == w.size()); // 判斷一下
        int n = w.size();
        if(n==0)
            return 0;

        vector<int> memo(C+1, -1); //memo有n行,每一行都是一個vector<int>,一共有C+1列

        for(int j=0; j<=C; ++j)
            memo[j] = ( j >= w[0] ? v[0] : 0 );  //j是此時揹包的容量
        
        for(int i=1; i<n; ++i)
            for(int j=C; j>=w[i]; --j){   // 條件是j>w[i]
                  // 可用容量大
                memo[j] = max(memo[j], v[i] + memo[j-w[i]] );
            }

        return memo[C];
    }

01揹包的變種:
完全揹包問題:每個物品可以無限使用~
(雖然每個物品可以無限使用,但是揹包容量有限,所以其實每個物品可以取到的個數是有最大值的!
===>所以把無限使用的物品揹包問題轉換爲有點使用物品的揹包問題~, 只是在選取的物品列表中有很多物品是重複的)
優化方案:(對於揹包容積很大的時候很好的優化思路)對於任何一個數字而言,都可以用一個二進制碼來表示,所以對於同一個物品,不一定向列表中添加1,而是添加1 2 4 8 16這樣的序列,表示同一個物品取了多少個。

多重揹包:每個物品不止一個,有num(i)個
多重揹包是更簡單的

多爲費用揹包問題:要考慮物品的體積和重量兩個維度!(也就是用三維數組)
物品加入更多約束:物品之間可以互相排斥;也可以互相依賴

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章