背包类型问题
一、最常见的0-1背包问题
问题描述:
给你一个可装载重量为 W 的背包和 N 个物品,每个物品有重量和价值两个属性。其中第 i 个物品的重量为 wt[i],价值为 val[i],现在让你用这个背包装物品,最多能装的价值是多少?
**这个题目中的物品不可以分割,要么装进包里,要么不装,不能说切成两块装一半。**这也许就是 0-1 背包这个名词的来历。
解决这个问题没有什么排序之类巧妙的方法,只能穷举所有可能
经典动态规划问题:
第一步要明确两点,「状态」和「选择」
先说状态,如何才能描述一个问题局面?只要给定几个可选物品和一个背包的容量限制,就形成了一个背包问题,对不对?所以状态有两个,就是「背包的容量」和「可选择的物品」。
再说选择,也很容易想到啊,对于每件物品,你能选择什么?选择就是「装进背包」或者「不装进背包」嘛。
明白了状态和选择,动态规划问题基本上就解决了,只要往这个框架套就完事儿了:
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
第二步要明确dp数组的定义。
首先看看刚才找到的「状态」,有两个,也就是说我们需要一个二维dp数组,一维表示可选择的物品,一维表示背包的容量。
dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]。
比如说,如果 dp[3][5] = 6,其含义为:对于给定的一系列物品中,若只对前 3 个物品进行选择,当背包容量为 5 时,最多可以装下的价值为 6。
根据这个定义,我们想求的最终答案就是dp[N][W]
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
把物品 i 装进背包,
不把物品 i 装进背包
)
return dp[N][W]
如果你没有把这第**i个物品装入背包**,那么很显然,最大价值dp[i][w]应该等于dp[i-1][w]。你不装嘛,那就继承之前的结果。
如果你把这第**i个物品装入了背包**,那么dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。
显然,你应该寻求剩余重量w-wt[i-1]限制下能装的最大价值,加上第i个物品的价值val[i-1],这就是装第i个物品的前提下,背包可以装的最大价值。
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
dp[i-1][w],
dp[i-1][w - wt[i-1]] + val[i-1]
)
return dp[N][W]
完整代码:
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
定义子问题 为:在前
个物品中挑选总重量不超过
的物品,每种物品至多只能挑选1个,使得总价值最大;这时的最优值记作
,其中
,
。
考虑第 个物品,无外乎两种可能:选,或者不选。
- 不选的话,背包的容量不变,改变为问题
;
- 选的话,背包的容量变小,改变为问题
。
最优方案就是比较这两种方案,哪个会更好些:
。
得到
代码实现:(动态规划找最大价值,回溯寻找挑选的商品)
import java.util.*;
/**
* @author SJ
* @date 2020/11/28
*/
public class DP_0or1Package {
//在背包可容纳的范围内装载的价值最大
public static int N;//物品数量
public static int Capacity;//背包容量
public static int[] wt;//每个物品的重量
public static int[] val;//每个物品的价值
public static int[][] dp;
public static void printDp(){
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[i].length; j++) {
System.out.print(dp[i][j]);
}
System.out.println();
}
}
static Scanner scanner = new Scanner(System.in);
public DP_0or1Package(int num, int capacity) {
N = num;
Capacity = capacity;
wt = new int[N];
for (int i = 0; i < N; i++) {
wt[i] = scanner.nextInt();
}
val = new int[N];
for (int i = 0; i < N; i++) {
val[i] = scanner.nextInt();
}
//dp[i][j]表示:前i个物品,当前的背包能装载的最大重量,可最大价值是dp[i][j]
dp = new int[N + 1][Capacity + 1];
}
public static int knapsack() {
for (int i = 1; i>=0&&i <= N; i++) {
for (int j = 1; j>=0&&j <= Capacity; j++) {
if (j < wt[i-1])//第i号物品装不下
dp[i][j] = dp[i - 1][j];
else {
dp[i][j] = Math.max(dp[i - 1][j - wt[i - 1]] + val[i - 1], dp[i - 1][j]);
}
}
}
return dp[N][Capacity];
}
//从dp数组后往前回溯,寻找挑选的商品
public static void backTrack(int i,int j,Stack<Integer> temp){
while (i>0&&j>0){
if (j<wt[i-1]){
i--;
}
else if (j>=wt[i-1]&&dp[i][j]==dp[i-1][j-wt[i-1]]+val[i-1]){
j=j-wt[i-1];
temp.push(i-1);
i--;
}
else if (j>wt[i-1]&&dp[i-1][j]==dp[i][j]){
i--;
}
}
}
public static void main(String[] args) {
int N=scanner.nextInt();
int W=scanner.nextInt();
new DP_0or1Package(N,W);
int knapsack = knapsack();
Stack<Integer> stack=new Stack<>();
backTrack(N,Capacity,stack);
System.out.println("背包所能容纳的做大价值为:"+knapsack);
System.out.print("所选物品编号为:");
while (!stack.empty()){
System.out.print(stack.pop()+" ");
}
}
}
结果:
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe"...
3 4
2 1 3
4 2 3
背包所能容纳的做大价值为:6
所选物品编号为:0 1
Process finished with exit code 0
方法二:
回溯法:穷举
回溯法
回溯法是一种非常有效的方法,有“通用的解题法”之称。它有点像穷举法,但是更带有跳跃性和系统性,他可以系统性的搜索一个问题的所有的解和任一解。回溯法采用的是深度优先策略。
回溯法在确定了解空间的结构后,从根结点出发,以深度优先的方式搜索整个解空间,此时根结点成为一个活结点,并且成为当前的扩展结点。每次都从扩展结点向纵向搜索新的结点,当算法搜索到了解空间的任一结点,先判断该结点是否肯定不包含问题的解(是否还能或者还有必要继续往下搜索),如果确定不包含问题的解,就逐层回溯;否则,进入子树,继续按照深度优先的策略进行搜索。当回溯到根结点时,说明搜索结束了,此时已经得到了一系列的解,根据需要选择其中的一个或者多个解即可。
回溯法解决问题一般分为三个步骤;
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间。
回溯法の解空间
就是要找出这个问题所有的解,在这些解里面筛选出最优解。
对每一个物品判断是装还是不装,然后画出一个不规范的的图:
在这个图中,连线旁边的数字:1表示装入背包中,0表示不装入背包。第一层中,我们判断是否放入A,放入就是1那条岔路,反之是0那条岔路,所以很直观地,我们就可以得到所有可能出现的8个情况:
{(1,1,1),(1,1,0),(1,0,1),(1,0,0),(0,1,1),(0,1,0),(0,0,1),(0,0,0)}
回溯法の公式
每种算法都可以选择递归和迭代的方式来写,显然这里用迭代方式来写是很麻烦的,可以说也没有人选择这样的方法,所以我们都采用递归来写。
子集树写法 我们的目的就是要遍历子集树上的每个解法,为了判断每一个解对不对,所以我们应该是深度遍历,所以我们需要:t(树的深度),数组x(每一层的结果选择)。先写出中心的遍历函数:
void backtrack(int t){
if(t>=n){
output(); //此时已到达叶子结点,用于输出一个结果
return;
}
//对分叉点遍历0,1两种情况
for(int i=0;i<=1;i++){
x[t]=i; //给第t层代表的选择赋可能选择的值
if(ok(t))
//ok是判断目前的一个解有可行的机会
backtrack(t+1);
}
}
此时我们应该注意,x数组的初始值应该都为0。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* @author SJ
* @date 2020/12/5
*/
public class Knap_Backtrack {
//回溯法解背包问题(dfs)
public static int Capacity=12;//背包容量
public static int N=4;//物品数量
public static int[] weight=new int[]{8, 6, 2, 3};//weight[i]代表第i个物品的重量
public static int[] val=new int[]{8, 6, 2, 3};//val[i]代表第i个物品的价值
public static List<Integer> chosen;
public static int Max_value=Integer.MIN_VALUE;//当前所作选择的最大价值
public static void main(String[] args) {
List<Integer> cs=new ArrayList<>();
dfs(0,0,0,cs);
System.out.println(Max_value);
System.out.println(Arrays.toString(chosen.toArray()));
}
public static void dfs(int i,int curValue,int curWeight,List<Integer> curChosen){
//搜索到排列树的第i层,判断第i个物品装还是不装
if (i==N){//最后一层已经判断结束
if (curValue>Max_value)
{
Max_value=curValue;
chosen=new ArrayList<>(curChosen);
}
return;
}
int tempTotalValue=curValue+val[i];
int tempTotalWeight=curWeight+weight[i];
if (tempTotalWeight<=Capacity){
curChosen.add(i);
dfs(i+1,tempTotalValue,tempTotalWeight,curChosen);
curChosen.remove(curChosen.size()-1);
;
}
dfs(i+1,curValue,curWeight,curChosen);
}
}
"C:\Program Files\Java\jdk1.8.0_131\bin\java.exe" ...
11
[0, 3]
Process finished with exit code 0