背包问题
[01背包]
(https://programmercarl.com/背包理论基础01背包-1.html#思路:~:text=%23-,动态规划:01背包理论基础,-本题力扣上)
背包基础理论还得是卡哥
二维dp数组
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
dp数组的含义:dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是dp[i][j];
dp递推公式:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
dp初始化:
- dp[i][0] 如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0;
- 由dp递推公式可知,i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。
遍历顺序:由于初始化第一列和第一行都已经初始化了,所以遍历顺序无论是先遍历物品或者先遍历背包都可以。
import java.util.Arrays;
public class BagProblem {
public static void main(String[] args) {
int[] weight = {1,3,4};
int[] value = {15,20,30};
int bagSize = 4;
testWeightBagProblem(weight,value,bagSize);
}
/**
* 初始化 dp 数组做了简化(给物品增加冗余维)。这样初始化dp数组,默认全为0即可。
* dp[i][j] 表示从下标为[0 - i-1]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
* 其实是模仿背包重量从 0 开始,背包容量 j 为 0 的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为 0。
* 可选物品也可以从无开始,也就是没有物品可选,即dp[0][j],这样无论背包容量为多少,背包价值总和一定为 0。
* @param weight 物品的重量
* @param value 物品的价值
* @param bagSize 背包的容量
*/
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods + 1][bagSize + 1]; // 给物品增加冗余维,i = 0 表示没有物品可选
// 初始化dp数组,默认全为0即可
// 填充dp数组
for (int i = 1; i <= goods; i++) {
for (int j = 1; j <= bagSize; j++) {
if (j < weight[i - 1]) { // i - 1 对应物品 i
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
dp[i][j] = dp[i - 1][j];
} else {
/**
* 当前背包的容量可以放下物品i
* 那么此时分两种情况:
* 1、不放物品i
* 2、放物品i
* 比较这两种情况下,哪种背包中物品的最大价值最大
*/
dp[i][j] = Math.max(dp[i - 1][j] , dp[i - 1][j - weight[i - 1]] + value[i - 1]); // i - 1 对应物品 i
}
}
}
// 打印dp数组
for(int[] arr : dp){
System.out.println(Arrays.toString(arr));
}
}
}
dp数组降维
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])。
遍历顺序:一维DP倒序遍历是为了保证每个物品只放入一次,而二维DP每层状态独立,所以不需要倒序遍历。
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
华为20220923
题目描述
给定一个大小为n的数组,请问存在多少种方案的子序列使得该子序列的和是原数组元素总和的一半。
输入
4
1 2 3 4
输出
2
范围说明:
- 数组长度 ( n ) 满足 ( 1 <= n <= 200 )
- 数组元素均大于0,且其总和不超过 ( 10^5 )
- 保证方案总数不超过 int 类型的最大值
找到塞满体积为sum(a)/2 背包方案数
dp[j] :体积为j背包最大总和(价值)
dp[0] = 1
dp[j] += dp[j-a[i]]
import java.util.Scanner;
/**
* @author 17259
* @create 2024-06-20 12:39
*/
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] a = new int[n + 1];
int s = 0;
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
s += a[i];
}
if (s % 2 != 0) {
System.out.println("0");
} else {
int[] dp = new int[(int) 1e5];
dp[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = s / 2; j >= a[i]; j--) {
dp[j] += dp[j-a[i]];
}
}
System.out.println(dp[s / 2]);
}
}
}
字节跳动
有 ( n ) 个角斗士,每个角斗士的战力为 ( a_i ) (1≤i≤n)。每次决斗中,战力高的角斗士胜利,其战力变为 ∣( a_i - a_j )∣,另一角斗士死亡。希望设计一个方案,使最后剩下的角斗士战力最小,或所有角斗士都死亡。
输入描述
- 第一行输入一个正整数 ( n ),代表角斗士的数量。
- 第二行输入 ( n ) 个正整数 ( a_i ),代表每个角斗士的战力。
约束条件:
- ( 1≤n≤100 )
- ( 1≤a_i≤100 )
输出描述
- 第一行输出一个整数 ( h ),代表最后的角斗士的战力。
- 第二行输出一个正整数 ( k ),代表总共的决斗次数。
- 接下来 ( k ) 行,每行输出一次决斗方案。
样例
输入
4
1 2 3 4
输出
0
3
1 2
3 4
2 4
思路
一维01背包
和力扣1349最后一块石头类似,
可以先参考卡哥关于这道力扣的题解
但是需要输出模拟决斗方案。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 读取角斗士数量
int n = sc.nextInt();
// 读取每个角斗士的战力
int[] people = new int[n];
int sum = 0;
for (int i = 0; i < n; i++) {
people[i] = sc.nextInt();
sum += people[i];
}
// 目标是总战力的一半
int target = sum / 2;
// 使用集合数组记录容量为 i 的背包需要的人的下标
Set<Integer>[] conts = new Set[target + 1];
for (int j = 0; j < conts.length; j++) {
conts[j] = new HashSet<>();
}
// 动态规划数组
int[] dp = new int[target + 1];
// 动态规划求解
for (int i = 0; i < people.length; i++) {
for (int j = target; j >= people[i]; j--) {
int cur = dp[j - people[i]] + people[i];
if (dp[j] <= cur) {
conts[j] = new HashSet<>(conts[j - people[i]]);
conts[j].add(i);
dp[j] = cur;
}
}
}
// 输出最后角斗士的战力
System.out.println(sum - 2 * dp[target]);
// 模拟决斗方案
Deque<Integer> t1 = new ArrayDeque<>(conts[target]);
Deque<Integer> t2 = new ArrayDeque<>();
for (int i = 0; i < people.length; i++) {
if (!conts[target].contains(i)) {
t2.offer(i);
}
}
int k = 0;
List<int[]> ans = new ArrayList<>();
while (!t1.isEmpty() && !t2.isEmpty()) {
int a = t1.poll();
int b = t2.poll();
ans.add(new int[]{a + 1, b + 1});
if (people[a] > people[b]) {
people[a] -= people[b];
t1.offer(a);
} else if (people[a] < people[b]) {
people[b] -= people[a];
t2.offer(b);
}
k++;
}
// 输出决斗次数和决斗方案
System.out.println(k);
for (int[] duel : ans) {
System.out.println(duel[0] + " " + duel[1]);
}
}
}
二维01背包
美团2023031804
有 ( N ) 件商品,每件商品有原价和折扣价。现有 ( X ) 元现金和 ( Y ) 张折扣券,求在这种情况下可以购买的最多商品数量及花费的最少钱数。
输入描述
- 第一行三个整数 ( N, X, Y ),表示商品数量、现金数量、折扣券数量。
- 接下来 ( N ) 行,每行两个整数,表示商品的原价和折扣价。
约束条件:
- ( 1 ≤ N ≤ 100 )
- ( 1 ≤ X ≤ 5000 )
- ( 1 ≤ Y ≤ 50 )
- 每个商品原价和折扣价均介于 ([1, 50]) 之间。
输出描述
- 一行两个整数,表示最多能买的商品数量和最少花费的钱数。
样例
输入
2 5 1
3 1
1 1
输出
2 2
解释:第一个商品折扣价购入,第二个商品原价购入,共获得 2 个商品,花费 2 元。
输入
5 10 2
10 6
5 3
7 4
4 3
15 3
输出
3 10
思路:二维01背包
这道题目思路与力扣474一个零类似
状态方程定义:
设第i个物品的原价为w1[i],折扣价为w2[i],定义状态方程f[i][j][k]为前i个物品中花费总金额不超过j,使用的优惠券数量不超过k的最大购买数量
如果不购买第i个物品,则有f[i][j][k]=max(f[i][j][k],f[i-1][j][k])
如果购买第i个物品
- 如果使用优惠券,则有f[i][j][k]=max(f[i][j][k],f[i][j-w2[i]][k-1]+1)
- 如果不使用优惠券,则有f[i][j][k]=f[i][j-w1[i]][k]+1
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author 17259
* @create 2024-06-21 09:19
*/
public class Main {
static final int N = 110;
static final int X = 5010;
static final int Y = 60;
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
int[] w1 = new int[N];
int[] w2 = new int[N];
int[][] dp = new int[X][Y];
String[] split = reader.readLine().split(" ");
int n = Integer.parseInt(split[0]);
int x = Integer.parseInt(split[1]);
int y = Integer.parseInt(split[2]);
for (int i = 1; i <= n; i++) {
String[] split1 = reader.readLine().split(" ");
w1[i] = Integer.parseInt(split1[0]);
w2[i] = Integer.parseInt(split1[1]);
}
int ans = 0;
int cost = x;
for (int i = 1; i <= n; i++) {
for (int j = x; j >= 0; j--) {
for (int k = y; k >= 0; k--) {
if (j >= w1[i]) {
dp[j][k] = Math.max(dp[j][k], dp[j - w1[i]][k] + 1);
}
if (k >= 1 && j >= w2[i]) {
dp[j][k] = Math.max(dp[j][k], dp[j - w2[i]][k - 1] + 1);
}
if (ans < dp[j][k]) {
ans = dp[j][k];
cost = j;
}
if (ans == dp[j][k]) {
cost = Math.min(cost, j);
}
}
}
}
System.out.println(ans + " " + cost);
}
}
完全背包
美团2023042303
在“农场大亨”游戏中,有一块土地,每个时间只能种植一种作物。目标是在有限时间内赚尽可能多的钱,可以反复购买种子和出售作物。
游戏信息:
- 有 ( n ) 种作物,每种作物的种子价格、成熟时间、卖出价格不同。
- 总时间为 ( m ) 天。
- 初始资金充足。
需要计算在 ( m ) 天内最多能赚多少钱。
输入描述
- 第一行两个整数 ( n ),( m ) ((1 ≤ n, m ≤ 1000))。
- 第二行 ( n ) 个整数,表示每种作物的成熟时间 ( t_i )((1 ≤ t_i ≤ m))。
- 第三行 ( n ) 个整数,表示每种作物的种子价格 ( a_i )((1 ≤ a_i ≤ 100000))。
- 第四行 ( n ) 个整数,表示每种作物的卖出价格 ( b_i )((a_i ≤ b_i ≤ 100000))。
输出描述
输出一个整数,表示最多能赚的钱。
样例1
输入
3 12
3 4 7
9 3 2
11 6 11
输出
12
解释
先种第二种作物,然后种第三种作物,共耗时 11 天,赚的钱为 (6 - 3 + 11 - 2 = 12)。
样例2
输入
10 100
81 21 66 63 48 25 23 88 71 65
56 12 94 57 57 6 37 63 87 64
62 68 99 93 88 96 47 65 97 69
输出
360
思路
完全背包
m
:背包容量,即总时间。t_i
:每种作物的成熟时间,对应背包问题中的重量weight[i]
。b_i - a_i
:每种作物的净收益,对应背包问题中的价值value[i]
。- 可以反复购买种子和出售作物,因此这是一个完全背包问题。
使用动态规划来解决这个完全背包问题:
dp[i][j]
:表示选择前i
个物品,在最大时长为j
的情况下的最大价值。
状态转移方程
-
选择第
i
个物品:
dp[i][j] = max(dp[i][j], dp[i-1][j-t_i] + (b_i - a_i)) -
不选择第
i
个物品:dp[i][j] = dp[i-1][j]
将其转化为一维数组的状态转移方程:
dp[j] = max(dp[j], dp[j - t_i] + (b_i - a_i))
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @author 17259
* @create 2024-06-23 16:14
*/
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String[] split = reader.readLine().split(" ");
int n = Integer.parseInt(split[0]);
int m = Integer.parseInt(split[1]);
int[] t = new int[n];
int[] a = new int[n];
int[] b = new int[n];
String[][] inputs = new String[3][];
inputs[0] = reader.readLine().split(" ");
inputs[1] = reader.readLine().split(" ");
inputs[2] = reader.readLine().split(" ");
for (int i = 0; i < n; i++) {
t[i] = Integer.parseInt(inputs[0][i]);
a[i] = Integer.parseInt(inputs[1][i]);
b[i] = Integer.parseInt(inputs[2][i]);
}
int[] dp = new int[m+1];
for (int i = 0; i < n; i++) {
for (int j = t[i]; j <= m; j++) {
dp[j] = Math.max(dp[j],dp[j-t[i]]+(b[i]-a[i]));
}
}
System.out.println(dp[m]);
}
}
多重背包
华为20221109
有若干火药枪和数量有限的火药,每种火药枪的威力不同,且每次开火前需要填充火药。需要在给定时间结束或火药耗尽之前给予敌人最大的伤害。
限制
- 火药枪每次开火的威力一样。
- 火药剩余量不小于火药枪的消耗量时才能开火。
- 填充火药之外的时间忽略不计。
- 不同种火药枪可以同时开火。
输入描述
第一行,整数 (N), (M), (T):
- (N):火药枪种类个数
- (M):火药数量
- (T):攻城时间
(1 ≤ N, M, T ≤ 1000)
接下来 (N) 行,每行三个整数 (A), (B), (C):
- (A):火药枪的威力
- (B):火药枪每次攻击消耗的火药量
- (C):火药枪每次攻击填充火药的时间
(0 ≤ A, B, C ≤ 100000)
输出描述
输出在给定时间结束或火药耗尽之前给予敌人最大的伤害。
样例
样例一:
输入
3 88 30
10 7 5
5 3 1
4 4 8
输出
145
解释
- 3 种火药枪,火药存量 88,攻城时间 30。
- 第 1 种火药枪威力 10,每次攻击消耗火药 7,每次攻击填充火药时间 5。
- 第 2 种火药枪威力 5,每次攻击消耗火药 3,每次攻击填充火药时间 1。
- 第 3 种火药枪威力 4,每次攻击消耗火药 4,每次攻击填充火药时间 8。
样例二:
输入
2 10 15
2 2 2
3 3 3
输出
10
解释
- 2 种火药枪,火药存量 10,攻城时间 15。
- 第 1 种火药枪威力 2,每次攻击消耗火药 2,每次攻击填充火药时间 2。
- 第 2 种火药枪威力 3,每次攻击消耗火药 3,每次攻击填充火药时间 3。
思路
多重背包
定义f[i][j]为选取前i个火药枪,火药消耗量为j的最大打击值。
多重背包的三重循环顺序
1.枚举物品
2.枚举体积
3.枚举每个物品选择的个数(0,1,2,...)
决策 f[i][j]=max(f[i][j],f[i-1][j-kv[i]]+kw[i])
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int N = 1010;
int[] a = new int[N];
int[] b = new int[N];
int[] c = new int[N];
int[][] f = new int[N][N];
int n = scanner.nextInt();
int m = scanner.nextInt();
int t = scanner.nextInt();
for (int i = 1; i <= n; i++) {
a[i] = scanner.nextInt();
b[i] = scanner.nextInt();
c[i] = scanner.nextInt();
}
// 多重背包问题,f[i][j]: 前i个火药枪,火药量为j的最大打击值
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int mx = Math.min(j / b[i], t / c[i]); // 限定当前大炮的最大个数
for (int k = 0; k <= mx; k++) {
f[i][j] = Math.max(f[i][j], f[i - 1][j - k * b[i]] + k * a[i]);
}
}
}
System.out.println(f[n][m]);
}
}