1. 程式人生 > >動態規劃入門——詳解完全揹包與多重揹包問題

動態規劃入門——詳解完全揹包與多重揹包問題

本文始發於個人公眾號:TechFlow,原創不易,求個關注


今天是演算法資料結構專題的第13篇文章,也是動態規劃專題的第二篇。

上一講當中我們一起學習了動態規劃演算法中的零一揹包問題,我們知道了所謂的零一揹包是指每一種物品只有一個,所以它的狀態只有0和1兩種,即拿或者不拿。而今天我們要來討論物品不止有一個的情況,物品不止有一個也分兩種,一種是不作任何限制,要多少有多少,這種稱為完全揹包問題,另一種是依然有個數限制,這種稱為多重揹包問題。

我們一個一個來看,我們先從其中比較簡單的完全揹包開始。由於我們這是一個連續的專題,沒有看過上篇文章或者是新關注的同學可以移步我們專題的第一篇:

動態規劃入門——詳解經典的零一揹包問題

完全揹包

在之前的文章當中,我們闡述了動態規劃當中狀態和決策以及狀態轉移的相關概念。在揹包問題當中,揹包的容量是狀態,而選擇哪個物品進行獲取則是決策,當我們制定了一個決策之後,揹包會從一個狀態轉移到另一個狀態。而動態規劃演算法就是列舉所有狀態和決策,獲得所有的狀態轉移,並且記錄這個過程中每個狀態能夠獲得的最優解。

在之前的文章當中,我們先遍歷了所有的決策,然後再枚舉了所有的狀態,計算在決策下進行轉移之後得到的結果。在之前的零一揹包問題當中,由於我們每個物品只能獲取一個,如果在前面的狀態執行了決策,那麼後面的狀態則不能進行相同的決策。這也就是動態規劃的後效性,而在完全揹包問題當中,我們去掉了這個限制,也就意味著決策之間不再有後效性,一個決策可以重複應用在各個狀態當中。

所以如果你能理解上面這段話,那麼整個演算法其實非常簡單,幾乎就是零一揹包的程式碼。只不過我們把其中倒敘遍歷的揹包狀態再”修正“回來。

之前我們為了避免物品的重複獲取,所以採用了倒敘遍歷的方法,如今我們不再對數量進行限制,意味著我們可以自由地採取決策進行轉移。要做到這點,就是單純的兩重迴圈,第一種列舉決策, 第二重列舉狀態,記錄所有轉移可能帶來的最優解即可。我們來看程式碼:

dp = [0 for _ in range(11)]

items = [[6, 10], [5, 8], [5, 9]]

# 遍歷物品
for v, w in items:
# 遍歷揹包空間(狀態)
# 更新vp+v的狀態,即當前容量放入物品之後的狀態
for vp in range(0, 10-v+1):
dp[vp+v] = max(dp[vp+v], dp[vp] + w)

print(max(dp]))

如果你還沒能完全理解其中的邏輯,我們可以對照一下程式碼再來理解一下。在第一種迴圈當中,我們遍歷了所有的物品,每一個物品對應了一種決策。每一個決策可以應用在各個狀態上,比如第一個物品是6, 15,代表它的體積是6,價值是15。那麼我們遍歷所有能夠應用這個決策的狀態,也就是在不超過揹包容量的情況下能夠放下的狀態。顯然對於一個體積是6的物品來說,只有0到4的狀態可以放下。比如說我們選擇狀態2,狀態2放下了這個物品之後,自然會轉移到狀態8,因為體積增加了6。有可能這個決策會使得狀態8獲得更好的結果,也有可能不會,如果會的話我們就更新一下狀態8記錄的值。這個從一個狀態採取決策到另一個狀態的過程就是狀態轉移。

完全揹包就是零一揹包的無限制版,從原理上來說,兩者的思路和做法基本上是一樣的。如果你能理解零一揹包,那麼完全揹包對你來說也一定不在話下。

細小的優化

在完全揹包當中,由於所有的物品都可以無限獲取。所以我們可以引入一些零一揹包不能進行的優化,比如對於同樣體積的物品而言,我們可以只保留價值最高的物品,將其他的物品過濾掉。這個思路很樸素,我想大家應該都能理解。

比如兩個物品體積都是3,一個價值是4,另一個價值是3,我們完全可以忽略價值是3的那一種。因為兩者帶來的狀態轉移是一樣的,但是明顯前者收益更好。而這個優化在零一揹包當中不可行是因為每個物品只有一個,很有可能會出現兩者都要的情況,所以不能簡單地替換。而在完全揹包當中則沒有這個問題。

多重揹包

和零一揹包以及完全揹包相比,多重揹包要難上一些,它的解法也非常多樣。我們今天先來看一些相對比較簡單的方法。

同樣,我們從最簡單的方法開始講起。最簡單的方法當然就是將多重揹包蛻化成零一揹包來解決,比如一個物品最多可以拿N個,我們就把它拆成N種物品,這N種每種物品最多拿一個,相當於我們一種物品可以最多拿N個。這個思路應該很簡單,大家都能想明白,但是有個很大的問題,就是複雜度。當然我們可以根據揹包的體積做一些優化,比如當物品的數量很多並且超過了揹包容量的時候,我們可以把超過容量的數量去掉,但是整體的複雜度還是很高。尤其是當我們揹包容量很大的時候。

那麼,我們怎麼來解決這個問題呢?

這裡要介紹一個比較通用的演算法,這個演算法可以用來優化很多問題,也是很多演算法的思想。它就是二進位制表示法。這個方法我們在之前的文章當中曾經講到過,思想非常簡單,但是非常實用。

二進位制表示法

所謂二進位制表示法就是將一個int型別的數表示成二進位制,整個演算法的思想就是這一句話,所以我想大家應該都能理解。但是我們為什麼要將一個int轉成二進位制,以及轉成二進位制之後怎麼樣來優化演算法,這個才是我們想知道的,也才是演算法的核心重點,不要著急,我們一點點來說明。

我們都知道在計算機系統當中都是以二進位制儲存的所有資料,最典型的就是整數。一個32位的int,可以表示最大21億的整數。這個都是我們已知的,但是換一個角度來看,一個21億的數最後用32個二進位制位就表示了,其實非常驚人。為什麼說二進位制是一個非常偉大的思想?不在於它難,而在於它高效地壓縮了資料。

我們進一步來看,32個二進位制位為什麼能表示這麼大的資料呢?因為這32位int表示的資料是不一樣的,第0位表示1,第1位表示2,第2位表示4……到了第31位的時候,表示的數已經非常龐大。我們用這32個數不同的組合來表示不同的數,換句話說範圍內的所有數最終都變成了這32個數中若干個的累加。我們寫成公式就是:,這裡的表示的是第i位的係數,它只有0和1兩個取值。

這個式子大家都熟悉,但是我們把它應用在方程當中可能很多人就不清楚了。比如說某個函式如果滿足這樣的性質:,如果直接求很麻煩,或者是開銷很大,我們就可以用和來獲得。同理,我們用在二進位制上,我們可以得到:

看到了嗎,我們把的值轉化成了最多32個值的和,在有些場景當中是很容易計算的,但是很難直接計算,這個時候我們通過二進位制轉化就會很簡單。

同理,累加理解了,累乘也就水到渠成。如果某個函式滿足:,那麼我們同樣可以用二進位制來表達:

對於多重揹包這個問題,顯然我們滿足的是累加性質。也就是說,對於一個較大的x而言,我們可以用若干個子狀態累加得到。由於,所以我們很容易發現,,也就是說這些子狀態之間彼此存在倍數關係。因此我們可以很輕鬆地計算出這些子狀態,再根據x的二進位制表示來累加求到,而直接計算則困難得多,計算量也大得多。

在這個問題當中,函式f表示的是我們拿取物品的價值。也就是說,某一種物品,假設最多有n個,並且單個的價值是p,那麼我們拿取2個就是2p,拿取4個就是4p,對於所有2的冪個數的價值都很容易計算。我們需要列舉這n個物品拿取的情況,我們列舉的範圍應該是[0, n]。我們將n轉化成二進位制之後,可以通過logn個2的冪排列組合的和得到[0, n]當中的任意一個數。那麼,我們只需要將2的冪個數的物品看成是新的物品,這樣,我們可以用新的物品的01組合,來代替原物品拿取0-n的所有情況。

舉個例子,我們有一個物品一共有15個,價值是3,其中15=,也就是說我們用4個二進位制位就可以表示1-15這15這數字。那麼我們用4種物品對映這4個二進位制位之後,就可以用這4種物品的組合來表示獲取1-15個原物品了。也就是說我們把15個價值是3的物品打了四個包,第一個包裡有一個,第二個包裡有兩個,第三個包裡有四個,第四個包裡有八個。如果我們要拿3個原物品,相當於拿第一和第二個包裹。如果我們要拿5個原物品,相當於拿第一個和第三個包裹。這樣我們就把多重揹包的問題轉化回了零一揹包。

我們之前說了,32位二進位制位就可以表示20億以上的數,所以雖然我們進行二進位制處理之後物品的數量會增多一些,但也是非常有限的。我們做個簡單的複雜度分析,假設物品的總數是N,每種物品最多M個,揹包的容量是V。如果用樸素的拆分方法,複雜度是,而使用二進位制拆分的複雜度是。和前者相比,從M到logM是一個巨大的優化,尤其當M很大的時候。

