给定n个容量为 W 1 W_1 W1, W 2 W_2 W2, W 3 W_3 W3, 。。。 W n W_n Wn,价值为 V 1 V_1 V1, V 2 V_2 V2, V 3 V_3 V3, … V n V_n Vn的物品和容量为C的背包,求这个物品中一个最有价值的子集,使得在满足背包的容量的前提下,包内的总价值最大。
为什么要叫做0-1背包: 因为对每个物品而言,只有两种选择,盘它或者不盘,盘它记为1,不盘记为0,我们不能将物品进行分割,比如只拿半个是不允许的。这就是这个问题被称为0/1背包问题的原因。
int maxValue(const std::vector<int>& w, const std::vector<int>& v, int bag){
}
解决这个问题没有什么巧妙的算法,只能穷举所有的可能。因此解决这类问题,直接根据套路走流程即可。
先从左往右依次尝试。举个例子,比如有物品[0, 1, 2],那么对每一个物品都去拿或者不拿
class Solution {
public:
// 前提:w,v都是正数 或者 零
// bag为背包容量,不能超过这个载重
// 返回:不超重的情况下,能够得到的最大价值
int maxValue(const std::vector<int>& w, const std::vector<int>& v, int bag){
// 去掉不合法的参数
if(w.empty() || v.empty() || w.size() != v.size() || bag < 0){
return 0;
}
// 写一个尝试函数
return process(w, v, 0, bag);
}
private:
// 当前考虑到了index号货物,从index....所有的货物可以自由选择(index就当做没有了)
// 但是做的选择不能超过背包容量
// 返回:最大价值
int process(const std::vector<int>& w, const std::vector<int>& v, int index, int rest){
// base case:
// 背包容量已经小于0,那么从当前index开始之后的货物都不能选择了,因为背包一定超重
if(rest < 0){ // 为什么不需要等于0. 因为根据题意,允许货物重量0
return 0;
}
// 当没有货物了,价值是0
if(index == w.size()){
return 0;
}
// 当前有货物,而且背包有容量
// 尝试1:不要当前的货物
int p1 = process(w, v, index + 1, rest);
// 尝试2:要当前的货物
int p2 = 0;
if(rest >= w[index]){ // 只有在背包容量大于当前货物容量时,才可以去要当前的货物
p2 = v[index] + process(w, v, index + 1, rest- w[index]) ;
}
// 上面两种决策中选择一个好的
return std::max(p1, p2);
}
};
int main(){
Solution a;
std::vector<int> w {3, 2, 4, 7};
std::vector<int> v {5, 6, 3, 19};
std::cout << a.maxValue(w, v, 11); // 25
}
(1)先举个例子,看暴力递归有没有重复调用,有,所以可以改成递归
(2)准备一个表。重点关注可变参数的范围
int process(const std::vector<int>& w, const std::vector<int>& v, int index, int rest)
index == w.size()
------0~N
bag < 0
------负数~bag(负数可以过滤)所以,准备一个二维数组:
std::vector<std::vector<int>> dp(N + 1, std::vector<int>(bag + 1));
(3)返回值,关注主函数是怎么调用的:
return process(w, v, 0, bag);
所以,应该返回dp[0][bag]
(4)接下来就该填表了
怎么填写呢?举个例子,然后根据上面的暴力递归
process(w, v, index + 1, bag - w[index]) ;
class Solution {
public:
// 前提:w,v都是正数 或者 零
// bag为背包容量,不能超过这个载重
// 返回:不超重的情况下,能够得到的最大价值
int maxValue(const std::vector<int>& w, const std::vector<int>& v, int bag){
// 去掉不合法的参数
if(w.empty() || v.empty() || w.size() != v.size() || bag < 0){
return 0;
}
int N = w.size();
std::vector<std::vector<int>> dp(N + 1, std::vector<int>(bag + 1, 0));
for (int index = N - 1; index >= 0; index--) { // 从最后一个物品开始看
for (int rest = 0; rest <= bag; rest++){ // 背包容量
int p1 = dp[index + 1][rest];
int p2 = 0;
if(rest >= w[index]){
p2 = v[index] + dp[index + 1][rest - w[index]];
}
dp[index][rest] = std::max(p1, p2);
}
}
return dp[0][bag];
}
};
(1)确定状态
(3)初始状态和边界情况
(4)计算顺序
小结:
(0)分析:当前有三个变化维度:第N个物品,背包的容量W、总的背包的价值V。
(1)这个问题的最优子结构
(2)推导状态转移方程。。要推导状态转移方程,就是要推导最优子结构和最终问题有什么关系呢?也就是说,3个金矿的最优选择和4个金矿的最优选择之间,是什么样的关系?
(3)问题的边界
(4)总结。当前问题的状态转移方程
F(n, w) = 0(n = 0或者w = 0)
F(n,w)=F(n-1,w),(n>=1,w
F(n,w)=max(F(n-1,w),F(n-1,w-p[n-1]+g[n-1])),(n>=1,w>=weight[n-1])
题目中一般会给定两个数组以及一个背包容量bagweight
vector<int> weight = {1, 3, 4};
vector<int> value = {15, 20, 30};
每次我们都往weight以及value中前插一个数字
vector<int> weight = {0, 1, 3, 4};
vector<int> value = {0, 15, 20, 30};
以补足边界条件没有物品时,或者没有背包容量时的情况
(1)确定dp数组以及下标的含义
(2)确定递归公式。面对第i个物品
(3)dp数组如何初始化
for (int i = 0 ; i < dp.size(); j++) {
dp[i][0] = 0;
}
for (int j = 0 ; i < dp[0].size(); j++) {
dp[0][j] = 0;
}
(4)确定遍历顺序
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])
,我们知道dp[i][j]要靠左上来推导,所以要从左到右,从上到下遍历(5)举例推导dp数组
代码:
class Solution {
public:
void bag_problem(vector<int> weight, vector<int> value, int bagweight) {
if(weight.empty() || bagweight == 0){
return;
}
weight.insert(weight.begin(), 0);
value.insert(value.begin(), 0);
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
int m = dp.size(), n = dp[0].size();
for(int i = 1; i < m; i++) { // 遍历物品
for(int j = 1; j < n; j++) { // 遍历背包容量
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j];
}else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
cout << dp[m - 1][n - 1] << endl;
}
};
前提
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以看出,如果把dp[i - 1]那一层直接拷贝到dp[i]
上,表示完全可以是dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
与其把dp[i - 1]
这一层拷贝到dp[i]
上,不如直接只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组)。
这就是滚动数组的由来。需要满足的条件是上一层可以重复利用,直接拷贝到当前层。
实现
这里还是有两个变化维度,物品、背包容量,所以需要一个双重for循环以枚举所有的可能
二维数组遍历时:
for(int i = 1; i < m; i++) { // 遍历物品
for(int j = 1; j < n; j++) { // 遍历背包容量
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j];
}else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
class Solution {
public:
void bag_problem(vector<int> weight, vector<int> value, int bagweight) {
if(weight.empty() || bagweight == 0){
return;
}
weight.insert(weight.begin(), 0);
value.insert(value.begin(), 0);
vector<int> dp(bagweight + 1, 0);
int m = dp.size();
for(int i = 1; i < m; i++) { // 遍历物品
for (int j = bagweight; j >= weight[i]; --j) { // 遍历背包容量
dp[j] = std::max(dp[j], dp[j - weight[i]] + value[i] );
}
}
cout << dp[bagweight] << endl;
}
};
在解决问题之前,为了描述方便,我们先定义一些变量,把背包问题抽象化:
明确要解决的问题,也就是说我们要操作N个物品,选出能够达到最大价值的最佳组合:
明确约束条件
对于第i个物品,有两种可能
总结:
V ( i , j ) = { V ( i − 1 , j ) , W i > j m a x ( V ( i − 1 , j ) , V i + V ( i − 1 , C − W i ) ) , W i < = j V(i, j) = \begin{cases} V(i-1, j) ,\,\, W_i >j\\ max(V(i-1, j), V_i + V(i-1, C- W_i)),\,\,W_i <=j\\ \end{cases} V(i,j)={V(i−1,j),Wi>jmax(V(i−1,j),Vi+V(i−1,C−Wi)),Wi<=j
package main
import "fmt"
func max(x, y int)int {
if x > y {
return x
}
return y
}
var v [5]int = [5]int {0,2,4,3,7};
var w [5]int = [5]int {0,2,3,5,5 };
const N int = 4 // 物品个数
const C = 10 // 背包大小
var dp_table [N + 1][C + 1 ]int ; // N + 1 和 C + 1是为了补充当背包装入物品 / 背包容量为0 时的状态
func show_table() {
for j := 0; j <= C ; j++ {
fmt.Printf("%5d",j)
}
fmt.Println("\t | \n-------------------------------------------------------------------------")
for i := 0; i <= N ; i++ {
for j := 0; j <= C ; j++ {
fmt.Printf("%5d",dp_table[i][j])
}
fmt.Println("\t | ", i)
}
}
func findMax() {
for i := 1; i <= N ; i++ { // 当面对第i号物品时
for j := 1; j <= C ; j++ { // 背包容量 1,2,3。。。。。
//dp_table[i][j]表示当背包容量为j时,前j个物品组合起来的最大价值
if j < w[i] { // 装不下时
dp_table[i][j] = dp_table[i-1][j]; // 背包容量为j时,前i个物品的最大价值就是前i-1个物品的最佳价值。 因为i从1开始,所以不用担心越界
}else{ // 装的下
dp_table[i][j] = max(dp_table[i-1][j], v[i] + dp_table[i - 1][j - w[i]]) // 装和不装之间的方案选择一个
// j - w[i] 表示装了i之后,容量减少的量。因为装得下,所以j - w[i]的最小值为0[也就是刚好装得下],所以不用担心数组越界
}
}
}
}
func main() {
findMax()
show_table()
}
动态规划求解如下:
package main
import "fmt"
const N=4
const W=10
var weight[]int=[]int {0, 2 , 4 , 3 , 7}
var val[] int = []int{0, 2, 3, 5, 5}
var record[N + 1][W+1]int //保存中间结果
func show(){ //初始化背包结果
fmt.Print(" |")
for j:=0;j<=W;j++{
fmt.Printf("%5d",j)
}
fmt.Println("\n---------------------------------------------------------------")
for i:=0;i<=N;i++{
fmt.Printf("%d |", i)
for j:=0;j<=W;j++{
fmt.Printf("%5d", record[i][j])
}
fmt.Println()
}
fmt.Println("**************************************************************")
}
//两个数之间最大值
func max(a int ,b int)int{
if a>b{
return a
}else{
return b
}
}
func findMax()int{
for i:=1;i<=N;i++{
for j:=weight[i];j<=W;j++{
record[i][j]=max(record[i-1][j],record[i-1][j-weight[i]]+val[i])
}
}
return record[N][W]
}
func main() {
show();
fmt.Println(findMax()) // 10
show();
}
在解决问题之前,为了描述方便,我们先定义一些变量,
我们先来明确一些问题:
- F ( 0 , C ) = 0 F(0, C)=0 F(0,C)=0的意思是:当没有物品放入背包时,不管背包容量多少,其最大价值为0
- F ( n , 0 ) = 0 F(n, 0)=0 F(n,0)=0的意思时:当背包容量为0时,最大价值一定是0【因为没地方放】
那这里的递推关系式是怎样的呢?对于第i个物品,有两种可能【放得下、放不下】:
举个例子:对于F(2, 5),如果下标2的物品容量为6,大于背包容量,那就不能装了,它的价值一定和F(1, 5)时一样的
也就是说,当第n件物品放不下时,最大价值就是
F ( i , j ) = F ( i − 1 , j ) F(i, j) = F(i - 1, j) F(i,j)=F(i−1,j)
在背包容量足够的前提下, 对于当前物品,有两种选择:【放、不放】
- 如果选择不放, 问题就转化为将n-1件物品放入容量为j的背包中, 可以得到的最大价值为 F ( i − 1 , j ) F(i-1, j) F(i−1,j)
- 如果选择放, 那么问题就转换为将i-1个物品放入剩余容量为 C − W i C-W_i C−Wi的背包中的问题, 此时获取的最大价值为: V i + F ( i − 1 , C − W i ) V_i + F(i-1, C- W_i) Vi+F(i−1,C−Wi)
- 为什么 C − W i C- W_i C−Wi?因为我们确定了要装i,所以必须至少留出 W i W_i Wi个空间
也就是说,当物品能够装得下时,我们应该在装和不装之间这两种方案中选择最优的一个,也就是
F ( i , j ) = m a x ( F ( i − 1 , j ) , V i + F ( i − 1 , C − W i ) ) F(i, j) = max(F(i-1, j), V_i + F(i-1, C- W_i)) F(i,j)=max(F(i−1,j),Vi+F(i−1,C−Wi))
原问题:将n件物品放入容量为c的背包,得到的最大价值。
子问题是:将i件物品让如容量为j的背包的,得到的最大价值。
package main
import "fmt"
func max(x, y int)int {
if x > y {
return x
}
return y
}
var w []int = []int {0, 2 , 4 , 3 , 7 }; // 体积
var v []int = []int {0, 2 , 3 , 5 , 5 }; // 价值
// findMax( i, c int) c表示剩下的背包容量,i表示还剩下的物品数量,也是物体的下标
func findMax(i, c int) int {
result := 0;
if (i == 0 || c == 0){ // 背包容量为0,或者没有物体了
return result
}
if(w[i] > c){ // 装不下
result = findMax(i-1, c) // 当前物品已经排除,需要寻找把i-1个物品放入c的最优选择
} else { // 可以装下
tmp1 := findMax(i-1, c); // 选择要
tmp2 := findMax(i-1, c-w[i]) + v[i]; // 还是不要
result = max(tmp1, tmp2); // 的最优选择
}
return result;
}
func main() {
c := findMax(4, 10) // 10
fmt.Print(c)
}
分治改进
package main
import "fmt"
const N=4
const W=10
var weight[]int=[]int {0, 2 , 4 , 3 , 7}
var val[] int = []int{0, 2, 3, 5, 5}
var record[N + 1][W+1]int //保存中间结果
func init(){ //初始化
for i:=0;i<=N;i++{
for j:=0;j<=W;j++{
record[i][j]=-1
}
}
}
func show(a, b int){ //初始化背包结果
fmt.Print(" |")
for j:=0;j<=W;j++{
fmt.Printf("%5d",j)
}
fmt.Println("\n---------------------------------------------------------------")
for i:=0;i<=N;i++{
fmt.Printf("%d |", i)
for j:=0;j<=W;j++{
if i == a && b == j {
fmt.Printf("%c[1;40;32m%5d%c[0m", 0x1B, record[i][j], 0x1B)
}else{
fmt.Printf("%5d", record[i][j])
}
}
fmt.Println()
}
fmt.Println("**************************************************************")
}
//两个数之间最大值
func max(a int ,b int)int{
if a>b{
return a
}else{
return b
}
}
//i是已经有的重量,total总量
func findMax(i int ,total int )int {
result:=0//结果
if i> N{
return result
}
if record[i][total]!=-1{ //如果数据已经记录,直接返回
fmt.Println("\n\n",i, total, ":")
show(i, total);
return record[i][total]
}
if weight[i]>total{
record[i][total]=findMax(i+1,total) //当前物品大于总量,跳出,计算下一个
}else{
record[i][total]=max(findMax(i+1,total),findMax(i+1,total-weight[i])+val[i])
}
fmt.Println("\n\n",i, total, ":")
show(i, total);
return record[i][total]
}
func main() {
show(-1, -1);
fmt.Println(findMax(0, W)) // 10
}
我们知道,我们必须探究每一个物品放入还是不放入背包的可能性。
我们用一个表来记录已经算过的价值
假设有四个物品,它们的价值和体积如下图所示:
接下来,我们来一一探究
对于表格:
0-1背包问题可以看成是二叉树的深度优先搜索
初始时, 有F(10, 0), 表示当前背包容量为10,当前背包里物品价值为0。
然后,我们从这些结果中,找出价值最大的那个,也就是13,这就是我们的最优选择,根据这个选择,依次找到它的所有路径,便可以知道该选哪几个珠宝,最终结果是:4,2,1。
这个也可以看成是一颗二叉决策树的深度遍历
滚动数组
假设我们外层遍历物品,内层是背包容量。 假设我们是从左到右,从上到下遍历
初始时:
装载第1个物品时,其重量是2,价值是2。
装载第2个物品时,其重量是3,价值是4。 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
1
,可选物品1(重量2,价值2),物品2(重量3,价值4)】.当前正在看物品2
3
超过了背包容量1
,一定不能装2,因此情况变为【背包容量为1,可选物品1(重量2,价值2)】6
,可选物品1(重量2,价值2),物品2(重量3,价值4).当前正在看物品2】
6
大于物品2的重量3
。因此可以选择装物品2和不装物品2假设我们外层遍历物品,内层是背包容量。 假设我们是从右到左,从上到下遍历
当给了物品1(重量2,价值2)时:
当给了物品1(重量2,价值2),物品2(重量3,价值4)
当给了物品1(重量2,价值2),物品2(重量3,价值4),物品3(重量3,价值5),面对物品3
假设我们外层遍历容量,内层是背包容量。 背包容量一定是倒序遍历。现在我们来选择物品
动态规划无非就是状态+选择。所以,第一步,要明确两点:【状态】和【选择】
明确了状态和选择,动态规划问题基本上就解决了一般,只要往下面这个框架里套就行了
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
第二步:明确dp数组以及索引i的定义
dp[i][w]
:
w
,这种情况下可以装的最大价值是dp[i][w]
dp[3][5] = 6
,含义为:对于给定的一系列物品中,如果只对物品num[0]、nums[1]、nums[2]、nums[3]进行选择,当背包容量为5时,最多可以装下的价值是6dp[N][W]
,也就是选择了N个物品,背包容量为W时的最大价值dp[0][..] = dp[..][0] = 0
,因为没有物品或者背包没有空间的时候,能装的最⼤价值就是 0。细化上面的框架
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装进背包]和[不把物品i装进背包]怎么用代码体现出来呢?这就要结合我们对dp数组的定义和我们的算法逻辑来分析了。
先看我们对dp数组的定义:
dp[i][j]
表示:对于物品[0…i],当前背包容量为w
时,能够装下的最大价值是dp[i][i]
那么,对于第i个物品:
dp[i][w] = dp[i - 1][w]
。
dp[i][w] =dp[i-1][w- wt[i]] + val[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]] + val[i]
)
return dp[N][W]
最后⼀步,把伪码翻译成代码,处理⼀些边界情况。
class Solution {
public:
void bag_problem(vector<int> weight, vector<int> value, int bagweight) {
if(weight.empty() || bagweight == 0){
return;
}
weight.insert(weight.begin(), 0);
value.insert(value.begin(), 0);
vector<vector<int>> dp(weight.size(), vector<int>(bagweight + 1, 0));
int m = dp.size(), n = dp[0].size();
for(int i = 1; i < m; i++) { // 遍历物品
for(int j = 1; j < n; j++) { // 遍历背包容量
if (j < weight[i]) {
dp[i][j] = dp[i - 1][j];
}else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
}
cout << dp[m - 1][n - 1] << endl;
}
};
⾄此,背包问题就解决了
彻底理解0-1背包问题
背包问题9讲解
【动态规划】01背包问题(通俗易懂,超基础讲解)
【动态规划】01背包问题—》 推荐