1. 程式人生 > 實用技巧 >回溯法之0-1揹包問題

回溯法之0-1揹包問題

回溯法之0-1揹包問題


1. 問題描述

​ 假設有\(n = 4\)個物品,有一個容量為\(c = 7\)的揹包,其中物品的重量陣列\(weight = {3, 5, 2, 1}\),物品的價值陣列\(value = {9, 10, 7, 4}\)。要求求解該包最多能裝下多少價值的物品。

2. 問題分析

​ 該問題與解裝載問題類似,在搜尋解空間樹時,只要其左兒子結點是一個可行的結點,就遞迴進去搜索其左子樹。當右子樹可能包含最優解時才進入右子樹搜尋,否則將右子樹剪去。
\(cleft\)是當前剩餘物品價值總和,\(cv\)是當前價值,\(bestValue\)是當前最優價值。當\(cv + cleft <= bestValue\)

時,可以剪去右子樹。計算右子樹更好的方式是,將剩餘物品依其單位重量價值排序(貪心求解0-1揹包的思想),然後依次裝入物品,直至裝不下時,再裝入該物品的一部分而裝滿揹包。由此得到的價值是右子樹中解的上界,判斷如果當前上界小於\(bestValue\),則不展開右子樹計算。
​ 對於本題,各個物品的單位重量價值為\(d = {3, 2, 3.5, 4}\),以物品單位重量價值的遞減序列裝入物品。先裝入物品4,然後裝入物品3和1。

3. 程式碼求解

​ 通過Bound函式計算當前結點處的上界。

// 通過Bound函式計算當前結點處的上界。
// cleft 表示當前剩餘的空間
// 將剩下未裝入揹包的物品按照單位重量價值的遞減序列依次放入揹包
// 最後揹包剩餘的空間使用切割物品的方式塞滿,獲得當前結點的價值上界
int Bound(int i) {
    int cleft = c - cw;
    int b = cv;
    while (i <= n && weight[i] <= cleft) {
        cleft -= weight[i];
        b += value[i];
        i++;
    }
    if (i <= n)
        b += value[i] / weight[i] * cleft;
    return b;
}

​ 通過BackTrack函式遞迴求解結果:

// i > n表示一條路徑訪問完成,則儲存當前的最優解(因為只有其為當前最優解才能訪問到葉子節點)
// 如果cw + weight[i] <= c進入左子樹,採用回溯進行遞迴依次向左子樹進行計算
// 採用Bound函式對右子樹進行剪枝,將右子樹結點上限小於當前最優解的分支直接剪去
void BackTrack(int i) {
    if (i > n) {
        bestValue = cv;
        return ;
    }
    if (cw + weight[i] <= c) {
        cw += weight[i];
        cv += value[i];
        BackTrack(i + 1);
        cw -= weight[i];
        cv -= value[i];
    }
    if (Bound(i + 1) > bestValue)
        BackTrack(i + 1);
}

4. 完整程式碼

/**
 * 回溯法之0-1揹包問題
 **/
#include <stdio.h>
#include <stdlib.h>

#define MAX 4

/**
 * c        揹包容量
 * n        物品數
 * weight   物品重量陣列
 * value    物品價值陣列
 * cw       當前重量
 * cv       當前價值
 * bestValue當前最佳價值
 **/
int c = 7;
int n = MAX;
int weight[MAX + 1] = {0};
int value[MAX + 1] = {0};
int cw, cv;
int bestValue;

struct article {
    int ID;
    float d;
};

// 氣泡排序
void BubbleSort(struct article a[], int n) {
    int i = 0, j = 0;
    for (i = 0;i < n - 1;i++) {
        for (j=0; j<n-1-i; ++j) {
            if (a[j].d < a[j + 1].d) {
                struct article tmp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = tmp;
            }
        }
    }
}

// 通過Bound函式計算當前結點處的上界。
// cleft 表示當前剩餘的空間
// 將剩下未裝入揹包的物品按照單位重量價值的遞減序列依次放入揹包
// 最後揹包剩餘的空間使用切割物品的方式塞滿,獲得當前結點的價值上界
int Bound(int i) {
    int cleft = c - cw;
    int b = cv;
    while (i <= n && weight[i] <= cleft) {
        cleft -= weight[i];
        b += value[i];
        i++;
    }
    if (i <= n)
        b += value[i] / weight[i] * cleft;
    return b;
}

// i > n表示一條路徑訪問完成,則儲存當前的最優解(因為只有其為當前最優解才能訪問到葉子節點)
// 如果cw + weight[i] <= c進入左子樹,採用回溯進行遞迴依次向左子樹進行計算
// 採用Bound函式對右子樹進行剪枝,將右子樹結點上限小於當前最優解的分支直接剪去
void BackTrack(int i) {
    if (i > n) {
        bestValue = cv;
        return ;
    }
    if (cw + weight[i] <= c) {
        cw += weight[i];
        cv += value[i];
        BackTrack(i + 1);
        cw -= weight[i];
        cv -= value[i];
    }
    if (Bound(i + 1) > bestValue)
        BackTrack(i + 1);
}

void main() {
    int W = 0, V = 0;
    int weight_tmp[MAX + 1] = {0, 3, 5, 2, 1};
    int value_tmp[MAX + 1] = {0, 9, 10, 7, 4};
    struct article d[MAX];

    for (int j = 1; j <= n; j++) {
        d[j - 1].ID = j;
        d[j - 1].d = 1.0 * value_tmp[j] / weight_tmp[j];
        V += value_tmp[j];
        W += weight_tmp[j]; 
    }

    if (W <= c) {
        printf("揹包容量為:%d\n", V);
        return;
    }

    BubbleSort(d, MAX);
    for (int i = 1; i <= n; i++) {
        weight[i] = weight_tmp[d[i - 1].ID];
        value[i] = value_tmp[d[i - 1].ID];
    }

    BackTrack(1);
    printf("%d\n", bestValue);

    system("pause");
}