背包问题是学习算法和数据结构时肯定会接触到的,我老早就了解到这个问题,可直到今天看到《挑战》书上才详细了解这个问题.
该问题的题设和要求如上。
拿到这个问题,最先想到的思路就是利用递归针对每个物品是否放入背包进行两种情况下的搜索。详细的源码和解释如下:
#include<iostream>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define Max_N 100
int n;
int sumWeight;
int value[Max_N];
int weight[Max_N];
//从位置为index的物品进行背包问题,留给它们的重量为leftWeight,返回这种情况下能装的最大价值物品的总价值
int solve1(int index,int leftWeight)
{
int res=0;
if(index>=n) return 0; //物品已用完,剩下的重量装不了物品,返回价值0
if(weight[index]>leftWeight) //当前位置的物品重量大于剩余重量,则不选择该物品,从它下一个物品进行背包问题
res=solve1(index+1,leftWeight);
else
//还剩物品,并且当前位置的物品重量小于剩余重量,则就取该物品和不取该物品两种情况进行递归
res=max(solve1(index+1,leftWeight),solve1(index+1,leftWeight-weight[index])+value[index]);
return res;
}
int main()
{
cin>>n>>sumWeight;
for(int i=0;i<n;i++)
{
cin>>weight[i]>>value[i];
}
//初始化递归条件,从下标为0的物品开始背包问题,剩给它们的重量就是总重量 sumWeight
cout<<"背包问题solve1()最大总价值:"<<solve1(0,sumWeight);
return 0;
}
上述程序的问题就在于其效率比较低,针对每个物品都分为两种情况下的分支进行递归,这样其复杂度就为O(2^n),那么能不能优化呢?答案是可以的。
我们首先可以简单地把这种递归树用图的形式表达出来(下面每个单元中的数字就对应递归函数solve1()中的两个参数):
;
我们可以看到在参数为(3,2)时,其之后的递归情况是不变的,那么我们这时候在第一次遇到参数为(3,2)的递归时,将其返回的结果记录下来,下次再遇到参数为(3,2)的递归时,直接获得已经存储的结果就行了,这样就不用继续递归。这样看来,每种参数形式,都最多只会调用一次,所以对于n*sumWeight种组合的参数形式,上述程序的复杂度只有O(n*sumWeight)。详细代码如下:
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<memory.h>
using namespace std;
#define Max_N 100
#define Max_M 10000
int n;
int sumWeight;
int value[Max_N];
int weight[Max_N];
int dp[Max_N][Max_M];
//从位置为index的物品进行背包问题,留给它们的重量为leftWeight,返回这种情况下能装的最大价值物品的总价值
int solve2(int index,int leftWeight)
{
//记忆化搜索,如果之前已经对这种递归情况搜索过,那么直接返回值
if(dp[index][leftWeight]>=0)
return dp[index][leftWeight];
int res=0;
if(index>=n) return 0; //物品已用完,剩下的重量装不了物品,返回价值0
if(weight[index]>leftWeight) //当前位置的物品重量大于剩余重量,则不选择该物品,从它下一个物品进行背包问题
res=solve2(index+1,leftWeight);
else
res=max(solve2(index+1,leftWeight),solve2(index+1,leftWeight-weight[index])+value[index]);
//为了进行递归话搜索,将这种情况下的递归结果存储起来
return dp[index][leftWeight]=res;
//还剩物品,并且当前位置的物品重量小于剩余重量,则就取该物品和不取该物品两种情况进行递归
}
int main()
{
cin>>n>>sumWeight;
for(int i=0;i<n;i++)
{
cin>>weight[i]>>value[i];
}
//初始化记忆化搜索的数组,使用 memset将dp数组全部初始化为-1,具体memeset的详细使用,这里不阐述,就记住只能初始化为0或-1即可
//使用 memset需要导入 memory.h或者string.h头文件
memset(dp,-1,sizeof(dp));
//初始化递归条件,从下标为0的物品开始背包问题,剩给它们的重量就是总重量 sumWeight
cout<<"背包问题solve2()最大总价值:"<<solve2(0,sumWeight);
return 0;
}
这种形式下的程序复杂度对于题目中的数据规模已经完全足够了。上述这种带有存储的搜索递归形式叫做记忆化搜索。
当然,想这种递归式搜索遍历的的函数,其参数是不固定。上述两种代码只是拥有两个参数,现在我们把当前已经装好的物品的总价值也当做参数,可以得到如下的程序:
#include<iostream>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define Max_N 100
int n;
int sumWeight;
int value[Max_N];
int weight[Max_N];
//从位置为index的物品进行背包问题,留给它们的重量为leftWeight,之前0~index-1物品的背包问题的总价值为curValue
//返回这种情况下能装的最大价值物品的总价值
int solve3(int index,int leftWeight,int curValue)
{
int res=0;
if(index>=n) return curValue; //物品已用完,剩下的重量装不了物品,返回当前总价值
if(weight[index]>leftWeight) //当前位置的物品重量大于剩余重量,则不选择该物品,从它下一个物品进行背包问题
res=solve3(index+1,leftWeight,curValue);
else
res=max(solve3(index+1,leftWeight,curValue),solve3(index+1,leftWeight-weight[index],curValue+value[index]));
return res;
//还剩物品,并且当前位置的物品重量小于剩余重量,则就取该物品和不取该物品两种情况进行递归
}
int main()
{
cin>>n>>sumWeight;
for(int i=0;i<n;i++)
{
cin>>weight[i]>>value[i];
}
//初始化递归条件,从下标为0的物品开始背包问题,剩给它们的重量就是总重量 sumWeight,当前总价值肯定为0,因为还没有物品被选中
//这种把参数情况写的比较全比较有利于进行递归式搜索下的剪枝
cout<<"背包问题solve3()最大总价值:"<<solve3(0,sumWeight,0);
return 0;
}
这种参数比较全的递归在需要剪枝的情况下们可以根据当前获得信息,不再由这条分支继续递归下去,直接返回。这样也可以提高程序效率。
PS:关于重点的动态规划下的背包问题的解决方案在下篇博文中会介绍~