1. 程式人生 > >揹包九講(九) 揹包問題問法的變化

揹包九講(九) 揹包問題問法的變化

        以上涉及的各種揹包問題都是要求在揹包容量(費用)的限制下求可以取到的最大價值,但揹包問題還有很多種靈活的問法,在這裡值得提一下。但是我認為,只要深入理解了求揹包問題最大價值的方法,即使問法變化了,也是不難想出演算法的。

         例如,求解最多可以放多少件物品或者最多可以裝滿多少揹包的空間。這都可以根據具體問題利用前面的方程求出所有狀態的值(f陣列)之後得到。

         還有,如果要求的是“總價值最小”“總件數最小”,只需簡單的將上面的狀態轉移方程中的max改成min即可。

        下面說一些變化更大的問法。

輸出方案

        一般而言,揹包問題是要求一個最優值,如果要求輸出這個最優值的方案,可以參照一般動態規劃問題輸出方案的方法:記錄下每個狀態的最優值是由狀態轉移方程的哪一項推出來的,換句話說,記錄下它是由哪一個策略推出來的。便可根據這條策略找到上一個狀態,從上一個狀態接著向前推即可。

        還是以01揹包為例,方程為

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

再用一個數組g[i][v],設g[i][v]=0表示推出f[i][v]的值時是採用了方程的前一項(也即f[i][v]=f[i-1][v]),g[i][v]表示採用了方程的後一項。注意這兩項分別表示了兩種策略:未選第i個物品及選了第i個物品。那麼輸出方案的虛擬碼可以這樣寫(設最終狀態為f[N][V]):

i=N
v=V
while(i>0)
    if(g[i][v]==0)
        print "未選第i項物品"
    else if(g[i][v]==1)
        print "選了第i項物品"
        v=v-c[i]

另外,採用方程的前一項或後一項也可以在輸出方案的過程中根據f[i][v]的值實時地求出來,也即不須紀錄g陣列,將上述程式碼中的g[i][v]==0改成f[i][v]==f[i-1][v],g[i][v]==1改成f[i][v]==f[i-1][v-c[i]]+w[i]也可。

輸出字典序最小的最優方案

        這裡“字典序最小”的意思是1..N號物品的選擇方案排列出來以後字典序最小。以輸出01揹包最小字典序的方案為例。

        一般而言,求一個字典序最小的最優方案,只需要在轉移時注意策略。首先,子問題的定義要略改一些。我們注意到,如果存在一個選了物品1的最優方案,那麼答案一定包含物品1,原問題轉化為一個揹包容量為v-c[1],物品為2..N的子問題。反之,如果答案不包含物品1,則轉化成揹包容量仍為V,物品為2..N的子問題。不管答案怎樣,子問題的物品都是以i..N而非前所述的1..i的形式來定義的,所以狀態的定義和轉移方程都需要改一下。但也許更簡易的方法是先把物品逆序排列一下,以下按物品已被逆序排列來敘述。

       在這種情況下,可以按照前面經典的狀態轉移方程來求值,只是輸出方案的時候要注意:從N到1輸入時,如果f[i][v]==f[i-1][i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同時成立,應該按照後者(即選擇了物品i)來輸出方案。

求方案總數

        對於一個給定了揹包容量、物品費用、物品間相互關係(分組、依賴等)的揹包問題,除了再給定每個物品的價值後求可得到的最大價值外,還可以得到裝滿揹包或將揹包裝至某一指定容量的方案總數。

        對於這類改變問法的問題,一般只需將狀態轉移方程中的max改成sum即可。例如若每件物品均是完全揹包中的物品,轉移方程即為

f[i][v]=sum{f[i-1][v],f[i][v-c[i]]}

初始條件f[0][0]=1。

        事實上,這樣做可行的原因在於狀態轉移方程已經考察了所有可能的揹包組成方案。

最優方案的總數

        這裡的最優方案是指物品總價值最大的方案。以01揹包為例。

        結合求最大總價值和方案總數兩個問題的思路,最優方案的總數可以這樣求:f[i][v]意義同前述,g[i][v]表示這個子問題的最優方案的總數,則在求f[i][v]的同時求g[i][v]的虛擬碼如下:

for i=1..N
   for v=0..V
        f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
        g[i][v]=0
        if(f[i][v]==f[i-1][v])
            inc(g[i][v],g[i-1][v])
        if(f[i][v]==f[i-1][v-c[i]]+w[i])
            inc(g[i][v],g[i-1][v-c[i]])

如果你是第一次看到這樣的問題,請仔細體會上面的虛擬碼。

求次優解、第K優解

        對於求次優解、第K優解類的問題,如果相應的最優解問題能寫出狀態轉移方程、用動態規劃解決,那麼求次優解往往可以相同的複雜度解決,第K優解則比求最優解的複雜度上多一個係數K。

        其基本思想是將每個狀態都表示成有序佇列,將狀態轉移方程中的max/min轉化成有序佇列的合併。這裡仍然以01揹包為例講解一下。

        首先看01揹包求最優解的狀態轉移方程:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

如果要求第K優解,那麼狀態f[i][v]就應該是一個大小為K的陣列f[i][v][1..K]。其中f[i][v][k]表示前i個物品、揹包大小為v時,第k優解的值。“f[i][v]是一個大小為K的陣列”這一句,熟悉C語言的同學可能比較好理解,或者也可以簡單地理解為在原來的方程中加了一維。顯然f[i][v][1..K]這K個數是由大到小排列的,所以我們把它認為是一個有序佇列。

        然後原方程就可以解釋為:f[i][v]這個有序佇列是由f[i-1][v]和f[i-1][v-c[i]]+w[i]這兩個有序佇列合併得到的。有序佇列f[i-1][v]即f[i-1][v][1..K],f[i-1][v-c[i]]+w[i]則理解為在f[i-1][v-c[i]][1..K]的每個數上加上w[i]後得到的有序佇列。合併這兩個有序佇列並將結果的前K項儲存到f[i][v][1..K]中的複雜度是O(K)。最後的答案是f[N][V][K]。總的複雜度是O(VNK)。

        為什麼這個方法正確呢?實際上,一個正確的狀態轉移方程的求解過程遍歷了所有可用的策略,也就覆蓋了問題的所有方案。只不過由於是求最優解,所以其它在任何一個策略上達不到最優的方案都被忽略了。如果把每個狀態表示成一個大小為K的陣列,並在這個陣列中有序的儲存該狀態可取到的前K個最優值。那麼,對於任兩個狀態的max運算等價於兩個由大到小的有序佇列的合併。

        另外還要注意題目對於“第K優解”的定義,將策略不同但權值相同的兩個方案是看作同一個解還是不同的解。如果是前者,則維護有序佇列時要保證佇列裡的數沒有重複的。

小結

        顯然,這裡不可能窮盡揹包類動態規劃問題所有的問法。甚至還存在一類將揹包類動態規劃問題與其它領域(例如數論、圖論)結合起來的問題,在這篇論揹包問題的專文中也不會論及。但只要深刻領會前述所有類別的揹包問題的思路和狀態轉移方程,遇到其它的變形問法,只要題目難度還屬於NOIP,應該也不難想出演算法。

        觸類旁通、舉一反三,應該也是一個OIer應有的品質吧。