一、实验名称
优先队列式分支限界法求解0-1背包问题:
(1)问题描述:优先队列式分支限界法求解0-1背包问题
(2)给定n种物品和一个背包。物品i的重量是wi,其价值为vi,背包的容量为c。应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
在选择装入背包的物品时,对每种物品i只有2种选择,即装入背包或不装入背包。不能将物品i装入背包多次,也不能只装入部分的物品i。
二、实验目的
通过上机实验,要求掌握优先队列式分支限界法求解 0-1 背包问题的问题描述、算法设计思想、程序设计。
三、实验原理
利用优先队列式分支限界法求解 0-1 背包问题,并计算出程序运行所需要的时间。
四、实验步骤
0-1背包问题的解空间可用子集树表示。
算法首先根据基于可行结点相应的子树最大价值上界优先级(结点的优先级定义为:以结点的价值上界作为优先级(由bound函数计算出)),从堆中选择一个节点(根节点)作为当前可扩展结点。
检查当前扩展结点的左儿子结点的可行性。如果是可行结点,则将它加入到子集树和活结点优先队列中。
当前扩展结点的右儿子结点一定是可行结点,仅当右儿子结点满足上界函数约束时,才将它加入子集树和活结点优先队列。
当扩展到叶节点时,算法结束,叶子节点对应的解即为问题的最优值。
举例:
假设有4个物品,物品的价值分别为p=(40, 42, 25, 12), 重量分别为w=(4, 7, 5, 3), 背包容量C=10,中间被剪掉的结点用×标记。
单位重量价值:[p1/w1, p2/w2, p3/w3, p4/w4] =(3, 2, 3.5, 4)
按照物品单位重量价值由大到小排序:
广度优先搜索解空间树:
若左儿子是可行节点,才搜索进入左子树,否则将左子树剪掉;
若右子树满足上界函数约束,才搜索右子树,否则将右子树剪掉。
根据基于可行结点相应的子树最大价值上界优先级(结点的优先级定义为:以结点的价值上界作为优先级(由bound函数计算出)),从堆中选择一个节点(根节点)作为当前可扩展结点。
先装入物品1,剩余的背包容量为6,只能装入物品2的6/7(即42*(6/7)=36)。 即上界为40+6*6=76;打x的部分因为up值已经小于等于bestp了,所以没必要继续递归了。
五、关键代码
1.计算up值
int Bound(int i,int n,int c,vector<Object> &obj)
{
int tmp_cleft = c - cw;
int tmp_cp = cp;
while(tmp_cleft >= obj[i].weight && i <= n)
{
tmp_cleft -= obj[i].weight;
tmp_cp += obj[i].price;
i++;
}
//装满背包
if(i <= n)
{
tmp_cp += tmp_cleft * obj[i].d;
}
return tmp_cp;
}
2.加入活性队列
void AddAliveNode(priority_queue<MaxHeapQNode *, vector<MaxHeapQNode *>, cmp> &q, MaxHeapQNode *E, int up, int wt, int curp, int i, int ch)
{
MaxHeapQNode *p = new MaxHeapQNode;
p->parent = E;
p->lchild = ch;
p->weight = wt;
p->upprofit = up;
p->profit = curp;
p->lev = i + 1;
q.push(p);
}
3.分支限界法
void MaxKnapsack(int n,int c,vector<int> &bestx,vector<Object> &obj)
{
priority_queue<MaxHeapQNode *, vector<MaxHeapQNode *>, cmp > q; // 大顶堆
MaxHeapQNode *E = NULL;
cw = cp = bestp = 0;
int i = 1;
int up = Bound(1,n,c,obj); //Bound(i)函数计算的是i还未处理时候的上限值
while(i != n + 1)
{
int wt = cw + obj[i].weight;
if(wt <= c)
{
if(bestp < cp + obj[i].price)
bestp = cp + obj[i].price;
AddAliveNode(q, E, up, cw + obj[i].weight, cp + obj[i].price, i, 1);
}
up = Bound(i+1,n,c,obj);//注意这里 up != up - obj[i].price而且 up >= up - obj[i].price
if(up >= bestp) //注意这里必须是大于等于
{
AddAliveNode(q, E, up, cw, cp, i, 0);
}
E = q.top();
q.pop();
cw = E->weight;
cp = E->profit;
up = E->upprofit;
i = E->lev;
}
for(int j = n; j > 0; --j)
{
bestx[obj[E->lev - 1].id] = E->lchild;
E = E->parent;
}
4.生成不同规模样例:
//生成规模为n的随机数
cout<<"请输入数据规模n:"<<endl;
int n,c;
cin>>n;
ofstream out("input1.txt");
out<<n<<'\n';
srand((unsigned)time(NULL));
// c (a,b]
int a=0,b=100;
out<< (rand() % (b-a))+ a + 1<<'\n';
// v数组
for(int i=0;i<n;i++){
out<<rand()<<' ';
if((i+1)%10==0) out<<'\n'; }
out<<'\n';
// w数组 (a,b]
for(int i=0;i<n;i++){
out<< (rand() % (b-a))+ a + 1<<' ';
if((i+1)%10==0) out<<'\n'; }
out.close();
六、测试结果
时间复杂度:O(2^n)
在最坏情况下,需要考虑搜索树中所有可能的组合,时间复杂度是指数级别的。
对于 0/1 背包问题的分枝定界算法,其时间复杂度可以表示为 O(2^n),因为算法在每个节点都需要考虑两种情况:选择当前物品或不选择当前物品。
但是,分枝定界算法通常通过启发式方法(如上界的计算和剪枝策略)来减少搜索空间,从而提高效率。因此,实际应用中的运行时间可能受到问题实例的特定条件和算法参数的影响。在一些情况下,算法能够在较短的时间内找到最优解。
与回溯法求解0-1背包问题作对比:
n从100-20000,每次递增100的测试结果:
n从100-20000,每次递增10的测试结果:
自定义输入n的数值的测试结果:
七、实验心得
通过这次实验,我对于优先队列式分支限界法求解 0-1 背包问题的过程和原理有了更加清楚的认识。在自己生成案例并测试运行时间的过程中,熟悉了随机化算法和运行时间的计算。
实验可改进的地方:随机化过程的加入可能是的不同规模的测试数据与理论值有偏差,可以通过每个输入规模多次测试来减小误差。
八、完整代码
#include <bits/stdc++.h>
#include <windows.h>
using namespace std;
class Object
{
public:
int id;
int weight;
int price;
float d;
};
class MaxHeapQNode
{
public:
MaxHeapQNode *parent;
int lchild;
int upprofit;
int profit;
int weight;
int lev;
};
//当前价值 当前重量 最优值 总重量 总价值
int cp, cw, bestp;
bool compare(const Object &a, const Object &b)
{
return a.d >= b.d;
}
struct cmp
{
bool operator()(MaxHeapQNode *&a, MaxHeapQNode *&b) const
{
return a->upprofit < b->upprofit;
}
};
//计算up值
int Bound(int i,int n,int c,vector<Object> &obj)
{
int tmp_cleft = c - cw;
int tmp_cp = cp;
while(tmp_cleft >= obj[i].weight && i <= n)
{
tmp_cleft -= obj[i].weight;
tmp_cp += obj[i].price;
i++;
}
//装满背包
if(i <= n)
{
tmp_cp += tmp_cleft * obj[i].d;
}
return tmp_cp;
}
//加入活性队列
void AddAliveNode(priority_queue<MaxHeapQNode *, vector<MaxHeapQNode *>, cmp> &q, MaxHeapQNode *E, int up, int wt, int curp, int i, int ch)
{
MaxHeapQNode *p = new MaxHeapQNode;
p->parent = E;
p->lchild = ch;
p->weight = wt;
p->upprofit = up;
p->profit = curp;
p->lev = i + 1;
q.push(p);
}
void MaxKnapsack(int n,int c,vector<int> &bestx,vector<Object> &obj)
{
priority_queue<MaxHeapQNode *, vector<MaxHeapQNode *>, cmp > q; // 大顶堆
MaxHeapQNode *E = NULL;
cw = cp = bestp = 0;
int i = 1;
int up = Bound(1,n,c,obj); //Bound(i)函数计算的是i还未处理时候的上限值
while(i != n + 1)
{
int wt = cw + obj[i].weight;
if(wt <= c)
{
if(bestp < cp + obj[i].price)
bestp = cp + obj[i].price;
AddAliveNode(q, E, up, cw + obj[i].weight, cp + obj[i].price, i, 1);
}
up = Bound(i+1,n,c,obj);//注意这里 up != up - obj[i].price而且 up >= up - obj[i].price
if(up >= bestp) //注意这里必须是大于等于
{
AddAliveNode(q, E, up, cw, cp, i, 0);
}
E = q.top();
q.pop();
cw = E->weight;
cp = E->profit;
up = E->upprofit;
i = E->lev;
}
for(int j = n; j > 0; --j)
{
bestx[obj[E->lev - 1].id] = E->lchild;
E = E->parent;
}
}
void OutPut(int n,vector<int> best_x)
{
cout<<"bestp = "<<bestp<<endl;;
cout<<"选取的物品为 : ";
for(int i = 1; i <= n; ++i)
{
if(best_x[i] == 1)
cout << i << " ";
}
}
class Object1
{
public:
int p; //价值
int w; //重量
int id; //物品的id
};
//当前价值 当前重量 最优值 总重量 总价值
int cp1, cw1, bestp1, total_w1, total_p1;
bool cmp1(const Object1 &a, const Object1 &b)
{
return a.p / a.w > b.p / b.w;
}
//计算up值
int Bound1(int i,int n,int c,vector<Object1> &O)
{
int temp_cw = c - cw1;
int temp_cp = cp1;
while(i <= n && temp_cw >= O[i].w)
{
temp_cw -= O[i].w;
temp_cp += O[i].p;
++i;
}
//装满背包
if(i <= n)
temp_cp += O[i].p / O[i].w * temp_cw;
return temp_cp;
}
void Backtrack(int i,int n,int c,vector<int> &x,vector<int> &best_x,vector<Object1> &O)
{
if(i > n)
{
bestp1 = cp1;
for(int i = 1; i <= n; ++i)
best_x[i] = x[i];
return;
}
if(cw1 + O[i].w <= c)
{
x[O[i].id] = 1;
cw1 += O[i].w;
cp1 += O[i].p;
Backtrack(i + 1,n,c,x,best_x,O);
cp1 -= O[i].p;
cw1 -= O[i].w;
x[O[i].id] = 0;
}
//向右求解的约束条件
if(Bound1(i + 1,n,c,O) > bestp1)
{
x[O[i].id] = 0;
Backtrack(i + 1,n,c,x,best_x,O);
}
}
void OutPut1(int n,vector<int> best_x)
{
cout<<"bestp = "<<bestp1<<endl;;
cout<<"选取的物品为 : ";
for(int i = 1; i <= n; ++i)
{
if(best_x[i] == 1)
cout << i << " ";
}
}
int main(int argc, char** argv) {
ofstream out1("output1.txt");
ofstream out2("output2.txt");
int n=20000,c;
while(n-=100){
// //生成规模为n的随机数
// cout<<"请输入数据规模n:"<<endl;
// int n,c;
// cin>>n;
ofstream out("input1.txt");
out<<n<<'\n';
srand((unsigned)time(NULL));
// c (a,b]
int a=0,b=100;
out<< (rand() % (b-a))+ a + 1<<'\n';
// v数组
for(int i=0;i<n;i++){
out<<rand()<<' ';
if((i+1)%10==0) out<<'\n';
}
out<<'\n';
// w数组 (a,b]
for(int i=0;i<n;i++){
out<< (rand() % (b-a))+ a + 1<<' ';
if((i+1)%10==0) out<<'\n';
}
out.close();
int i,maxi,mini;
LARGE_INTEGER nFreq,nBegin,nEnd;
double time;
ifstream in("input1.txt");
//物品数量 背包容量
in>>n>>c;
vector <Object> o(n+1);
for(i=1;i<=n;i++){
in>>o[i].price;
o[i].id=i;
}
for(i=1;i<=n;i++){
in>>o[i].weight;
o[i].d=1.0 * o[i].price / o[i].weight;
}
vector<int> best_x(n+1); //记录暂时选取状态 记录最优选取状态
QueryPerformanceFrequency(&nFreq);
QueryPerformanceCounter(&nBegin);
//依照物品单位重量价值排序
sort(o.begin()+1, o.end(), compare);
MaxKnapsack(n,c,best_x,o);
OutPut(n,best_x);
QueryPerformanceCounter(&nEnd);
time=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart;
cout<<"查询时间-分支限界:"<<time<<endl<<endl;
out1<<n<<' '<<time<<endl;
in.close();
ifstream in1("input1.txt");
//物品数量 背包容量
in1>>n>>c;
vector <Object1> o1(n+1);
for(i=1;i<=n;i++){
in1>>o1[i].p;
o1[i].id=i;
}
for(i=1;i<=n;i++){
in1>>o1[i].w;
}
cp1 = cw1 = total_w1 = total_p1 = bestp1 = 0;
for(int i = 1; i <= n; ++i)
{
total_p1 += o1[i].p;
total_w1 += o1[i].w;
}
vector<int> x1(n+1);
vector<int> best_x1(n+1); //记录暂时选取状态 记录最优选取状态
QueryPerformanceFrequency(&nFreq);
QueryPerformanceCounter(&nBegin);
//依照物品单位重量价值排序
sort(o1.begin()+1, o1.end(), cmp1);
Backtrack(1,n,c,x1,best_x1,o1);
OutPut1(n,best_x1);
QueryPerformanceCounter(&nEnd);
time=(double)(nEnd.QuadPart-nBegin.QuadPart)/(double)nFreq.QuadPart;
cout<<"查询时间:"<<time<<endl<<endl;
out2<<n<<' '<<time<<endl;
in.close();
}
out1.close();
return 0;
}
九、绘图代码
import matplotlib.pyplot as plt
def read_file(filename):
data = []
with open(filename, 'r') as file:
for line in file:
x, y = map(float, line.split())
data.append((x, y))
return data
# 读取三个文件的数据
file1_data = read_file('F:\\3-CourseMaterials\\3-1\\3-算法设计与分析\实验\lab4\\1-code\\1-优先队列式分支限界法求解0-1背包问题\\output1.txt')
file2_data = read_file('F:\\3-CourseMaterials\\3-1\\3-算法设计与分析\实验\lab4\\1-code\\1-优先队列式分支限界法求解0-1背包问题\\output2.txt')
# 分别提取 x 和 y 的值
file1_x, file1_y = zip(*file1_data)
file2_x, file2_y = zip(*file2_data)
print(file1_x)
print(file1_y)
print(file2_x)
print(file2_y)
# 绘制图形,指定不同颜色
plt.plot(file1_x, file1_y, label='Solution 1', color='red')
plt.plot(file2_x, file2_y, label='Solution 2', color='green')
# 添加图例、标签等
plt.legend()
plt.title('Data from Three Files')
plt.xlabel('X-axis')
plt.ylabel('Y-axis')
# 显示图形
plt.show()