上一节,我们讨论了01背包问题,说明了*递归与分治法 与 动态规划DP的区别和联系,介绍了缓存的概念*。以下,我们用DC、DP、cache分别表示分治法、动态规划和缓存。本节,我们讨论01背包的另外两种形似——
完全背包和多重背包问题,分析DP问题的另外一些情况。
例一:完全背包问题
同样有n种价值和重量分别为weight[i] and value[i], 背包大小W。限制条件:每种物品数目是无限的。问:能挑选出来的总价值最大的物品组合总重量?
分析一:这里,我们无视数量有限这个条件,可以得到下列递归式:
dp[i+1][j] = max{dp[i][j-k*weight[i]] + k*value[i] | k>=0且k*weight[i]<=j}
下面,来实现这个动态规划(留给读者)。很不幸,我们发现这次的时间复杂度变成了三重循环,O(nW^2). 因为,此时将子问题组成原问题,需要的组合数目是W个而不是常数项。
分析二:
对上述递归式进行变形,可以变成dp[i+1][j] = max{dp[i][j], dp[i+1][j-w[i]] + v[i]};这样就可以将时间复杂度从O(nW^2)转化成O(nW).这个变形可以完全用数学化公式来实现,对应的物理意义:从前i+1号物品中选取j重的组合包含两种:1)不包含i+1号物品dp[i][j] 2) 包含至少一个i+1号物品dp[i+1][j-w[i]] + v[i].
优化2:DP数组的重复利用
另外,我们注意到,在在递推关系dp[i+1][j] = max{dp[i][j], dp[i+1][j-w[i]] + v[i]}中,dp[i+1][j]中,计算d[i+1][j]的时候仅仅用到了上一行左侧的数据,所有我们可以对数组进行重复利用,这样能够减少空间上的时间复杂度和程序执行时间(程序具有更好的局部性):
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
例二:01背包问题之二
和第一节中同样的背包问题,我们增加如下的限制条件:
1< n< 100
1< wi < 10^7
1< vi < 100
1< W < 10^9
此时,显然O(nW)的算法时间复杂度就不是能够接受的了;此时我们需要对子问题的定义作出变形—— dp[i][j]原来表示前i号物品选取重量不超过j的组合能产生的最大值, 现在dp[i][j]:前i号物选取价值为j的组合占用的最小重量,最后取出dp[n][j]<=W 的最大的j即可。
此时:初始化dp[0][0]=0;dp[0][j]=INF
dp[i+1][j] = min(dp[i][j], dp[i][j - v[i]] + w[i]);
例子三:多重部分和问题
有n种大小不同的数字ai,每种mi个,是否可以从中选取组合,使得它们的和是K
限制条件:
n<100;ai, mi<10^5; K<10^5
分析一:套用完全背包问题
dp[i][j]:前i号数字是否能组成和为j的组合;这样得到递推关系dp[i+1][j] ||= dp[i][j-k*a[i]];
时间复杂度O(nKK);这个时间复杂度并不好;一般来说DP的子问题不应该用来存储bool的结果,这意味着我们损失了更多的信息。
我们仔细来分析一下时间复杂度主要耗费在什么地方,先看没有优化之前的代码
for (i = 0; i < n; ++i){
for (j = 0; j <=K; ++j){
for (k = 0; k < m[i] && k*a[i] <=j ; ++k){
dp[i+1][j] |= dp[i][j-k*a[i]];
}
}
}
分析2:“ 优化成最优子结构”
显然,for k 部分的循环,直观感觉是有冗余。分类一下,如果能从前i中数字选择出K和,a[i]可能被选取1次以上或者没有被选取
如果a[i]没有被选取,那么意味着dp[i][j]=true;
如果a[i]被选取了1次以上,意味着dp[i+1][j-a[i]]=true;
也就是说,dp[i+1][j]本来是和dp[i][j-k*a[i]]这么多子问题相关的;但是现在被转化成两个子问题dp[i][j] 和dp[i+1][j-a[i]],是不是很神奇?很显然,在这里,我们通过分治法对子问题进行了合并。
进一步,如果定义dp[i+1][j]:前i种数加和得到j的时候,a[i]剩余的个数,则有
dp[i+1][j]={
m[i]; if(dp[i][j]>=0)
dp[i+1][j-a[i]] -1;(j-a[i]>=0 && dp[i+1][j-a[i]]>=0)
-1;(otherwise)
}
总结:在计数原理中,这种 子问题分类合并的思想和方法非常常用!