前言
最近在学习各种背包问题 动态规划知识 记个博客录一下
一、0-1背包
问题描述:
一共有N件物品,第i(i从1开始)件物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?
状态 选择:
状态有两个,就是「背包的容量」和「可选择的物品」
选择就是「装进背包」或者「不装进背包」
定义状态dp:
dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
base case
就是dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
- 不装入第i件物品,即dp[i−1][j];
- 装入第i件物品(前提是能装下),即dp[i−1][j−w[i]] + v[i]。
状态转移方程为:
// j >= w[i]
dp[i][j] = max(dp[i−1][j], dp[i−1][j−w[i]]+v[i])
代码:
// java版本
int knapsack(int [] wt, int [] val, int W) {
int N = val.length;
int [][] dp = new int[N+1][W+1];
//base case 初始化 限重为0的背包最大价值
for(int i = 0;i <= N;i++){
dp[i][0] = 0;
}
for(int i = 0;i <= W;i++){
dp[0][i] = 0;
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= W; j++) {
if (j - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][j] = dp[i - 1][j];
} else {
// 装入或者不装入背包,择优
dp[i][j] = max(dp[i - 1][j - wt[i-1]] + val[i-1],
dp[i - 1][j]);
}
}
}
return dp[N][W];
}
压缩状态 一维数组优化版
0-1背包 第二循环逆序
由上述状态转移方程可知,dp[i][j]的值只与dp[i-1][0,…,j-1]有关,所以我们可以采用动态规划常用的方法(滚动数组)对空间进行优化(即去掉dp的第一维)。需要注意的是,为了防止上一层循环的dp[0,…,j-1]被覆盖,循环的时候 j 只能逆向枚举(空间优化前没有这个限制)
定义状态dp:
dp[i]表示将N件物品装进限重为i的背包可以获得的最大价值, 0<=i<=W
代码为:
// java版本
int knapsack(int [] wt, int [] val, int W) {
int N = val.length;
int []dp = new int[W+1];
//base case 初始化
for(int i = 0; i <= W;i++){
dp[i] = 0;
}
for (int i = 0; i < N; i++) {
for (int j = W; j >= 0; j--) {
if (j - wt[i] >= 0) {
// 不装或装入背包,择优
dp[j] = max(dp[j], dp[j - wt[i]] + val[i]);
}
}
}
return dp[W];
}
背包问题 求路径问题
必须用二维数组记录
// java版本
import java.util.*;
public class Main {
public static int knapsack(int [] wt, int [] val,int W) {
int N = val.length;
int [][] dp = new int[N+1][W+1];
//base case 初始化 限重为0的背包最大价值
for(int i = 0;i <= N;i++){
dp[i][0] = 0;
}
for(int i = 0;i <= W;i++){
dp[0][i] = 0;
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= W; j++) {
if (j - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
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]);
}
}
}
//物品是否被选择
int[] x = new int[N+1];
//从状态矩阵右下角的最优值开始往前回溯
for(int i=N;i>1;i--) {
//如果当前状态的解等于上一个状态的解,表示当前物品没有放进背包
if(dp[i][W] == dp[i-1][W]){
x[i]=0;
}
//如果当前状态的解不等于上一个状态的解,说明当前物品被放进背包
else {
x[i] = 1;
W -= wt[i-1];//当前状态的最优解是通过m[i-1][c-wt[i-1]]+v[i-1]得到的,
//因此下一步从dp[i-1][W-wt[i-1]]开始继续回溯,
//按照同样的方法判断第i-1个物品有没有被装进背包
}
}
//上面的for循环可以判断第2,3,4……个物品是否被装进背包
//因此还需要单独对第一个物品进行判断
//如果经过上面的for循环之后背包剩余容量c能够装下第一个物品
//也就是m[1][c]>0,说明第一个物品肯定被装进了背包
x[1] = (dp[1][W]>0) ? 1 : 0;
for (int i = 1; i <= N; i++) {
System.out.print(x[i]);
}
return 0;
}
public static void main(String[] args) {
int[] w = new int[]{0, 4, 6, 2, 2, 5, 1};
int[] v = new int[]{0, 8, 10, 6, 3, 7, 2};
int N = v.length;
int W = 12;
knapsack(w,v,W);
}
}
二、完全背包
问题描述:
完全背包(unbounded knapsack problem)与01背包不同就是每种物品可以有无限多个:
一共有N种物品,每种物品有无限多个,第i(i从1开始)种物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?
状态 选择:
和0-1背包一样
状态有两个,就是「背包的容量」和「可选择的物品」
选择就是「装进背包」或者「不装进背包」
定义状态dp:
和0-1背包一样
dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
base case
就是dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
- 不装入第i件物品,即dp[i−1][j];
- 装入第i件物品(前提是能装下),即dp[i][j−w[i]] + v[i]。和0-1背包不同!!!
状态转移方程为: 和0-1背包不同!!!
// j >= w[i]
dp[i][j] = max(dp[i−1][j], dp[i][j−w[i]]+v[i])
代码
//二维数组的解法
//代码和0-1背包类似
//只是状态转移方程不同
压缩状态 一维数组优化版
完全背包 第二循环正序
也可进行空间优化,优化后不同点在于这里的 j 只能正向枚举而01背包只能逆向枚举!!!,因为这里的max第二项是dp[i]而01背包是dp[i-1],即这里就是需要覆盖而01背包需要避免覆盖。
代码和0-1背包最大的不同就是正向枚举!!!
int knapsack(int [] wt, int [] val, int W) {
int N = val.length;
int []dp = new int[W+1];
//base case 初始化
for(int i = 0; i <= W;i++){
dp[i] = 0;
}
for (int i = 0; i < N; i++) {
for (int j = 0; j <= W; j++) {
if (j - wt[i] >= 0) {
// 不装或装入背包,择优
dp[j] = max(dp[j], dp[j - wt[i]] + val[i]);
}
}
}
return dp[W];
}
例题:
leetcode
322. 零钱兑换
题目:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。
518. 零钱兑换 II
题目:给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
三、多重背包
多重背包(bounded knapsack problem)与前面不同就是每种物品是有限个:一共有N种物品,第i(i从1开始)种物品的数量为n[i],重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?
我们从装入第 i 种物品多少件出发:装入第i种物品0件、1件、…n[i]件(还要满足不超过限重 j/w[i])。
k为装入第i种物品的件数, k <= min(n[i], j/w[i])
所以状态方程为:
dp[i][j] = max{(dp[i-1][j − kw[i]] + kv[i]) for every k}
同理也可以进行空间优化,而且 j 也必须逆向枚举,优化后伪代码为
// 完全背包问题思路二伪代码(空间优化版)
dp[0,...,W] = 0
for i = 1,...,N
for j = W,...,w[i] // 必须逆向枚举!!!
for k = [0, 1,..., min(n[i], j/w[i])]
dp[j] = max(dp[j], dp[j−k*w[i]]+k*v[i])
总结
以上为背包问题的基本知识 详情可了解