题目
有4个重量分别为{5,3,2,1}的物品,它们的价值分别为{4,4,3,1},给定一个容量为W=6的背包。设计从这些物品中选取一部分物品放入该背包的方案,每个物品要么选中要么不选中,要求选中的物品不仅能够放到背包中,而且具有最大的价值。并对下表所示的4个物品求出W=6时的所有解和最佳解。
物品编号 | 重量 | 价值 |
---|---|---|
1 | 5 | 4 |
2 | 3 | 4 |
3 | 2 | 3 |
4 | 1 | 1 |
解析
一、回溯法
求解思路
通过递归尝试所有可能的物品组合,并在每步判断当前组合是否符合约束条件(总重量不能超过W=6),如果不符合,则回溯到上一步继续尝试其他组合。
算法设计步骤
-
定义一个递归函数,尝试将当前物品放入背包或不放入背包。
-
在递归函数中,如果当前组合的总重量小于或等于背包容量W,更新最大价值,更新背包容量。
-
在尝试放入下一个物品时,如果总重量超过W,则回溯。
-
记录所有符合条件的组合。
代码实现
#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)、装修任务中的成本等
前者要求“界”越大越好,也就是“上界”
后者要求“界”越小越好,也就是“下界”
算法设计步骤
-
使用优先队列存储节点,根据节点的上界进行排序。
-
初始时,将根节点(空集)加入队列。
-
从队列中取出一个节点,计算包含下一个物品和不包含下一个物品两种情况的上界。
-
如果当前节点的上界高于当前已知最大价值,则继续搜索,否则剪枝。
-
更新最大价值和最佳组合。
代码实现
#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;
}
三、蛮力法
求解思路
通过枚举所有可能的物品组合,检查每个组合的总重量和总价值,并找出符合条件的最大价值组合。
算法设计步骤
-
生成所有物品的所有可能组合(2^n个组合)。
-
对每个组合计算总重量和总价值。
-
如果组合的总重量小于或等于背包容量W,记录该组合的总价值。
-
找出所有符合条件的组合中的最大总价值组合。
代码实现
#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;
}
四、贪心法
求解思路
通过计算每个物品的价值密度(价值/重量),优先选择价值密度最高的物品放入背包,直到背包装满为止。
算法设计步骤
-
计算每个物品的价值密度(价值/重量)。
-
按照价值密度从高到低对物品排序。
-
依次选择价值密度最高的物品放入背包,直到背包装满或没有更多的物品可以选择。
-
记录选择的物品和总价值。
代码实现
注意!下面的代码的输出结果是排序之后的数组的索引(是按排好序的数组顺序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) |
分支限界法 | 通过上界剪枝,效率较高,保证最优解 | 实现复杂,需要优先队列和上界计算 | 物品数量较多,且需要最优解 |
贪心法 | 实现简单,计算效率高 | 不保证最优解,适合近似最优解 | 问题规模较大,且对最优解要求不高 |
-
物品数量较少:使用蛮力法来保证找到最优解。
-
物品数量中等:使用回溯法,通过剪枝在合理时间内找到最优解。
-
物品数量较多:使用分支限界法,通过上界剪枝提高效率,找到最优解。
-
问题规模较大且对最优解要求不高:使用贪心法,快速找到近似最优解。