HJ16 買い物リスト - グループでのナップザックの問題解決
トピックリンク参照HJ16 ショッピングリスト_Niuke题研_Niuke.com
1、温故知新
この問題は動的計画法で解く必要があります。まず、動的計画法を使用して 01 ナップサック問題と完全なナップサック問題を解くChatGPT
方法を理解しましょう。答えは次のとおりです。ChatGPT
- 動的プログラミングとは何ですか?
動的プログラミング (Dynamic Programming、DP) は、多段階の意思決定問題や最適化問題を解決するために一般的に使用されるアルゴリズムのアイデアです。動的プログラミングは、問題を複数のサブ問題に分解し、サブ問題を解決するプロセス中に中間結果を保存することで、計算の繰り返しを回避し、アルゴリズムの効率を向上させます。
動的プログラミングは、次の性質の問題を解決するためによく使用されます。
最適な部分構造: 問題に対する最適な解決策には、その部分問題に対する最適な解決策が含まれています。
重複する部分問題: 問題は複数の重複する部分問題に分解でき、これらの部分問題の解決策は再利用できます。
動的プログラミング アルゴリズムは通常、設計フェーズと実装フェーズの 2 つのフェーズに分かれています。設計段階では、問題の特性を解析し、状態表現方法を設計し、状態遷移方程式を定義し、境界条件を決定する必要があります。実装段階では、設計段階の考え方に従って、循環的または再帰的に問題に対する最適な解決策を見つける必要があります。
一般的な動的計画問題には、ナップザック問題、最長共通部分列問題、最短経路問題、最大サブセグメント合計問題などが含まれます。動的プログラミングアルゴリズムは、画像認識、自然言語処理、データマイニングなどの実用分野に幅広く応用されています。
- 01 ナップザック問題を解決する動的プログラミング アルゴリズムを実装するには Java を使用しますか?
以下は、ナップザック問題を解決するために 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]); } }
上記のコードでは、まず 3 つの配列を定義します。
values
配列はアイテムの値を保存し、weights
配列はアイテムの重量を保存し、capacity
変数はバックパックの容量を保存します。dp
次に、動的プログラミング プロセス中に中間結果を保存するための 2 次元配列を定義します。次に、2 つのネストされたループを通じて各アイテムと各容量をトラバースし、動的プログラミングのアイデアを使用して、
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 ナップザック問題のコードにいくつかの変更を加えることができ、ループの 2 番目の層が小さいものから大きいものまで走査され、計算時にすべての可能な値が
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 Shopping List_Niuke Topic_Niuke.comの質問は、最初2時間考えたのですが解決策が書けませんでした(エラーコードは以下の通りです、参考までに)
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. 解決策
その後、私は問題解決法 | #商品#_牛客博客を参照しましたが、彼はこの問題を解決するためにバックパックをグループ化するという解決法を使用しました。つまり、問題の意味に応じて、バックパックに入れることができる解決策を分割します。バッグを一度に 4 つの状況に分けます。
- 主要部分を 1 つだけ結合します。
- メインパーツと最初のアクセサリーを結合します。
- メイン部分と 2 番目のアクセサリを結合します。
- 本体と 2 つの付属品を結合します。
ChatGPT
グループ化されたバックパックの問題の説明と解決策を理解することで、グループ化されたバックパックを解決するにはどうすればよいですか?
グループ化されたバックパックとは、複数のアイテムを複数のグループに分け、各グループ内で 1 つのアイテムのみを選択してバックパックに入れることができるバックパックの合計価値を最大化するためのアイテムの選択方法を指します。
0/1 ナップサック問題や完全なナップサック問題と比較すると、グループ ナップサック問題の項目は異なるグループに分割されており、問題の解決にいくつかの異なるアイデアがもたらされます。項目をグループごとに個別に扱い、各グループに対して 0/1 ナップザック問題と同様の状態遷移方程式を使用し、最後に各グループの結果を結合することができます。
具体的には、2 次元配列 f[i][j] を使用して、最初の i グループのアイテムを容量 j のナップザックに入れることで得られる最大値を表すことができます。ここで、i は個数を表します。グループと j はバックパックの容量を表します。バックパックに入れるアイテムは各グループで 1 つだけ選択できるため、転送方程式では各グループを個別に扱う必要があります。
f[i][j] = max{f[i-1][jk w[i][j]]+k v[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配列は各グループのバックパックの容量を表し、戻り値はバックパックが保持できる最大値を表します。
ソリューション|#商品#_Niuke blogが提供する 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得到最终满意度
}
}
}