背包问题丨蛮力法、回溯法、分支限界法、贪心法方案

题目

有4个重量分别为{5,3,2,1}的物品,它们的价值分别为{4,4,3,1},给定一个容量为W=6的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且具有最大的价值。并对下表所示的4个物品求出W=6时的所有解和最佳解。

物品编号重量价值
154
234
323
411

解析

一、回溯法

求解思路

通过递归尝试所有可能的物品组合,并在每步判断当前组合是否符合约束条件(总重量不能超过W=6),如果不符合,则回溯到上一步继续尝试其他组合。

算法设计步骤

  1. 定义一个递归函数,尝试将当前物品放入背包或不放入背包。

  2. 在递归函数中,如果当前组合的总重量小于或等于背包容量W,更新最大价值,更新背包容量。

  3. 在尝试放入下一个物品时,如果总重量超过W,则回溯。

  4. 记录所有符合条件的组合。

代码实现

#include <iostream>
#include <vector>
using namespace std;

/*参数说明:
weights:输入的各个物品的重量
values:输入的各个物品的价值
W:约束条件,背包限重
index:累计递归判断的第几个物品,从0开始
current_weight:当前背包的总重量
current_value:当前背包的总价值
max_value:在当次递归中得到的满足条件的最大价值,也就是最优解(也是最终输出的结果)
current_combination:当前背包内的物品组合
best_combination:在当次递归中得到的满足条件的最大价值,也就是最优解佳组合(也是最终输出的结果)
*/

void knapsack_backtrack(vector<int>& weights, vector<int>& values, int W, int index, int current_weight, int current_value, int& max_value, vector<int>& current_combination, vector<int>& best_combination) {
    if (current_weight <= W && current_value > max_value) {
        max_value = current_value;
        best_combination = current_combination;
    }
    if (index == weights.size()) return;//已经搜索了全部物品
    
    // 不选择当前物品
    knapsack_backtrack(weights, values, W, index + 1, current_weight, current_value, max_value, current_combination, best_combination);
    
    // 选择当前物品
    if (current_weight + weights[index] <= W) {
        current_combination.push_back(index + 1);//将选择的物品压入当前组合
        knapsack_backtrack(weights, values, W, index + 1, current_weight + weights[index], current_value + values[index], max_value, current_combination, best_combination);
        current_combination.pop_back();//回溯
    }
}


int main() {
    vector<int> weights = {5, 3, 2, 1};
    vector<int> values = {4, 4, 3, 1};
    int W = 6;
    int max_value = 0;
    vector<int> current_combination;
    vector<int> best_combination;
    
    knapsack_backtrack(weights, values, W, 0, 0, 0, max_value, current_combination, best_combination);
    
    // 输出结果
    cout << "最佳组合的价值: " << max_value << endl;
    cout << "最佳组合的物品: ";
    for (int item : best_combination) {
        cout << item << " ";
    }
    cout << endl;
    return 0;
}

二、分支限界法

求解思路

是对回溯法的一种改进,通过计算上界来剪枝,避免不必要的搜索。每次选择时,计算当前状态下的最优可能解,如果不可能超过当前已知最优解,则剪枝

“界”是一种说法,其本质是满足当前题目/条件下的最优状态/值。

比如说背包问题中的最大价值(<W)、装修任务中的成本等

前者要求“界”越大越好,也就是“上界”

后者要求“界”越小越好,也就是“下界”

算法设计步骤

  1. 使用优先队列存储节点,根据节点的上界进行排序

  2. 初始时,将根节点(空集)加入队列。

  3. 从队列中取出一个节点,计算包含下一个物品和不包含下一个物品两种情况的上界

  4. 如果当前节点的上界高于当前已知最大价值,则继续搜索,否则剪枝。

  5. 更新最大价值和最佳组合。

代码实现

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

// 定义一个结构体来表示状态空间树中的节点
struct Node {
    int level;       // 节点在决策树中的层级
    int value;       // 到目前为止包括的物品的总价值
    int weight;      // 到目前为止包括的物品的总重量
    int bound;       // 子树中最大价值的上界
    vector<int> items; // 包括的物品索引
    // 重载操作符 < 使得优先队列可以按 bound 值进行排序
    bool operator<(const Node& other) const {
        return bound < other.bound;
    }
};

