實驗內容
本實驗要求基於算法設計與分析的一般過程(即待求解問題的描述、算法設計、算法描述、算法正確性證明、算法分析、算法實現與測試),在針對0-1背包問題求解的實踐中理解動態規划 (Dynamic Programming, DP) 方法的思想、求解策略及步驟。
作為挑戰:可以考慮基於跳躍點的改進算法,以及對連續型物品重量/背包容量的支持。
實驗目的
- 理解動態規划方法的核心思想以及動態規划方法的求解過程;
- 從算法分析與設計的角度,對0-1背包問題的基於DP法求解有更進一步的理解。
實驗結果
步驟1
理解問題,給出問題的描述。
n個物體,1個背包。對物品i,其價值為\(v_i\),重量為\(W_i\),背包的容量為 \(W\),如何選取物品,是的背包中裝入的物品的總價值最大?
在約束條件為:選取物品的重量小於等於背包重量的情況下,盡可能讓背包中物品的總價值最大。
根據問題描述,設計如下的約束條件和目標函數:
約束條件:
目標函數:
問題現在等價於,尋找一個在滿足約束條件情況下,並使目標函數達到最大的解 \(X=(x_1,x_2,...,x_n)\)
步驟2
算法設計,包括策略與數據結構的選擇
設計用二維數組對物品信息進行記錄: \(C[i][j]\) 用來記錄如果當前還有\(i\)個物品,背包容量還剩\(j\)的情況下,當前背包所能得到的最大價值。
很容易發現條件即:$$C[0][j] = C[i][0] = 0$$
遞歸定義應該為:
可以這樣理解,每個物品我可以選擇是否加入到背包中,首先判斷,當前物品是否重量已經大於背包所能容納的重量;如果能容納該物體,則進行判斷加入該物品\(C[i-1][j-w_i]+v_i\)得到的價值更高,還是不加入該物品\(C[i-i][j]\)所能得到的物品的總價值更高。
步驟3
描述算法。希望采用源代碼以外的形式,如偽代碼或流程圖等;
偽代碼表示:
01PACKAGE(n,w,v,W) // n為物品的個數,w為重量, v為價值,W為背包容量
// C[1..n,1..n]為最優解
for i=1 to n:
do C[i][0] = 0
for j=1 to W:
do C[0][j] = 0
for i=1 to n:
for j=1 to W:
do
if j < w[i]:
then C[i][j]=C[i-1][j]
else
then C[i][j]=max{C[i-1][j],C[i-1][j-w[i]]+v[i]}
return C
步驟4
算法的正確性證明。需要這個環節,在理解的基礎上對算法的正確性給予證明;
對該算法進行最優子結構的證明:
假設\(X=(x_1,x_2,x_3,...,x_n)\)是背包問題的最優解,那么\((x_2,..,x_n)\)是下面問題的一個最優解:
目標函數: $$max\sum_{i=2}^nv_ix_i$$
即除去第一個物品以后的子問題
證明如下:(反證法)
設\(X=(x_2,...,x_n)\)不是上述子問題的最優解,設\(Y=(y_2,...,y_n)\)是上述問題的最優解,則\(Y\)所求的目標函數的值一定比X求得的目標函數的值更大,即:
又\(Y\)滿足約束條件:\(\sum_{i=2}^nw_iy_i \leq W-w_1x_1\),即 \(w_1x_1+\sum_{i=2}^nw_iy_i \leq W\),該不等式證明\((x_1,y_1,y_2,...,y_n)\)是原問題的一個解。
在公式 \((5)\) 中左右同時加上\(v_1x_1\),可得:$$ v_1x_1+\sum_{i=2}^nv_iy_i > v_1x_1+\sum_{i=2}^nv_ix_i$$,說明\((x_1,y_1,y_2,...,y_n)\)要比\((x_1,x_2,...,x_n)\) 方案價值更高,所以\((x_1,x_2,...,x_n)\)不是最優解,產生了矛盾。
所以其最優子結構的性質得證。
步驟5
算法復雜性分析,包括時間復雜性和空間復雜性;
- 求解0-1背包問題部分
時間復雜性分析:
由於只需要遍歷進行,所以只需要考慮循環中的復雜度即可。
空間復雜度,主要是生成數組時占用的空間。
- 得到最優解部分
時間復雜度分析:
只需要一個循環即可。
空間復雜度為:$$O(1)$$
步驟6
算法實現與測試。附上代碼或以附件的形式提交,同時貼上算法運行結果截圖;
# -*- coding: utf-8 -*-
"""
Created on Fri Sep 28 12:44:40 2018
@theme: 算法准備-01背包問題
@author: pprp
"""
import numpy as np
def solvePackage(n,w,v,W):
"""solve the 01 package problem"""
C=np.zeros((n+1,W+1))
for i in range(n):
C[i][0]=0
for i in range(W):
C[0][i]=0
for i in range(1,n+1):
for j in range(1,W+1):
if j < w[i-1]:
C[i][j]=C[i-1][j]
else:
C[i][j]=max(C[i-1][j],C[i-1][j-w[i-1]]+v[i-1])
return C
def getSolution(n,w,W,C):
j = W
x = np.zeros(n)
for i in range(n,0,-1):
if C[i][j]==C[i-1][j]:
x[i-1] = 0
else:
x[i-1] = 1
j -= w[i-1]
return x
if __name__ == "__main__":
n = 11
w = np.array([2, 6, 3, 4, 2, 8, 2, 4, 7, 5, 1])
v = np.array([10,23,5,34,23,17,22,32,12,15,32])
W = 15
C=solvePackage(n,w,v,W)
x=getSolution(n,w,W,C)
print("packages:",x)
print("Output:\n",C)
實驗結果
packages: [1. 0. 0. 1. 1. 0. 1. 1. 0. 0. 1.]
驗證結果:10+34+23+22+32+32=153
實驗總結
動態規划基本思想
動態規划算法通常是用來解決某種最優性質的問題。基本思想是將帶求解問題划分為若干個子問題,先求解子問題,然后從子問題的解得到原問題的解。動態規划與分治法的區別在於,動態規划的子問題可能是互相重疊的重復計算的,分治法則是相互獨立的。可以用一個表來記錄子問題是否已經求解,這樣可以避免重復求解。
動態規划應用條件
需要滿足最優化原理、無后效性和重疊性。
-
最優化原理,一個最優化策略的子策略一定是最優的,就是滿足最優子結構的性質。
-
無后效性,一個階段以前各階段的狀態無法直接硬性它未來的決策,只能通過當前的這個狀態。
-
重疊性,就是記錄已經解決過的問題,需要存儲已經解決過的問題,空間復雜度比較大,是一種以空間換時間的算法。
難點
動態規划的難點在於,如何根據問題的最優子結構的性質,構造動態規划方法中的遞歸公式或動態規划方程。就比如本問題中,如何設計這個方程才是難點所在。
遇到的問題
在逆向求解使用的哪幾個背包的時候,由於對問題理解的不深刻,導致匯總的時候發現計算結果出現了問題,也就是\(getSolution\)這個函數出現了問題。應該逆向進行求解問題,也就是從后往前進行推導,更改了循環的方向以后就可以得到最終的結果了。
心得
只有在真正理解算法的基礎上,然后加以偽代碼的梳理,這時候寫才能一氣呵成。另外還需要對代碼計算出來的結果進行人工核查,防止某些問題被忽略掉。另外這個問題已經被老師分析的比較透徹,所以寫起來沒有太大的困難。但是遇見新的問題的時候,如何構造解決問題的方法才是難點所在。