數據結構和算法面試題系列—揹包問題總結

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏c++

0 概述

揹包問題包括0-1揹包問題、徹底揹包問題、部分揹包問題等多種變種。其中,最簡單的是部分揹包問題,它能夠採用貪心法來解決,而其餘幾種揹包問題每每須要動態規劃來求解。本文主要來源於《揹包問題九講》,我選擇了比較簡單的0-1揹包問題和徹底揹包問題進行彙總。同時給出實現代碼,若有錯誤,請各位大蝦指正。本文代碼在 這裏git

1 部分揹包問題

部分揹包問題描述: 有 N 件物品和一個容量爲 C 的揹包。第 i 件物品的重量是 w[i],價值是 v[i]。求解將哪些物品裝入揹包可以使價值總和最大。注意這裏不要求把物品整個裝入,能夠只裝入一個物品的部分。github

解法: 部分揹包問題常採用貪心算法來解決,先對每件物品計算其每單位重量價值 v[i]/w[i],而後從具備最大單位價值的物品開始拿,而後拿第二大價值的物品,直到裝滿揹包。按照這種貪心策略拿到的必然是價值總和最大,這個比較簡單,實現代碼就略去了。面試

2 0-1揹包問題

0-1揹包問題描述

有 N 件物品和一個容量爲 C 的揹包。第 i 件物品的重量是 w[i],價值是v[i]。求解將哪些物品裝入揹包可以使價值總和最大。注意物品只能要麼拿要麼不拿,這也正是 0-1 的意義所在。能夠把部分揹包問題看做是拿金粉,而 0-1 揹包問題則是拿金塊,一個可分,一個不可分。算法

分析

這是最基礎的揹包問題,特色是:每種物品僅有一件,能夠選擇放或不放。 用子問題定義狀態:即 f[i][w] 表示前 i 件物品恰放入一個容量爲 c 的揹包能夠得到的最大價值。則其狀態轉移方程即是:數組

f[i][c] = max{f[i-1][c], f[i-1][c-w[i]]+v[i]} 
複製代碼

這個方程很是重要,基本上全部跟揹包相關的問題的方程都是由它衍生出來的。因此有必要將它詳細解釋一下:將前 i 件物品放入容量爲 c 的揹包中 這個子問題,若只考慮第i件物品的策略(放或不放),那麼就能夠轉化爲一個只牽扯前 i-1 件物品的問題。bash

  • 若是不放第 i 件物品,那麼問題就轉化爲 前 i-1 件物品放入容量爲 v 的揹包中,價值爲 f[i-1][c]
  • 若是放第i件物品,那麼問題就轉化爲 前 i-1 件物品放入剩下的容量爲 c-w[i] 的揹包中,此時能得到的最大價值就是 f[i-1][c-w[i]]再加上經過放入第 i 件物品得到的價值 v[i]。

優化空間複雜度

