【动态规划篇】01 背包的逆袭:如何用算法装满你的 “财富背包”

简介: 【动态规划篇】01 背包的逆袭:如何用算法装满你的 “财富背包”

开启这场旅行吧,下面我们先简单通过生动的举例来说明一下啥是01背包吧:

一·01背包问题描述:
01 背包问题是一个经典的组合优化问题,可以描述为:给定一个固定容量为C 的背包和n 个物品,每个物品i有其对应的重量wi和vi价值 ,要求在不超过背包容量的前提下,选择若干物品放入背包,使得放入背包中的物品总价值最大。这里的 “01” 表示对于每个物品,我们只有两种选择:放入背包(用 1 表示)或不放入背包(用 0 表示)。

二.01背包问题举例说明:
假设我们有一个容量 C=10 的背包,以及以下 5 个物品:

各个重量价值如图所示:

三.01背包解决思路及例子分析 :
下面我们分别从暴搜,贪心,以及动归三个算法开始去分析,最终会得到动归算法占明显优势:

3.1暴力搜索法(穷举法):
思路:

尝试所有可能的物品组合,计算每种组合的总重量和总价值,找出不超过背包容量且总价值最大的组合。

下面看展示:

对于上述 5 个物品,我们可以考虑所有的组合:

组合 1:不选任何物品,总重量为 0,总价值为 0。

组合 2:只选物品 1,总重量为 2,总价值为 3。

组合 3:只选物品 2,总重量为 3,总价值为 4。

组合 4:选物品 1 和物品 2,总重量为2+3=5 ,总价值为 3+4=7.

以此类推,最终会找出总重量不超过 10 且总价值最大的组合。

然而这样一定好嘛,可想而知当数据特别大肯定就不行了。

缺点:时间复杂度为O(2^n) ,当物品数量 n较大时,计算量巨大,效率极低。

3.2贪心算法:
思路:

有多种贪心策略,常见的有价值优先、重量优先和价值密度(价值 / 重量)优先。

下面我们还是以上面那个图开始举例子说明:

3.2.1价值优先策略:
首先选择价值最大的物品,即物品 5,其价值为 10,但重量为 9。放入背包后,背包容量还剩10-9=1 ,无法再放入其他物品。总价值为 10。

3.2.2重量优先策略:
首先选择重量最小的物品,即物品 1,其重量为 2,价值为 3。然后考虑物品 2,重量为 3,价值为 4,此时背包容量还剩10-2-3=5 。继续考虑物品 3,重量为 4,价值为 5,总重量变为 2+3+4=9,总价值为 3+4+5=12,此时背包容量还剩 1,无法再放入物品 4 或物品 5。总价值为 12。

3.2.3价值密度优先策略:
计算每个物品的价值密度:物品 1 的价值密度为 ,物品 2 的价值密度为 ,物品 3 的价值密度为 ,物品 4 的价值密度为 ,物品 5 的价值密度为 。按照价值密度从大到小排序,先选物品 4,重量为 5,价值为 8,背包容量还剩 5。再选物品 1,重量为 2,价值为 3,背包容量还剩 3。此时无法再放入其他物品,总价值为8+3=11 。

到这里,细心的小伙伴是不是,早就发现,并不是我们预期的,甚至得不到我们想要的答案,因此这种对于一般01背包要精确求取最佳问题的时候就不合适了。

缺点:贪心算法不能保证一定能找到最优解,只能找到近似最优解。

3.3动态规划法:
思路:

使用一个二维数组dp[i][j]表示前 个物品放入容量为i的背包的最大价值j状态转移方程为:

那么下面我们还是根据上面的表模拟一下(dp表还是多开一个空间):

对于dp[i][0] 和 dp[0][j] 都初始化为 0(表示没有物品或背包容量为 0 时的最大价值为 0)

当i==1(也就是第一个物品):

当i==2(也就是第二个物品):

依次类推,填满整个 dp数组,最终 dp[5][10]就是最终答案 .

最终得到的dp[5][10]的值将是该问题的最优解,其时间复杂度为O(nC),空间复杂度为O(nC) ,对于n和 C不是特别大的情况,是一种比较高效的方法。

四.实际应用举例:
下面我们简单说一下关于01背包模版有那些变形应用:

4.1资源分配问题:
假设你是一个工厂的老板,有一笔预算 用于购买新设备,市场上有 种设备,每种设备 有购买成本 和预期收益 ,你需要决定购买哪些设备才能使总收益最大,同时不超过预算,这就是一个 01 背包问题。

