題目
有N件物品和一個(gè)容量為V的背包。第i件物品的費(fèi)用是c[i],價(jià)值是w[i]。求解將哪些物品裝入背包可使價(jià)值總和最大。
基本思路
這是最基礎(chǔ)的背包問(wèn)題,特點(diǎn)是:每種物品僅有一件,可以選擇放或不放。
用子問(wèn)題定義狀態(tài):即f[i][v]
表示前i
件物品恰放入一個(gè)容量為v
的背包可以獲得的最大價(jià)值。則其狀態(tài)轉(zhuǎn)移方程便是:
f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
這個(gè)方程非常重要,基本上所有跟背包相關(guān)的問(wèn)題的方程都是由它衍生出來(lái)的。所以有必要將它詳細(xì)解釋一下:“將前i
件物品放入容量為v
的背包中”這個(gè)子問(wèn)題,若只考慮第i
件物品的策略(放或不放),那么就可以轉(zhuǎn)化為一個(gè)只牽扯前i-1
件物品的問(wèn)題。如果不放第i
件物品,那么問(wèn)題就轉(zhuǎn)化為“前i-1
件物品放入容量為v
的背包中”,價(jià)值為f[i-1][v]
;如果放第i
件物品,那么問(wèn)題就轉(zhuǎn)化為“前i-1
件物品放入剩下的容量為v-c[i]
的背包中”,此時(shí)能獲得的最大價(jià)值就是f[i-1][v-c[i]]
再加上通過(guò)放入第i
件物品獲得的價(jià)值w[i]
。
優(yōu)化空間復(fù)雜度
以上方法的時(shí)間和空間復(fù)雜度均為O(N*V)
,其中時(shí)間復(fù)雜度基本已經(jīng)不能再優(yōu)化了,但空間復(fù)雜度卻可以?xún)?yōu)化到O(V)
。
先考慮上面講的基本思路如何實(shí)現(xiàn),肯定是有一個(gè)主循環(huán)i=1..N
,每次算出來(lái)二維數(shù)組f[i][0..V]
的所有值。那么,如果只用一個(gè)數(shù)組f[0..V]
,能不能保證第i
次循環(huán)結(jié)束后f[v]
中表示的就是我們定義的狀態(tài)f[i][v]
呢?f[i][v]
是由f[i-1][v]
和f[i-1][v-c[i]]
兩個(gè)子問(wèn)題遞推而來(lái),能否保證在推f[i][v]
時(shí)(也即在第i
次主循環(huán)中推f[v]
時(shí))能夠得到f[i-1][v]
和f[i-1][v-c[i]]
的值呢?事實(shí)上,這要求在每次主循環(huán)中我們以v=V..0
的順序推f[v]
,這樣才能保證推f[v]
時(shí)f[v-c[i]]
保存的是狀態(tà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]]}
一句恰就相當(dāng)于我們的轉(zhuǎn)移方程f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]}
,因?yàn)楝F(xiàn)在的f[v-c[i]]
就相當(dāng)于原來(lái)的f[i-1][v-c[i]]
。如果將v
的循環(huán)順序從上面的逆序改成順序的話,那么則成了f[i][v]
由f[i][v-c[i]]
推知,與本題意不符,但它卻是另一個(gè)重要的背包問(wèn)題P02最簡(jiǎn)捷的解決方案,故學(xué)習(xí)只用一維數(shù)組解01背包問(wèn)題是十分必要的。
事實(shí)上,使用一維數(shù)組解01背包的程序在后面會(huì)被多次用到,所以這里抽象出一個(gè)處理一件01背包中的物品過(guò)程,以后的代碼中直接調(diào)用不加說(shuō)明。
過(guò)程ZeroOnePack
,表示處理一件01背包中的物品,兩個(gè)參數(shù)cost
、weight
分別表明這件物品的費(fèi)用和價(jià)值。
procedure ZeroOnePack(cost,weight)
for v=V..cost
f[v]=max{f[v],f[v-cost]+weight}
注意這個(gè)過(guò)程里的處理與前面給出的偽代碼有所不同。前面的示例程序?qū)懗?code>v=V..0是為了在程序中體現(xiàn)每個(gè)狀態(tài)都按照方程求解了,避免不必要的思維復(fù)雜度。而這里既然已經(jīng)抽象成看作黑箱的過(guò)程了,就可以加入優(yōu)化。費(fèi)用為cost
的物品不會(huì)影響狀態(tài)f[0..cost-1]
,這是顯然的。
有了這個(gè)過(guò)程以后,01背包問(wèn)題的偽代碼就可以這樣寫(xiě):
for i=1..N
ZeroOnePack(c[i],w[i]);
初始化的細(xì)節(jié)問(wèn)題
我們看到的求最優(yōu)解的背包問(wèn)題題目中,事實(shí)上有兩種不太相同的問(wèn)法。有的題目要求“恰好裝滿背包”時(shí)的最優(yōu)解,有的題目則并沒(méi)有要求必須把背包裝滿。一種區(qū)別這兩種問(wèn)法的實(shí)現(xiàn)方法是在初始化的時(shí)候有所不同。
<p>如果是第一種問(wèn)法,要求恰好裝滿背包,那么在初始化時(shí)除了f[0]
為0
其它f[1..V]
均設(shè)為-∞
,這樣就可以保證最終得到的f[N]
是一種恰好裝滿背包的最優(yōu)解。
如果并沒(méi)有要求必須把背包裝滿,而是只希望價(jià)格盡量大,初始化時(shí)應(yīng)該將f[0..V]
全部設(shè)為0
。
<p>為什么呢?可以這樣理解:初始化的f數(shù)組事實(shí)上就是在沒(méi)有任何物品可以放入背包時(shí)的合法狀態(tài)。如果要求背包恰好裝滿,那么此時(shí)只有容量為0的背包可能被價(jià)值為0的nothing“恰好裝滿”,其它容量的背包均沒(méi)有合法的解,屬于未定義的狀態(tài),它們的值就都應(yīng)該是-∞了。如果背包并非必須被裝滿,那么任何容量的背包都有一個(gè)合法解“什么都不裝”,這個(gè)解的價(jià)值為0,所以初始時(shí)狀態(tài)的值也就全部為0了。
這個(gè)小技巧完全可以推廣到其它類(lèi)型的背包問(wèn)題,后面也就不再對(duì)進(jìn)行狀態(tài)轉(zhuǎn)移之前的初始化進(jìn)行講解。
小結(jié)
01背包問(wèn)題是最基本的背包問(wèn)題,它包含了背包問(wèn)題中設(shè)計(jì)狀態(tài)、方程的最基本思想,另外,別的類(lèi)型的背包問(wèn)題往往也可以轉(zhuǎn)換成01背包問(wèn)題求解。故一定要仔細(xì)體會(huì)上面基本思路的得出方法,狀態(tài)轉(zhuǎn)移方程的意義,以及最后怎樣優(yōu)化的空間復(fù)雜度。