01 揹包問題的三種寫法

不妨先用最樸素的方法,針對每個物品是否放入揹包進行搜索:

// 輸入
int n, W;
int w[MAX_N], v[MAX_N];

// 從第 i 個物品開始挑選總重小於 j 的部分
int rec(int i, int j) {
    int res;
    if (i == n) {
        // 已經沒有剩餘物品了
        res = 0;
    } else if (j < w[i]) {
        // 無法挑選這個物品
        res = rec(i + 1, j);
    } else {
        // 挑選和不挑選的兩種情況都嘗試一下
        res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
    }
    return res;
}

void solve() {
    printf("%d\n", rec(0, W));
}

只不過,這種方法的搜索深度是 n,而且每一層的搜索都需要兩次分支,最壞的就需要 O(2^{n}) 的時間,當 n 比較大時就沒辦法解了。由於 rec 在遞歸調用時存在重複調用的情況,所以我們可以把第一次計算時的結果記錄下來,省略掉第二次以後的重複計算試試看。

int dp[MAX_N + 1][MAX_W + 1];  // 記憶化數組

int rec(int i, int j) {
    if (dp[i][j] >= 0) {
        // 已經計算過的話直接使用之前的結果
        return dp[i][j];
    }
    int res;
    if (i == n) {
        res = 0;
    } else if (j < w[i]) {
        res = rec(i + 1, j);
    } else {
        res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]);
    }

    // 將結果儲存在數組中
    return dp[i][j] = res;
}

void solve() {
    // 用 -1 表示尚未計算過,初始化整個數組
    memset(dp, -1, sizeof(dp));
    printf("%d\n", rec(0, W));
} 

對於同樣的參數,只會在第一次被調用到時執行遞歸部分,第二次之後都會直接返回。參數的組合不過 nW 種,而函數內只調用兩次遞歸,所以只需要 O(nW) 的複雜度就能解決問題。這種方法一般被稱爲記憶化搜索

如果對記憶化搜索還不是很熟練的話,可以寫成窮竭搜索的寫法:

// 目前選擇的物品價值總和是 sum,從第 i 個物品之後的物品中挑選重量總和小於 j 的物品
int rec(int i, int j, int sum) {
    int res;
    if (i == n) {
        // 已經沒有剩餘物品了
        res = sum;
    } else if (j < w[i]) {
        // 無法挑選這個物品
        res = rec(i + 1, j, sum);
    } else {
        // 挑選和不挑選的兩種情況都嘗試一下
        res = max(rec(i + 1, j, sum), rec(i + 1, j - w[i], sum + v[i]));
    }
    return res;
}

在需要剪枝的情況下,可能會像這樣把各種參數都寫在函數上,但在這種下會讓記憶化搜索難以實現,需要注意。

接下來,我們來仔細研究一下前面的算法利用到的這個記憶化數組。記 dp[i][j] 爲根據 rec 的定義,從第 i 個物品開始挑選總重小於 j 時,總價值的最大值。於是就有如下遞推式:

dp[n][j] = 0

dp[i][j] = \left\{\begin{matrix} dp[i + 1][j] & & (j < w[i]) \\ max(dp[i+1][j], dp[i+1][j - w[i]] + v[i]) & & (else) \end{matrix}\right.

這樣不用寫遞歸函數,直接利用遞推式將各項的值計算出來,簡單的二重循環也能解決這一問題。

int dp[MAX_N + 1][MAX_W + 1];  // DP 數組

void solve() {
    for (int i = n - 1; i >= 0; i--) {
        for (int j = 0; j <= W; j++) {
            if (j < w[i]) {
                dp[i][j] = dp[i + 1][j];
            } else {
                dp[i][j] = max(dp[i + 1][j], dp[i + 1][j - w[i]] + v[i]);
            }
        }
    }
    printf("%d\n", dp[0][W]);
}

這個算法的複雜度與前面的相同,也是 O(nW) ,但是簡潔了很多。以這種方式一步步按順序求出問題的解的方法被稱作動態規劃法,也就是常說的 DP

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章