分别用蛮力法、回溯法、分支限界法、贪心算法和动态规划法,求解0/1背包问题。
目录
0-1背包问题:
有n件物品和一个容量为W的背包。第i件物品的价值是v[i],重量是w[i]。求解将哪些物品装入背包可使价值总和最大。所谓01背包,表示每一个物品只有一个,要么装入,要么不装入。
一、简单算法
最简单的算法是:蛮力法,即尝试各种可能的商品组合,并找出价值最高的组合。
这样显然是可行的,但是速度非常慢。在只有3件商品的情况下,你需要计算8个不同的集合;当有4件商品的时候,你需要计算16个不同的集合。每增加一件商品,需要计算的集合数都将翻倍!这种算法的运行时间是O(2ⁿ),真的是慢如蜗牛。
1)实现代码
#include<stdio.h>
#include<vector>
using namespace std;
vector<vector<int>>ps; //存放幂集
//求1~n的幂集ps
void PSet(int n)
{
vector<vector<int>>ps1; //子幂集
vector<vector<int>>::iterator it; //幂集迭代器
vector<int>s;
ps.push_back(s); //向ps中添加{}空集合
for (int i = 1; i <= n; i++) { //循环添加1~n
ps1 = ps; //将上一步得到的幂集存放在ps1中
for (it = ps1.begin(); it != ps1.end(); ++it) {
(*it).push_back(i); //每次循环在ps1的每个集合末尾添加i
}
for (it = ps1.begin(); it != ps1.end(); ++it) {
ps.push_back(*it);
}
}
}
//求所有方案和最佳方案
void Search(int w[], int v[], int W)
{
int count = 0; //方案编号
int sumw, sumv; //当前方案的总重量和总价值
int best_i, best_sumw = 0, best_sumv = 0; //最佳方案编号、总重量和总价值
vector<vector<int>>::iterator it; //幂集迭代器
vector<int>::iterator sit; //幂集集合元素迭代器(子迭代器)
printf("序号\t选择物品\t总重量\t总价值\t能否装入\n");
for (it = ps.begin(); it != ps.end(); ++it) {
printf("%d\t", count+1);
sumw = sumv = 0;
printf("{");
for (sit = (*it).begin(); sit != (*it).end(); ++sit) {
printf("%d", *sit);
sumw += w[*sit - 1]; //w数组下标从0开始
sumv += v[*sit - 1]; //v数组下标从0开始
}
printf("}\t\t%d\t%d\t", sumw, sumv);
if (sumw <= W) {
printf("能\n");
if (sumv > best_sumv) { //比较求最优方案
best_i = count;
best_sumw = sumw;
best_sumv = sumv;
}
}
else printf("否\n");
count++; //方案编号+1
}
printf("0/1背包问题,最佳方案为:");
printf("选择物品{ ");
for (sit = ps[best_i].begin(); sit != ps[best_i].end(); ++sit) {
printf("%d ", *sit);
}
printf("},");
printf("总重量:%d,总价值:%d\n", best_sumw, best_sumv);
}
int main() {
int n = 4, W = 6;
int w[] = { 5,3,2,1 };
int v[] = { 4,4,3,1 };
PSet(n); //求1~n的幂集ps
Search(w, v, W); //求所有方案和最佳方案
return 0;
}
2)运行结果图
3)复杂度分析
蛮力法求解0/1背包问题的时间复杂度为:O(2^n)
二、回溯算法
1)实现代码
#include<stdio.h>
#include<iostream>
using namespace std;
#define MAXN 30 //最多物品数
//问题表示
int n, W; //n个数,W容量
double w[MAXN], v[MAXN]; //物品重量和价值
int x[MAXN]; //存放最终解
int bestp; //存放最优解的总价值
//求解0/1背包问题
void Backtrack(int t, int tw, int tv, int op[])
{
if (t > n) { //找到一个叶子结点
if (tw <= W && tv > bestp) { //找到一个满足条件的更优解,保存它
bestp = tv;
for (int j = 1; j <= n; j++)
x[j] = op[j];
}
}
else { //还没搜寻玩所有物品
if (tw + w[t] <= W) { //左孩子结点剪枝:满足条件才放入第i个物品
op[t] = 1; //选取第i个物品
Backtrack(t+1,tw+w[t],tv+v[t],op);
}
op[t] = 0; //不选取第i个物品,回溯
Backtrack(t + 1, tw, tv, op);
}
}
void disp_bestx() {
int i;
int sumw = 0;
cout << "放入购物车的物品序号为:";
for (i = 1; i <= n; i++) {
if (x[i] == 1) {
cout << i << " ";
sumw += w[i];
}
}
cout << endl;
cout << "放入购物车的物品最大价值为:" << bestp << ",总重量为:" << W << endl;
}
int main() {
cout << "输入物品个数n:"; cin >> n;
cout << "输入购物车容量W:"; cin >> W;
cout << "依次输入每个物品的重量w和价值v,用空格分开:";
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
int op[MAXN]; //存放临时解
Backtrack(1, 0, 0, op);
disp_bestx();
return 0;
}
2)运行结果图
3)复杂度分析
最不理想的情况下,回溯法求解0/1背包问题的时间复杂度为:O(2^n)
由于其对蛮力法进行优化后的算法,其复杂度一般比蛮力法要小。
空间复杂度:有n个物品,即最多递归n层,存储物品信息就是一个一维数组,即回溯法求解0/1背包问题的空间复杂度为:n。
三、分支限界算法
1)实现代码
#include <iostream>
#define N 30
using namespace std;
int n;double W; //n个数,W容量
double w[N];double v[N]; //物品重量和价值
bool x[N];
bool best_x[N]; //存储最优方案
double now_v; //当前价值
double remain_v; //剩余价值
double now_w; //当前容量
double best_v; //最优价值
double Bound(int k) //计算分枝结点k的上界
{
remain_v = 0;
while (k <= n) {
remain_v += v[k];
k++;
}
return remain_v + now_v;
}
void Backtrack(int t)
{
if (t > n) { //是否到达叶节点
for (int i = 1; i <= n; i++) {
best_x[i] = x[i]; //记录回溯的最优情况
}
best_v = now_v; //记录回溯中的最优价值
return;
}
if (now_w + w[t] <= W) { //约束条件,是否放入。放入考虑左子树,否则考虑右子树
x[t] = 1;
now_w += w[t];
now_v += v[t];
Backtrack(t + 1); //进行下一个节点的分析
now_w -= w[t]; //在到达叶节点后进行回溯
now_v -= v[t];
}
if (Bound(t + 1) > best_v) { //限界条件,是否剪枝。若放入t后不满足约束条件则进行到此处,然后判断若当前价值加剩余价值都达不到最优,则没必要进行下去
x[t] = 0;
Backtrack(t + 1);
}
}
void Knapsack(double W, int n)
{
double sum_w = 0;
double sum_v = 0;
best_v = 0;
for (int i = 0; i < n; i++) {
sum_w += w[i];
sum_v += v[i];
}
Backtrack(1);
cout << "放入购物车的物品最大价值为:" << best_v << endl;
cout << "放入购物车的物品序号为:" << endl;
for (int i = 1; i <= n; i++) {
if(x[i] == 1)
cout << i << " ";
}
}
int main()
{
cout << "输入物品个数n:"; cin >> n;
cout << "输入购物车容量W:"; cin >> W;
cout << "依次输入每个物品的重量w和价值v,用空格分开:\n";
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
Knapsack(W, n);
return 0;
}
2)运行结果图
3)复杂度分析
分支限界法求解0/1背包问题的时间复杂度为:O(2^n)
四、贪心算法
1)实现代码
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define MAXN 51
int n;
int W;
struct NodeType
{
int w;
int v;
int p; //性价比p=v/w
bool operator<(const NodeType& s)const
{
return p > s.p;
}
};
NodeType A[MAXN];
int maxv;
int x[MAXN];
void Knap() //求解背包问题并返回总价值
{
maxv = 0; //maxv初始化为0
int weight = W; //背包中能装入的余下重量
memset(x, 0, sizeof(x)); //初始化x向量
int i = 1;
while (A[i].w <= weight) //物品i能够全部装入背包时,循环
{
x[i] = 1; //装入物品i
weight -= A[i].w; //减少背包中能装入的余下重量
maxv += A[i].v; //计算装入物品i后的总价值
i++;
}
}
void disp_bestx() {
int sumw = 0;
cout << "放入购物车的物品序号为:";
for (int j = 1; j <= n; j++) {
if (x[j] == 1) {
cout << j << " ";
sumw += A[j].w;
}
}
cout << endl;
cout << "放入购物车的物品最大价值为:" << maxv << ",总重量为:" << sumw << endl;
}
int main()
{
cout << "输入物品个数n:"; cin >> n;
cout << "输入购物车容量W:"; cin >> W;
cout << "依次输入每个物品的重量w和价值v,用空格分开:" << endl;;
for (int i = 1; i <= n; i++) {
cin >> A[i].w >> A[i].v;
}
for (int i = 1; i <= n; i++) {
A[i].p = A[i].v / A[i].w;
}
sort(A + 1, A + 1 + n);
Knap();
disp_bestx();
return 0;
}
2)运行结果图
3)复杂度分析
贪心法求解0/1背包问题的时间复杂度为:O( nlog(n) )
五、非递归-动态规划
1)实现代码
#include<stdio.h>
#include<iostream>
using namespace std;
#define max(x,y) ((x)>(y)?(x):(y))
#define MAXN 30 //最多物品数
#define MAXW 100 //最大限制重量
//问题表示
int n, W; //n个数,W容量
int w[MAXN], v[MAXN]; //物品重量和价值
//求解结果表示
int dp[MAXN][MAXW];
int x[MAXN];
int bestp; //存放最优解的总价值
//用动态规划法求0/1背包问题
void Knap()
{
int i, r;
for (i = 0; i <= n; i++) //置边界条件dp[i][0] = 0
dp[i][0] = 0;
for (r = 0; r <= W; r++) //置边界条件dp[0][r] = 0
dp[0][r] = 0;
for (i = 1; i <= n; i++) {
for (r = 1; r <= W; r++) {
if (r < w[i])
dp[i][r] = dp[i - 1][r];
else
dp[i][r] = max(dp[i - 1][r], dp[i - 1][r - w[i]] + v[i]);
}
}
}
void Buildx() //回推求最优解
{
int i = n, r = W;
bestp = 0;
while (i >= 0) {
if (dp[i][r] != dp[i - 1][r]) {
x[i] = 1;
bestp += v[i];
r = r - w[i];
}
else
x[i] = 0;
i--;
}
}
int main() {
cout << "输入物品个数n:"; cin >> n;
cout << "输入最大容量W:"; cin >> W;
cout << "依次输入每个物品的重量w和价值v,用空格分开:";
for (int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
Knap();
Buildx();
printf("最优方案\n");
printf("选取物品为:");
for (int i = 1; i <= n; i++)
if (x[i] == 1)
printf("%d ", i);
printf("\n");
printf("总价值=%d\n", bestp);
return 0;
}
2)运行结果图
3)复杂度分析
动态规划法求解0/1背包问题的时间复杂度为:O(nW)
空间复杂度为:O(nW)