基于动态规划方法求解0-1背包等问题
一.题目
n个物品和1个背包。对物品i,其价值为vi,重量为wi,背包容量为W。如何选取物品装入背包,使背包中所装入的物品的总价值最大?其中,wi, W都是正整数。
二.分析
首先明确背包容量的变化对能装入物品的总数存在一定的限制关系,大多数情况下n个物品不一定都能装入背包(若都能装入,则都装入时必定为价值最大)。
其次需要明确,如果按照贪心算法,求解到的只是局部最优,
1.价值最大的先装入背包
2.重量最轻的先装入背包
3.价值与重量的比值最大的先装入背包
都不一定得到最优结果,某些情况是近似最优解,因为可能存在背包容量>物品总重量的情况。
而现在需要考虑的是全局最优,
第一种方法是暴力地将物品满足要求的的所有排列组合列出来,找到其价值最大的情况,这种方法较为复杂,本次不做讨论。
第二种方法是采用动态规划的方法,这种方法基于分治的思想,但又与分治不同
具体在于动态规划的3个基本要素:
1.最优子结构性质
一个问题的最优解可以由其子问题的最优解有效构造出来,保证了原问题最优解可以由子问题的最优解推导而来,为动态规划的基础。基于最优子结构导出递归公式或动态规划基本方程是解决一切动态规划问题的基本方法。
2.子问题重叠性质
求解过程中有些子问题出现多次而存在重叠,第一次遇到就加以解决并保存,若再次遇到就无需重复计算而直接查表得到,从而提高求解效率。注意该性质不是必要条件,也不是动态规划的核心,但无该形式该方法即没有优势(没有重叠子问题直接用分治解即可)。
3.自底向上的求解方法
鉴于子问题重叠的性质,采用自底向上的方法,先填停止条件,求解每一级子问题并保存,直到得到原问题的解。
背包问题可被描述为若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,是一种多阶段动态规划问题
一般可使用递推或递归的写法实现动态规划,其中递归方法可被称为记忆化搜索。
动态规划的两种实现方法:
- 备忘录的自顶向下法
(按自然递归编写过程,通常通过数组保存子问题的解)某些特殊情况下,并未真正递归地考察所有子问题。 - 自底向上法
(需要恰当定义子问题规模的概念,按由大到小的顺序求解)由于没有频繁的调用递归函数的开销,时间复杂度函数通常有更小的系数。
背包问题可被描述为若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,是一种多阶段动态规划问题
一般可使用递推或递归的写法实现动态规划,其中递归方法可被称为记忆化搜索。
动态规划的求解步骤:
1.确定是否有最优子结构性质,考察是否适合该方法
2.自底向上,以最小问题作为边界点向上求解
3.递归定义最优值,包括停止条件和递归式
4.充分利用子问题重叠性质
贪心与动态规划:
贪心和动态规划都要求原问题具有最优子结构。但贪心法并不等待子问题求解完毕后再选择使用哪一个,而是通过一种策略直接选一个子问题求解,抛弃没有选择的子问题,直接在上一步选择的基础上继续选择。动态规划会考虑所有子问题,寻找整体最优。而贪心法只是单链的选择局部最优。
分治法与动态规划:
动态规划基于分治思想和解决冗余(对相同子问题查表获得)因此动态规划中各个子问题可以存在重叠,而分治法中各个子问题独立。分治法解决的问题不一定是最优化问题,而动态规划解决的一定是最优化问题。
三.算法设计与伪代码
数组w[n]:存放n个物品的重量
数组v[n]:存放n个物品的价值
W:背包重量
数组C[n+1][W+1]:存放每次迭代执行结果,C[i][j]为C[n][W]子问题的最优解,表示放的物品在前i个内且背包容量为j时装入物品的最大价值。
数组x[n]:存放所装入背包的物品状态
该问题满足动态规划的最优解条件(具体内容在总结里),采用动态规划的思想进行求解,将问题分成多个子问题,每个物品可以选择是否加入到背包中,首先判断,当前物品是否重量已经大于背包所能容纳的重量;如果能容纳该物体,则进行判断加入该物品C[i−1][j-w[i]]+v[i]得到的价值更高,还是不加入该物品C[i−1][j]所能得到的物品的总价值更高。
设C[i][j]表示放的物品在前i个内且背包容量为j时装入物品的最大价值,问题C[n][w]为最终求解的问题。
若只考虑第i件物品求解策略(放或不放),那么就可以转化为一个和i-1件物品有关的问题。
放:前i-1件物品放入容量为j-w[i]的背包,此时获得最大价值为C[i-1][j-w[i]]+v[i]
不放:前i-1件物品放入容量为j的背包,此时获得最大价值等于C[i-1][j]
KNAPSACK_01(w,v,W)
for i = 1 to n
do C[i][0]=0//背包剩余容量为0时,装入物品最大价值为0
for j=1 to w
do C[0][j]=0//物品为0时,装入物品最大价值为0
for i = 1 to n
for j = 1 to w
if j < w [i]//背包剩余容量不够
then C[i][j]=C[i-1][j]//装不了第i个物品,不装入
else
then C[i][j]=max{C[i-1][j],C[i-1][j-w[i]]+v[i]}//在不装入该物品和装入该物品中间选择较大值
return C
TRACEBACK-01(w, W, C) //x[1..n]为构建的最优解
Let x[0..n]be a new array
n = w.length
j = W //remaining capacity剩余容量
for i = n to 1
do if C[i][j] == C[i-1][j] then//如果装前n个物品价值最大值=装前n-1个物品价值最大值
x[i] = 0 //没装第n个物品
else
x[i] = 1 //装了第n个物品
j -= w[i] //update capacity更新容量,减去放第i件物品容量
return x
易混:
C[i-1][j-w[i]]+v[i] //表明单纯是用容量换了新增加一个物品的代价,此时一定是相对于上一个状态的最优解
如果取第i件物品放入背包,则背包容量还剩j-w[i],所以要取前i-1件物品放入背包剩余容量j-w[i]所获得的最大价值为f[i-1][j-w[i]]
C[i][j-w[i]]=c[i-1][j]+v[i] //容易简单理解为这句,但不能这么写,C[i][j]的上一状态并不一定是C[i-1][j]
四.算法复杂性分析
注意空间复杂度不包括输入输出(函数参数,函数返回值等)
时间复杂度:
由双层for循环可知为O(wn),注意w与n为不同量纲
空间复杂度:
若不返回C为O(wn),否则为O(1)
五.代码实现
#include <iostream>
#include <math.h>
using namespace std;
int **KNAPSACK_01(int n,int W,int *w,int *v)
{
int **C=new int *[n+1];
for(int i=0; i<=n; i++)
{
C[i]=new int [W+1];
}
for(int i=0; i<=n; i++)
{
for(int j=0; j<=W; j++)
{
C[i][j]=0;
}
}
for(int i=1; i<=n; i++)
{
for(int j=1; j<=W; j++)
{
if(j<w[i])
{
C[i][j]=C[i-1][j];
}
else
{
C[i][j]=max(C[i-1][j],C[i-1][j-w[i]]+v[i]);
}
}
}
return C;
}
int *TRACEBACK_01(int n,int *w, int W, int **C)
{
int *x=new int [n+1];
int j=W;
for(int i=n; i>0; i--)
{
if(C[i][j]==C[i-1][j])
{
x[i]=0;
}
else
{
x[i]=1;
j-=w[i];
}
}
return x;
}
void output(int n,int W,int** C,int* x)
{
for(int i=0; i<=n; i++)
{
for(int j=0; j<=W; j++)
{
cout<<C[i][j]<<" ";
}
cout<<endl;
}
cout<<endl;
cout<<"the max value is "<<C[n][W];
cout<<endl;
for(int i=1; i<=n; i++)
{
if(x[i]==1)
{
cout<<"x["<<i<<"]="<<x[i]<<endl;
}
}
}
int main()
{
/*
test
5 10
2 2 6 5 4
6 3 5 4 6
*/
int *w,*v,n,W,**C,*x;
cin>>n>>W;
w=new int[n+1],v=new int[n+1];
C=new int *[n+1];
x=new int [n+1];
for(int i=0; i<=n; i++)
{
C[i]=new int [W+1];
}
for(int i=1; i<=n; i++)
{
cin>>w[i];
}
for(int i=1; i<=n; i++)
{
cin>>v[i];
}
C=KNAPSACK_01(n,W,w,v);
x=TRACEBACK_01(n,w, W, C);
output(n,W,C,x);
return 0;
}
运行结果