DAY45:动态规划(五)背包问题:01背包理论基础+二维DP解决01背包问题

news/2025/1/3 8:25:11/

文章目录

    • 背包问题大纲
    • 01背包
    • 01背包暴力解法
    • 01背包二维DP解法
      • 二维DP数组的解法
        • DP数组含义
        • 递推公式
        • 初始化二维DP数组(比较重要)
        • 遍历顺序(比较重要)
      • 二维DP数组完整版
        • 思路总结
        • 返回值为什么是二维数组最后一个元素
        • DP推导过程与数组含义进一步理解
        • 递推公式理解

对于面试的话,掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包

背包问题大纲

在这里插入图片描述
leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。

而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。

所以背包问题的理论基础重中之重是01背包

leetcode上没有纯背包题目,都是用背包问题的思想去解决和应用,也就是需要转化为01背包问题

01背包

01背包是有N种物品,每种物品只有一个。完全背包是有N种物品,每种物品有无限个

多重背包是有N种物品,每种物品个数各不相同。

这几类问题主要体现在物品个数不同

纯粹的01背包问题如下所示:

有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i]每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

在这里插入图片描述
在这里插入图片描述

01背包暴力解法

每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是 O(2^n),这里的n表示物品数量。

暴力的解法是指数级别的时间复杂度。所以才需要动态规划的解法来进行优化

#include<bits/stdc++.h>
using namespace std;struct Item {int weight;int value;
};int maxValue = 0;
Item items[3] = {{1, 15}, {3, 20}, {4, 30}};
int n = 3;
int capacity = 4;void backtrack(int i, int totalWeight, int totalValue) {if (i == n || totalWeight == capacity) {if (totalValue > maxValue) maxValue = totalValue;return;}backtrack(i + 1, totalWeight, totalValue);if (totalWeight + items[i].weight <= capacity)backtrack(i + 1, totalWeight + items[i].weight, totalValue + items[i].value);
}int main() {backtrack(0, 0, 0);cout << "最大价值是:" << maxValue << endl;return 0;
}

backtrack 函数尝试包括或不包括当前位置的物品,如果包括,那么将它的价值和重量加到总价值和总重量中。在每次选择后,程序递归到下一个物品。如果到达物品列表的末尾,或者已经达到背包的最大容量,该函数会检查是否找到了更高的价值,并更新全局最大值(maxValue)。

01背包二维DP解法

二维DP数组的解法

我们先用二维数组的方式去求解。

DP数组含义

我们先定义一个二维DP数组dp。dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

递推公式

确定递推公式的时候,我们要确定dp[i][j]可以由哪几个方向推出来。当前背包的状态就取决于是不是放入物品i。放入物品i是一个状态,不放物品i又是另一个状态。

  • 如果不放物品i背包容量为j,那么背包当前的最大价值为:dp[i-1][j],也就是背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
  • 放物品i,那么放物品i的最大价值,就是背包容量-物品i的容量所能放的最大价值+物品i的价值dp[i-1][j-weight[i]]+value[i]

放物品i和不放物品i的两个值,取最大值,就是dp[i][j]对应的遍历到i情况下的最大价值

//递推公式
dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);

初始化二维DP数组(比较重要)

关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱

首先由递推公式可以看出,dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]),因此为了避免数组下标越界,必须满足i>=1,i=0的情况需要单独分析;也就是dp[0][j]需要进行初始化

同时j-weight[i]因为是不固定数值,因此for循环里单独写if(j<=weight[i]) dp[i][j]=dp[i-1][j]即可。从这个角度出发,我们为了dp[i][j]=dp[i-1][j]这个式子成立,也需要对dp[0][j]进行初始化

关于dp[i][0]是否需要初始化的问题,实际上因为j会对if(j<weight[i])单独做判断,因此没有必要初始化dp[i][0],j从0开始是完全可以的。

DP数组情况如下图所示。

在这里插入图片描述

  • 初始化的时候注意dp数组的定义,dp数组定义是第i个东西放进背包时,背包的最大价值为dp[i],因此当物品i为0的时候,初始化第一行dp[0][j]就要看物品0放进去的时候,背包有多少价值
  • 同理,初始化第一列的时候,也是看背包容量为0的时候,对应价值是多少(全是0)
  • 实际上只初始化i=0就可以了,j=0不需要初始化因为遍历会单独写条件
vector<vector<int>>dp(n+1,vector<int>(n+1,0));//全部初始化为0
for(int j=0;j<=bagweight;j++){if(j<weight[0]) dp[0][j] = 0;//如果背包容量小于放入物品0的weight,dp[i][j]=0elsedp[0][j]=value[0];
}

遍历顺序(比较重要)

