揹包九講-揹包問題彙總
揹包九講
前言
本篇文章是我(dd_engi)正在進行中的一個雄心勃勃的寫作計劃的一部分,這個計劃的內容是寫作一份較為完善的NOIP難度的動態規劃總結,名為《解動態規劃題的基本思考方式》。現在你看到的是這個寫作計劃最先發布的一部分。
揹包問題是一個經典的動態規劃模型。它既簡單形象容易理解,又在某種程度上能夠揭示動態規劃的本質,故不少教材都把它作為動態規劃部分的第一道例題,我也將它放在我的寫作計劃的第一部分。
讀本文最重要的是思考。因為我的語言和寫作方式向來不以易於理解為長,思路也偶有跳躍的地方,後面更有需要大量思考才能理解的比較抽象的內容。更重要的是:不大量思考,絕對不可能學好動態規劃這一資訊學奧賽中最精緻的部分。
講解大綱
第一講 01揹包問題,這是最基本的揹包問題,每個物品最多隻能放一次。
第二講 完全揹包問題,第二個基本的揹包問題模型,每種物品可以放無限多次。
第三講 多重揹包問題,每種物品有一個固定的次數上限。
第四講 混合三種揹包問題,將前面三種簡單的問題疊加成較複雜的問題。
第五講 二維費用的揹包問題,一個簡單的常見擴充套件。
第六講 分組的揹包問題,一種題目型別,也是一個有用的模型。後兩節的基礎。
第七講 有依賴的揹包問題,另一種給物品的選取加上限制的方法。
第八講 泛化物品,我自己關於揹包問題的思考成果,有一點抽象。
第九講 揹包問題問法的變化,試圖觸類旁通、舉一反三。
附:USACO中的揹包問題
第一講 01揹包問題
題目
有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使價值總和最大。
基本思路
這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。則其狀態轉移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。所以有必要將它詳細解釋一下:“將前i件物品放入容量為v的揹包中”這個子問題,若只考慮第i件物品的策略(放或不放),那麼就可以轉化為一個只牽扯前i-1件物品的問題。如果不放第i件物品,那麼問題就轉化為“前i-1件物品放入容量為v的揹包中”,價值為f[i-1][v];如果放第i件物品,那麼問題就轉化為“前i-1件物品放入剩下的容量為v-c[i]的揹包中”,此時能獲得的最大價值就是f[i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i]。
優化空間複雜度
以上方法的時間和空間複雜度均為O(N*V),其中時間複雜度基本已經不能再優化了,但空間複雜度卻可以優化到O(V)。
先考慮上面講的基本思路如何實現,肯定是有一個主迴圈i=1..N,每次算出來二維陣列f[i][0..V]的所有值。那麼,如果只用一個數組f[0..V],能不能保證第i次迴圈結束後f[v]中表示的就是我們定義的狀態f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]兩個子問題遞推而來,能否保證在推f[i][v]時(也即在第i次主迴圈中推f[v]時)能夠得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事實上,這要求在每次主迴圈中我們以v=V..0的順序推f[v],這樣才能保證推f[v]時f[v-c[i]]儲存的是狀態f[i-1][v-c[i]]的值。虛擬碼如下:
for i = 1..N
for v = V..0
f[v] = max{ f[v], f[v - c[i]] + w[i] };
其中的f[v]=max{f[v],f[v-c[i]]}一句恰就相當於我們的轉移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]},因為現在的f[v-c[i]]就相當於原來的f[i-1][v-c[i]]。如果將v的迴圈順序從上面的逆序改成順序的話,那麼則成了f[i][v]由f[i][v-c[i]]推知,與本題意不符,但它卻是另一個重要的揹包問題P02最簡捷的解決方案,故學習只用一維陣列解01揹包問題是十分必要的。
事實上,使用一維陣列解01揹包的程式在後面會被多次用到,所以這裡抽象出一個處理一件01揹包中的物品過程,以後的程式碼中直接呼叫不加說明。
過程ZeroOnePack,表示處理一件01揹包中的物品,兩個引數cost、weight分別表明這件物品的費用和價值。
procedure ZeroOnePack(cost,weight)
for v=V..cost
f[v]=max{f[v],f[v-cost]+weight}
注意這個過程裡的處理與前面給出的虛擬碼有所不同。前面的示例程式寫成v=V..0是為了在程式中體現每個狀態都按照方程求解了,避免不必要的思維複雜度。而這裡既然已經抽象成看作黑箱的過程了,就可以加入優化。費用為cost的物品不會影響狀態f[0..cost-1],這是顯然的。
有了這個過程以後,01揹包問題的虛擬碼就可以這樣寫:
for i=1..N
ZeroOnePack(c[i],w[i]);
初始化的細節問題
我們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求“恰好裝滿揹包”時的最優解,有的題目則並沒有要求必須把揹包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不同。
如果是第一種問法,要求恰好裝滿揹包,那麼在初始化時除了f[0]為0其它f[1..V]均設為-∞,這樣就可以保證最終得到的f[N]是一種恰好裝滿揹包的最優解。
如果並沒有要求必須把揹包裝滿,而是隻希望價格儘量大,初始化時應該將f[0..V]全部設為0。
為什麼呢?可以這樣理解:初始化的f陣列事實上就是在沒有任何物品可以放入揹包時的合法狀態。如果要求揹包恰好裝滿,那麼此時只有容量為0的揹包可能被價值為0的nothing“恰好裝滿”,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是-∞了。如果揹包並非必須被裝滿,那麼任何容量的揹包都有一個合法解“什麼都不裝”,這個解的價值為0,所以初始時狀態的值也就全部為0了。
這個小技巧完全可以推廣到其它型別的揹包問題,後面也就不再對進行狀態轉移之前的初始化進行講解。
小結
01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的型別的揹包問題往往也可以轉換成01揹包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間複雜度。