您的位置:首页 > 其它

初探01背包问题

2016-05-23 22:36 218 查看
01背包问题是DP总最基本的问题,作为刚刚入门的小菜,01背包问题让我对DP有了一定的认识,同时提高了问题的分析能力。

代码还仅仅停留在直接思维的过程,是最初级的处理01背包问题的代码,比较便于理解。代码如下:

#include <iostream>
using namespace std;

int Max(int A,int B)  //求出最大值
{
return A>B?A:B;
}

int main()
{
int n;
int bag,matter;
int value[100]={};
int cost[100]={};
int f[100][100]={};
int  max=-1;
cin>>n;  //输入数据的组数
while(n--)
{
max=-1; //记录最大值
cin>>bag>>matter;  //输入背包容量和物品数量
for(int k=1;k<=matter;k++)   //输入物品价值和重量
{
cin>>value[k]>>cost[k];
}
for(int i=1;i<=matter;i++)   //从前1个开始遍历前matter个物品
{
for(int j=0;j<=bag;j++)   //从容积为0开始遍历背包的容积到bag
{
f[i][j]=f[i-1][j];    //默认将当前的最大价值置为不放第i个物品时的值
if(j>=cost[i])			//如果背包有空间可以放当前第i个物品
{
//比较不放第i个的时候(不放i)和 以减小i-1容积后再加上第i个物品价值的总价值(放i)的大小
//确保当前背包可用的空间是一定的
f[i][j]=Max(f[i-1][j],f[i-1][j-cost[i]]+value[i]);
max=f[i][j]>max?f[i][j]:max;//记录最大值
}
}
}
}
cout<<max<<endl;
cin>>n;
}


看过背包问题的基本样式后,我们可以试着简化代码,以下部分解释十分详尽,转自http://blog.csdn.net/insistgogo/article/details/8579597

-----------------------------------------------------------------------------------------------------------------------------------

优化空间复杂度

上述的方法,我们使用二维数组 f[i][v] 保存中间状态,这里我们可以使用一维数组f[v]保存中间状态就能得到结果

分析

我们现在使用f[v]保存中间状态,我们想要达到的效果是,第i次循环后,f[v]中存储的是前i个物体放到容量v时的最大价值

在回顾下之前讲过的状态转移方程:

[cpp] view
plain copy

f[i][v] = max(f[i - 1][v],f[i - 1][v - weight[i]] + cost[i])  

我们可以看到,要想得到 f[i][v],我们需要知道 f[i - 1][v] 和 f[i - 1][v - weight[i]],由于我们使用二维数组保存中间状态,所以可以直接取出这两个状态。

当我们使用一维数组存储状态时,f[v]表示,在执行i次循环后(此时已经处理i个物品),前i个物体放到容量v时的最大价值,即之前的f[i][v]。与二维相比较,它把第一维隐去了,但是二者表达的含义还是相同的,只不过针对不同的i,f[v]一直在重复使用,所以,也会出现第i次循环可能会覆盖第i - 1次循环的结果。

为了求f[v],我们需要知道,前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v]  和 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]。

难点:由于我们只使用一维数组存储,则在求这两个子问题时就没有直接取出那么方便了,因为,第i次循环可能会覆盖第i - 1次循环的结果。

现在我们来求这两个值
1)前i - 1个物品放到容量v的背包中带来的收益,即之前的f[i - 1][v] :
由于,在执行在i次循环时,f[v]存储的是前i个物体放到容量v时的最大价值,在求前i个物体放到容量v时的最大价值(即之前的f[i][v])时,我们是正在执行第 i 次循环,f[ v ]的值还是在第 i - 1  次循环时存下的值,在此时取出的 f[ v
]就是前i - 1个物体放到容量v时的最大价值,即f[i - 1][v]。
2)前i - 1件物品放到容量为v - weight[i]的背包中带来的收益,即之前的f[i - 1][v - weight[i]] + cost[i]
由于,在执行第i次循环前,f[0 ~ V]中保存的是第i - 1次循环的结果,即是前i - 1个物体分别放到容量0 ~ V时的最大价值,即f[i - 1][0 ~ V]。
则,在执行第i次循环前,f 数组中v - weight[i]的位置存储就是我们要找的 前i - 1件物品放到容量为v - weight[i]的背包中带来的收益 (即之前的f[i - 1][v - weight[i]]),这里假设物品是从数组下标1开始存储的。