4.2任务分配问题:
你是一个项目经理,有一个总的工作时长 ,有 个任务,每个任务 需要耗费的时间为 且能带来的收益为 ,你需要选择哪些任务来完成,以达到总收益最大,这可以转化为 01 背包问题。

4.3盗贼的选择问题:
一个盗贼进入一个房子,他的背包容量有限(比如体积或重量限制),房子里有各种宝物,每个宝物有相应的体积和价值,盗贼要选择哪些宝物带走,能使他带走的宝物总价值最大,这是 01 背包问题的经典场景。

五·01背包经典模版(装满及非装满版本):
下面我们就以动态规划的解法来解答01背包问题吧;以一道模版题为例:

测试用例1:

输入:3 5 输出:14
2 10 9
4 5
1 4

解释:装第一个和第三个物品时总价值最大,但是装第二个和第三个物品可以使得背包恰好装满且总价值最大。

测试用例2:

输入:3 8 输出:8
12 6 0
11 8
6 8

解释: 装第三个物品时总价值最大但是不满,装满背包无解。

牛客网原题链接:【模板】01背包牛客题霸牛客网

下面我们就用动归思想来完成它吧:

5.1题目叙述:

由题目要求可知,此题要通过两个dp状态表示等来解决;一个是小于等于,一个一定等于(对应我们的装满和非装满)。下面我们一个个分析:

非装满状态(二维dp非优化版):
首先我们可以看出它要求的状态有两个(如果我们直接搞dp[i]要么体积无法推导,要么价值无法;故这里我们先假设它是二维dp然后去定义再去推导,看看成不成立)。

因此我们假设:

dp[i][j]表示到i位置时候,背包体积是不超过j时的最大价值。

这个第一问其实是比较容易得,不用一定要装满;下面我们模拟一下过程得出状态转移方程:

得到状态转移方程:

难道这就完了吗?当然没有还要考虑初始化以及有没有越界行为的处理:

因此我们还是多开一行:

分析下标为0的行(也就是价值是0,但是体积不断增大)由定义得出:这的dp就是0.

分析下标是0的列(也就是体积是0,但是价值可以有很多):我们没体积故价值也就是0.

其次还有个极为细节的处理:

想必大家在写状态方程时候就想到了,下标可能越界;也就是我们的j减去i位置的体积是可能存在负数;即当i-1处不超过的体积放入i位置的物品后就超过了;因此我们还要判断一下是否成立如果不,我们就只能选择不放了。

其次就是还要注意我们增加了虚拟节点;要时刻注意原数组与dp数组下标交错时的对应关系。

这样就可以表示了:

