您的位置:首页 > 其它

算法设计与分析 0-1背包的多种求法

2015-03-25 11:01 337 查看

                                                求解整数背包问题

 

整数背包问题简介

     整数背包问题即0/1背包问题。对每种物品或者全取或者一点都不取,不允许只取一部分。现有n种物品,对1<=i<=n,已知第i种物品的重量为正整数Wi,价值为正整数Vi,背包能承受的最大载重量为正整数W,现要求找出这n种物品的一个子集,使得子集中物品的总重量不超过W且总价值尽量大。

穷举法:

    求解整数背包问题的穷举算法,用穷举法解决0-1背包问题,需要考虑给定n个物品集合的所有子集,找出所有可能的子集(总重量不超过背包重量的子集),计算每个子集的总重量,然后在他们中找到价值最大的子集。考虑给定n个物品集合的所有子集,找出所有可能的子集(总重量不超过背包重量的子集),计算每个子集的总重量,然后在他们中找到价值最大的子集。 

<span style="font-family:SimSun;font-size:18px;">#include <iostream>
#include <vector>

using namespace std;

class PackEnum
{

protected:
vector<int> m_p; //N个背包的价格
vector<int> m_w; //N个背包的重量
int m_c; //背包的容量
int m_num; //物品的件数

public:
//构造函数
PackEnum();
PackEnum(vector<int>& p,vector<int>& w, int c,int n)
:m_p(p),m_w(w),m_c(c),m_num(n)
{}

//获取背包内物品的最大值
int GetBestValue() const
{
int bestValue=0; //背包最大价值
int currentValue =0; //当前背包中物品的价值
int currentWeight =0; //当前背包中物品的重量

const unsigned int max = 2 << m_num; //2的N次方

//枚举所有可能的解
for(unsigned int i =0; i<max;++i)
{

//清空背包
currentValue =0;
currentWeight =0;

//取2进制数的每一位
unsigned int bit = i;
int j =0;
while(bit !=0)
{
currentWeight += m_w[j] * (bit & 1);
currentValue += m_p[j] * (bit & 1);
//如果bit的最低位为1,则bit&1 = 1 ,否则为0

bit >>=1;
++j;
}

//保存最优解
if(currentWeight <=m_c
&& bestValue < currentValue)
{
bestValue = currentValue;
}

}
return bestValue;
}

};

int main(void)
{
//测试程序
int n;
int c;

cout << "请输入物品的件数" << endl;
cin >>n;
cout << "请输入背包的容量" << endl;
cin >>c;
vector<int> w(n);
vector<int> p(n);

cout << "请输入物品的重量:" << endl;
for(int i=0;i<n;++i)
cin >> w[i];
cout << "请输入物品的价格:" << endl;
for(int j=0;j<n;++j)
cin >> p[j];

PackEnum pack(p,w,c,n);

int bestValue = pack.GetBestValue();

cout << "背包内的物品的最大价值为:" << bestValue << endl;

return 0;
}

</span>

动态规划法:

   0-1背包问题可以看作是寻找一个序列,对任一个变量 的判断是决定=1还是=0.在判断完之后,已经确定了,在判断时,会有两种情况:

  (1) 背包容量不足以装入物品i,则=0,背包的价值不增加;

  (2) 背包的容量可以装下物品i,则=1,背包的价值增加。

这两种情况下背包的总价值的最大者应该是对判断后的价值。

令表示在前i个物品中能够装入容量为j的背包的物品的总价值,则可以得到如下的动态规划函

数: 


       
<span style="font-family:SimSun;font-size:18px;">#include <stdio.h>
#include <iostream>
using namespace std;
int c[30][100];//表示把前i个物品装入容量为j的背包中获得的最大价值
int x[30]; //存放背包的选择情况
int knapsack(int w[],int v[],int m,int n)
{
int i,j;
for(i=0;i<n+1;i++)//初始化第0列
c[i][0]=0;
for(j=0;j<m+1;j++)//初始化第0行
c[0][j]=0;
for(i=1;i<n+1;i++)//计算第i行,进行第i次迭代
for(j=1;j<m+1;j++)
{
if(w[i]<=j)
{
//如果本物品的价值加上背包剩下空间能放下物品的价值大于上一次选择的最佳方案则更新c[i][j]的值
if(v[i]+c[i-1][j-w[i]]>c[i-1][j])
c[i][j]=v[i]+c[i-1][j-w[i]];
else
c[i][j]=c[i-1][j];
}
else
c[i][j]=c[i-1][j];
}
//求装入背包的物品
j=m;
for(i=n;i>0;i--)
{
if(c[i][j]>c[i-1][j])
{
x[i]=1;
j=j-w[i];
}
else
x[i]=0;
}
return(c
[m]);//返回背包取得的最大价值
}

