背包问题(从递归回溯到动态规划)(01背包和完全背包)

动态规划

方法论

1.确定dp数组以及下标的含义

2.确定递推公式

3.dp数组如何初始化

4.确定遍历顺序

5.举例推导dp数组

背包问题的模板

背包问题分类:

常见的背包类型主要有以下几种:
1、0/1背包问题:每个元素最多选取一次
2、完全背包问题:每个元素可以重复选择
3、组合背包问题:背包中的物品要考虑顺序
4、分组背包问题:不止一个背包,需要遍历每个背包

而每个背包问题要求的也是不同的,按照所求问题分类,又可以分为以下几种:
1、最值问题:要求最大值/最小值 -》dp[j] = max(dp[j], dp[j - nums[i]] + 1)或者dp[j] = max(dp[j], dp[j - nums[i]] + nums[i])
2、存在问题:是否存在…………,满足…………-》dp[j] = dp[j] || dp[j - nums[i]]
3、组合问题:求所有满足……的排列组合-》dp[j] += dp[j - nums[i]]

分类解题模板

背包问题大体的解题模板是两层循环,分别遍历物品nums和背包容量target,然后写转移方程,
根据背包的分类我们确定物品和容量遍历的先后顺序,根据问题的分类我们确定状态转移方程的写法

首先是背包分类的模板
1、0/1背包:外循环nums,内循环target,target倒序且target>=nums[i];
2、完全背包:外循环nums,内循环target,target正序且target>=nums[i];
3、组合背包:外循环target,内循环nums,target正序且target>=nums[i];
4、分组背包:这个比较特殊,需要三重循环:外循环背包bags,内部两层循环根据题目的要求转化为1,2,3三种背包类型的模板

然后是问题分类的模板
1、最值问题: dp[i] = max/min(dp[i], dp[i-nums]+1)或dp[i] = max/min(dp[i], dp[i-num]+nums);
2、存在问题(bool):dp[i]=dp[i]||dp[i-num];
3、组合问题:dp[i]+=dp[i-num];

背包问题

01背包

题目要求:有N件物品和一个最多能背W的背包,每一件物品的重量无w[i],价值为v[i]。每一件物品只能用一次,求解将哪些物品放入背包中可以使得背包中的物品价值总量最大。

第一种思路应该是暴力递归,只有N件物品,每一件物品只有一件,所以每一件物品只有两种状态:选或者不选。

(回溯)
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, W;
int w[N], v[N];// 物体的重量和价值
int dp[N][N];  
int ans;

void dfs(int index, int val) 
{
   
    if (W < 0) return ;// 如果背包不能够剩余的空间就直接返回
    
    if (index == n + 1) {
   
        if (val > ans) {
   
            ans = val;
        }
        return ;
    }
    
    //不选第index件物品
    dfs(index + 1, val);
    
    // 选择第index件物品
    val += v[index];// 将加上相应的价值
    W -= w[index];  // 减去相应的重量
    dfs(index + 1, val);
    W += w[index];  // 撤销
    val -= v[index];
}

int main()
{
   
    cin >> n >> W;
    for (int i = 1; i <= n; i ++) cin >> w[i] >> v[i];
    
    int val = 0;
    dfs(1, val);

    cout << ans << endl;
    return 0;
}

第二种思路就是动态规划,因为背包容量为W的背包可以拆解成更小的背包,w1 , w2,w3 …所以根据大问题化成小问题的思想就可以使用动态规划。并且01背包问题,相较前面的问题而言多了一个约束条件,就是不仅要将物品放入背包中,背包也是有容量的,由于这个约束条件所以才需要挑选,否则每次只要选择价值最大的物品即可。正因为这个有容量的考虑,所以这次就不可以只用一个一维的数组来保存前面的计算的结果,还要在多加一维数组来记录背包的容量。

1.dp数组的含义

dp[i][j]表示前i个物品中任意选取,放入容量为j的背包,可以得到的最大价值总和为多少。

2.递推公式

将前i个物品这个大问题拆解成小问题,即只讨论第i个物品,前i - 1个默认已经递推计算完成了。

对于第i个物品来说只有两种状态:

1.背包中不放第i个物品,那么前i个物品中的价值总和前i - 1个物品中的价值总和相等。即dp[i][j] = dp[i - 1][j]

2.背包中方如第i个物品,那么前i个物品中价值总和等于第i个物品的价值加上前i - 1个物品中选择任意物品放入背包容量为W - w[i](背包的容量因为放入第i个物品而缩小了)。

在上面两者中选择最大值,即dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i])

小技巧:添加第一列空背包容量即没有背包已将满了这是必须要的,因为当背包容量为0的时候需要加dp[i][0] (等于0)。但是如果加上上面的第一行(即空背包),可以简便计算。因为递推公式中:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);都需要用到上一层数组中的数字,但是如果不加第一行的话,因为第0行没有上一层,所以就需要先给第一行初始化,在使用递推公式。如果加上第一行的话,默认空背包的时候没有价值,这样就可以不用初始化了,因为也可以使用递推公式推出来了。

3.初始化dp数组

3.1

ps:如果不加第一行需要初始化第0行的代码:

for (int i = W; i >= w[0]; i --) {
   // 逆序初始化
    dp[0][i] = dp[0][i - w[0]] + v[0];
}

3.2

如果使用第一行的dp数组,第一行全部初始化为0即可。

4.遍历顺序

如果是最常规的遍历方法可以根据递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);可知需要用到上一行的计算过的数组,所有从上到下,从左到右的遍历即可。

如果要优化到一维数组的话就必须要从右往左的遍历(下面在优化dp的时候会有详细配图解释)。

5.举例dp

w[i] = {1, 2, 3, 4}

v[i] = {2, 4, 4, 5}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ukvit4kA-1626131653984)(C:\Users\张昊宇\AppData\Roaming\Typora\typora-user-images\image-20210707115427384.png)]

(朴素动规)
#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
int n, W;
int w[N], v[N];
int dp[N][N];

int main()
{
   
    cin >> n >> W;
    for (int i = 1; i <= n; i ++) cin >> w[i] >> v[i];
    
    for (int i = 1; i <= n; i ++) {
   
        for (int j = 1; j <= W; j ++) {
   
            if (j < w[i]) {
   
                dp
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hyzhang_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值