背包问题是这样的:
关于背包问题和动态规划:
背包问题的经典就在于它可以清晰的反映出递归的本质、动态规划的巧妙之处、还能让我们理解深度遍历算法。
动态规划的精髓就在于任何顶点只与它相邻的顶点有关,处理一个顶点时只需要从它的相邻顶点出发,其实递归也是如此,每次出入递归函数的参数也一定是从其有关联的参数得来的。显然,对于这种问题,我们只需要从边界出发,逐阶段找到最终阶段的解。
暴力解决(递归、深度优先算法)背包问题的代码:
#include <iostream>
#define maxn 1000
using namespace std;
int n,V,w[maxn],c[maxn];//题目中要求输入的数据
int maxc=0;
void DFS(int index,int sumw,int sumc)//物品号、当前总重量、当前总价值
{
if(index==n){//对于已经选完的
if(sumw<=V&&sumc>maxc){
maxc=sumc;
}
return;
}
DFS(index+1,sumw,sumc);//不选index
DFS(index+1,sumw+w[index],sumc+c[index]);//选index
}
int main()
{
cin>>n>>V;
for(int i=0;i<n;i++){
cin>>w[i];
}
for(int i=0;i<n;i++){
cin>>c[i];
}
DFS(0,0,0);
cout<<maxc;
return 0;
}
显然每次都要进入选和不选两条路中,对于n个顶点那就需要O(2n)的时间复杂度,但是用动态规划就只需要O(nV)的时间复杂度。
动态规划解决背包问题:
为什么能用动态规划来解决呢?其实我们可以在暴力解决的算法中看到其实在进行计算时都是要从当前index开始分支(选不选)一直到n,这样显然是要经过许多重复和无意义的计算,因为第index件物品选不选产生的最大值只与第index-1件物品的最大值有关。
换一个样例:
1)思路:
所以我们让dp[ i ] [ j ]表示在选前i件物品放入容量恰好为j的背包中获得的最大价值,这样我们再根据选和不选得到dp[ i ] [ j ]的状态转移方程:
不选第i件物品:dp[ i ] [ j ] = dp[ i - 1 ] [ j ];
选第i件产品:dp[ i ] [ j ] = dp[ i - 1 ] [ j - w[ i ] ] + c[ i ];
dp[ i - 1 ] [ j - w[ i ] ]就是前i-1件物品在容量为 j - w[ i ]的背包中的最大价值。
2)首先初始化:
dp[0][j]=dp[i][0]=0;(0<= i <=n,0<= j <=V)
也就是放零件物品的最大价值为0,背包容量为0的最大价值为0。
3)这样我们就可以写出对应的代码:
int i,j;
//填表
for(i=1;i<=n;i++){
for(j=1;j<=V;j++){
if(j<w[i])//装不下了不选i
dp[i][j]=dp[i-1][j];
else {//还能装,选i
if(dp[i-1][j]>dp[i-1][j-w[i]]+c[i])//不装i价值大
dp[i][j]=dp[i-1][j];
else//装i价值大
dp[i][j]=dp[i-1][j-w[i]]+c[i];
}
}
}
显然这样做只需要O(nV)的时间复杂度。
最终得到下图这样的结果,一定要自己填初始化的那个图体会过程
但是,我们从图中可以看出每轮j的循环dp都至少是从对应w[i]开始发生变化的,所以我们可以简化上面的代码:
int i,j;
//填表
for(i=1;i<=n;i++){
for(j=w[i];j<=V;j++){
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
}
}
要想的得到最大价值还需要遍历整个dp数组找到最大值即可。
4)如果我们想获得由哪些物品组成了最大价值怎么办?
根据dp的状态转移方程我们可以得到:
bool visited[n]={false};//标记i是否被选中
void FindPath(int i,int j)//寻找最大的组成方式
{
if(i>=0){
if(dp[i][j]==dp[i-1][j]){//前后相等说明没装i
visited[i]=false;//标记未被选中
FindPath(i-1,j);
}else if( j-w[i]>=0 && dp[i][j]==dp[i-1][j-w[i]]+c[i] ){
visited[i]=true;//标记已被选中
FindPath(i-1,j-w[i]);//回到装i之前的容量
}
}
}
然后再遍历所有物品输出visited为true的物品即可
5)优化空间复杂度:
观察所得到的结果图表可以看出,其实每一步计算dp[i][j]时它只与dp[i-1][j-w[i]]到dp[i-1][j]的数据有关,也就是上方的一行从头到正上方停止这个区域,也就是说其实dp的一维从1到n的意义就不大了,因为计算时的顺序也是1到n,所以我们用dp[j]来代替dp[i][j]:
状态转移方程,每一次推导dp(i)(j)是通过dp(i-1)(j-w(i))来推导的,所以一维数组中j的扫描顺序应该从大到小(V到0),否者前一次循环保存下来的值将会被修改,从而造成错误。
int dp[V]={0};
int i,j;
//填表
for(i=1;i<=n;i++){
for(j=V;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
}
}
同样,遍历dp找最大值即是最大价值。
然而不足的是,虽然优化了动态规划的空间,但是该方法不能找到最大价值的组成,因为动态规划寻找解组成一定得在确定了最大价值的前提下再往回找解的构成,而优化后的动态规划只用了一维数组,之前的数据已经被覆盖掉,没办法寻找,所以两种方法各有其优点。
6)如果每件物品可以选无限次呢?
只要每次选择后都更新i即可。
int i,j;
//填表
for(i=1;i<=n;i++){
for(j=w[i];j<=V;j++){
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+c[i]);
}
}