一、0-1背包问题
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。每种物品都只有1件。
基本思想
令dp[v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。对第i件物品有两种选择策略:
- 不放第i件物品,问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值。
- 放第i件物品,问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值。
状态转移方程:dp[v] = max{dp[v], dp[v-w[i]]+c[i]}
代码
int V, n;
int dp[maxv], c[maxn], w[maxn];
fill(dp, 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];
}
注意:这里v的枚举顺序必须是逆序,否则dp值可能会被覆盖。
具体来说,我们是由第i-1次循环的两个状态推出第i个状态的,在第i次循环中,还没执行到v-w[i]时,我们保存的应该是第i-1次循环中得到的dp[v-w[i]],而如果是正向枚举,由于v-w[i]小于v,我们这里已经保存了第i次循环时得到的dp[v-w[i]],显然是不对的。
二、完全背包问题
有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。每种物品都有无穷件(与01背包的唯一区别),不超过容量v即可。
基本思想
仍然令dp[v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。对第i件物品有两种选择策略:
- 不放第i件物品,问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值。
- 放第i件物品,由于每种物品可以放任意件,放完第i件物品还可以继续放,所以问题不应转化为前i-1件物品的dp[v-w[i]],而是转移到第i件物品的dp[v-w[i]],因此完全背包问题适合正向枚举。
状态转移方程:dp[v] = max{dp[v], dp[v-w[i]]+c[i]}
代码
int V, n;
int dp[maxv], c[maxn], w[maxn];
fill(dp, dp + v, 0); //边界
for(int i = 1; i <= n; i++){
for(int v = w[i]; v <= V; v++)
dp[v] = max(dp[v], dp[v - w[i]] + c[i]);
}
//与01背包的唯一区别是正向枚举
多重背包问题
有n件物品,每件物品的重量为w[i],价值为c[i],最多有s[i]件可用。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。
朴素算法
将s[i]件物品拆分,得到物品总量为s[i]的累加,转化为0-1背包问题。(时间复杂度高)
状态转移方程:
dp[v] = max{dp[v], dp[v - j * c[i]] + j * w[i] | 0 <=j <= s[i]}
代码如下:
int V, n;
int c, w, s; //价值 重量 数量
int dp[maxv] = {0};
for(int i = 0; i < n; i++){
scanf("%d%d%d", &c, &w, &s);
for(int j = 1; j <= s; j++){
for(int v = V; v >= j * w; v--)
dp[v] = max(dp[v], dp[v - j * w] + j * c);
}
}
二进制优化
基本思路:
将第i种物品分成若干件物品,每件物品为1,2,4,…,2k-1,s[i]-2k+1个i物品放在一起,则最终第i种物品的数量由原先的s[i]变为log(s[i]),每件物品的重量与价值均改变。假设我们有14件i物品,则将其分为4堆:1件、2件、4件、7件。于是将这四堆物品任意组合,我们可以得到14种可能性,即1~14的所有物品数量(14的二进制为1110,而四个堆的数量二进制为1,10,100,111,前三个堆组合可得到1-7,第四个堆和其他的组合可得到8-14)。这样做可避免枚举s[i]件物品,降低复杂度。
之后就可将其转化为0-1背包问题,把每一堆的logs[i]件物品拆分。
代码如下:
int V, n, cnt = 0;
int c, w, s; //价值 重量 数量
int dp[maxv] = {0};
int val[maxn], cost[maxn]; //分堆后每个堆的物品重量和总价值
for(int i = 0; i < n; i++){
scanf("%d%d%d", &c, &w, &s);
for(int j = 1; j <= s; j *= 2){
val[++cnt] = w * j;
cost[cnt] = c * j;
s -= j;
}
val[++cnt] = w * s;
cost[cnt] = c * s;
}
//对cnt种物品进行0-1背包处理
for(int i = 1; i <= cnt; i++)
for(int v = V; v >= val[i]; v--)
dp[v] = max(dp[v], dp[v - val[i]] + cost[i]);