leetcode題解(0-1揹包問題)

吳軍旗發表於2019-03-04

主要介紹0-1揹包問題,及一些leetcode題解

0-1揹包問題

定義

有一個揹包,他的容量為C(Capacity)。現在有n中不同的物品,編號為0…n-1,其中每一件物品的重量為w(i),價值為v(i)。問可以向這個揹包中盛放哪些物品,使得在不超過揹包容量的基礎上,物品的總價值最大。

解題思路

這類組合問題,我們都可以使用遞迴來完成。只是我們在其中能不能找到重疊子問題,最優子結構進而轉化成記憶化搜尋或動態規劃來解決。

對於這個問題,我們有兩個約束條件

  • 首先我們要在n個物品裡選
  • 第二點他的容量要小於等於一個給定的數值C。

狀態定義

注意這個問題要使用兩個變數來定義狀態。

F(n,C)考慮將n個物品放進容量為C的揹包,使得價值最大。

狀態轉移

對於F(i,c),有兩種情況,將第i個物品加入和直接忽略第i個物品

F(i,C) = max{F(i-1, C), v(i) + F(i-1, C-w(i))}
複製程式碼

程式碼實現

記憶化搜尋法


    class Knapsack01{
    
    private:
        vector<vector<int>> memo;
    
        // 用 [0...index]的物品,填充容積為c的揹包的最大價值
        int bestValue(const vector<int> &w, const vector<int> v, int index, int c){
    
            if( c <= 0 || index < 0 ) {
                return 0;
            }
    
            if( memo[index][c] != -1 ) {
                return memo[index][c];
            }
    
            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;
        }
    public:
        int knapsack01(const vector<int> &w, const vector<int> &v, int C){
    
            assert( w.size() == v.size() && C >= 0 );
            int n = w.size();
            if( n == 0 || C == 0 )
                return 0;
    
            memo = vector<vector<int>>( n, vector<int>(C+1,-1));
            return bestValue(w, v, n-1, C);
        }
    };
    
複製程式碼

使用動態規劃自底向上解決

模擬一下,每個位置都由兩個位置決定

leetcode題解(0-1揹包問題)


    class Knapsack01{
    
    public:
        // 用 [0...index]的物品,填充容積為c的揹包的最大價值
        int knapsack01(const vector<int> &w, const vector<int> &v, int C){
    
            assert( w.size() == v.size() && C >= 0 );
    
            if ( n == 0) {
                return 0;
            }
    
            int n = w.size();
            vector<vector<int>> memo( n, vector<int>(C+1,0));
    
            for ( int j = 0; j <= C; j++ ) {
                memo[0][j] = ( j >= w[0] ? v[0] : 0 );
            }
    
            for ( int i = 1; i < n; i++ ) {
                for ( int j = 0; j <= C; j++ ) {
                    // 0~i這些物品容積為j的揹包獲得的最大值
                    memo[i][j] = memo[i-1][j];
                    if( j >= w[i] ) {
                        memo[i][j] = max( memo[i][j], v[i] + memo[i-1][j-w[i]]);
                    }
                }
            }
    
            return memo[n-1][C];
        }
    };
    
複製程式碼

0-1 揹包問題優化

分析

上面的0-1揹包問題的時間複雜度:O(nC),空間複雜度:O(nC)。

我們分析一下狀態轉移方程:F(i,C) = max{F(i-1, C), v(i) + F(i-1, C-w(i))}
第i行元素只依賴於第i-1行元素。理論上,只需要保持兩行元素。空間複雜度:O(2*C)=O(C)。

兩行輪流使用完成揹包問題

    
    class Knapsack01{
    
    public:
        int knapsack01(const vector<int> &w, const vector<int> &v, int C){
            assert( w.size() == v.size() && C >= 0 );
            int n = w.size();
            if( n == 0 && C == 0 )
                return 0;
    
            vector<vector<int>> memo( 2, vector<int>(C+1,0));
    
            for( int j = 0 ; j <= C ; j ++ )
                memo[0][j] = ( j >= w[0] ? v[0] : 0 );
    
            for( int i = 1 ; i < n ; i ++ )
                for( int j = 0 ; j <= C ; j ++ ){
                    memo[i%2][j] = memo[(i-1)%2][j];
                    if( j >= w[i] )
                        memo[i%2][j] = max( memo[i%2][j], v[i] + memo[(i-1)%2][j-w[i]]);
                }
            return memo[(n-1)%2][C];
        }
    };
    
