01背包
例题引入:
有N件物品和一个容量为V的背包。第 i 件物品的费用是w[i],价值是v[i],求将哪些物品装入背包可使价值总和最大。
这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。
用子问题定义状态:即f[i][j]表示前 i 件物品恰放入一个容量为 j 的背包可以获得的最大价值。则其状态转移方程便是:
f[i][j] = max( f[i - 1][j], f[i - 1][j - w[i]] + v[i] )
“将前 i 件物品放入容量为 j 的背包中”这个子问题,若只考虑第 i 件物品的策略(放或不放),那么就可以转化为一个只牵扯前 i − 1 件物品的问题。如果不放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入容量为 j 的背包中”,价值为f[i − 1][ j ];如果放第 i 件物品,那么问题就转化为“前 i − 1 件物品放入剩下的容量为j − w[i]的背包中”,此时能获得的最大价值就是f[i − 1][j − w[i]] 再加上通过放入第 i 件物品获得的价值 v[i]。
来看一张图, 看看二位数组中存的是什么
最外层 i 的循环代表选取的物品, 然后是枚举容量来找最大价值
状态转移: 若选择当前物品, 那么当前物品的价值 + 当前容量减去当前物品的重量的容量所能容纳的最大价值就是选择当前物品得到的最大价值
以上的时间复杂度和空间复杂度均为 O(VN), 时间复杂度不能优化了, 但是空间复杂度可以优化到 O(N)
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
我们所需要的dp[i - 1][j] 其实这里等价于 dp[i] 的
而我们求的dp[j] 使用dp[j] 的值就是二维数组中的dp[i - 1][j]
为什么要从 V 到 0 进行遍历? 因为 这样才能保证推 f[j]f[j] 时 f[j − w[i]] 保存的是状态f[i − 1][j − w[i]] 的值
完全背包
例题引入:
有 N 种物品和一个容量为 V 的背包,每种物品都有无限件可用。第 i 种物品的费用是 w[i],价值是 v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
f[j] = max(f[j], f[j - w[i]] + v[i]);
细心的读者会发现,这个代码与01背包的代码只有 j 的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么01背包中要按照 j = V . . . 0 的逆序来循环。这是因为要保证第 i 次循环中的状态f[i][j]是由状态f[i − 1][j − w [i]] 递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第 i 件物品”这件策略时,依据的是一个绝无已经选入第 i 件物品的子结果f[i − 1][j − w[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第 i 种物品”这种策略时,却正需要一个可能已选入第 i 种物品的子结果f[i] [ j − w[i]],所以就可以并且必须采用j = 0… V 的顺序循环. 这就是这个简单的程序为何成立的道理。值得一提的是,上面的伪代码中两层 for 循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。
首先说完全背包的正序枚举
假设你现在有一个体积为 V 的背包,有一个体积为 3 ,价值为 4 的物品,如果正序枚举的话,等我们填充到 3 这个位置,就会得到价值为4的物品,
等我们在填充到 6 这个位置时,发现还可以造成更大的价值,也就是把这个物品再用一遍
这就是完全背包为什么体积正序枚举的原因
下面说01背包的倒序枚举
题目一样,此时倒序填充背包,为了方便从 6 开始
此时,在体积为6的背包中有了价值为4的物品,但体积为3的背包中物品的价值仍然为0,也就是说填充体积为6的背包只得到了填充体积为3的背包的价值,等再枚举到体积为3的背包时,我们才填充了体积为3的背包,接下来枚举物品是就不会再重复使用这个物品了,所以这个物品只被放了一次,最后的状态是这样的