伪代码

[cpp] view
plain copy

for i=1..N //枚举物品  

    for v=V..0 //枚举容量,从大到小  

        f[v]=max{f[v],f[v-weight[i]] + cost[i]};  

由上面伪代码可知,在执行第 i 次循环时,需要把背包容量由V..0都要遍历一遍,检测第 i 件物品是否能放。

逆序枚举容量的原因:

注意一点,我们是由第 i - 1 次循环的两个状态推出 第 i 个状态的,而且 v  > v - weight[i],则对于第i次循环,背包容量只有当V..0循环时,才会先处理背包容量为v的状况,后处理背包容量为 v-weight[i] 的情况。

具体来说,由于,在执行v时,还没执行到v - weight[i]的,因此,f[v - weight[i]]保存的还是第i - 1次循环的结果。即在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,此时f[v-weight[i]]存储的是f[i - 1][v-weight[i]]。

相反,如果在执行第 i 次循环时,背包容量按照0..V的顺序遍历一遍,来检测第 i 件物品是否能放。此时在执行第i次循环 且 背包容量为v时,此时的f[v]存储的是 f[i - 1][v] ,但是,此时f[v-weight[i]]存储的是f[i][v-weight[i]]。

因为,v  > v - weight[i],第i次循环中,执行背包容量为v时,容量为v - weight[i]的背包已经计算过,即f[v - weight[i]]中存储的是f[i][v - weight[i]]。即,对于01背包,按照增序枚举背包容量是不对的。

代码

[cpp] view
plain copy

#include <iostream>  

using namespace std;  

  

const int N = 3;//物品个数  

const int V = 5;//背包最大容量  

int weight[N + 1] = {0,3,2,2};//物品重量  

int value[N + 1] = {0,5,10,20};//物品价值  

  

int f[V + 1] = {0};  

  

int Max(int x,int y)  

{  

    return x > y ? x : y;  

}  

  

/* 

目标:在不超过背包容量的情况下,最多能获得多少价值 

 

子问题状态:f[j]:表示前i件物品放入容量为j的背包得到的最大价值 

 

状态转移方程:f[j] = max{f[j],f[j - weight[i]] + value[i]} 

 

初始化:f数组全设置为0 

*/  

int Knapsack()  

{  

    //初始化  

    memset(f,0,sizeof(f));  

    //递推  

    for (int i = 1;i <= N;i++) //枚举物品  

    {  

        for (int j = V;j >= weight[i];j--) //枚举背包容量,防越界,j下限为 weight[i]  

        {  

            f[j] = Max(f[j],f[j - weight[i]] + value[i]);  

        }  

    }  

    return f[V];  

}  

  

int main()  

{  

    cout<<Knapsack()<<endl;  

    system("pause");  

    return 1;  

}  

但是,增序枚举背包容量会达到什么效果:它会重复的装入某个物品,而且尽可能多的,使价值最大,当然不会不超过背包容量

-----------------------------------------------------------------------------------------------------------------------------------

通过上述文章讲解,可以明白如何用一维数组实现二维数组的功能,其原因就在于状态转移式f[i][v]主要依赖于f[i-1],

同时还明白了一位数组实现背包问题,对背包要逆序遍历的原因,因为一旦被遍历访问,f[i-1][v]就会变成f[i][v]。

参考博客:
http://www.cnblogs.com/xy-kidult/archive/2013/03/25/2970313.html http://blog.csdn.net/insistgogo/article/details/8579597
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ACM 杭电