// 计算节点 u 的上界
int bound(Node u, int n, int W, vector<int>& weights, vector<int>& values) {
    // 如果当前重量已经超过背包容量,返回 0 作为上界
    if (u.weight >= W) return 0;
    
    int profit_bound = u.value;  // 当前节点的价值作为上界的初始值
    int j = u.level + 1;         // 下一个要考虑的物品
    int totweight = u.weight;    // 当前总重量

    // 尽可能多地加入完整的物品直到超重
    while (j < n && totweight + weights[j] <= W) {
        totweight += weights[j];
        profit_bound += values[j];
        j++;
    }

    // 如果还有剩余容量,将剩余容量按单位重量价值最高的物品部分填充
    if (j < n) profit_bound += (W - totweight) * values[j] / weights[j];
    return profit_bound;
}


// 分支限界法解决 0/1 背包问题
void knapsack_branch_bound(vector<int>& weights, vector<int>& values, int W, int& max_value, vector<int>& best_combination) {
    priority_queue<Node> pq;     // 优先队列用于存储节点
    int n = weights.size();      // 物品数量
    Node u, v;                   // 定义两个节点 u 和 v
    u.level = -1;                // 初始化 u 为根节点
    u.value = 0;
    u.weight = 0;
    u.bound = bound(u, n, W, weights, values);  // 计算 u 的上界
    pq.push(u);                  // 将 u 入队

    // 遍历优先队列直到队列为空
    while (!pq.empty()) {
        u = pq.top();            // 取出队首元素
        pq.pop();                // 弹出队首元素

        // 如果当前节点的上界大于 max_value,则进一步探索该节点的子节点
        if (u.bound > max_value) {
            // 生成包含下一个物品的子节点
            v.level = u.level + 1;
            v.weight = u.weight + weights[v.level];
            v.value = u.value + values[v.level];
            v.items = u.items;
            v.items.push_back(v.level + 1);

            // 如果子节点的重量在容量限制内且价值大于当前已知最大值,更新 max_value 和 best_combination
            if (v.weight <= W && v.value > max_value) {
                max_value = v.value;
                best_combination = v.items;
            }

            // 计算子节点的上界,并将其加入优先队列
            v.bound = bound(v, n, W, weights, values);
            if (v.bound > max_value) pq.push(v);

            // 生成不包含下一个物品的子节点
            v.weight = u.weight;
            v.value = u.value;
            v.items = u.items;
            v.bound = bound(v, n, W, weights, values);
            if (v.bound > max_value) pq.push(v);
        }
    }
}


int main() {
    vector<int> weights = {5, 3, 2, 1};  // 物品的重量
    vector<int> values = {4, 4, 3, 1};   // 物品的价值
    int W = 6;                           // 背包的容量
    int max_value = 0;                   // 最大价值初始化为 0
    vector<int> best_combination;        // 最佳组合初始化为空

    // 调用分支限界法解决背包问题
    knapsack_branch_bound(weights, values, W, max_value, best_combination);

    // 输出结果
    cout << "最佳组合的价值: " << max_value << endl;
    cout << "最佳组合的物品: ";
    for (int item : best_combination) {
        cout << item << " ";
    }
    cout << endl;
    return 0;
}

三、蛮力法

求解思路

通过枚举所有可能的物品组合,检查每个组合的总重量和总价值,并找出符合条件的最大价值组合。

算法设计步骤

  1. 生成所有物品的所有可能组合(2^n个组合)。

  2. 对每个组合计算总重量和总价值

  3. 如果组合的总重量小于或等于背包容量W,记录该组合的总价值。

  4. 找出所有符合条件的组合中的最大总价值组合

代码实现

#include <iostream>
#include <vector>
using namespace std;

