前言
“实战算法”是本专栏的第三个部分,本篇博文是第一篇博文,主要收集了常见的背包问题及其变种,如有需要,可:
一、01背包
-
说明: 所有东西都只有一个,可以选择装或者不装
-
公式:
public static int solve(int[] costs, int[] values, int budget, int n){
int[] pkg = new int[budget + 1];
for (int i = 0; i < n; i++){
for (int j = budget; j >= costs[i]; j--)
pkg[j] = Math.max(pkg[j], pkg[j - costs[i]] + values[i]);
}
return pkg[budget];
}
二、变种01背包1——多重背包
-
说明: 每个东西的数量不一样,是一个有限的数
-
公式:
public static int solve(int[] costs, int[] values, int[] counts, int budget, int n) {
int[] pkg = new int[budget + 1];
for (int i = 0; i < n; i++) {
while (counts[i]-- > 0) {
for (int j = budget; j >= costs[i]; j--) {
pkg[j] = Math.max(pkg[j], pkg[j - costs[i]] + values[i]);
}
}
}
return pkg[budget];
}
注: 有一种特殊情况,即
cost
就是value
(如一定容积的牛车拉草,拉的体积越大越好),此时调用solve
的时候传入前两个参数都是cost
或者value
即可;
三、变种01背包2
-
说明: 多一个交易限制
tradeFloor
, 即budget
必须大于tradeFloor[i]
,才可以用cost[i]
买物品i,详见Proud Merchants。 -
公式:
private static class Merchandise{
int cost;
int tradeFloor;
int value;
public Merchandise(int cost, int tradeFloor, int value){
this.cost = cost;
this.tradeFloor = tradeFloor;
this.value = value;
}
}
public static int solve(List<Merchandise> list, int budget, int n){
Collections.sort(list, (a, b) -> (a.tradeFloor - a.cost) - (b.tradeFloor - b.cost));
int[] pkg = new int[budget + 1];
for (int i = 0; i < n; i++){
Merchandise m = list.get(i);
for (int j = budget; j >= m.tradeFloor; j--){
pkg[j] = Math.max(pkg[j], pkg[j - m.cost] + m.value);
}
}
return pkg[budget];
}
注: 先要按照
tradeFloor[i] – cost[i]
来排序,然后和正常01基本一样,就是第二层循环里的循环条件要把cost[i]
换成tradeFloor[i]
,即达到交易条件才能交易。
四、完全背包
-
说明: 所有东西的数量都是无限的
-
公式:
public static int solve(int[] costs, int[] values, int budget, int n){
int[] pkg = new int[budget + 1];
for (int i = 0; i < n; i++){
for (int j = costs[i]; j <= budget; j++)
pkg[j] = Math.max(pkg[j], pkg[j - costs[i]] + values[i]);
}
return pkg[budget];
}
注: 和01背包的区别是,01背包是逆序递减,完全背包是顺序递增
五、变种完全背包
-
说明: 求最少能装多少价值,把性价比最低的装进去
-
公式:
public static int solve(int[] costs, int[] values, int budget, int n, int maxCost){
int[] pkg = new int[budget + 1];
Arrays.fill(pkg, 1, budget + 1, maxCost + 1);
for (int i = 0; i < n; i++){
for (int j = costs[i]; j <= budget; j++){
pkg[j] = Math.min(pkg[j], pkg[j - costs[i]] + values[i]);
}
}
return pkg[budget];
}
注:
① 因为求最大时,Integer.minValue + 多少都不会溢出,但是 Integer.maxValue + 任何数都溢出,所以这里多了一个参数maxCost,即最大的价格,填充初始数组时需要使用maxCost + 1,判断是否有可行的结果就判断返回值是否等于maxCost + 1,等于就不存在,否则返回值即是所求最小值。
② 和最大值不同之处在于,不用Integer.MinValue填充数组了,换用上述正值maxCost + 1,比较也不用max了,换用min。
③ Arrays.fill有坑,填充的时候不包含toIndex。
六、二维背包
-
说明: 就是花费和预算有两个维度,比如质量和体积
-
公式: (展示的是二维完全背包)
public static int solve(int[] costs1, int[] costs2, int[] values, int budget1, int budget2, int n) {
int[][] pkg = new int[budget1 + 1][budget2 + 1];
for (int i = 0; i < n; i++) {
for (int j = costs1[i]; j <= budget1; j++) {
for (int k = costs2[i]; k <= budget2; k++) {
pkg[j][k] = Math.max(pkg[j][k], pkg[j - costs1[i]][k - costs2[i]] + values[i]);
}
}
}
return pkg[budget1][budget2];
}
注: 多一维,解向量就多一维,循环就多一层。
七、分组背包
-
说明: 有多组物品,每组物品最多只能选一个,也可以一个都不选
-
公式:
private static class CostAndValue{
int cost;
int value;
public CostAndValue(int cost, int value){
this.cost = cost;
this.value = value;
}
}
public static int solve(List<List<CostAndValue>> groups, int budget) {
int[] pkg = new int[budget + 1];
groups.forEach((group) -> {
for (int j = budget; j >= 0; j--) {
for (CostAndValue cav : group) {
if (j >= cav.cost) {
pkg[j] = Math.max(pkg[j], pkg[j - cav.cost] + cav.value);
}
}
}
});
return pkg[budget];
}
注: 注意三层循环加一个判断的结构
for (所有的组k)
for (int j = V; j >= 0; j--)
for (所有属于组k的i)
if (j >= w[i])
f[j] = max(f[j], f[j - w[i]] + v[i])
八、注意
初始化pkg
时,如果要求结果必须把budget
用完,需要pkg[0] = 0
,其他位全是Integer.MIN_VALUE
,如果不要求用完budget
,全部是0
即可。
后记
这些都是博主在实战过程中遇到的各种背包问题,及其解法,希望对大家有用。