最後,還有一個小問題,我們的物品數量並不一定剛好能分成若干個2的冪的和,這種情況下怎麼辦呢?其實也簡單,我們把分剩下的部分單獨打一個包就好了。比如如果物品的數量是10,10=1+2+4+3,所以最後一個包就是3。雖然我們用1+2也能表示3,但是這並不會影響結果的正確性。

到這裡,多重揹包的解法就介紹完了,說了這麼多其實也只是介紹了二進位制表示這個方法而已。理解了這個方法,它就轉化成了零一揹包。不得不說這個方法實在是非常巧妙,並且除了在揹包問題之外,在許多其他問題中也有類似的運用。所以這個方法不建議錯過。

最後,我們來看下程式碼,首先我們來看下二進位制拆分的部分:

def binary_divide(cnt, volume, price):
divides = []
for i in range(32):
# 從0位開始列舉
cur = 1 << i
# 如果小於列舉值,說明已經拆分完畢了
if cnt < cur:
# 把剩下的部分打包
divides.append((cnt, cnt * volume, cnt * price))
break
else:
# 否則繼續拆分,打包1 << i個物品
cnt -= cur
divides.append((cur, cur * volume, cur * price))
return divides

進行完二進位制拆分之後,這個問題就轉化成了零一揹包。我們只需要套用零一揹包的程式碼就可以了:

# 物品,分別是數量,體積和單位價格
items = [(10, 3, 5), (5, 6, 3), (2, 2, 4)]
volume = 20
dp = [0 for _ in range(volume+1)]
new_items = []
for i in items:
# 二進位制拆分
new_items.extend(binary_divide(*i))

for item in new_items:
v, p = item[1], item[2]
for i in range(volume-v, -1, -1):
dp[i + v] = max(dp[i+v], dp[i] + p)
print(dp[20])

通過神乎其神的二進位制表示法,我們將多重揹包問題又還原成了零一揹包,不得不說實在是神奇。但二進位制表示法並不是唯一的方案,我們也可以不用二進位制來完成這道題。這涉及到一種全新的方法,由於篇幅限制,我們會在下篇文章當中和大家一起學習。

今天關於多重揹包和完全揹包的文章就到這裡,如果覺得有所收穫,請順手點個關注或者轉發吧,你們的舉手之勞對我來說很重要。

相關推薦

動態規劃入門——完全揹包多重揹包問題

本文始發於個人公眾號:TechFlow,原創不易,求個關注 今天是演算法資料結構專題的第13篇文章,也是動態規劃專題的第二篇。 上一講當中我們一起學習了動態規劃演算法中的零一揹包問題,我們知道了所謂的零一揹包是指每一種物品只有一個,所以它的狀態只有0和1兩種,即拿或者不拿。而今天我們要來討論物品不止有一個的

動態規劃入門——經典問題零一揹包

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注 今天是週三演算法與資料結構專題的第12篇文章,動態規劃之零一揹包問題。 在之前的文章當中,我們一起探討了二分、貪心、排序和搜尋演算法,今天我們來看另一個非常經典的演算法——動態規劃。 在acm-icpc競賽領域,動態規劃是一個非常大的範

硬幣找零,最長上升子序列,揹包問題等動態規劃問題

1.硬幣找零 如果我們有面值為 1 元、3 元和 5 元的硬幣若干枚,如何用最少的硬幣湊夠 11 元? 首先我們思考一個問題,如何用最少的硬幣湊夠 i 元(i<11)?為什麼要這麼問呢? 兩個原因:1.當我們遇到一個大問題時,總是習慣把問題的規模變小,這樣便於分析討論。 2.這

(轉)dp動態規劃分類

fun balance card 給定 def bits eve 回文串 好的 dp動態規劃分類詳解 轉自:http://blog.csdn.NET/cc_again/article/details/25866971 動態規劃一直是ACM競賽中的重點,同時又是難點,因為

動態規劃演算法

問題描述: 有兩個字串,求最長公共子串的長度,例如 "ANKNGIEK" "AKGBIOK" 上面這兩個字串,公共子序列為AKGIK,長度為5。最長公共子序列就是說有一個字串在兩個字串中都出現過,這裡只考慮從頭到尾的順序,也就是說AKGIK,這個字串中的字母,在第一個字串中出現

動態規劃dp

