四月份還沒寫,不能這么荒廢了呀,趕緊水一篇吧,哈哈。前些日子回顧了DP的一些基礎(chǔ),就做一下整理吧,從0-1背包開始。
本節(jié)回顧0-1背包的基本模型,關(guān)于它的實(shí)現(xiàn)有很多種寫法,這里對不同實(shí)現(xiàn)做個(gè)簡單列舉,主要是寫代碼練手了,主要有以下幾方面內(nèi)容:
==0-1背包問題定義 & 基本實(shí)現(xiàn)
==0-1背包使用滾動(dòng)數(shù)組壓縮空間
==0-1背包使用一維數(shù)組
==0-1背包恰好背滿
==0-1背包輸出最優(yōu)方案
========================================
0-1背包問題定義 & 基本實(shí)現(xiàn)
問題:有個(gè)容量為V大小的背包,有很多不同重量weight[i](i=1..n)不同價(jià)值value[i](i=1..n)的物品,每種物品只有一個(gè),想計(jì)算一下最多能放多少價(jià)值的貨物。
DP的關(guān)鍵也是難點(diǎn)是找到最優(yōu)子結(jié)構(gòu)和重疊子問題,進(jìn)而找到狀態(tài)轉(zhuǎn)移方程,編碼就相對容易些。最優(yōu)子結(jié)構(gòu)保證每個(gè)狀態(tài)是最優(yōu)的,重疊子問題也即n狀態(tài)的求法和n-1狀態(tài)的求法是一樣的;DP在實(shí)現(xiàn)上一般是根據(jù)狀態(tài)轉(zhuǎn)移方程自底向上的迭代求得最優(yōu)解(也可以使用遞歸自頂向下求解)。
回到0-1背包,每個(gè)物體i,對應(yīng)著兩種狀態(tài):放入&不放入背包。背包的最優(yōu)解是在面對每個(gè)物體時(shí)選擇能夠最大化背包價(jià)值的狀態(tài)。0-1背包的狀態(tài)轉(zhuǎn)移方程為
1
|
f(i,v) = max{ f(i-1,v), f(i-1,v-c[i])+w[i] } |
f(i,v)表示前i個(gè)物體面對容量為v時(shí)背包的最大價(jià)值,c[i]代表物體i的cost(即重量),w[i]代表物體i的價(jià)值;如果第i個(gè)物體不放入背包,則背包的最大價(jià)值等于前i-1個(gè)物體面對容量v的最大價(jià)值;如果第i個(gè)物體選擇放入,則背包的最大價(jià)值等于前i-1個(gè)物體面對容量v-cost[i]的最大價(jià)值加上物體i的價(jià)值w[i]。
對于實(shí)現(xiàn),一般采用一個(gè)二維數(shù)組(狀態(tài)轉(zhuǎn)移矩陣)dp[i][j]來記錄各個(gè)子問題的最優(yōu)狀態(tài),其中dp[i][j]表示前i個(gè)物體面對容量j背包的最大價(jià)值。
下面給出0-1背包的基本實(shí)現(xiàn),時(shí)間復(fù)雜度為O(N*V),空間復(fù)雜度也為O(N*V),初始化的合法狀態(tài)很重要,對于第一個(gè)物體即f[0][j],如果容量j小于第一個(gè)物體(編號(hào)為0)的重量,則背包的最大價(jià)值為0,如果容量j大于第一個(gè)物體的重量,則背包最大價(jià)值便為該物體的價(jià)值。為了能單步驗(yàn)證每個(gè)狀態(tài)的最優(yōu)解,程序最后將狀態(tài)轉(zhuǎn)移矩陣的有效部分輸出到了文件。
代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
#include <iostream> using namespace std; /* 0-1背包 版本1 * Time Complexity O(N*V) * Space Complexity O(N*V) * 設(shè) V <= 200 N <= 10 * 狀態(tài)轉(zhuǎn)移方程:f(i,v) = max{ f(i-1,v), f(i-1,v-c[i])+w[i] } */ int maxValue[11][201]; /* 前i個(gè)物體面對容量j的最大價(jià)值,即子問題最優(yōu)解 */ int weight[11]; int value[11]; int V, N; void main() { int i, j; scanf("%d %d",&V, &N); for(i = 0; i < N; ++i) { scanf("%d %d",&weight[i],&value[i]); } for(i = 0; i < N; ++i) { for(j = 0; j <= V; ++j) /* 容量為V 等號(hào) */ { if(i > 0) { maxValue[i][j] = maxValue[i-1][j]; if(j >= weight[i]) /* 等號(hào) */ { int tmp = maxValue[i-1][j-weight[i]] + value[i]; maxValue[i][j] = ( tmp > maxValue[i][j]) ? tmp : maxValue[i][j]; } }else /* 數(shù)組第0行賦值 */ { if(j >= weight[0]) maxValue[0][j] = value[0]; } } } printf("%d",maxValue[N-1][V]); /* 重定向輸出結(jié)果到文件 */ freopen("C:\\dp.txt","w",stdout); for(i = 0; i <= N; ++i) { for(j = 0; j <= V; ++j) { printf("%d ",maxValue[i][j]); } printf("\n"); } } |
測試用例:
1
2
3
4
5
6
7
|
10 3 3 4 4 6 5 7 |
程序輸出的狀態(tài)轉(zhuǎn)移矩陣如下圖,第一行表示第1個(gè)物體對于容量0至V時(shí)的最優(yōu)解,即背包最大價(jià)值。該實(shí)現(xiàn)方法是0-1背包最基本的思想,追蹤狀態(tài)轉(zhuǎn)移矩陣有助于加深理解,POJ上單純的0-1背包題目也有不少,如3624等,可以水一下,加深理解。
========================================
0-1背包使用滾動(dòng)數(shù)組壓縮空間
所謂滾動(dòng)數(shù)組,目的在于優(yōu)化空間,從上面的解法我們可以看到,狀態(tài)轉(zhuǎn)移矩陣使用的是一個(gè)N*V的數(shù)組,在求解的過程中,我們可以發(fā)現(xiàn),當(dāng)前狀態(tài)只與前一狀態(tài)的解有關(guān),那么之前存儲(chǔ)的狀態(tài)信息已經(jīng)無用了,可以舍棄的,我們只需要空間存儲(chǔ)當(dāng)前的狀態(tài)和前一狀態(tài),所以只需使用2*V的空間,循環(huán)滾動(dòng)使用,就可以達(dá)到跟N*V一樣的效果。這是一個(gè)非常大的空間優(yōu)化。
代碼如下,我們可以在每輪內(nèi)循環(huán)結(jié)束后輸出當(dāng)前狀態(tài)的解,與上面使用二維數(shù)組輸出的狀態(tài)轉(zhuǎn)移矩陣對比,會(huì)發(fā)現(xiàn)是一樣的效果,重定向輸出到文本有助加深理解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
|
#include <iostream> using namespace std; /* 0-1背包 版本2 * Time Complexity O(N*V) * Space Complexity O(2*V) * 設(shè) V <= 200 N <= 10 * 狀態(tài)轉(zhuǎn)移方程:f(i,v) = max{ f(i-1,v), f(i-1,v-c[i])+w[i] } */ int maxValue[2][201]; /* 前i個(gè)物體面對容量j的最大價(jià)值,即子問題最優(yōu)解 */ int weight[11]; int value[11]; int V, N; void main() { int i, j, k; scanf("%d %d",&V, &N); for(i = 0; i < N; ++i) { scanf("%d %d",&weight[i],&value[i]); } for(i = 0; i < N; ++i) { for(j = 0; j <= V; ++j) /* 容量為V 等號(hào) */ { if(i > 0) { k = i & 1; /* i%2 獲得滾動(dòng)數(shù)組當(dāng)前索引 k */ maxValue[k][j] = maxValue[k^1][j]; if(j >= weight[i]) /* 等號(hào) */ { int tmp = maxValue[k^1][j-weight[i]] + value[i]; maxValue[k][j] = ( tmp > maxValue[k][j]) ? tmp : maxValue[k][j]; } }else /* 數(shù)組第0行賦值 */ { if(j >= weight[0]) maxValue[0][j] = value[0]; } } } printf("%d",maxValue[k][V]); /* 重定向輸出結(jié)果到文件 */ freopen("C:\\dp.txt","w",stdout); for(i = 0; i <= 1; ++i) { for(j = 0; j <= V; ++j) { printf("%d ",maxValue[i][j]); } printf("\n"); } } |
這種空間循環(huán)滾動(dòng)使用的思想很有意思,類似的,大家熟悉的斐波那契數(shù)列,f(n) = f(n-1) + f(n-2),如果要求解f(1000),是不需要申請1000個(gè)大小的數(shù)組的,使用滾動(dòng)數(shù)組只需申請3個(gè)空間f[3]就可以完成任務(wù)。
========================================
0-1背包使用一維數(shù)組
使用滾動(dòng)數(shù)組將空間優(yōu)化到了2*V,在背包九講中提到了使用一維數(shù)組也可以達(dá)到同樣的效果,個(gè)人認(rèn)為這也是滾動(dòng)思想的一種,由于使用一維數(shù)組解01背包會(huì)被多次用到,完全背包的一種優(yōu)化實(shí)現(xiàn)方式也是使用一維數(shù)組,所以我們有必要理解這種方法。
如果只使用一維數(shù)組f[0…v],我們要達(dá)到的效果是:第i次循環(huán)結(jié)束后f[v]中所表示的就是使用二維數(shù)組時(shí)的f[i][v],即前i個(gè)物體面對容量v時(shí)的最大價(jià)值。我們知道f[v]是由兩個(gè)狀態(tài)得來的,f[i-1][v]和f[i-1][v-c[i]],使用一維數(shù)組時(shí),當(dāng)?shù)趇次循環(huán)之前時(shí),f[v]實(shí)際上就是f[i-1][v],那么怎么得到第二個(gè)子問題的值呢?事實(shí)上,如果在每次循環(huán)中我們以v=v…0的順序推f[v]時(shí),就能保證f[v-c[i]]存儲(chǔ)的是f[i-1][v-c[i]]的狀態(tài)。狀態(tài)轉(zhuǎn)移方程為:
1
|
v = V...0; f(v) = max{ f(v), f(v-c[i])+w[i] } |
我們可以與二維數(shù)組的狀態(tài)轉(zhuǎn)移方程對比一下
1
|
f(i,v) = max{ f(i-1,v), f(i-1,v-c[i])+w[i] } |
正如我們上面所說,f[v-c[i]]就相當(dāng)于原來f[i-1][v-c[i]]的狀態(tài)。如果將v的循環(huán)順序由逆序改為順序的話,就不是01背包了,就變成完全背包了,這個(gè)后面說。這里舉一個(gè)例子理解為何順序就不是01背包了
假設(shè)有物體z容量2,價(jià)值vz很大,背包容量為5,如果v的循環(huán)順序不是逆序,那么外層循環(huán)跑到物體z時(shí),內(nèi)循環(huán)在v=2時(shí),物體z被放入背包,當(dāng)v=4時(shí),尋求最大價(jià)值,物體z放入背包,f[4]=max{f[4],f[2]+vz},這里毫無疑問后者最大,那么此時(shí)f[2]+vz中的f[2]已經(jīng)裝入了一次物體z,這樣一來該物體被裝入背包兩次了就,不符合要求,如果逆序循環(huán)v,這一問題便解決了。
代碼如下,為了加深理解,可以在內(nèi)循環(huán)結(jié)束輸出每一個(gè)狀態(tài)的情況到文本中,會(huì)發(fā)現(xiàn)與使用二維數(shù)組時(shí)的狀態(tài)轉(zhuǎn)移矩陣都是一樣一樣的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
#include <iostream> using namespace std; /* 0-1背包 版本3 * Time Complexity O(N*V) * Space Complexity O(V) * 設(shè) V <= 200 N <= 10 * 狀態(tài)轉(zhuǎn)移方程:v = V...0; f(v) = max{ f(v), f(v-c[i])+w[i] } */ int maxV[201]; /* 記錄前i個(gè)物品中容量v時(shí)的最大價(jià)值 */ int weight[11]; int value[11]; int V, N; void main() { int i, j; scanf("%d %d",&V, &N); for(i = 0; i < N; ++i) { scanf("%d %d",&weight[i],&value[i]); } /* * 對于第i輪循環(huán) * 求出了前i個(gè)物品中面對容量為v的最大價(jià)值 */ for(i = 0; i < N; ++i) { /* * 內(nèi)循環(huán)實(shí)際上講maxV[0...v]滾動(dòng)覆蓋前一輪的maxV[0...V] * 可輸出對照使用二維數(shù)組時(shí)的情況 * j從V至0逆序是防止有的物品放入背包多次 */ for(j = V; j >= weight[i]; --j) /* weight > j 的物品不會(huì)影響狀態(tài)f[0,weight[i-1]] */ { int tmp = maxV[j-weight[i]]+value[i]; maxV[j] = (maxV[j] > tmp) ? maxV[j] : tmp; } } printf("%d",maxV[V]); } |
可以看出,使用一維數(shù)組,代碼非常簡練。
========================================
0-1背包恰好背滿
在01背包中,有時(shí)問到“恰好裝滿背包”時(shí)的最大價(jià)值,與不要求裝滿背包的區(qū)別就是在初始化的時(shí)候,其實(shí)對于沒有要求必須裝滿背包的情況下,初始化最大價(jià)值都為0,是不存在非法狀態(tài)的,所有的都是合法狀態(tài),因?yàn)榭梢允裁炊疾谎b,這個(gè)解就是0,但是如果要求恰好裝滿,則必須區(qū)別初始化,除了f[0]=0,其他的f[1…v]均設(shè)為-∞或者一個(gè)比較大的負(fù)數(shù)來表示該狀態(tài)是非法的。
這樣的初始化能夠保證,如果子問題的狀態(tài)是合法的(恰好裝滿),那么才能得到合法的狀態(tài);如果子問題狀態(tài)是非法的,則當(dāng)前問題的狀態(tài)依然非法,即不存在恰好裝滿的情況。
代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
#include <iostream> using namespace std; int maxV[201]; /* 記錄前i個(gè)物品中容量v時(shí)的最大價(jià)值 */ int weight[11]; int value[11]; int V, N; void main() { int i, j; scanf("%d %d",&V, &N); for(i = 0; i < N; ++i) { scanf("%d %d",&weight[i],&value[i]); } for(i = 1; i <= V; ++i) /* 初始化非法狀態(tài) */ { maxV[i] = -100; } for(i = 0; i < N; ++i) { for(j = V; j >= weight[i]; --j) { int tmp = maxV[j-weight[i]]+value[i]; maxV[j] = (maxV[j] > tmp) ? maxV[j] : tmp; } } } |
為了加深理解,輸出每輪循環(huán)的狀態(tài)矩陣如下,對照每個(gè)物體的情況,就會(huì)理解為什么做那樣的初始化了。
========================================
0-1背包輸出最優(yōu)方案
一般來講,背包問題都是求一個(gè)最優(yōu)值,但是如果要求輸出得到這個(gè)最優(yōu)值的方案,就可以根據(jù)狀態(tài)轉(zhuǎn)移方程往后推,由這一狀態(tài)找到上一狀態(tài),依次向前推即可。
這樣就可以有兩種實(shí)現(xiàn)方式,一種是直接根據(jù)狀態(tài)轉(zhuǎn)移矩陣向前推,另一種就是使用額外一個(gè)狀態(tài)矩陣記錄最優(yōu)方案的路徑,道理都是一樣的。當(dāng)然也可以使用一維數(shù)組,代碼更為簡練,這里不羅列,相關(guān)代碼可以到這里下載。
代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
#include <iostream> using namespace std; /* 0-1背包 輸出最優(yōu)方案 2 直接根據(jù)狀態(tài)數(shù)組算 * Time Complexity O(N*V) * Space Complexity O(N*V) * 設(shè) V <= 200 N <= 10 * 狀態(tài)轉(zhuǎn)移方程:f(i,v) = max{ f(i-1,v), f(i-1,v-c[i])+w[i] } */ int maxValue[11][201]; /* 記錄子問題最優(yōu)解 */ int weight[11]; int value[11]; int V, N; void main() { int i, j; scanf("%d %d",&V, &N); for(i = 0; i < N; ++i) { scanf("%d %d",&weight[i],&value[i]); } for(i = 0; i < N; ++i) { for(j = 0; j <= V; ++j) { if(i > 0) { maxValue[i][j] = maxValue[i-1][j]; if(j >= weight[i]) { int tmp = maxValue[i-1][j-weight[i]] + value[i]; maxValue[i][j] = ( tmp > maxValue[i][j]) ? tmp : maxValue[i][j]; } }else { if(j >= weight[0]) maxValue[0][j] = value[0]; } } } printf("%d\n",maxValue[N-1][V]); i = N-1; j = V; while(i >= 0) { if(maxValue[i][j] == maxValue[i-1][j-weight[i]] + value[i]) { printf("%d ",i); j = j - weight[i]; } --i; } } |
01背包是背包問題的基礎(chǔ),加深理解的最好方式就是動(dòng)手寫一下,然后對照最終的狀態(tài)轉(zhuǎn)移矩陣一一比對。
本