複製程式碼

繼續優化

只使用一行完成揹包問題,比較複雜:我們從右向左重新整理內容

leetcode題解(0-1揹包問題)

程式碼實現


    class Knapsack01{
    
    public:
        int knapsack01(const vector<int> &w, const vector<int> &v, int C){
            assert( w.size() == v.size() && C >= 0 );
            int n = w.size();
            if( n == 0 || C == 0 )
                return 0;
    
            vector<int> memo(C+1,0);
    
            for( int j = 0 ; j <= C ; j ++ )
                memo[j] = ( j >= w[0] ? v[0] : 0 );
    
            for( int i = 1 ; i < n ; i ++ )
                for( int j = C ; j >= w[i] ; j -- )
                    memo[j] = max( memo[j], v[i] + memo[j-w[i]]);
    
            return memo[C];
        }
    };
    
    
複製程式碼

0-1揹包問題變種

  • 多重揹包問題:每個物品不止1個,有num(i)個
  • 完全揹包問題:每個物品可以無限使用
  • 多維費用揹包問題:要考慮物品的體積和重量兩個維度
  • 物品間加入更多約束:物品間可以相互排斥;也可以相互依賴

面試中的0-1揹包問題

leetcode 416. 分割等和子集

paste image

分析

典型的揹包問題,在n個物品中選出一定物品,填滿sum/2的揹包。

記憶化搜尋法

    
    class Solution {
    private:
        // memo[i][c] 表示使用索引為[0...i]的這些元素,是否可以完全填充一個容量為c的揹包
        // -1 表示為未計算; 0 表示不可以填充; 1 表示可以填充
        vector<vector<int>> memo;
    
        // 使用nums[0...index], 是否可以完全填充一個容量為sum的揹包
        bool tryPartition(const vector<int> &nums, int index, int sum){
    
            if( sum == 0 ) {
                return true;
            }
    
            if( sum < 0 || index < 0 ) {
                return false;
    
            }
    
            if( memo[index][sum] != -1 ) {
                return memo[index][sum] == 1;
            }
    
            memo[index][sum] = (tryPartition(nums, index-1 , sum ) ||
                                tryPartition(nums, index-1 , sum - nums[index] ) ) ? 1 : 0;
    
            return memo[index][sum] == 1;
        }
    public:
        bool canPartition(vector<int>& nums) {
    
            int sum = 0;
            for( int i = 0 ; i < nums.size() ; i ++ ){
                assert( nums[i] > 0 );
                sum += nums[i];
            }
    
            if( sum%2 ) {
                return false;
            }
    
            memo = vector<vector<int>>(nums.size(), vector<int>(sum/2+1,-1));
            return tryPartition(nums, nums.size()-1 , sum/2 );
        }
    };
    
複製程式碼

自底向上使用動態規劃

    
    class Solution {
    
    public:
        bool canPartition(vector<int>& nums) {
    
            int sum = 0;
            for( int i = 0 ; i < nums.size() ; i ++ ){
                assert( nums[i] > 0 );
                sum += nums[i];
            }
    
            if( sum%2 ) {
                return false;
            }
    
            int n = nums.size();
            int C = sum / 2;
            vector<bool> memo(C+1, false);
    
            for ( int i = 0; i <= C; i++ ) {
                memo[i] = ( nums[0] == i );
            }
    
            for ( int i = 1; i < n; i++ ) {
                for ( int j = C; j >= nums[i]; j-- ) {
                    memo[j] = memo[j] || memo[ j - nums[i] ];
                }
            }
    
            return memo[C];
        }
    };
    
複製程式碼

相似問題

  • leetcode 322
  • leetcode 377
  • leetcode 474
  • leetcode 139
  • leetcode 494

-------------------------華麗的分割線--------------------

看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

個人部落格番茄技術小棧掘金主頁

想了解更多,歡迎關注我的微信公眾號:番茄技術小棧

番茄技術小棧

相關文章