HJ16 购物单 - 分组背包问题求解
题目链接参考 HJ16 购物单_牛客题霸_牛客网
1、温故知新
这道题需要通过动态规划来求解,首先先通过ChatGPT
了解下如何利用动态规划求解01背包问题和完全背包问题,以下是ChatGPT
的答案
- 动态规划是什么?
动态规划(Dynamic Programming,DP)是一种常用的算法思想,用于解决多阶段决策问题,以及最优化问题。动态规划通过将问题分解成多个子问题,并在解决子问题的过程中存储中间结果,从而避免重复计算,提高算法效率。
动态规划通常用于求解具有以下性质的问题:
最优子结构:一个问题的最优解包含其子问题的最优解。
重叠子问题:问题可以被分解为多个重叠的子问题,这些子问题的解可以被重复利用。
动态规划算法通常分为两个阶段:设计阶段和实现阶段。在设计阶段,需要分析问题的特点,设计状态表示方法,定义状态转移方程,确定边界条件。在实现阶段,需要按照设计阶段的思路,通过循环或递归的方式,求解问题的最优解。
常见的动态规划问题包括背包问题、最长公共子序列问题、最短路径问题、最大子段和问题等。动态规划算法在实际应用中具有广泛的应用,例如图像识别、自然语言处理、数据挖掘等领域。
- 用java实现动态规划算法来解决01背包问题?
下面是使用Java实现动态规划算法来解决背包问题的代码示例:
public class Knapsack { public static void main(String[] args) { int[] values = {60, 100, 120}; // 物品价值 int[] weights = {10, 20, 30}; // 物品重量 int capacity = 50; // 背包容量 int n = values.length; // 物品个数 int[][] dp = new int[n+1][capacity+1]; // 动态规划数组 // 填充动态规划数组 for (int i = 1; i <= n; i++) { for (int j = 1; j <= capacity; j++) { if (weights[i-1] > j) { dp[i][j] = dp[i-1][j]; // 背包容量不足,无法放入第i个物品 } else { dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weights[i-1]] + values[i-1]); // 选择价值更大的物品 } } } // 输出最大价值 System.out.println("Max value: " + dp[n][capacity]); } }
在上述代码中,我们首先定义了三个数组:
values
数组存储物品的价值,weights
数组存储物品的重量,capacity
变量存储背包的容量。然后我们定义了一个dp
二维数组来存储动态规划过程中的中间结果。接下来,我们通过两个嵌套循环来遍历每一个物品和每一个容量,使用动态规划的思想,根据当前物品是否放入背包,来更新
dp
数组。具体来说,如果当前物品的重量大于当前背包的容量,那么当前物品无法放入背包,所以背包的最大价值和上一个物品时背包的最大价值是一样的;如果当前物品的重量小于等于当前背包的容量,那么我们需要比较选择当前物品和不选择当前物品两种情况下的背包最大价值,选择其中更大的一个。最后,我们输出
dp[n][capacity]
,即为背包中物品的最大价值。值得注意的是,这里我们使用的是0-1背包问题,即每个物品要么放入背包,要么不放入。如果是完全背包问题,即每个物品可以无限次放入背包,那么在动态规划数组更新时需要修改为
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-weights[i-1]] + values[i-1]);
。
- 完全背包问题是什么,如何通过修改上面的代码来实现?
完全背包问题是指:有一组物品,每种物品都有无限件可用,给定一个背包容量,问怎么装能够使背包的总价值最大?
与
0/1
背包问题不同,完全背包问题中每种物品可以选择多次放入背包,所以在状态转移方程中,需要将j-w[i]
这个状态从之前的状态中取到最大值,而不是只取一次。针对完全背包问题,可以对上面的 0/1 背包问题代码进行一些修改,将第二层循环从小到大遍历,这样可以保证在计算
f[j-w[i]]+v[i]
时,已经算过了j-w[i]
这个状态的所有可能的取值,不会漏算任何可能的方案。修改后的代码如下所示:
public static int knapsack(int[] w, int[] v, int c) { int n = w.length; int[] f = new int[c+1]; for (int i = 0; i < n; i++) { for (int j = w[i]; j <= c; j++) { f[j] = Math.max(f[j], f[j-w[i]]+v[i]); } } return f[c]; }
其中,w 数组表示每个物品的重量,v 数组表示每个物品的价值,c 表示背包的容量,返回值为背包能够装下的最大价值。
2、尝试过程
对于HJ16 购物单_牛客题霸_牛客网这道题,一开始我想了两小时也写不出解决方案(错误代码如下,仅供比较参考)
package main.category.动态规划.level2.HJ16_购物单;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Main_error {
public static void updateMainPart(int[][] dp, int[] mainPart_flag, int[] val, int i, int j, Map<Integer,Integer> map){
int max = dp[i][j];
while(i > 0 && j > 0){
if(max > dp[i-1][j]){
mainPart_flag[map.get(i)] = 1; //当前第i个主件已购入,map.get(i)是对主件序号进行重映射
j = j - val[i]; //回溯到没有购入第i个主件的状态
if(j > 0) max = dp[i][j];
}else{
i = i - 1; //回溯到上一个商品购入时的状态
}
}
}
//元素整体向后移动
public static void shift(int[] arr, int val, int i){
if(i != arr.length - 1){
for (int j = arr.length - 1; j > i ; j--) {
arr[j] = arr[j-1];
}
arr[i] = val;
}
}
//主件在前,附件在后,返回最后一个主件的位置
public static int sort(int[] v, int[] w, int[] q, int v_i, int w_i, int q_i, int index){
if(q_i == 0){
index += 1;
shift(v,v_i,index);
shift(w,w_i,index);
shift(q,q_i,index);
}
return index;
}
//对于分组背包问题,不能直接用0/1背包问题来求解
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String[] tokens = scanner.nextLine().split(" ");
int N = Integer.parseInt(tokens[0]),m = Integer.parseInt(tokens[1]); //N表示总钱数,m表示可购买的物品个数。
int[] v = new int[m + 1]; //该物品的价格
int[] w = new int[m + 1]; //该物品的权重
int[] q = new int[m + 1]; //该物品是主件还是附件:如果q为0则为主件,主件编号为索引值;如果q>0则为附件,附件编号为q
HashMap<Integer,Integer> map = new HashMap<Integer, Integer>();
int i = 1;
//对商品进行排序,先购入主件,再购入附件
int index = 0;
while(i <= m){
String[] temp = scanner.nextLine().split(" ");
int v_i = Integer.parseInt(temp[0]);
int w_i = Integer.parseInt(temp[1]);
int q_i = Integer.parseInt(temp[2]);
v[i] = v_i;
w[i] = w_i;
q[i] = q_i;
map.put(index,i);
index = sort(v, w, q, v_i, w_i, q_i, index); //由于主件编号与原序列的索引号有关,如果要排序,需要先进行重映射
i++;
}
/**
* 约束条件:
* 1) 如果要买归类为附件的物品,必须先买该附件所属的主件,且每件物品只能购买一次
* 2)每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件。
* 3) 满意度计算公式:v[j_1]*w[j_1]+v[j_2]*w[j_2]+ … +v[j_k]*w[j_k]v[j]
* 理解:在0-1背包问题上加了上面的约束条件
* 动态转移方程:
* 假设当前要加入的物体价值为val,则dp[j][k] = max(dp[j-1][k], dp[j][k-val] + val)
* 如果加入的是主件,则可以直接加入;如果加入的是附件,则需要判断对应主件是否存在
*/
int[][] dp = new int[m + 1][N + 1];
int[] mainPart_flag = new int[m + 1]; //表示当前状态的主件是否已购入:0表示没有,1表示有
for (i = 1; i <= m; i++) {
for (int j = 1; j <= N; j++) {
//先判断是否为主件
if(q[i] == 0){
//当前主件只能购买一次
if (v[i] > j || mainPart_flag[i] == 1){
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); //使用不包含第i物品/包含第i物品的最新状态:
}
else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - v[i]] + (v[i] * w[i]));
}
}else{
//在加入附件之前,先对dp进行回溯,更新主件的购买信息
updateMainPart(dp, mainPart_flag, v, i, j,map);
//判断该附件对应主件是否已经购买,如果已购买则可以选择加入该商品
if(mainPart_flag[q[i]] == 1){
if(v[i] > j){
dp[i][j] = dp[i-1][j]; //使用不包含当前物品的最新状态
}else{
dp[i][j] = Math.max(dp[i-1][j],dp[i][j - v[i]] + (v[i] * w[i]));
}
}
}
}
}
System.out.println(dp[m][N]);
}
}
而且越改越吃力,最后放弃了。
3、解决方案
后来参考了题解 | #购物单#_牛客博客,他用分组背包的解法来解这道题,即先根据题意,把能一次性放入包中的方案划分成四种情况:
- 仅加入一个主件;
- 加入主件和第一个附件;
- 加入主件和第二个附件;
- 加入主件和两个附件;
通过ChatGPT
了解了分组背包的问题描述和解法:分组背包如何求解?
分组背包是指一组物品,其中每个物品都被划分为不同的组别,每个组别中的物品只能选择一件放入背包中,问如何选择物品使得背包的总价值最大。
相比于 0/1 背包问题和完全背包问题,分组背包问题中的物品被分为不同的组别,这给问题的求解带来了一些不同的思路。我们可以将物品按照组别分别处理,针对每个组别使用类似于 0/1 背包问题的状态转移方程,最后将各个组别的结果进行合并。
具体的,可以使用一个二维数组 f[i][j] 来表示前 i 个组的物品放入容量为 j 的背包中所能获得的最大价值,其中 i 表示组别的个数,j 表示背包的容量。每个组别中的物品只能选择一件放入背包中,因此转移方程中需要对每个组别进行单独处理:
f[i][j] = max{f[i-1][j-kw[i][j]]+kv[i][j]},其中 k 为第 i 组中物品的数量,w[i][j] 表示第 i 组中第 j 个物品的重量,v[i][j] 表示第 i 组中第 j 个物品的价值。
因此,我们需要在循环中分别枚举每个组别,然后对每个组别中的物品进行背包问题的求解。
代码如下所示:
public static int groupKnapsack(int[][] w, int[][] v, int[] c) { int n = w.length; int m = c.length; int[][] f = new int[n+1][]; for (int i = 0; i <= n; i++) { f[i] = new int[m+1]; } for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { f[i][j] = f[i-1][j]; for (int k = 1; k <= w[i-1].length; k++) { if (j >= w[i-1][k-1] && f[i][j] < f[i-1][j-k*w[i-1][k-1]]+k*v[i-1][k-1]) { f[i][j] = f[i-1][j-k*w[i-1][k-1]]+k*v[i-1][k-1]; } } } } return f[n][m]; }
其中,w 数组表示每个物品的重量,v 数组表示每个物品的价值,c 数组表示每个组别的背包容量,返回值为背包能够装下的最大价值。
参考了题解 | #购物单#_牛客博客提供的java实现方法,并在其中加了一些注释,代码如下:
package main.category.动态规划.level2.HJ16_购物单;
import java.util.*;
public class Main {
/**
* 参考答案:https://blog.nowcoder.net/n/477ed49f893941bbb20ef8a0651acfd0?f=comment
* @param args
*/
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
/**
* 题目的要求是:
* 1) 如果要买归类为附件的物品,必须先买该附件所属的主件,且每件物品只能购买一次
* 2)每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件。
* 3) 满意度计算公式:v[j_1]*w[j_1]+v[j_2]*w[j_2]+ … +v[j_k]*w[j_k]v[j]
*/
while (sc.hasNextLine()) {
int money = sc.nextInt();
int m = sc.nextInt();
sc.nextLine();
money /= 10; //为了减少dp大小,将money和单价都除以10
/**
* 由于题目要求:
* 每个主件可以有 0 个、 1 个或 2 个附件。附件不再有从属于自己的附件,因此在每行输入中,输入某个主件的附件最多为两个,
* 因此可以用一个int[m+1][3]的二维数组,绑定组件与附件之间的关系
*/
int[][] prices = new int[m+1][3];
int[][] weights = new int[m+1][3];
for (int i = 1; i <= m; i++) {
int a = sc.nextInt();
int b = sc.nextInt();
int c = sc.nextInt();
a /= 10;//price //单价除以10
b = b * a;//weight
if (c == 0) {
// 主件
prices[i][0] = a;
weights[i][0] = b;
} else if (prices[c][1] == 0) {
// 附件1
prices[c][1] = a;
weights[c][1] = b;
} else {
// 附件2
prices[c][2] = a;
weights[c][2] = b;
}
sc.nextLine();
}
int[][] dp = new int[m+1][money+1];
for (int i = 1; i <= m; i++) {
for(int j = 1; j <= money; j++) {
int a = prices[i][0];
int b = weights[i][0];
int c = prices[i][1];
int d = weights[i][1];
int e = prices[i][2];
int f = weights[i][2];
/**
* 分组背包问题:可以进行多次的0-1背包求解,其中每次放入背包的方案由主件和0、1、2个附件得到
*/
dp[i][j] = j - a >= 0 ? Math.max(dp[i-1][j], dp[i-1][j-a] + b) : dp[i-1][j]; //单买主件
dp[i][j] = j-a-c >= 0 ? Math.max(dp[i][j], dp[i-1][j-a-c] + b + d):dp[i][j]; //买主件 + 附件1
dp[i][j] = j-a-e >= 0 ? Math.max(dp[i][j], dp[i-1][j-a-e] + b + f):dp[i][j]; //买主件 + 附件2
dp[i][j] = j-a-c-e >= 0 ? Math.max(dp[i][j], dp[i-1][j-a-c-e] + b +d + f):dp[i][j]; //买主件 + 附件1 + 附件2
}
}
System.out.println(dp[m][money] * 10); //乘上10得到最终满意度
}
}
}