int main() {
    vector<int> weights = {5, 3, 2, 1};  // 物品的重量
    vector<int> values = {4, 4, 3, 1};   // 物品的价值
    int W = 6;                           // 背包的容量
    int n = weights.size();              // 物品数量
    int max_value = 0;                   // 最大价值初始化为0
    vector<int> best_combination;        // 最佳组合初始化为空
    
    // 枚举所有可能的组合
    for (int i = 0; i < (1 << n); ++i) { // 遍历从0到2^n-1的所有数字
        int total_weight = 0;            // 当前组合的总重量
        int total_value = 0;             // 当前组合的总价值
        vector<int> combination;         // 当前组合的物品索引
        
        for (int j = 0; j < n; ++j) {    // 检查每一位是否为1
            if (i & (1 << j)) {          // 如果第j位是1,包含第j个物品
                total_weight += weights[j]; // 增加当前组合的总重量
                total_value += values[j];   // 增加当前组合的总价值
                combination.push_back(j + 1); // 记录当前物品索引
            }
        }
        
        // 如果当前组合的重量在容量限制内且价值大于当前最大值,更新max_value和best_combination
        if (total_weight <= W && total_value > max_value) {
            max_value = total_value;
            best_combination = combination;
        }
    }
    
    // 输出结果
    cout << "最佳组合的价值: " << max_value << endl;
    cout << "最佳组合的物品: ";
    for (int item : best_combination) {
        cout << item << " ";
    }
    cout << endl;
    return 0;
}

四、贪心法

求解思路

通过计算每个物品的价值密度(价值/重量),优先选择价值密度最高的物品放入背包,直到背包装满为止。

算法设计步骤

  1. 计算每个物品的价值密度(价值/重量)。

  2. 按照价值密度从高到低对物品排序。

  3. 依次选择价值密度最高的物品放入背包,直到背包装满或没有更多的物品可以选择

  4. 记录选择的物品和总价值。

代码实现

注意!下面的代码的输出结果是排序之后的数组的索引(是按排好序的数组顺序push的),所以输出结果是1 2 3,也就是对应原来的2 3 4.

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 定义一个结构体来表示物品
struct Item {
    int weight;     // 物品重量
    int value;      // 物品价值
    double density; // 物品的价值密度 (价值/重量)
};

// 比较函数,用于按照价值密度从高到低排序
bool compare(Item a, Item b) {
    return a.density > b.density;
}

int main() {
    vector<Item> items = {{5, 4, 0}, {3, 4, 0}, {2, 3, 0}, {1, 1, 0}}; // 初始化物品
    int W = 6; // 背包容量

    // 计算每个物品的价值密度
    for (auto& item : items) {
        item.density = (double)item.value / item.weight;
    }

    // 按价值密度从高到低排序
    sort(items.begin(), items.end(), compare);

    int total_weight = 0; // 背包当前总重量
    int total_value = 0;  // 背包当前总价值
    vector<int> chosen_items; // 选择的物品索引

    // 选择物品
    for (int i = 0; i < items.size(); ++i) {
        if (total_weight + items[i].weight <= W) { // 如果加上当前物品后不超过容量
            total_weight += items[i].weight;      // 增加背包的总重量
            total_value += items[i].value;        // 增加背包的总价值
            chosen_items.push_back(i + 1);        // 记录选择的物品索引
        }
    }

    // 输出结果
    cout << "选择的物品总价值: " << total_value << endl;
    cout << "选择的物品: ";
    for (int item : chosen_items) {
        cout << item << " ";
    }
    cout << endl;
    return 0;
}

*分析比较四种方法的优劣性

方法优点缺点适用场景
蛮力法简单直接,保证最优解计算复杂度高,适合小规模问题物品数量较少(如 n≤20n \leq 20n≤20)
回溯法剪枝减少计算,保证最优解实现复杂,最坏情况下复杂度高物品数量中等(如 n≤40n \leq 40n≤40)
分支限界法通过上界剪枝,效率较高,保证最优解实现复杂,需要优先队列和上界计算物品数量较多,且需要最优解
贪心法实现简单,计算效率高不保证最优解,适合近似最优解问题规模较大,且对最优解要求不高
  • 物品数量较少:使用蛮力法来保证找到最优解。

  • 物品数量中等:使用回溯法,通过剪枝在合理时间内找到最优解。

  • 物品数量较多:使用分支限界法,通过上界剪枝提高效率,找到最优解。

  • 问题规模较大对最优解要求不高:使用贪心法,快速找到近似最优解。

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值