int main()
{
int m;//存放背包容量
int n;//存放物品数量
int i,j;
int w[100];//存放重量
int v[100];//存放价值
cout<<"0-1背包问题——动态规划法"<<endl;
cout << "请输入物品的件数" << endl;
cin >>n;
cout << "请输入背包的容量" << endl;
cin >>m;
cout<<"请一次输入每个物品的重量(w)和价值(v):"<<endl;
for(i=1;i<n+1;i++)
cin>>w[i]>>v[i];
cout<<"背包的最优解为:"<<endl;
cout<<knapsack(w,v,m,n)<<endl;
/*
cout<<"最优解条件下的选择的背包为:"<<endl;
for(i=1;i<=n;i++)
cout<<x[i]<<"\t";
cout<<endl;
cout<<"在此过程中用到的C
[m]数组数据为:"<<endl;
for(i=0;i<n+1;i++)
for(j=0;j<m+1;j++)
{
cout<<c[i][j]<<"\t";
if(j==m) cout<<endl;
}
*/
return 0;
}

</span>

回溯法:

    求解整数背包问题的回溯算法,首先要将问题进行适当的转化,得出状态空间树。 这棵树的每条完整路径都代表了一种解的可能。通过深度优先搜 索这棵树,枚举每种可能的解的情况;从而得出结果。但是,回溯法中通过构造约束函数,可以大大 提升程序效率,因为在深度优先搜索的过程中,不断的将每个解(并不一定是完整的,事实上这也就是构造约束函数的意义所在)与约束函数进行对照从而删除一些 不可能的解,这样就不必继续把解的剩余部分列出从而节省部分时间。

<span style="font-family:SimSun;font-size:18px;"><span style="font-size:14px;">#include <iostream>
#include <vector>

using namespace std;

class PackBackTrack
{

protected:
vector<int> m_p; //N个背包的价格
vector<int> m_w; //N个背包的重量
int            m_
4000
c; //背包的容量
int            m_num; //物品的件数
int            bestValue;            //背包最大价值
int            currentValue;        //当前背包中物品的价值
int            currentWeight;        //当前背包中物品的重量
private:
//辅助函数,用于回溯搜索
void BackTrack(int depth)
{
if(depth >= m_num)    //达到最大深度
{
if(bestValue < currentValue)  //保存最优解
bestValue = currentValue;
return ;
}

if(currentWeight +m_w[depth] <= m_c)  //是否满足约束条件
{
currentWeight += m_w[depth];
currentValue += m_p[depth];

//选取了第i件物品
BackTrack(depth+1); //递归求解下一个结点

//恢复背包的容量和价值
currentWeight -= m_w[depth];
currentValue  -= m_p[depth];
}
//不取第i件物品
BackTrack(depth+1);
}
public:
//构造函数
PackBackTrack();
PackBackTrack(vector<int>& p,vector<int>& w, int c,int n)
:m_p(p),m_w(w),m_c(c),m_num(n)
{
bestValue =0;
currentValue =0;
currentWeight =0;
}
//获取背包内物品的最大值
int GetBestValue()
{
BackTrack(0);
return bestValue;
}
};

int main(void)
{
//测试程序
int n;
int c;
cout<<"0-1背包问题——回溯法"<<endl;
cout << "请输入物品的件数" << endl;
cin >>n;
cout << "请输入背包的容量" << endl;
cin >>c;
vector<int> w(n);
vector<int> p(n);
cout << "请输入物品的重量:" << endl;
for(int i=0;i<n;++i)
cin >> w[i];
cout << "请输入物品的价格:" << endl;
for(int j=0;j<n;++j)
cin >> p[j];
PackBackTrack pack(p,w,c,n);
int bestValue = pack.GetBestValue();
cout << "背包内的物品的最大价值为:" << bestValue << endl;
return 0;
}</span>
</span>



分支限界法:

     分枝界限可以说是dfs和bfs的结合,综合了DFS算法空间复杂度低和BFS时间复杂度低的优点。分枝界限法在回溯法的基础上更进了一步,在回溯法中, 一旦从问题状态空间树导出的解不满足约束函数,我们就将其分枝剪掉。在处理此类问题时,回溯法的思想可以进一步强化。在回溯法的基础上加上两个额外的条件 就变成了分支界限法

 1.  对于一棵状态空间树的每一个结点所代表的部分解, 我们要提供一种算法,计算出通过这个部分解所繁衍也的任何解在目标函数上的最佳边界。