以上方法的時間和空間複雜度均爲 O(CN),其中時間複雜度應該已經不能再優化了,但空間複雜度卻能夠優化到 O(N)。 因爲在計算 f[i][c] 的時候,咱們只須要用到 f[i-1][c]f[i-1][c-w[i]],因此徹底能夠經過一維數組保存它們的值,這裏用到的小技巧就是須要從 c=C...0 開始反推,這樣就能保證在求 f[c] 的時候 f[c-w[i]] 保存的是 f[i-1][c-w[i]] 的值。注意,這裏不能從 c=0...C 這樣順推,由於這樣會致使 f[c-w[i]] 的值是 f[i][c-w[i]] 而不是 f[i-1][c-w[i]。這裏能夠優化下界,其實只須要從 c=C...w[i] 便可,能夠避免不須要的計算。僞代碼以下所示:數據結構

for i=0..N-1
    for c=C..w[i]
        f[c]=max{f[c],f[c-w[i]]+v[i]};
複製代碼

最終實現代碼以下:數據結構和算法

int knap01(int N, int C, int w[], int v[])
{
    int *f = (int *)calloc(sizeof(int), C+1);
    int i, c;

    for (i = 0; i < N; i++) {
        for (c = C; c >= w[i]; c--) {
            f[c] = max(f[c], f[c-w[i]] + v[i]);
        }
        printf("%d: ", i+1);
        printIntArray(f, C+1); // 打印f數組
    }
    return f[C];
}
複製代碼

測試結果以下,即在揹包容量爲 10 的時候裝第1和第2個物品(索引從0開始),總重量爲 4+5=9,最大價值爲 5+6=11。測試

參數:
w = [3, 4, 5] //物品重量列表
v = [4, 5, 6] //物品價值列表
C = 10

結果(打印數組f,i爲選擇的物品索引,c爲揹包重量,值爲揹包物品價值):
         
i/c 0 1 2 3 4 5 6 7 8 9 10
 0: 0 0 0 4 4 4 4 4 4 4 4 
 1: 0 0 0 4 5 5 5 9 9 9 9 
 2: 0 0 0 4 5 6 6 9 10 11 11 

KNap01 max: 11
複製代碼

初始化的細節問題

咱們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。有的題目要求「剛好裝滿揹包」時的最優解,有的題目則並無要求必須把揹包裝滿。一種區別這兩種問法的實現方法是在初始化的時候有所不一樣。

若是是第一種問法,要求剛好裝滿揹包,那麼在初始化時除了 f[0] 爲 0 其它 f[1..C] 均設爲 -∞,這樣就能夠保證最終獲得的 f[N] 是一種剛好裝滿揹包的最優解。若是並無要求必須把揹包裝滿,而是隻但願價格儘可能大,初始化時應該將 f[0..C] 所有設爲0。

爲何呢?能夠這樣理解:初始化的 f 數組事實上就是在沒有任何物品能夠放入揹包時的合法狀態。若是要求揹包剛好裝滿,那麼此時只有容量爲 0 的揹包可能被價值爲 0 的東西 「剛好裝滿」,其它容量的揹包均沒有合法的解,屬於未定義的狀態,它們的值就都應該是 -∞ 了。若是揹包並不是必須被裝滿,那麼任何容量的揹包都有一個合法解「什麼都不裝」,這個解的價值爲0,因此初始時狀態的值也就所有爲0了。

3 徹底揹包問題

問題描述

有 N 種物品和一個容量爲 C 的揹包,每種物品都有無限件可用。第i種物品的重量是 w[i],價值是v[i]。求解將哪些物品裝入揹包可以使這些物品的重量總和不超過揹包容量,且價值總和最大,物品不能只裝部分。

基本思路

這個問題很是相似於0-1揹包問題,所不一樣的是每種物品有無限件。也就是從每種物品的角度考慮,與它相關的策略已並不是取或不取兩種,而是有取0件、取1件、取2件...等不少種。若是仍然按照解01揹包時的思路,令 f[i][c] 表示前 i 種物品恰放入一個容量爲 c 的揹包的最大權值。仍然能夠按照每種物品不一樣的策略寫出狀態轉移方程,像這樣:

f[i][c] = max{f[i-1][c-k*w[i]]+ k*w[i]| 0<=k*w[i]<=c }
複製代碼

這跟0-1揹包問題同樣有O(CN)個狀態須要求解,但求解每一個狀態的時間已經不是常數了,求解狀態 f[i][c] 的時間是 O(c/w[i]),總的複雜度能夠認爲是 O(CN*Σ(c/w[i])),是比較大的。實現代碼以下:

/*
 * 徹底揹包問題
 */
int knapComplete(int N, int C, int w[], int v[])
{
    int *f = (int *)calloc(sizeof(int), C+1);
    int i, c, k;
    for (i = 0; i < N; i++) {
        for (c = C; c >= 0; c--) {
            for (k = 0; k <= c/w[i]; k++) {
                f[c] = max(f[c], f[c-k*w[i]] + k*v[i]);
            }
        }
        printf("%d: ", i+1);
        printIntArray(f, C+1);
    }
    return f[C];
}
複製代碼

使用與0-1揹包問題相同的例子,運行程序結果以下,最大價值爲 13,即選取 2個重量3,1個重量4的物品,總價值最高,爲 4*2 + 5 = 13

i/c: 0 1 2 3 4 5 6 7 8 9 10
0:   0 0 0 4 4 4 8 8 8 12 12 
1:   0 0 0 4 5 5 8 9 10 12 13 
2:   0 0 0 4 5 6 8 9 10 12 13 

KNapComplete max: 13
複製代碼

轉換爲0-1揹包問題

既然01揹包問題是最基本的揹包問題,那麼咱們能夠考慮把徹底揹包問題轉化爲01揹包問題來解。最簡單的想法是,考慮到第i種物品最多選 C/w[i] 件,因而能夠把第 i 種物品轉化爲 C/w[i] 件費用及價值均不變的物品,而後求解這個01揹包問題。這樣徹底沒有改進基本思路的時間複雜度,但這畢竟給了咱們將徹底揹包問題轉化爲01揹包問題的思路:將一種物品拆成多件物品。

更高效的轉化方法是:把第 i 種物品拆成重量爲 w[i]*2^k、價值爲 w[i]*2^k 的若干件物品,其中 k 知足 w[i]*2^k<=C。這是二進制的思想,由於無論最優策略選幾件第 i 種物品,總能夠表示成若干個 2^k 件物品的和。這樣把每種物品拆成 O(log C/w[i]) 件物品,是一個很大的改進。但咱們有更優的 O(CN) 的算法。

進一步優化—O(CN)解法

咱們能夠採用與0-1揹包問題相反的順序遍歷,從而能夠獲得 O(CN) 的解法,僞代碼以下:

for i=0..N-1
    for c=w[i]..C
        f[c]=max{f[c],f[c-w[i]]+v[i]};
複製代碼

這個僞代碼與0-1揹包僞代碼只是 C 的循環次序不一樣而已。0-1揹包之因此要按照 v=V..0的逆序來循環。這是由於要保證第i次循環中的狀態 f[i][c] 是由狀態 f[i-1][c-w[i]] 遞推而來。換句話說,這正是爲了保證每件物品只選一次,保證在考慮「選入第i件物品」這件策略時,依據的是一個絕無已經選入第i件物品的子結果 f[i-1][c-w[i]]。而如今徹底揹包的特色恰是每種物品可選無限件,因此在考慮「加選一件第i種物品」這種策略時,卻正須要一個可能已選入第i種物品的子結果 f[i][c-w[i]],因此就能夠而且必須採用 c=w[i]..C 的順序循環。這就是這個簡單的程序爲什麼成立的道理。實現代碼以下:

/**
 * 徹底揹包問題-仿01揹包解法
 */
int knapCompleteLike01(int N, int C, int w[], int v[])
{
    int *f = (int *)calloc(sizeof(int), C+1);
    int i, c;
    for (i = 0; i < N; i++) {
        for (c = w[i]; c <= C; c++) {
            f[c] = max(f[c], f[c-w[i]] + v[i]);
        }
        printf("%d: ", i+1);
        printIntArray(f, C+1);

    }
    return f[C];
}
複製代碼

參考資料

相關文章
相關標籤/搜索