0/1 Knapsack Problem

程度★★ 難度★★★

Knapsack Problem

將一群物品儘量塞進背包裡面,令背包裡面的物品總價值最高。僅考慮背包只有負重限制(似乎太天真了一點)。

至於要讓背包裡面的物品總價值最低的話,那就是什麼東西都不要放進背包了吧,沒有什麼好討論的。

這裡向大家提供一本專述背包問題的論著:http://www.or.deis.unibo.it/knapsack.html

0/1 Knapsack Problem

背包問題是很經典的問題,亦引申出許多變形和應用。這裡我們介紹基本款式:0/1背包問題。

文言的說法是:每種類型的物品只會放進背包零個或一個。通俗的說法是:每個物品都是不同類型的,每種物品都只有一個。

0/1背包問題基本上是NP問題,當數值範圍不大時,能以DP處理之。

UVa 431 624 990 10130 10819

摘要

讓背包裡面的物品總價值最大
此時背包裡面最少會用掉多少空間、最多會剩下多少空間
此時背包裡面放了哪些物品
此時背包裡面的物品有哪些不同的組合方式
此時背包裡面的物品有幾種不同的組合方式
此時背包裡面的物品盡量最少(多)個

分割問題的方法

當我們把一件物品放進背包裡面時,會讓總價值變高,並且讓背包變重。對某一件物品來說,我們可以選擇放或不放;然後移去這件物品所帶來的影響,將問題縮小成子問題。遞迴式便可設計為:

c(n, w) = max( c(n-1, w), c(n-1, w-weight[n]) + cost[n] )
               ^^^^^^^^^  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
               不放 -> 0            有放 -> 1

n:第0個到第n個物品要放進背包內。
w:背包負重上限。
c(n, w):只有第0個到第n個物品,負重限制為w,此時的背包問題答案。
weight[n]:第n個物品的重量。
cost[n]:第n個物品的價值。

考慮一下東西放不進去時的情形,以及沒有東西時的情形。