動態規劃(dynamic programming)是運籌學的一個分支,是求解決策過程(decision process)最優化的數學方法。 分類 編輯 動態規劃一般可分為線性動規,區域動規,樹形動規,揹包動規四類。 舉例: 線性動規:攔截導彈(最長非遞增子序列

dp動態規劃分類

動態規劃一直是ACM競賽中的重點,同時又是難點,因為該演算法時間效率高,程式碼量少,多元性強,主要考察思維能力、建模抽象能力、靈活度。 *****************************************************************

動態規劃演算法及經典例題

動態規劃 什麼是動態規劃? 動態規劃的大致思路是把一個複雜的問題轉化成一個分階段逐步遞推的過程,從簡單的初始狀態一步一步遞推,最終得到複雜問題的最優解。 基本思想與策略編輯: 由於動態規劃解決的問題多數有重疊子問題這個特點,為減少重複計算,對每一個子問題只解一次,將其不同階段的不同狀態儲存在一個二維陣列中。

動態規劃的單調佇列優化(含多重揹包

什麼是單調佇列 單調佇列就是元素單調的佇列,譬如一個佇列中的元素為1,2,3,4,5,6,單調遞增,這就是一個單調佇列。咱們先看一道單調佇列的模板題:poj2823/洛谷P1886 怎麼維護單調佇列呢?譬如維護一個單調遞增的佇列,就是要進入一個元素的時候,把

揹包問題小總結 習題(動態規劃01揹包(第k優完全揹包多重揹包)acm杭電HDU2639,HDU2602,HDU1114,HDU2191

1、01揹包(每種物品只有一個) 題目 有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。 求解將哪些物 品裝入揹包可使價值總和最大。 基本思路 這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。 用子問題定義狀態:    

動態規劃入門-完全揹包(硬幣兌換問題)

C - 完全揹包在一個國家僅有1分,2分,3分硬幣,將錢N兌換成硬幣有很多種兌法。請你程式設計序計算出共有多少種兌法。Input每行只有一個正整數N,N小於32768。Output對應每個輸入,輸出兌換方法數。Sample Input2934 12553Sample Outp

01揹包 ,完全揹包,多重揹包 dp (動態規劃入門dp)

dp 動態規劃,確實難啃, 光 最簡單的 揹包問題,就 費老大勁. 思想! 思想! 思想!   類似於遞推, 區域性找 關係.  揹包問題,  就兩種狀態  放還是不放?   其實關於揹包放不放的

動態規劃入門——多重揹包單調優化

本文始發於個人公眾號:**TechFlow**,原創不易,求個關注 今天是演算法與資料結構的第14篇文章,也是動態規劃專題的第三篇。 在之前的文章當中,我們介紹了多重揹包的二進位制拆分的解法。在大多數情況下,這種解法已經足夠了,但是如果碰到極端的出題人可能還是會被卡時間。這個時候只能用更加快速的方法,也

深入探討Linux靜態庫動態庫的(轉)

share 分享 命名 one .com 過程 程序 簡單介紹 mage 2.生成動態庫並使用 linux下編譯時通過 -shared 參數可以生成動態庫(.so)文件,如下 庫從本質上來說是一種可執行代碼的二進制格式,可以被載入內存中執行。庫分靜態庫和動態庫兩種。

jvm原理(34)虛方法表動態分派機制

編寫程式碼: public class MyTest7 { public static void main(String[] args) { Animal animal = new Animal(); Animal

轉:JAVAWEB開發之許可權管理(二)——shiro入門以及使用方法、shiro認證shiro授權

原文地址:JAVAWEB開發之許可權管理(二)——shiro入門詳解以及使用方法、shiro認證與shiro授權 以下是部分內容,具體見原文。 shiro介紹 什麼是shiro shiro是Apache的一個開源框架,它將軟體系統的安全認證相關的功能抽取出來,實現使用者身份認證,許可權授權、加密、會話

揹包問題:01揹包完全揹包多重揹包

參考連結: 揹包問題是動態規劃演算法的一個典型例項,首先介紹動態規劃演算法: 動態規劃: 基本思想: 動態規劃演算法通常用於求解具有某種最優性質的問題。在這類問題中, 可能會有很多可行解。沒一個解都對應於一個值,我們希望找到具有最優值的解。胎動規

揹包問題——“完全揹包及實現(包含揹包具體物品的求解)

原文地址:http://blog.csdn.net/wumuzi520/article/details/7014830   完全揹包是在N種物品中選取若干件(同一種物品可多次選取)放在空間為V的揹包裡,每種物品的體積為C1,C2,…,Cn,與之相對應的價值為W1

揹包問題(0-1揹包完全揹包多重揹包)

揹包問題 一個揹包總容量為V, 現在有N個物品, 第i個物品容量為weight[i], 價值為value[i], 現在往揹包裡面裝東西, 怎樣裝才能使揹包內物品總價值最大.主要分為3類: 1. 0-1揹包, 每個物品只能取0個,或者1個. 2. 完全揹

Java動態代理機制(JDK動態代理CGLIB動態代理區別)

代理是一種常用的設計模式,其目的就是為其他物件提供一個代理以控制對某個物件的訪問。代理類負責為委託類預處理訊息,過濾訊息並轉發訊息,以及進行訊息被委託類執行後的後續處理。在講述動態代理前,我們先通過一個例子瞭解一下什麼是靜態代理,這裡以事務控制為例。 1.靜態