算法模板-01背包
本文轉(zhuǎn)載自Maple博客的算法模板-01背包問題,轉(zhuǎn)載請注明出處。
題目描述
最基本的01背包問題描述如下:
有NNN件物品和一個容量為VVV的背包,放入第iii個物品需要耗費(fèi)的代價為CiC_{i}Ci?,得到的價值為WiW_{i}Wi?,求解將哪些物品裝入背包可使價值總和最大。
解法
基本思路
最基礎(chǔ)的背包問題,其每一件物品只有一個,可以選擇放或者不放。一般采用二維動態(tài)規(guī)劃來解決問題,通常定義其子狀態(tài)為dp[i][v]dp[i][v]dp[i][v],表示前iii個物品放入到容量為vvv的背包中的最大的價值總和。那么這個值是如何得到的呢,對于第iii個物品,無非就是兩種情況,放或者不放。如果不放或者根本放不下(v<Civ < C_{i}v<Ci?),那么問題等同于前i?1i - 1i?1個物品放入到容量為vvv的背包的情況,即:
dp[i][v]=dp[i?1][v]dp[i][v] = dp[i - 1][v]dp[i][v]=dp[i?1][v]
如果選擇放,那么問題就等同于前i?1i - 1i?1個物品放入到容量為v?Civ - C_{i}v?Ci?的背包的情況,即:
dp[i][v]=dp[i?1][v?Ci]dp[i][v] = dp[i - 1][v - C_{i}]dp[i][v]=dp[i?1][v?Ci?]
最終dp[i][v]dp[i][v]dp[i][v]的取值就兩者中更大的一個。
初始化dp數(shù)組時的細(xì)節(jié)
在大多數(shù)動態(tài)規(guī)劃解法的題目中,初始化一直是很重要并且較難思考的一件事情。一般在求最優(yōu)解的背包問題中,有兩種問法,一個是要求“恰好裝滿背包”時的最優(yōu)解,另一個則是不需要恰好裝滿背包,只求最優(yōu)解。
如果是第一種情況,要求恰好裝滿背包,那么在初始化的時候,除了dp[0][0]dp[0][0]dp[0][0]為0,其它的dp[0][1...V]dp[0][1...V]dp[0][1...V]均設(shè)為?∞-\infty?∞。說明此時只有容量為0的背包可以在什么也不裝的情況下被“恰好裝滿”,其價值為0,而容量為1…V的背包,無法在沒有物品的情況下被裝滿,沒有合法的解,所以設(shè)定為?∞-\infty?∞。
對于第二種情況,題目并沒有要求背包裝滿,只是希望價值盡可能大,那么dp[0][0...V]dp[0][0...V]dp[0][0...V]均初始化為0。因為任何容量的背包都有一個合法解“什么都不裝”,這個解的價值為0,所以初始化狀態(tài)的值也就全部為0了。
背包問題的優(yōu)化
上述的基本思路中,時間和空間的復(fù)雜度均為O(VN)O(VN)O(VN),其中的空間復(fù)雜度是可以被優(yōu)化到O(V)O(V)O(V)的。從dp[i][v]dp[i][v]dp[i][v]的更新方程中不難看出,其實dp[i]dp[i]dp[i]的值,只和dp[i?1]dp[i - 1]dp[i?1]有關(guān),如下圖:
那我們在一開始初始化的時候,只需要初始化dp[0...V]dp[0...V]dp[0...V]大小的數(shù)組即可,每一次對物品的循環(huán)(iii)都只需要更新這一行數(shù)組就行。但需要注意的是,更新的時候要逆序更新,即從V更新到0。
舉個例子,假如正向更新,訪問到了v=6v=6v=6的時候,你需要通過new_dp[6]=max(old_dp[6],old_dp[3])new\_dp[6] = max(old\_dp[6], old\_dp[3])new_dp[6]=max(old_dp[6],old_dp[3]),而由于是正向更新,v=3v=3v=3的值已經(jīng)被更新成了new_dp[3]new\_dp[3]new_dp[3],old_dp[3]old\_dp[3]old_dp[3]已經(jīng)無法被訪問,所以無法更新,所以這一層循環(huán)需要你需更新。
題目列表
在這里列舉leetcode上四題經(jīng)典01背包的題目。
- leetcode-416 分割等和子集
- leetcode-474 一和零
- leetcode-494 目標(biāo)和
- leetcode-879 盈利計劃
416 分割等和子集
原題鏈接
這題要求判斷是否一個數(shù)組分成相等的兩個子數(shù)組。
即選取若干個數(shù)字,剛好值為和的一半。
Python代碼如下:
優(yōu)化空間復(fù)雜度的版本如下:
class Solution:def canPartition(self, nums: List[int]) -> bool:n = len(nums)if n < 2:return Falsetotal = sum(nums)if total % 2 != 0:return Falsetarget = total // 2dp = [True] + [False] * targetfor i, num in enumerate(nums):for j in range(target, num - 1, -1):dp[j] |= dp[j - num]return dp[target]474 一和零
原題鏈接
這題要求給你一個字符串?dāng)?shù)組,里面的字符串均為0和1組成,例如:strs = [“10”, “0001”, “111001”, “1”, “0”],再給你m=5m=5m=5和n=3n=3n=3,讓你找出0的個數(shù)小于mmm且1的個數(shù)小于nnn的最大子集的長度。
這題同經(jīng)典背包問題不同的是,它有兩重限制,需要采用三維動態(tài)規(guī)劃完成,Python代碼如下:
雖然是三維動態(tài)規(guī)劃,但其也可以進(jìn)行空間復(fù)雜度的優(yōu)化,其代碼如下:
class Solution:def findMaxForm(self, strs: List[str], m: int, n: int) -> int:k = len(strs)if m == 0 and n == 0: return 0dp = [[0] * (n + 1) for _ in range(m + 1)]dp[0][0] = 0for s in range(1, k + 1):for i in range(m, -1, -1):for j in range(n, -1, -1):nums0, nums1 = self.count(strs[s - 1])if nums0 <= i and nums1 <= j:dp[i][j] = max(dp[i][j], dp[i - nums0][j - nums1] + 1)return dp[m][n]def count(self, s):count0, count1 = 0, 0for ss in s:if ss == '0':count0 += 1elif ss == '1':count1 += 1return count0, count1494 目標(biāo)和
原題鏈接
題目要求,將給的數(shù)組中每個數(shù)字前面添加“+++”或者“?-?”,使得整個公式的和為題目給的target,問一共有多少種公式。
例如,對于nums = [1,1,1,1,1], target = 3,則共有5種公式:
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
第一眼看上去這題與背包問題并不一致,需要做一點(diǎn)小小的改變。通過所有數(shù)的總和以及給出的target,我們可以計算出所有前面符號為“?-?”的數(shù)的總和為total?target2\frac{total - target}{2}2total?target?。原題也就轉(zhuǎn)換成,要求出有多少種挑選的方法可以使得挑選出來的數(shù)字和為total?target2\frac{total - target}{2}2total?target?,和416類似。此外,若total?target2\frac{total - target}{2}2total?target?根本就只是小數(shù),那說明一種方法都沒有,直接返回0。
Python代碼如下:
優(yōu)化版本的Python代碼如下:
class Solution:def findTargetSumWays(self, nums: List[int], target: int) -> int:total = sum(nums)if total < target: return 0if (total - target) % 2 == 1: return 0neg = (total - target) // 2n = len(nums)dp = [0] * (neg + 1)dp[0] = 1for i in range(1, n + 1):for j in range(neg, -1, -1):if nums[i - 1] <= j:dp[j] += dp[j - nums[i - 1]]return dp[-1]879 目標(biāo)和
原題鏈接
原題看上去比較復(fù)雜,其實跟474一樣,是二重限制的背包問題,需要三重動態(tài)規(guī)劃來解。
Python代碼如下:
其中,在狀態(tài)轉(zhuǎn)移的時候,dp[k]dp[k]dp[k]并不是直接等于dp[k?profit[i?1]]dp[k - profit[i - 1]]dp[k?profit[i?1]],這是因為kkk代表著至少利潤是kkk,k?profit[i?1]k - profit[i - 1]k?profit[i?1]是可能為負(fù)的,也是合法狀態(tài),但數(shù)組中并沒有存儲“利潤至少為負(fù)”的狀態(tài),所以需要改成dp[max(0,k?profit[i?1])]dp[max(0, k - profit[i - 1])]dp[max(0,k?profit[i?1])],因為“利潤至少為負(fù)”和“利潤至少為零”是等價的。
優(yōu)化版本的Python代碼如下:
總結(jié)
- 上一篇: 动态规划算法-07背包问题进阶
- 下一篇: PoolFormer解读