有n个物品,每个物品体积是costs = {c1,c2,...cn},每个物品的价值是values = {v1,v2,...vn},每个物品只能取一次(所谓01背包,即每个物品只有两种状态:放或不放)。现在有体积为v的背包,问将这些物品放入该背包,能得到的最大价值是多少?并输出最大时的选择方案。
推导:设dp[i][j]为将前i个物品放入j的背包能取得的最大值,我们来看dp[i][j]可由哪些值得到?
1、若第i个物品不放,则dp[i][j] = dp[i - 1][j];
2、若第i个物品放入背包,则dp[i][j] = dp[i - 1][j - costs[i]] + values[i](下标从1开始)。
那么,01背包的状态转移方程就为dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - costs[i]] + values[i]) (i,j都不为0,否则dp[i][j] = 0,因为i = 0时表示什么物品都不放;而j = 0时表示背包的容量为0)
由此方程可写出代码:
for(int i = 1;i <= n;i++)
for(int j = 0;j <= v;j++)
if(j < costs[i])
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - costs[i]] + values[i]);
由此可看出01背包的时间复杂度为O(nv),空间复杂度为O(n2)。我们来看它的空间如何优化。由上面的代码可以看出,dp[i][j]由dp[i - 1][k](0 <= k <= j)得来。那么可以用滚动数组,优化成如下形式:
for(int i = 1;i <= n;i++)
for(int j = v;j >= costs[i];j--)
dp[j] = max(dp[j],dp[j - costs[i]] + values[i]);
这里第二层循环为什么是逆序从v到costs[i]?因为dp[i][j]是由dp[i - 1][k](0 <= k <= j)得来的,变成滚动数组后,那么dp[j]就应该是由dp[k](0 <= k <= j)而来。那么,如果从0到v循环的话,dp[j]前面的值已经改变,那么就出错了。而逆序就不会出现这种问题。好了,那么如何输出最优方案呢?我们可以用path[i][j]表示从(i - 1,path[i][j])到(i,j)的转移,那么path[i][j]的值根据转移方程也就有两种情况:path[i][j] = j或path[i][j] = k(0 <= k < j)。这两种情况有什么区别?由状态方程可知,dp[i][j] = dp[i - 1][j]时,就表示第i个物品没有放入背包,那么当path[i][j] = j时自然就表示第i个物品没有放入背包。而后一种就表示第i个物品放入背包了。那么只要将放入背包的物品值输出就行了。
#define N 1000
struct ZeroOnePack
{
/*
*numOfres:物品的个数
*volumeOfpack:背包容量
*path:记录路径
*costs[i]:第i个物品的消耗
*values[i]:第i个物品的价值
*求放进容量为volumeOfpack的背包能取得的最大价值
*/
int dp[N];
int path[N][N];
int costs[N],values[N],numOfres,volumeOfpack;
ZeroOnePack(int numOfres,int volumeOfpack,int costs[],int values[])
{
this->numOfres = numOfres;
this->volumeOfpack = volumeOfpack;
for(int i = 1;i <= this->numOfres;i++)
this->costs[i] = costs[i],this->values[i] = values[i];
//若是求刚好能填满背包时的最大价值,那么此处的初始化要改下,将dp[0] = 0,而dp[1...volumeOfpack] = 负无穷
//为什么?因为要求刚好能填满的话,那么初始时只有dp[0]是符合要求的,其它都是非法值
memset(dp,0,sizeof(dp));
memset(path,0,sizeof(path));
}
int getMaxValue()
{
for(int i = 1;i <= numOfres;i++)
{
int j;
for(j = volumeOfpack;j >= costs[i];j--)
{
int tmp = dp[j - costs[i]] + values[i];
if(dp[j] < tmp)
{
dp[j] = tmp;
path[i][j] = j - costs[i];
}
else
path[i][j] = j;
}
for(;j >= 0;j--)
path[i][j] = j;
}
return dp[volumeOfpack];
}
/*
* 输出路径,path[i][j] = res,表示从(i - 1,res)到(i,j)有一条路径,但这条路径包含第i个物品与否,要看res是否等于j
* 初始参数为(numOfres,volumeOfpack)
*/
void getPath(int i,int j)
{
if(!i || !j)
return;
if(path[i][j] == j)
{
getPath(i - 1,j);
return;
}
getPath(i - 1,+path[i][j]);
printf("values[%d]:%d ",i,values[i]);
}
};
题目:
1、hdu 2602 模板题
2、hdu 1203 模板题,只不过需要将条件变化一下,求至少得到一份offer的最大概率即求得不到offer的最小概率,初始化为dp[0] = 1.0
3、hdu 2191 模板题
4、poj 3624 模板题
5、hdu 2966 01背包的变形。题目求被抓概率小于p时能偷到的最多钱。模板形的01背包,每个物品的代价cost[j]为整数,所以能用在数组下标的计算中,而这题的代价是概率,是小数,那么自然不能再这样用了。我们可以将问题转化下,求偷到钱m时不被抓的最大概率,那么此时的代价即为每个的钱,而价值即为每个物品不被抓的概率。最后只要从dp[1...sum(m1,m2,...mn)]中选一个概率最大且偷到的钱最多的就可以了。
6、poj 3628 将01背包计算排列组合。题目中给出牛的高度集合length = {l1,l2,...ln}及书架高度H,求牛组成成的高度大于H的最小值。这题自然不是模板01背包,与hdu2966一样,我们将问题转化为dp[i][j] = {0,1}表示前i只牛能否组成j的高度,就是枚举它能组成的高度。由此可知,状态转移方程同01背包是一样的。只要dp[i - 1][j]或者dp[i - 1][j - length[i]]能组成,那么dp[i][j]也就能组成这样的高度。那么最后只要找一个dp[n][k]为1且k >= H的就行了。
7、poj 1976 该题果断说明理解经典的dp是多么地重要。题目的要求是每次选取的时候,需要连续的d个物品,背包容量即为车头的数量3。那么,此时的状态转移方程 应该是dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - dt] + num[i] + num[i + 1] + ... + num[i + d - 1),其实 dt >= d。按这个方程写出的代码时间复杂度为(n3),那么,我们可以用滚动数组纪录dp[i - 1][j - dt]的最大值,每次去更新,就可将时间复杂度降为O(n2)。
8、poj 1948 利用二维的01背包枚举两条边的长度。设dp[k][i][j]为前k个长度能否组成两条长度分别为i和j的边。那么转移方程就为dp[k][i][j] = dp[k - 1][i][j] || dp[k - 1][i - length[k]][j] || dp[k - 1][i][j - length[k]]。最后只要找一个符合的就行了。
由上几题可以看出,理解经典dp方程,并在此基础上进行扩展是很重要的。上面其实好多题都是利用01背包去枚举数据,然后最终得到最优值。所以在这些题目中01背包只是中间过程,而且在不同的问题中dp的初始条件是不同的。