背包问题
问题描述:
有n个重量和价值分别为wi,vi的背包。从这些背包中挑选出总重量不超过W的背包,求所有挑选方案中价值总和的最大值
一
不妨先从最原始的方法来考虑,从第一个背包开始,选这个背包是一种情况。不选这个背包是一种情况,然后又从这两种情况继续往下分,选第二个背包或者不选,依次往下,直到最后一个背包。这样每一种情况最后都会得到一个值,找到这些值中间的最大的那个,就是题目中所求的。
采用递归的方法实现上述算法
#include<bits/stdc++.h>
using namespace std;
vector<Bag> bag;
int n;
struct Bag
{
//weight表示这个背包的重量,value表示这个背包的价值
int weight,value;
};
//这里递归函数的意思是从序号为i(第一个背包序号为0)的背包到最后一个背包
//在这些背包里选择出小于等于重量wei的最大价值
int recur(int i,int wei)
{
//递归函数返回条件,i==n表示已经选完了所有的背包
if(i==n)return 0;
int val=0;
//当前的这个背包的重量如果大于了能选择的最大重量,就不能选择这个背包
//让val=recur(i+1)(wei)的意思是既然序号为i的背包不能选
//那就求从下个背包直到最后一个背包能选出的最大价值
if(bag[i].weight>wei)val=recur(i+1)(wei);
else
{
//如果能选这个背包,再分为两种情况,选或者不选
//如果选了当前的这个背包,能选的最大重量就要变小,也就是wei-bag[i].weight
//求出选和不选两种情况中的最大值
val=max(bag(i+1)(wei),bag(i+1)(wei-bag[i].weight)+bag[i].value);
}
return val;
}
int main() {
ios::sync_with_stdio(false);
//n表示背包的数量
cin>>n;
//读入每一个背包的重量和价值
for(int i=0;i<n;i++)
{
Bag b;
cin>>b.weight>>b.value;
bag.push_back(b);
}
//读入所能选的最大重量
int W;
cin>>W;
cout<<recur(0,W);
return 0;
}
二
递归的复杂度太高,当数据量一大起来,所需要的时间会大大增加,如果把递归的进程跟踪一下就会发现,有些相同参数的情况被调用了两次,白白浪费了时间,考虑以记忆化搜索的形式进行优化。这就跟用递归求斐波那契数列时的情况差不多。专门拿一个数组把第一次计算时的结果保存下来,然后第二次遇到相同参数的情况时就可以直接返回。
#include<bits/stdc++.h>
using namespace std;
//这就是用到的记忆化数组
//开多大需要根据具体情况而定,这里只是作示范之用
int dpArray[5][6];
int n;
vector<Bag> bag;
struct Bag
{
int weight,value;
};
int recur(int i,int wei)
{
//如果遇到了相同参数的情况,就直接返回已经计算好的结果
if(dpArray[i][wei]!=-1)return dpArray[i][wei];
if(i==n)return 0;
int val=0;
if(bag[i].weight>wei)val=recur(i+1)(wei);
else
{
val=max(recur(i+1)(wei),recur(i+1)(wei-bag[i].weight)+bag[i].value);
}
//将结果记录在数组中
return dpArray[i][wei]=val;
}
int main() {
ios::sync_with_stdio(false);
//将数组每个值初始化为-1,表示还没有计算过
memset(dpArray,-1,sizeof(dpArray));
cin>>n;
for(int i=0;i<n;i++)
{
Bag b;
cin>>b.weight>>b.value;
bag.push_back(b);
}
int W;
cin>>W;
cout<<recur(0,W);
return 0;
}
三
从上面的递归函数中可以发现,我们所使用的记忆化数组中的值dpArray[i][j]就是递归函数的返回值,可以等价的认为dpArray[i][j]=recur(i,j),我们所要求取的最终结果就是dpArray[0][W];
而dpArray[i][j]的值是通过两条途径得到的
一就是当前背包选不了的时候
dpArray[i][j]=dpArray[i+1][j];
二是可以选当前背包的时候
dpArray[i][j]=max(dpArray[i+1][j],dpArray[i+1][j-bag[i].weight]*bag[i].value);
当我们总结出来上述规律后,其实就可以发现,我们不需要递归函数,而只需要一个二层循环,就可以计算出dpArray[0][W]的值
#include<bits/stdc++.h>
using namespace std;
int dpArray[5][6];
struct Bag
{
int weight,value;
};
int main() {
ios::sync_with_stdio(false);
//注意,这里初始化改为了0
//因为要让dpArray[n][0到W]为0,作为初始值
memset(dpArray,0,sizeof(dpArray));
int n;
cin>>n;
vector<Bag> bag;
for(int i=0;i<n;i++)
{
Bag b;
cin>>b.weight>>b.value;
bag.push_back(b);
}
int W;
cin>>W;
for(int i=n-1;i>=0;i--)
{
for(int j=0;j<=W;j++)
{
if(bag[i].weight>j)dpArray[i][j]=dpArray[i+1][j];
else dpArray[i][j]=max(dpArray[i+1][j],dpArray[i+1][j-bag[i].weight]*bag[i].value);
}
}
cout<<dpArray[0][W];
return 0;
}
通俗地说,用这种求出递推式然后用数组一步一步得出问题解的方法就是常说的动态规划法
四
现在我们求出了问题的解,那到底是选择了哪几个背包得出的这个解呢?寻找最优解的组成也被称作最优解的回溯;
也就是求最优解的逆过程。
思路就是从dpArray[0][W]开始,往回遍历。
如果dpArray[i][j]=dpArray[i+1][j],就表示序号为i的这个背包没有被选择,它不是最优解的组成部分。然后需要让i+1
如果dpArray[i][j]!=dpArray[i+1][j],就说明序号为i的这个背包被选择了,它就是最优解的一部分。然后需要让i+1,j-bag[i].weight
//用来存储最优解是由哪些背包组成的
vector<Bag> ans;
int i=0,j=W;
while(j!=0)
{
if(dpArray[i][j]==dpArray[i+1][j])i++;
else
{
ans.push_back(bag[i]);
j-=bag[i].weight;
i++;
}
}