1. 程式人生 > 實用技巧 >動態規劃之6:揹包問題

動態規劃之6:揹包問題

揹包問題

揹包問題是一種組合優化的NP 完全問題:有N 個物品和容量為W的揹包,每個物品都有自己的體積w 和價值v,求拿哪些物品可以使得揹包所裝下物品的總價值最大。如果限定每種物品只能選擇0 個或1 個,則問題稱為0-1 揹包問題;如果不限定每種物品的數量,則問題稱為無界揹包問題或完全揹包問題。

0-1 揹包

我們可以定義一個二維陣列 dp 儲存最大價值,其中 dp[i][j] 表示前 i 件物品體積不超過 j 的情況下能達到的最大價值。在我們遍歷到第 i 件物品時,在當前揹包總容量為 j 的情況下,如果我們不將物品 i 放入揹包,那麼dp[i][j]= dp[i-1][j],即前 i 個物品的最大價值等於只取前 i-1 個物品時的最大價值;如果我們將物品 i 放入揹包,假設第 i 件物品體積為w,價值為v,那麼我們得到 p[i][j] = dp[i-1][j-w] + v

。我們只需在遍歷過程中對這兩種情況取最大值即可。

int knapsack(vector<int> weights, vector<int> values, int N, int W)
{
	vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
	for (int i = 1; i <= N; ++i)
	{
		int w = weights[i - 1], v = values[i - 1];
		for (int j = 1; j <= W; ++j) {
			if (j >= w) {
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v);
			}
			else {
				dp[i][j] = dp[i - 1][j];
			}
		}
	}
	return dp[N][W];
}

進一步可以進行空間壓縮:

int knapsack(vector<int> weights, vector<int> values, int N, int W) {
	vector<int> dp(W + 1, 0);
	for (int i = 1; i <= N; ++i) 
	{
		int w = weights[i - 1], v = values[i - 1];
		for (int j = W; j >= w; --j) {
			dp[j] = max(dp[j], dp[j - w] + v);
		}
	}
	return dp[W];
}

完全揹包

在完全揹包問題中,一個物品可以拿多次。完全揹包問題的狀態轉移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v),其與 0-1 揹包問題的差別僅僅是把狀態轉移方程中的第二個 i-1 變成了 i。

int knapsack(vector<int> weights, vector<int> values, int N, int W) 
{
	vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
	for (int i = 1; i <= N; ++i) {
		int w = weights[i - 1], v = values[i - 1];
		for (int j = 1; j <= W; ++j) 
		{
			if (j >= w) {
				dp[i][j] = max(dp[i - 1][j], dp[i][j - w] + v);
			}
			else {
				dp[i][j] = dp[i - 1][j];
			}
		}
	}
	return dp[N][W];
}

狀態空間壓縮:

int knapsack(vector<int> weights, vector<int> values, int N, int W) 
{
	vector<int> dp(W + 1, 0);
	for (int i = 1; i <= N; ++i) 
	{
		int w = weights[i - 1], v = values[i - 1];
		for (int j = w; j <= W; ++j) {
			dp[j] = max(dp[j], dp[j - w] + v);
		}
	}
	return dp[W];
}

分割等和子集

分割等和子集

本題可以等價於 0-1 揹包問題,設所有數字的和為 sum,我們的目標是選出一部分使得他們的和為 sum/2。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
		int sum  = accumulate(nums.begin(),nums.end(),0);
		if(sum % 2) return false; //@ 如果和是奇數,不可能平分
		int target = sum / 2,n = nums.size();
		vector<vector<bool>> dp(n+1,vector<bool>(target+1,false));
		
		for(int i=0;i<=n;++i)
			dp[i][0] = true;
		
		for(int i=1;i<=n;++i)
			for(int j=nums[i-1];j<=target;++j)
				dp[i][j] = dp[i-1][j] || dp[i-1][j-nums[i-1]];
		
		return dp[n][target];
    }
};

也可以對本題進行空間壓縮。注意對數字和的遍歷需要逆向。

class Solution {
public:
    bool canPartition(vector<int>& nums) {
		int sum  = accumulate(nums.begin(),nums.end(),0);
		if(sum % 2) return false; //@ 如果和是奇數,不可能平分
		int target = sum / 2,n = nums.size();
		vector<bool> dp(target+1,false));	
		dp[0] = true;
		
		for(int i=1;i<=n;++i)
			for(int j=target;j>=nums[i-1];--j)
				dp[j] = dp[j] || dp[j-nums[i-1]];
		
		return dp[target];
    }
};

一和零

一和零

這是一個多維費用的 0-1 揹包問題,有兩個揹包大小, 0 的數量和 1 的數量。我們在這裡直
接展示三維空間壓縮到二維後的寫法。

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
		vector<vector<int>> dp(m+1,vector<int>(n+1,0));
		for(const string & str : strs)
		{
			auto [count0,count1] = count(str);
			for(int i=m;i>=count0;--i)
				for(int j=n;j>=count1;--j)
					dp[i][j] = max(dp[i][j],1+dp[i-count0][j-count1]);
		}
		return dp[m][n];
    }
	
	//@ 輔助函式,統計0,1的數量
	pair<int,int> count(const string& s)
	{
		int count0 = s.length(),count1 = 0;
		for(const char &c : s)
		{
			if(c == '1')
			{
				++count1;
				--count0;
			}
		}
		return make_pair(count0,count1);
	}
};

零錢兌換

零錢兌換

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if (coins.empty() || amount <= 0)
            return 0;

        int MAX =  INT_MAX -1; //@ 防止 INT_MAX + 1 溢位
        vector<int> dp(amount + 1, MAX);
        dp[0] = 0;
        for (int i = 1; i <= amount; ++i)    
            for(auto coin : coins)
            {
                if(coin <= i)
                    dp[i] = min(dp[i], dp[i -coin] + 1);
            }   
        return dp.back() == MAX ? -1 : dp.back();
    }
};

零錢兌換 II

零錢兌換 II

class Solution {
public:
    int change(int amount, vector<int>& coins) {
     vector<int> dp(amount+1,0);
     dp[0] = 1;
     for(auto coin : coins)
         for(int i = coin;i<=amount;++i)
            dp[i] += dp[i-coin];
     
     return dp[amount];
    }
};