int N,V;
cin>>N>>V;
vector>v(N);
//ans1:
for(int i=0;i>v[i].first>>v[i].second;
for(int i=1;i<=N;i++){
for(int j=1;j<=V;j++){
dp[i][j]=max(dp[i-1][j],j>=v[i-1].first?dp[i-1][j-v[i-1].first]+v[i-1].second:0);
}
输出答案dp[N][V]即可。

装满状态(二维dp非优化版):
装满状态相当于比上面的状态加大了一点难度:

下面我们先读题目要求:

这也是需要注意的;当遍历到i位置的选择要么是可以装满状态要么是不能装满两个状态要区分,并注意答案输出:

这里只是把j的含义改成等于即可:

dp[i][j]只是修改了j的含义变成等于j时的最大价值。

这里我们来做个约定:

因为我们无价值肯定dp值是0;但是个当前位置不能装满;也就是我们就不可以是0了;首先可以装满j的价值肯定是个非负数;因此这里如果不能装满我们假设是-1即可咯。

这么一看是不是和刚才非装满大差不多:是的,但是还是要处理细节问题的(由于多了装满的限制),下面我们就分析刚刚的状态转移方程来做细节处理:

这就完了嘛?

当然,还要考虑我们的初始化;肯定是和上面都是0不同了:

我们根据这一题的输出提醒一下:因为我们定义的全局dp;而第二次用就要memset清空dp表了。

我们根据上面的定义初始化dp表有两种写法:

//写法一:
// dp[i][j]=max(dp[i-1][j],//这里是博主采用的最爱的三目运算符完成的
// j>=v[i-1].first&&dp[i-1][j-v[i-1].first]!=-1?
// dp[i-1][j-v[i-1].first]+v[i-1].second:-1);
//写法二:
// // dp[i][j]=dp[i-1][j];//直接先变成上一个(无论是-1还是不是,如果是-1后面选的话符合要求就会替换掉,如果后者前后两种情况都不成立就自然是-1了)
// // if( dp[i-1][j-v[i-1].first]!=-1&&j>=v[i-1].first)
// //dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i-1].first]+v[i-1].second);
// }
最后到了输出答案的时候可千万不要忘了-1代表着输出0哦!!

解答代码:

memset(dp,0,sizeof dp);
// for(int i=1;i<=V;i++)dp[0][i]=-1;//由定义可知,当0个位置处,无法填满背包
// for(int i=1;i<=N;i++){
// for(int j=1;j<=V;j++){
//此外每次都要判断要用到的之前的位置是否能填满,也就是dp值不能是-1才能用否则无意义
//写法一:
// dp[i][j]=max(dp[i-1][j],
// j>=v[i-1].first&&dp[i-1][j-v[i-1].first]!=-1?
// dp[i-1][j-v[i-1].first]+v[i-1].second:-1);
//写法二:
// // dp[i][j]=dp[i-1][j];
// // if( dp[i-1][j-v[i-1].first]!=-1&&j>=v[i-1].first)
// //dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i-1].first]+v[i-1].second);
// }
// }
// cout<<(dp[N][V]==-1?0:dp[N][V])<<endl;

二维下的解答代码汇总:

解法1(普通二维):

include

using namespace std;
const int n=1005;
//dp[i][j]表示到i位置时候,背包体积是不超过j时的最大价值
// int dp[n][n]{0};
// int main()
// {
// int N,V;
// cin>>N>>V;
// vector>v(N);
// //ans1:
// for(int i=0;i>v[i].first>>v[i].second;
// for(int i=1;i<=N;i++){
// for(int j=1;j<=V;j++){
// dp[i][j]=max(dp[i-1][j],j>=v[i-1].first?dp[i-1][j-v[i-1].first]+v[i-1].second:0);
// }
// }
// cout<<dp[N][V]<<endl;
// //ans2:
///dp[i][j]只是修改了j的含义变成等于j时的最大价值;规定无法填满背包为dp值为-1

// memset(dp,0,sizeof dp);
// for(int i=1;i<=V;i++)dp[0][i]=-1;//由定义可知,当0个位置处,无法填满背包
// for(int i=1;i<=N;i++){
// for(int j=1;j<=V;j++){
//此外每次都要判断要用到的之前的位置是否能填满,也就是dp值不能是-1才能用否则无意义
//写法一:
// dp[i][j]=max(dp[i-1][j],
// j>=v[i-1].first&&dp[i-1][j-v[i-1].first]!=-1?
// dp[i-1][j-v[i-1].first]+v[i-1].second:-1);
//写法二:
// // dp[i][j]=dp[i-1][j];
// // if( dp[i-1][j-v[i-1].first]!=-1&&j>=v[i-1].first)
// //dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i-1].first]+v[i-1].second);
// }
// }
// cout<<(dp[N][V]==-1?0:dp[N][V])<<endl;

// return 0;
// }

下面我们来进行滚动数组的优化(这里要改的地方也不多,我们来做一下分析):

非装满状态(一维dp滚动数组优化版):
原理:

这里相当于把上面的dp二维给覆盖式填表了;由于每次要用到i-1位置的dp值故这里采取的是从右到左的覆盖式填表;可以保证每次取到的是i-1的dp值而并非是i之前填写的dp值。

下面是不是还是有点迷糊?我们来画图分析一下(这里我们每一行展示的是遍历到的i行时候的(相当于只在原行操作了)):

这么一优化,我们的判断条件也就少了:比如:

我们不是如果选的话还是要判断下标不能越界(也就是j和当前i的体积之间关系):

那么还有什么要注意的嘛? 就是我们如果不选的话它是还是dpj

int N,V;
cin>>N>>V;
vector>v(N);
//ans1:
for(int i=0;i>v[i].first>>v[i].second;
for(int i=1;i<=N;i++){
for(int j=V;j>=v[i-1].first;j--){
dp[j]=max(dp[j],dp[j-v[i-1].first]+v[i-1].second);
}
}
cout<<dp[V]<<endl;
下面还要注意情况全局dp值呀!

装满状态(一维dp滚动数组优化版):
这里装满问题比上面就多了一个判断是否装满(是否为-1)的情况而已,这里就不多说了,直接展示代码:

for(int i=1;i<=V;i++)dp[i]=-1;
for(int i=1;i<=N;i++){
for(int j=V;j>=v[i-1].first;j--){
dp[j]=max(dp[j],
dp[j-v[i-1].first]!=-1?
dp[j-v[i-1].first]+v[i-1].second:-1);
}
}
cout<<(dp[V]==-1?0:dp[V])<<endl;
下面我们总结下对滚动优化后是如何对代码优化的:

总结过渡修改方法:1.反向遍历填表

             2、把dp有关i下标的都干掉

             3·这里还有个小优化:防止下标越界(如果拿i位置,背包就不会够):

             如果选它不合适那就不选;也就是对于下标j<v[i-1]即不选让它就是上一次i-1对 应的值 即可;由于我们是覆盖式填表(覆盖上次i-1结果)因此,直接做不操作,即第二次循环只到v[i-1]即可。

滚动数组优化后解答代码汇总:

include

using namespace std;
const int n=1005;
int dp[n]{0};
int main()
{
int N,V;
cin>>N>>V;
vector>v(N);
//ans1:
for(int i=0;i>v[i].first>>v[i].second;
for(int i=1;i<=N;i++){
for(int j=V;j>=v[i-1].first;j--){
dp[j]=max(dp[j],dp[j-v[i-1].first]+v[i-1].second);
}
}
cout<=v[i-1].first;j--){
dp[j]=max(dp[j],
dp[j-v[i-1].first]!=-1?
dp[j-v[i-1].first]+v[i-1].second:-1);
}
}
cout<<(dp[V]==-1?0:dp[V])<<endl;

return 0;
}

六·01背包总结:
归根结底还是动归的过程(参考博客动归做法小结:【动态规划篇】步步带你深入解答成功AC最优包含问题(通俗易懂版)-CSDN博客);其次就是根据它的题意分析填表及初始化问题(根据选还是不选),以及它类似的衍生题(就是保证最大还要有限制:从选还是不选入手);然后可选的去做好滚动数组优化(注:这里如果可以进行滚动数组降维覆盖实的优化,那么它的状态表示只能和上一个即i-1有关才可)(在二维dp完成后),来减少一些代码及降低复杂度等;后面会出有关完全背包的后序希望大家多多关注呀!!!

相关文章
|
1月前
|
存储 算法 Java
算法系列之动态规划
动态规划(Dynamic Programming,简称DP)是一种用于解决复杂问题的算法设计技术。它通过将问题分解为更小的子问题,并存储这些子问题的解来避免重复计算,从而提高算法的效率。
35 4
算法系列之动态规划
|
5月前
|
存储 算法
深入了解动态规划算法
深入了解动态规划算法
127 1
|
5月前
|
算法 测试技术 C++
【动态规划算法】蓝桥杯填充问题(C/C++)
【动态规划算法】蓝桥杯填充问题(C/C++)
|
1月前
|
算法 安全 调度
【动态规划篇】穿越算法迷雾:约瑟夫环问题的奇幻密码
【动态规划篇】穿越算法迷雾:约瑟夫环问题的奇幻密码
|
2月前
|
算法 Java C++
【潜意识Java】蓝桥杯算法有关的动态规划求解背包问题
本文介绍了经典的0/1背包问题及其动态规划解法。
72 5
|
8月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
96 8
|
5月前
|
算法
动态规划算法学习三:0-1背包问题
这篇文章是关于0-1背包问题的动态规划算法详解,包括问题描述、解决步骤、最优子结构性质、状态表示和递推方程、算法设计与分析、计算最优值、算法实现以及对算法缺点的思考。
206 2
动态规划算法学习三:0-1背包问题
|
4月前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
99 2
|
5月前
|
算法
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
这篇文章介绍了动态规划算法中解决最大上升子序列问题(LIS)的方法,包括问题的描述、动态规划的步骤、状态表示、递推方程、计算最优值以及优化方法,如非动态规划的二分法。
120 0
动态规划算法学习四:最大上升子序列问题(LIS:Longest Increasing Subsequence)
|
5月前
|
算法
动态规划算法学习二:最长公共子序列
这篇文章介绍了如何使用动态规划算法解决最长公共子序列(LCS)问题,包括问题描述、最优子结构性质、状态表示、状态递归方程、计算最优值的方法,以及具体的代码实现。
249 0
动态规划算法学习二:最长公共子序列

热门文章

最新文章