c(n, w) =
 { 0                                      , if n = 0
 {
 { c(n-1, w)                              , if n > 0
 {                                          and w-weight[n] < 0
 {
 { max( c(n-1, w),                        , if n > 0
 {      c(n-1, w-weight[n]) + cost[n] )     and w-weight[n] >= 0

物品一開始的先後順序是無所謂的,最後得出的答案都會一樣。

讓背包裡面的物品總價值最大

只要計算所有小問題的答案,那麼必然可以推導出大問題的答案。建立二維表格後,依序計算每個小問題吧!

因為計算時只需要用到上方和左上方的格子,所以其實只需要一條陣列就夠了。不過計算次序需要改為由陣列後端開始,才不會覆蓋掉需要拿來計算的陣列格子。

物品的價值與重量可以合併設計成一個物件,讓人容易理解。

方才所採用的方法會計算到所有可能的問題,然而並不是所有問題都需要計算,故採用Top-down的方式會比較快。

此時背包裡面最少會用掉多少空間、最多會剩下多少空間

計算完畢之後,從表格的右方開始往左搜尋即可。當發現最佳的cost將要變小時,表示該處為最節省空間的地方。

此時背包裡面放了哪些物品

另外建立一個二維陣列,紀錄每一個問題的答案,是由哪個子問題推導出來的。每個問題只會有放或不放兩種情形,所以只要記錄放或不放便可以了。

這段程式碼只能找出其中一種組合方式,會是字典順序最小的一種組合方式,只是印出來的順序剛好前後顛倒。

改成這樣,就得到字典順序最大的一種組合方式。

此時背包裡面的物品有哪些不同的組合方式

若要找出所有方式,那就要寫個遞迴了吧?就我所知,目前尚未存在有效率的方法,能夠求出所有的配置方式。

此時背包裡面的物品有幾種不同的組合方式

概念等同於「Staircase Walk」,每當遇到放與不放都沒有差別的時候,就表示這是不同的組合方式,要進行計數。

此時背包裡面的物品盡量最少(多)個

有一種消極的方法是增加問題維度。重新設計recurrence為:

c(n, w, t) = max( c(n-1, w-weight[n], t-1) + cost[n] , c(n-1, w, t) )

t:放入的物品個數。

建立三維的表格,算出每一格的答案,然後窮舉所有可能的t,觀察那些答案,比較優劣,就可以得到最佳答案。

有一種比較積極的方法,是在計算過程當中,同步紀錄背包裡面放了多少個物品。遇到放與不放都沒有差別的時候,就用物品的個數來決定要不要放,採用物品較少的方式。

附帶一提,這個方法也可以用來找出此時背包裡面最少會用掉多少空間、最多會剩下多少空間。

另一種分割問題的方法

想像物品放入背包時是照物品編號順序來放。由於每一種物品都可能是最後一個放入背包的物品,遞迴式可設計為:

c(n, w) = max( 0 ,                                   都不放
               c(0, w-weight[0]) + cost[0] ,         最後是放第0個物品
               c(1, w-weight[1]) + cost[1] ,         最後是放第1個物品
               ... ,                                 ...
               c(n-1, w-weight[n-1]) + cost[n-1] )   最後是放第n-1個物品

n:第0個到第n個物品要放進背包內。
w:背包負重上限。
c(n, w):只有第0個到第n個物品,負重限制為w,此時的背包問題答案。
weight[n]:第n個物品的重量。
cost[n]:第n個物品的價值。

讓背包裡面的物品總價值最大

複雜度

時間複雜度是O(NW),空間複雜度是O(NW)。只算一個問題時,空間複雜度為O(W)。

Unbounded Knapsack Problem

程度★★ 難度★★★

Unbounded Knapsack Problem

物品有許多種類,每一種物品都無限量供應的背包問題。

UVa 10898

分割問題的方法

解法類似於0/1 Knapsack Problem。由於每種物品都改為無限供應,因此分割問題時,必須考慮每一種物品的用量。

c(n, w) = max( c(n-1, w - 0*weight[n]) + cost[n] ,   用去零個第n種物品
               c(n-1, w - 1*weight[n]) + cost[n] ,   用去一個第n種物品
               c(n-1, w - 2*weight[n]) + cost[n] ,   用去兩個第n種物品
               ...                               )   ...

n:第0種到第n種物品要放進背包內。
w:背包負重上限。
c(n, w):只有第0種到第n種物品,負重限制為w,此時的背包問題答案。
weight[n]:第n種物品的重量。
cost[n]:第n種物品的價值。

另一種分割問題的方法

仿照0/1 Knapsack Problem的方式,以一個物品的去留,將原問題分割成小問題。

c(n, w) = max( c(n-1, w), c(n, w-weight[n]) + cost[n] )
                            ^^
                            只有這裡不同,因為物品無限量供應。

n:第0種到第n種物品要放進背包內。
w:背包負重上限。
c(n, w):只有第0種到第n種物品,負重限制為w,此時的背包問題答案。
weight[n]:第n種物品的重量。
cost[n]:第n種物品的價值。

又一種分割問題的方法

以最後放入背包的物品,將原問題分割成小問題。這是最簡潔的方式!

c(w) = max( c(w - weight[0]) + cost[0] ,       最後放入的是第0種物品
            c(w - weight[1]) + cost[1] ,       最後放入的是第1種物品
            ... ,                              ...
            c(w - weight[N-1]) + cost[N-1] )   最後放入的是第N-1種物品

N:物品種類數目。
w:背包負重上限。
c(w):全部物品都可使用,負重限制為w,此時的背包問題答案。
weight[n]:第n種物品的重量。
cost[n]:第n種物品的價值。

這個方式很特別,精簡了一個維度,時間複雜度降低成O(NW),空間複雜度降低成為O(W)。

複雜度

時間複雜度是O(NW),空間複雜度是O(W)。只算一個問題時,空間複雜度為O(W)。

Bounded Knapsack Problem(Under Construction!)

程度★★ 難度★★★

Bounded Knapsack Problem

物品有許多種類,每一種物品都是限量供應的背包問題。

UVa 10086

分割問題的方法

考慮每一種物品的用量即可。

c(n, w) = max( c(n-1, w - 0*weight[n]) + cost[n] ,       用去零個第n種物品
               c(n-1, w - 1*weight[n]) + cost[n] ,       用去一個第n種物品
               ... ,          
               c(n-1, w - num[n]*weight[n]) + cost[n])   用去num[n]個第n種物品

n:第1種到第n種物品要放進背包內。
w:背包負重上限。
c(n, w):只有第1個到第n種物品,負重限制為w,此時的背包問題答案。
weight[n]:第n種物品的重量。
cost[n]:第n種物品的價值。
num[n]:第n種物品的數量。

複雜度

時間複雜度是O(NWK),空間複雜度是O(NW)。

每一種類的物品都分成 1 2 4 8 ... 個物品同捆
物品數目就下降成 logC
O(logC*N*W)

另一種是用deque做
每個同餘系分開做就行了
O(NW)

Money Changing Problem

程度★★ 難度★★★

Money Changing Problem

給定許多種不同面額的錢幣,能否湊得某個價位?每種面額的錢幣都無限供應,一定夠用。

Money Changing Problem其實就是Unbounded Knapsack Problem的變形:物品變成錢幣,物品重量變成面額,物品價值變成「湊得到、湊不到」兩種情況,累計價值的方式從加法運算變成OR運算。

Money Changing Problem的各種延伸問題

能否湊得某個價位(Money Changing Problem)
印出某個價位的其中一種湊法
印出某個價位的各種湊法
湊得某個價位的湊法共有幾種(Coin Change Problem)
湊得某個價位的最少(多)錢幣用量(Change-Making Problem)
湊得某個價位的最少(多)錢幣種類
所有無法湊得的價位當中,最大的價位(Frobenius Number)
所有無法湊得的價位共有幾種
限制錢幣用量,求得Frobenius Number加一(Postage Stamp Problem)

另外還有一些有關係的問題。

一個數字集合,挑幾個數字,總和恰為零(Subset Sum Problem)
一個數字集合,挑幾個數字,總和恰為整體總和的一半(Partition Problem)
N個不同重量物品,M個不同耐重箱子,用最少箱子裝所有物品(Bin Packing Problem)

以下將介紹其中幾個問題。

能否湊得某個價位(Money Changing Problem)

跟Unbounded Knapsack Problem的原理一模一樣。唯一要小心的是,設定表格的初始值,是把0元設定為可以湊到,而其他價位設定為湊不到。0元可以湊到的原因,是發現把0元設定為可以湊到,並不會違反遞迴公式,而且初始化也變簡單了;正常情況下,是無法湊到0元的。

c(n, m) =    c(n-1, m - 0*price[n])   用去零個第n種錢幣
          or c(n-1, m - 1*price[n])   用去一個第n種錢幣
          or c(n-1, m - 2*price[n])   用去兩個第n種錢幣
          or ...                      ...

n:用第0種到第n種錢幣來湊得價位。
m:欲湊得的價位值。
c(n, m):用第0種到第n種錢幣是否能湊得價位m。
price[n]:第n種錢幣的面額大小。
c(n, m) =
 { true                            , if n < 0 and m = 0    [Initial]
 { false                           , if n < 0 and m > 0    [Initial]
 { false                           , if n < 0 and m < 0    [Exterior]
 { c(n-1, m - 0*price[n]) or ...   , if n >= 0 and m > 0   [Compute]

能否湊得某個價位(Money Changing Problem)

c(n, m) = c(n-1, m) or c(n, m-price[n])
          ^^^^^^^^^    ^^^^^^^^^^^^^^^
          保持一樣      用去一個錢幣

n:用第0種到第n種錢幣來湊得價位。
m:欲湊得的價位值。
c(n, m):用第0種到第n種錢幣是否能湊得價位m。
price[n]:第n種錢幣的面額大小。
c(n, m) =
 { true               , if n < 0 and m = 0    [Initial]
 { false              , if n < 0 and m > 0    [Initial]
 { false              , if n < 0 and m < 0    [Exterior]
 { c(n-1, m)          , if n >= 0             [Compute]
 {                      and m-price[n] < 0
 { c(n-1, m) or       , if n >= 0             [Compute]
 { c(n, m-price[n])     and m-price[n] >= 0

儲存問題的表格,其實就是一個集合,改用bitset可以節省記憶體空間。可參考「Set」。

能否湊得某個價位(Money Changing Problem)

c(m) =    c(m - price[0])     最後是用第0種錢幣
       or c(m - price[1])     最後是用第1種錢幣
       or ...                 ...
       or c(m - price[n-1])   最後是用第n-1種錢幣

m:欲湊得的價位值。
c(m):這些錢幣是否能湊得價位m。
price[n]:第n種錢幣的面額大小。
c(m) =
 { false                                         , if m < 0
 { true                                          , if m = 0
 { c(m - price[0]) or ... or c(m - price[n-1])   , if m > 0

與上一個段落的程式碼做比較,可以發現兩支程式碼迴圈的層序恰好相反,非常有趣。

UVa 10306 10898 10261

湊得某個價位的湊法共有幾種(Coin Change Problem)

分割問題的方法就和Money Changing Problem一樣!只是將OR運算改為加法運算罷了。湊得0元的湊法設定為1,也是用遞迴公式逆推出來的。

c(n, m) = c(n-1, m) + c(n, m-price[n])
          ^^^^^^^^^   ^^^^^^^^^^^^^^^
        不用這種錢幣    用去一個錢幣

n:用第0種到第n種錢幣來湊得價位。
m:欲湊得的價位值。
c(n, m):用第0種到第n種錢幣湊得價位m的湊法數目。
price[n]:第n種錢幣的面額大小。
c(n, m) =
 { 1                  , if n < 0 and m = 0    [Initial]
 { 0                  , if n < 0 and m > 0    [Initial]
 { 0                  , if n < 0 and m < 0    [Exterior]
 { c(n-1, m)          , if n >= 0             [Compute]
 {                      and m-price[n] < 0
 { c(n-1, m) +        , if n >= 0             [Compute]
 { c(n, m-price[n])     and m-price[n] >= 0

至於下面的遞迴公式是錯誤的,它算的是排列數,不是組合數:

c(m) =   c(m - price[0])     最後是用第0種錢幣
       + c(m - price[1])     最後是用第1種錢幣
       + ...                 ...
       + c(m - price[n-1])   最後是用第n-1種錢幣

m:欲湊得的價位值。
c(m):這些錢幣是否能湊得價位m。
price[n]:第n種錢幣的面額大小。

UVa 147 357 674 10313 430

湊得某個價位的最少錢幣用量(Change-Making Problem)

分割問題的方法就和Money Changing Problem一樣!只是將湊不湊的到,改成湊到的錢幣數量,並且將OR運算改為加法運算罷了。如果錢幣數為無限大,則表示湊不到。

c(n, m) = min( c(n-1, m) , c(n, m-price[n]) + 1 )
               ^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^
               保持一樣        用去一個錢幣

n:用第0種到第n種錢幣來湊得價位。
m:欲湊得的價位值。
c(n, m):用第0種到第n種錢幣湊得價位m,最少所需要的錢幣數。
price[n]:第n種錢幣的面額大小。

湊得某個價位的最少錢幣用量(Change-Making Problem)

當各種錢幣的面額有特別的關係時,有速度很快的Greedy演算法可以求得答案,詳細情形請參考「Change-Making Problem: Cashier's Algorithm」。

湊得某個價位的錢幣用量,有哪幾種可能性。

錢幣用量不多時,可直接以int變數(可充作32個元素的bitset)或long long變數(可充作64個元素的bitset)來作為一個bitset,利用位元運算來求出所有可能性。

這裡列出幾題可以利用上述手法來解決的題目。這些題目不盡然都是錢幣無限供應的問題,各位得自行轉化一下。

UVa 242 10032 10690 10930

所有無法湊得的價位當中,最大的價位(Frobenius Number)

Sebastian Böcker, Zsuzsanna Lipták. A Fast and Simple Algorithm for the Money Changing Problem. Algorithmica, 2007, 48, 413-432.

當問題給定的數值範圍不大時,便可以結合同餘理論、DP、Greedy,在多項式時間內解決這個問題,其時間複雜度是O(N*a1),N為面額種類數,a1是最小的錢幣面額值。【待補文字】

這個演算法也可以同時解決Money Changing Problem。

限制錢幣用量,求得Frobenius Number加一(Postage Stamp Problem)

【待補文字】

能否湊得某個價位,但是錢幣限量供應。

先前所討論的都是錢幣無限供應的情況。若是錢幣限量供應,就必須考慮每一個錢幣用量,就如同最一開始的recurrence:

c(n, m) =    c(n-1, m - 0*price[n])        用去零個第n種錢幣
          or c(n-1, m - 1*price[n])        用去一個第n種錢幣
          or ...                           ...
          or c(n-1, m - num[n]*price[n])   用去num[n]個第n種錢幣

n:用第0種到第n種錢幣來湊得價位。
m:欲湊得的價位值。
c(n, m):用第0種到第n種錢幣是否能湊得價位m。
price[n]:第n種錢幣的面額大小。
num[n]:第n種錢幣的供應數量。

事實上有一個比較簡潔的做法,是用Greedy演算法,在節省錢幣用量的前提下,盡量的湊出各種價位。

UVa 233 711

Banzhaf Power Index

程度★★ 難度★★

Banzhaf Power Index

有一項決策要投票表決,一人一票,不得投廢票,過半數贊成則通過,反之則否決。投票者有許多派系組成,各個派系都相當團結,同樣派系的人,要嘛全部都是投贊成票,要嘛全部都是投反對票。然而有些派系人多,有些派系人少,人多的派系能左右大局,人少的派系卻勢單力薄。於是產生一個問題:有能力對最終決策造成影響的是哪些派系?影響能力又是多少?

一個派系有能力對決策造成影響,是指所有派系都設定立場之後,此派系一旦改變立場,會馬上顛倒決策結果。換個角度來說,是指此派系之外的所有派系都投完票之後,此派系若全數投贊成票,則會使決策順利通過,反之若全數投反對票,則會使決策無法通過。

Banzhaf Power Index的計算方式是這樣的:一個派系X的Banzhaf Power Index = 派系X影響決策的情況數目 ÷ (派系1影響決策的情況數目 + ... + 派系N影響決策的情況數目)。所有派系的Banzhaf Power Index的總和會是1。

藉由Banzhaf Power Index,可以看出各派系的實力,也可以看出投票表決是否公平。

1.
A派系9票、B派系9票、C派系7票、D派系3票、E派系1票、F派系1票。
總共投票數為30票。過半數之票數為16票。

2.
以A派系為例,A派系影響決策的情況,一共有16種:

   AB AC ABC ABD ABE ABF ACD ACE ACF
   ABDE ABDF ABEF ACDE ACDF ACEF ADEF

派系有出現,表示投贊成票;派系無出現,表示投反對票。
拿掉A則會逆轉決策結果。

3.
可以發現D派系、E派系、F派系,
完全無法介入結果,沒有任何影響力:

  | votes | power |  BPI
--+-------+-------+-------
A |   9   |  16   | 16/48
B |   9   |  16   | 16/48
C |   7   |  16   | 16/48
D |   3   |   0   |   0
E |   1   |   0   |   0
F |   1   |   0   |   0
--+-------+-------+--------
  |  30   |  48   |  1.0

計算一個派系影響決策的情況數目

甲、移除此派系。
乙、剩餘派系進行投票,並算出各種不同總票數之下的情況數目。
丙、搜尋靠近過半票數的情況,累計這些情況數目。

是Partition problem的延伸應用。

時間複雜度

時間複雜度是O(N^2 * S),N是派系數目,S是總投票數,或者說是過半票數。

UVa 430 435