2. 目前所求得的最佳解

    有了这两个条件,我们可以用当前求得的最佳解和所有结点的最佳边界比较,如果某结点的最佳边界不能超越当前最佳解(在求最大化问题中,该结点的最佳上界不大于当前最佳解,在求最小化问题中,该结点的最佳下界不小于当前最佳解),则将其剪掉。这就是分枝界限的主要相思。

   

<span style="font-family:SimSun;font-size:18px;"><span style="font-size:14px;">#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;

//背包问题,使用分枝界限求解,

//保存背包内物品选择情况的结点类
struct Node
{
int value;		 // 价格
int weight;             // 重量
int ub;			//价格上界
vector<bool> solution;	//物品选择,1表示选择,0表示未选择

};
bool myCmp1(const Node& left,const Node& right)
{
return left.ub > right.ub;
}

struct Thing
{
int weight;
int value;
};//物品
bool myCmp2(const Thing& left,const Thing& other)
{
return (left.value/left.weight) > (other.value/other.weight);
}

//背包类
class Pack
{

protected:
int			m_c; //背包的容量
int			m_num; //物品的件数
vector<Thing> m_thing;
private:
//计算最价值最佳上界
void CaculateUb(Node& node)
{
node.ub = node.value + (m_c-node.weight)*m_thing[node.solution.size()].value;
}
public:
//构造函数
Pack();
Pack(vector<Thing>& t, int c,int n)
:m_thing(t),m_c(c),m_num(n)
{
//按价值重量比排序
sort(m_thing.begin(),m_thing.end(),myCmp2);
}
//获取背包内物品的最大值
int GetBestValue()
{
vector<Node> state;//状态空间树

//初始化根结点
Node root;
root.value=root.weight =0;
root.solution.clear();
CaculateUb(root);
state.push_back(root);
//在此使用最大堆来实现,堆的顶总是保存ub最大的结点,
//因此如果堆顶的结点已经全部分配完成,就表示已经找到最优解
while(state.front().solution.size() < m_num)
{
//取具有最大上界的结点扩展
pop_heap(state.begin(),state.end(),myCmp1);
Node expand = state.back();
state.pop_back();
Node expLeft,expRight;
//选择下一件物品
if(expand.weight+m_thing[expand.solution.size()].weight <=m_c)
{
expLeft.weight = expand.weight+m_thing[expand.solution.size()].weight;
expLeft.value  = expand.value + m_thing[expand.solution.size()].value;
expLeft.solution = expand.solution;
expLeft.solution.push_back(true);
CaculateUb(expLeft);
state.push_back(expLeft);
}
//不选择下一件物品
expRight.weight = expand.weight;
expRight.value  = expand.value;
expRight.solution = expand.solution;
expRight.solution.push_back(false);
CaculateUb(expRight);
state.push_back(expRight);
//生成最大堆
make_heap(state.begin(),state.end(),myCmp1);
}
return state.front().ub;
}
};

int main(void)
{
//测试程序
int n;
int c;
cout<<"0-1背包问题——分支限界法"<<endl;
cout << "请输入物品的件数" << endl;
cin >>n;
cout << "请输入背包的容量" << endl;
cin >>c;
vector<Thing> w(n);
cout << "请输入物品的重量:" << endl;
for(int i=0;i<n;++i)
cin >> w[i].weight;
cout << "请输入物品的价格:" << endl;
for(int j=0;j<n;++j)
cin >> w[j].value;
Pack pack(w,c,n);
int bestValue = pack.GetBestValue();
cout << "背包内的物品的最大价值为:" << bestValue << endl;
return 0;
}</span>
</span>


  各种算法在解背包问题时的比较如下表所示:

算法名称

时间复杂度

优点

缺点

改进

穷举法

最优解

速度慢

剪枝

动态规划法

最优解

速度慢

递归方程求解

回溯法

最优解

速度慢

改进剪枝

分枝限界法

最优解

速度慢

优化限界函数

     背包问题是NP完全问题。半个多世纪以来,该问题一直是算法与复杂性研究的热门话题。通过对0-1背包问题的算法研究可以看出,回溯法和分枝限界法等可以得到问题的最优解,可是计算时间太慢;动态规划法也可以得到最优解,当时,算法需要的计算时间,这与回溯法存在一样的缺点——计算速度慢;采用贪心算法,虽然耗费上优于前者,但是不一定是最优解。在本次报告中参考了许多文献,自己对于动态规划、回溯、分支限界有了更深的了解。通过本次试验,了解了时空复杂度对算法的影响。对时间和空间在计算机中的权衡有了进一步的了解。同时对于算法的理解不断加深,在程序编写中可以将几个相关的算法混合使用,采用它们各自的优点。在上面的回溯法中结合BFS和DFS在时间和空间上的优点,改进而来。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息