回溯法之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");
}
posted @ 2020-12-11 20:18  Thoughtful_z  阅读(212)  评论(0)    收藏  举报