背包类的题目要有两层for循环。一个for遍历物品i,另一个遍历背包容量j。

实际上,对于二维DP数组实现的01背包这两层for循环的内外嵌套是可以颠倒的。也就是说先遍历物品/背包都是可以的。

我们重新观察这个DP数组,可以发现,因为遍历到i的时候,要么dp取值是放入i,要么是不放入i。因此i的状态是由i上方的元素和左上方的元素决定的。(图中红色三角和绿色三角)

在这里插入图片描述
也就是说,我们遍历到i的时候,需要保证i的左上方和正上方都有数值。因此我们把两个先后顺序进行举例,如下图:(橙色三角是已经初始化的部分)

在这里插入图片描述
可以看出,无论是先遍历背包还是先遍历物品,都能保证当前遍历元素的左上方和正上方都有数值。因此两种遍历方式都可以。

二维DP数组完整版

背包最大重量为4。

物品重量和价值为:

在这里插入图片描述
问背包能背的物品最大价值是多少?

  • 递推公式思想同样是,对于每一个背包大小,都计算出当前背包大小能存放物品的最大价值,那么遍历到指定背包大小的时候,也是最大价值。DP递推公式对于每一个背包数值都适用。
  • 递推公式的大小比较,是已经装了上个物品的状态和能够装下当前物品,并且已经装了当前物品的状态比较。因此if(j<weight[i])这个判断只是在看当前的背包容量能不能给新来的物品腾地方,只要j>=weight[i]+1,就说明能给当前物品腾出位置。
#include <bits/stdc++.h>
using namespace std;//传入的是背包容量与物品重量和价值
int knapsack(vector<int>& weight, vector<int>& value, int bagWeight) {int n = weight.size();//获取物品数量//创建i*j二维数组,i是物品,j是背包容量vector<vector<int>>dp(n+1,vector<int>(bagWeight+1,0));//二维数组初始化for(int j=0;j<=bagWeight;j++){if(j<weight[0]) continue; //保持0的数值elsedp[0][j] = value[0];//第一行物品0的情况,也就是i=0那一行的情况}//遍历物品和背包for(int i=1;i<n;i++){for(int j=0;j<=bagWeight;j++){//看看当前的背包容量能不能给新来的物品腾地方,只要j>=weight[i]+1,就说明能给当前物品腾出位置,当前物品不是在和装了上个物品的状态比较,而是在和没有当前物品的状态比较if(j<weight[i]) dp[i][j]=dp[i-1][j];/else{//放这个i和不放这个i的情况对比,选最大的dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);}}}return dp[n-1][bagWeight];
}void test_knapsack() {vector<int> weight = {1, 3, 4};vector<int> value = {15, 20, 30};int bagWeight = 4;cout << knapsack(weight, value, bagWeight) << endl;
}int main() {test_knapsack();
}

思路总结

对于每一种物品(纵轴上的索引i),对于每一个背包大小(横轴上的索引j),都计算出当前背包大小能存放物品的最大价值。如果当前背包的容量无法装下物品i,那么dp[i][j]的值就等于dp[i-1][j],否则,需要在“不放入物品i”和“放入物品i”这两种选择中选取价值最大的,即max(dp[i-1][j], dp[i-1][j - weight[i]] + value[i])

这样,遍历到指定背包大小的时候,得到的就是当前背包可以装下物品的最大价值。所以说,这个DP递推公式对于每一个背包数值都适用

我们需要时刻注意,遍历到每一个物品的时候dp[i][j]表示的都是:在考虑前i个物品,并且背包容量为j的情况下,背包可以装下的最大价值

返回值为什么是二维数组最后一个元素

到了最后,就是考虑第[0–n]个物品,背包为j的时候,可以装下的最大价值。因此最后的结果,就是二维数组的最后一个元素。(考虑到了最后一个物品)。这也是DP数组含义相关的问题。

DP推导过程与数组含义进一步理解

当我们不清楚返回值or不清楚递推逻辑的时候,一定要先回去看DP数组与其关联下标的含义

本题中,DP数组的含义就是,考虑第[0–i]个物品,背包容量为j的情况下,dp[i][j]代表的就是当前背包的最大价值

物品个数为3,容量为4,对应value是表格中的情况,DP数组预期如下:

在这里插入图片描述
在动态规划的过程中,对于每一个背包的大小(j)和每一个物品(i),我们需要决定是否将物品i放入背包。这里的决策基于两种情况的比较:

  1. 不放入物品i:这种情况下,背包的价值等于没有考虑物品i时,只考虑前i-1个物品,背包容量为j时候,背包的最大价值,即dp[i-1][j]。(看DP数组的定义)
  2. 放入物品i:这种情况下,只有当背包的容量j大于等于物品i的重量weight[i]时,才能将物品i放入背包。放入物品i后,背包的价值等于考虑前i-1个物品,且背包剩余容量j-weight[i]时的最大价值加上物品i的价值,也就是dp[i-1][j-weight[i]]+value[i]。(也是通过DP数组含义推算得到)

