01背包(ZeroOnePack): 有N件物品和一个容量为V的背包,每种物品均只有一件,第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使价值总和最大。
完全背包(CompletePack): 有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
01背包
对于01背包,每一个物体都有两种选择,放或者不放。我们可以使用一个二维数组dp[i][j]来模拟这个过程。dp[i][j]表示在背包容量为j的限制下,在前面i个(包括第i个)的物品中选择所能获取到的最大价值。于是,可以得到如下的状态转移方程:
dp[i][j]=0 i=0或j=0
dp[i][j]=max(dp[i-1][j-w[i]]+v[i], dp[i-1][j]) i≠0且j≠0
上面的第二个等式基于如下事实:若dp[i][j]表示在给定容量为j的背包中,选择前面i个物品所获取的最大价值,那么如果决定选择第i+1个物品,则dp[i+1][j+w[i+1]]=dp[i][j]+v[i+1]。这两个等式其实是等价的。当然,如果不选择第i+1个物品,dp[i+1][j]=dp[i][j]。
事实上,我们只需要一个一维数组即可完成上述迭代,使用dp[j]表示在当前状态下容量为j的背包所可以获取到的最大价值,这里说的“当前状态”指的是下面代码的一次外循环。
for(int i = 1; i <= n; i++){
for(int j = V; j >= w[i]; j--){
dp[j] = max(dp[j - w[i]] + v[i], dp[j]);
}
}
值得注意的是,上面的内循环是逆循环。因为只有这样才能保证该状态和前一状态的递推关系。
完全背包
这里我们仍然使用一维数组dp来表示迭代过程,同理,dp[j]表示在当前状态下容量为j的背包所可以获取到的最大价值。
for(int i = 1; i <= n; i++){
for(int j = w[i]; j <= V; j++){
dp[j] = max(dp[j - w[i]] + v[j], dp[j]);
}
}
内循环是正循环。二者的根本差异体现在物体i是否可以重复拿取。对于完全背包来说,当内循环的j=n*w[i](n为非负整数)时,等价于将n个物品i放入背包中。
例题
题目:一个正整数n分解成x(1≤x≤$10^{6}$)个正整数的立方和,求x最小值是多少?
可能初看该题目属于贪心问题,类似于“找零钱”的题目,但是该题并不具备数字上的倍数关系,如96=64+8+8+8+8=64+27+1+1+1+1+1,答案明显是5,而贪心会算到7[1]。其实,该题应该属于完全背包问题,同一个数可以被选择多次。我们可以将每一个正整数的立方看成是一个物品,目标是求将容量为n的背包填满,并且使得物品的个数最少。我们使用dp[j]表示填满容量为j的背包所需的最小物品个数。因此可以很容易写出状态转移方程:
初始化: dp[0] = 0
迭代过程: dp[j] = min(dp[j - i * i * i] + 1, dp[j])
迭代过程的含义:j-i*i*i可以通过加上i*i*i转移到j,一共需要(dp[j-i*i*i]+1)个正整数,将其与上一个状态的dp[j]比较,选择最小值即可。
#include<math.h> #include<iostream> #define INF 0x3fffffff; using namespace std; int dp[1000010]; int main() {int n; while(cin >> n){ for(int i = 0; i <= n; i++) dp[i] = INF; int x = pow(n, 1. / 3) + 1; for(int i = 1; i <= x; i++){ int low_bound = i * i * i; dp[0] = 0; for(int j = low_bound; j <= n; j++){ dp[j] = min(dp[j], dp[j - low_bound] + 1); } } cout << dp[n] << endl; } return 0; }
参考: