背包状态转移方程
动态规划是求最优解的数学方法,它利用各阶段之间的关系,逐个求解,最终求得全局最优解。
笼统地来说,思路就是一个数组dp[],手动设置dp[0],递推求dp[1]...最后得dp[n]就是所求。
设计动态规划算法时,最重要的就是明确:
- dp[i]表示什么?
- 如何使用dp[0...i-1]求得dp[i],也就是状态转移方程是什么?
- 边界值dp[0]是多少?
在解题找不到思路时,第一个应该思考的就是状态转移方程是什么,这个递推关系在哪里?有递推关系的状态才能作为这个动态规划算法中的状态。没有递推关系的状态是无法解题的,举个例子就是,最长上升子序列中,正确的方法是设dp[i]为以位置i为结尾的最长上升子序列长度,因为这样dp[i]可以由dp[0...i-1]获得,而如果将dp[i]设为前i个数组成的最长上升子序列长度,dp[i]与dp[0...i-1]就没有递推关系,无法求得最后结果。
背包问题:给出n件物品各自的价值和体积,和一个容量为V的背包,将物品放入背包,不能超出容量求最大价值。
作为动态规划中一类经典问题,背包问题的递推关系就是:已知前i - 1件物品放入后的最大价值,前i件物品最大价值就是第i件放与不放的区别。
01背包
①背包可以不被装满
有一个容量为V的背包,和一些物品,这些物品分别有两个属性,体积w和价值v,每种物品只有一个。
状态:dp[i][j]表示在总体积不超过j的情况下,前i个物品所能达到的最大价值
状态转移方程:dp[i][j] = max(dp[i - 1][j - w] + v, dp[i - 1][j])
最后要求的是dp[n][V],也就是体积不超过V的条件下,所有物品所能达到的最大价值
最开始能知道的是dp[0][0...V] = 0,也就是不装入任何物品的情况下,能达到的最大价值是0
for (int i = 1; i <= n; i++) { //循环每一个物品
for (int j = 0; j < list[i].w; j++) { //可用的总体积小于物品i的体积
dp[i][j] = dp[i - 1][j]; //物品i只能不加
}
for (int j = list[i].w; j <= V; j++) { //可用的总体积大于等于物品i的体积
//取物品i加或不加的最大值
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - list[i].w] + list[i].v);
}
}
i为0时的边界值已经确定,只需要遍历i,求解各状态时的最大值,最后得到的dp[n][V]即为所求,以下面的数据模拟上述递推过程。
物品序号 | 体积 | 价值 |
1 | 3 | 4 |
2 | 4 | 1 |
3 | 2 | 3 |
背包容量V | 8 |
空间优化
for (int i = 1; i <= n; i++) { //循环每一个物品
for (int j = V; j >= list[i].w; j--) { //可用的总体积大于等于物品i的体积
//取物品i加或不加的最大值
dp[j] = max(dp[j], dp[j - list[i].w] + list[i].v);
}
}
将二维数组优化为一维时,本轮修改黄色方块处值,需要蓝色方块处值与上一行相同,由图中蓝黄方块位置可以看出,蓝色位置小于等于黄色,所以使用一维数组时,倒序修改数组值。比较的dp[j]实际是dp[i - 1][j],修改的dp[j]实际是dp[i][j]。
②背包必须被装满
只需要将初始状态修改为dp[0]=0,其他为负无穷。
完全背包
物品可以放任意个。只需要在01背包基础上,改为正序修改dp[j]。
j遍历到6时,最大容量可以容纳两个物品i,而dp[6]正好由dp[3]和dp[6]决定,此时的dp[3]就是已经放入一个物品i之后的价值,dp[6]在一个物品i的基础上再加入一个物品i。这样在遍历每一个物品的过程中,通过这样的方式考虑了物品i加入任意个的情况。
多重背包
多重背包是物品只能放有限个。将物品个数按二进制拆分。
int w, v, k;
for (int i = 1; i <= n; i++) {
cin >> w >> v >> k;
int c = 1;
while (k > c) {
len++;
k -= c;
list[len].w = w * c;
list[len].v = v * c;
c *= 2;
}
len++;
list[len].w = w * k;
list[len].v = v * k;
}
参考
《王道机试指南》