递推公式理解

只要 j >= weight[i] ,就说明背包的容量足以容纳当前的物品。

if (j < weight[i]) 这个判断在检查当前背包的容量是否足以装下当前考虑的物品。如果背包的容量不足以装下当前的物品,那么 dp[i][j] 的值就应当等于没有装入当前物品,背包容量为j,且只装入前 i - 1 个物品时的最大价值,即 dp[i - 1][j]

如果 j >= weight[i],说明背包的容量足以装下当前的物品。此时,我们需要在两种选择之间取最优:一种是选择不装入当前的物品,背包的总价值即为 dp[i - 1][j];另一种是选择装入当前的物品,然后在剩余的背包容量中尽可能装入更多价值的物品,此时的背包总价值即为 dp[i - 1][j - weight[i]] + value[i]dp[i][j] 即为这两种选择中的最大值,这就是动态规划的状态转移方程。


http://www.ppmy.cn/news/849377.html

相关文章

微信朋友群自动点赞

我一直都不太喜欢给别人点赞&#xff0c;某一年(貌似是17年)微信出了一次朋友圈年报&#xff0c;那一整年我就点出去了几个赞&#xff0c;要知道当时我微信好友应该有300。我觉得这是我不喜欢参与社交活动在网络世界的一种体现吧。不给别人点赞也没啥坏处&#xff0c;但你不评不…

微信评论测试点

功能 1.点击发表评论能否正常弹出输入框&#xff0c;输入框内是否显示默认文字&#xff1b; 2.正常评论的输入内容限制&#xff08;中文&#xff0c;英文&#xff0c;数字&#xff0c;字符等&#xff09;&#xff0c;能否输入特殊字符&#xff0c;表情&#xff0c;图片&#x…

微信打字的隐藏鸿蒙系统,为什么有些人微信聊天只打字不发语音?

我就是这种人&#xff0c;简单从我的角度分析一下。 1&#xff0c;同样一个意思。文字比语音意思更容易表达清楚&#xff0c;“阅读”时间更短。对于接收方体验更好。 2&#xff0c;大家都碰到过60秒语音。对于接收方有多痛苦不言而喻。我不喜欢&#xff0c;别人肯定也不喜欢。…

微信推出“腾讯电子签”具有提醒对方还钱

有没有过这种尴尬&#xff0c;借人钱之后&#xff0c;不好意思开口要回来。别急&#xff0c;试试这个催收不用自己来。其实是一个名为【腾讯电子签】的小程序&#xff0c;主要用于管理各种收据、双方签订租房合同等等。据说&#xff0c;这个功能在朋友间借款的时候&#xff0c;…

1v1微信聊天测试点

1v1微信聊天测试点 在不同的测试策略的维度上进行分析 1 UI角度 ui是否符合需求是否美观 2 功能角度 2.1 入口 聊天界面的入口 2.2 出口 聊天界面的出口 2.3 发送信息 发送纯文字&#xff0c;图片&#xff0c;文件&#xff0c;表情&#xff0c;语音、视频,文字表情消息…

Kotlin高仿微信-第11篇-单聊-语音

Kotlin高仿微信-项目实践58篇详细讲解了各个功能点&#xff0c;包括&#xff1a;注册、登录、主页、单聊(文本、表情、语音、图片、小视频、视频通话、语音通话、红包、转账)、群聊、个人信息、朋友圈、支付服务、扫一扫、搜索好友、添加好友、开通VIP等众多功能。 Kotlin高仿…

你吐槽过微信语音消息吗?

微信作为一款国民 APP&#xff0c;日活 10 亿&#xff0c;不夸张的说&#xff0c;很多的人生活已经不能离开微信了&#xff0c;但在使用微信的同时也存在着一些让人不是那么满意的地方&#xff0c;今天我们来聊聊微信语音的问题。 先来看下大家是怎么吐槽微信语音消息的。 在我…

怎么把mp3转发微信语音发出去,从技术角度分析可行性

有什么需要帮助的&#xff0c;看不明白的 &#xff0c;可以加微信 258032791 做微信营销的朋友&#xff0c;很多喜欢做群营销&#xff0c;个人营销&#xff0c; 这个时候&#xff0c;如果人工一句句话去说&#xff0c;肯定能累个半死&#xff0c;如果每天应付几百人 能把你累…