文章目录
1 多阶段动态规划问题
多阶段动态规划问题:有一类动态规划可解的问题,它可以描述为若干有序序列的阶段,且每一个阶段都只与上一个阶段有关。
如上图所示状态F属于阶段3,它由状态2的状态C和状态D推得。显然对于这种问题,只需要从第一个问题开始,按照阶段的顺序解决每个阶段中状态的计算,就可以得到最后一个阶段中的状态的解。
0-1背包就是这样一个列子。
2 0-1背包问题
2.1 问题描述
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价最大。其中每件物品都只有1件。
样例:
5 8 //n==5,V==8
3 5 1 2 2 //w[i]
4 5 1 2 3 //c[i]
2.2 求解
2.2.1 暴力求解
显然用暴力枚举每一件物品放或者不放进背包,显然每件物品都有两种选择,因此n件物品就有2 n种情况,而O(2n)复杂度显然很糟糕,用动态规划可以将复杂度降为O(nV)
实现代码:
#include <cstdio>
const int MAXN = 30;
int w[MAXN],c[MAXN];
int n, V, maxValue;
void DFS(int index, int sumW, int sumC){//当前的物件编号、总重量、总价值
if(index == n) return;
DFS(index + 1, sumW, sumC);
//剪枝,只有加入重量后未超过V,才加入V
if(sumW + w[index] <= V){
if(sumC + c[index] > maxValue) maxValue = sumC + c[index];
DFS(index + 1, sumW + w[index], sumC + c[index]);
}
}
int main(int argc, char const *argv[])
{
scanf("%d%d", &n, &V);
for (int i = 0; i != n; ++i)
{
scanf("%d", &w[i]);
}
for (int i = 0; i != n; ++i)
{
scanf("%d", &c[i]);
}
DFS(0, 0, 0);//初始为零物件,当前总质量和总价值为0
printf("%d\n", maxValue);
return 0;
}
2.2.2 动态规划
2.2.2.1 时间复杂度优化
令dp[i][v]表示前i件物品( 1 i n, 0 v V)恰好装入容量为v的背包中所能获得的最大值。
考虑对i件物品的选择策略,有两种策略:
- 1 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量v的背包中所能获得的最大价值,也即dp[i-1][v];
- 2 放第i件物品,那么问题转化为前i-1件物品恰好装入容量v-w[i]的背包中所能获得的最大价值,也即dp[i-1][v - w[i]] + c[i]。
由这两种策略,且要求获得最大价值,因此:
由于dp[i][v]只与前状态dp[i-1][]有关,所以可以枚举i从1到n,v从0到V,通过边界dp[0][v]=0(0 v V)(前0件物品放入任何容量v的背包中都只能获得价值0)。由于dp[i][v]表示的是恰好为v的情况,所以需要枚举dp[n][v](0 v V),取其最大值才是最后的结果。
代码:
for (int i = 1; i <= n; ++i)
{
for (int v = w[i]; v <= V; ++v)
{
dp[i][v] = max(dp[i-1][v], dp[i-1][v -w[i]]+ c[i]);
}
}
2.2.2.1 空间复杂度优化
时间和空间复杂度都是O(nV),使用滚动数组的方法可以将空间复杂度优化,降低为O(V)。
- 滚动数组:滚动数组是DP中的一种编程思想。简单的理解就是让数组滚动起来,每次都使用固定的几个存储空间,来达到压缩,节省存储空间的作用。
如上图所示,每次计算dp[i][v]时,总是只需要dp[i-1][v]左边部分(图中阴影部分),且当计算dp[i+1][]部分时,dp[i-1]又完全用不到(只需用到dp[i]),因此可以直接开一个一维数组dp[v] (即把第一维省略),枚举方向变为i从1到n,v从V到0(倒序),这样状态方程变为
理解为每计算出一个dp[i][v],将相当于把dp[i-1][v]抹掉,因为后面的运算中dp[i-1][v]再也用不到了。
代码如下
for (int i = 1; i <= n; ++i)
{
for (int v = V; v >= w[i]; --v)//逆序枚举V
{
dp[v] = max(dp[v], dp[v -w[i]]+ c[i]);
}
}
- 注意:如果用二维数组枚举,v的枚举正序和倒序都可以,如果使用一维数组存放,v必须倒着枚举(如果还是正序枚举,会导致有物件被重复计算,不符合每个物件只有一个的情况)。
2.2.2.2 完整求解代码
样例:
5 8 //n==5,V==8
3 5 1 2 2 //w[i]
4 5 1 2 3 //c[i]
输入:
5 8
3 5 1 2 2
4 5 2 1 3
输出:
10
代码:
#include <cstdio>
#include <algorithm>
using std::max;
const int maxn = 100;//物品最大件数
const int maxv = 1000;//v的上限
int w[maxn];//重量
int c[maxn];//价值
int dp[maxv];//能获得的最大价值
int main(int argc, char const *argv[])
{
int n, V;
scanf("%d%d", &n, &V);
for (int i = 1; i <= n; ++i)
{
scanf("%d", &w[i]);
}
for (int i = 1; i <= n; ++i)
{
scanf("%d", &c[i]);
}
//边界:前0件物品放入任何容量的背包价值都只能是0
for (int v = 0; v <= V; ++v)
{
dp[v] = 0;
}
for (int i = 1; i <= n; ++i)
{
for (int v = V; v >= w[i] ; --v)
{
dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
}
}
int max = 0;
for (int v = 0; v <= V; ++v)
{
if(dp[v] > max){
max = dp[v];
}
}
printf("%d\n", max);
return 0;
}
2.2.2.3 总结
动态规划如何避免重复计算在0-1背包问题上非常明显,一开始的暴力枚举每件物品放或者不放入背包时,忽略了第i件物品放或者不放而产生的最大值是完全由前面的i-1件物品的最大值来决定的,而暴力做法无视了它。
0-1背包问题的每一个问题都可以看成一个阶段,这个阶段的状态由dp[i][0]~dp[i][V],它们均由上一个阶段的状态得到。
-
事实上,对于能够划分阶段的问题来说,都可以尝试把阶段作为状态的一维,这样可以更方便的满足无后效性的状态。
-
如果当前设计的状态不满足无后效性,不妨把状态进行升维,即增加一维或多维来表示相应的信息,这样可能就能满足无后效性
3 完全背包问题
3.1 问题描述
有n种物品,每种物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价最大。其中每件物品都有无穷件。
3.2 思路
令dp[i][v]表示前i件物品( 1 i n, 0 v V)恰好装入容量为v的背包中所能获得的最大值。
考虑对i件物品的选择策略,有两种策略:
- 1 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量v的背包中所能获得的最大价值,也即dp[i-1][v];
- 2 放第i件物品,这里处理和01-背包问题不同,因为0-1背包问题每个物件都只能选择一件,因此选择放第i件物品必须转移到dp[i-1][v-w[i]]这个状态,但是完全背包却不同,可以放任意件,放了第i件物品后还可以放第i件物品,直到二维的v-w[i]无法保持大于等于0为止。
由上面的分析可以写出状态转移方程:
边界:d[0][v] = 0 (0 v V)
与0-1背包问题的区别在于max的第二个参数是dp[i]而不是dp[i-1]。
写成一维,
状态转移方程:
边界:d[v] = 0 (0
v
V)
与0-1背包问题的区别在于v的枚举必须是正向枚举。
代码:
for (int i = 0; i <= n; ++i)
{
for (int v = w[i]; v <= V; ++v)//正向枚举v
{
dp[v] = max(dp[v], dp[v -w[i]] + c[i]);
}
}
如上图所示,计算dp[i][v]需要用到阴影部分的,如果让v从小到大枚举,d[i][v-w[i]]就总是已经计算出来的结果;而计算出dp[i][v]之后dp[i-1][v]就再也用不到了,可以直接覆盖。
3.3 示例
样例:
5 8 //n==5,V==8
3 5 1 2 2 //w[i]
4 5 1 2 3 //c[i]
输入:
5 8
3 5 1 2 2
4 5 2 1 3
输出:
16
代码:
#include <cstdio>
#include <algorithm>
using std::max;
const int maxn = 100;//物品最大件数
const int maxv = 1000;//v的上限
int w[maxn];//重量
int c[maxn];//价值
int dp[maxv];//能获得的最大价值
int main(int argc, char const *argv[])
{
int n, V;
scanf("%d%d", &n, &V);
for (int i = 0; i < n; ++i)
{
scanf("%d", &w[i]);
}
for (int i = 0; i < n; ++i)
{
scanf("%d", &c[i]);
}
//边界:前0件物品放入任何容量的背包价值都只能是0
for (int v = 0; v <= V; ++v)
{
dp[v] = 0;
}
for (int i = 0; i <= n; ++i)
{
for (int v = w[i]; v <= V; ++v)
{
dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
}
}
int max = 0;
for (int v = 0; v <= V; ++v)
{
if(dp[v] > max){
max = dp[v];
}
}
printf("%d\n", max);
return 0;
}