news 2026/8/19 23:58:15

《代码随想录》刷题打卡day32:动态规划-背包问题part03

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
《代码随想录》刷题打卡day32:动态规划-背包问题part03

文章目录

      • 完全背包:
          • 二维dp数组
        • 【52.携带研究材料】
          • 一维dp数组
        • 【518.零钱兑换II】
          • 二维dp数组解法:
          • 一维dp数组解法:
        • 【377.组合总和Ⅳ】
        • 【70.爬楼梯(进阶版)】

完全背包:

二维dp数组

定义:有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

完全背包和01背包问题唯一不同的地方就是,每种物品有无限件

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

dp[i] [j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少

  1. 确定递推公式:

在01背包中,背包先空留出物品1的容量,此时容量为1,只考虑放物品0的最大价值是 dp[0] [1],因为01背包每个物品只有一个,既然空出物品1,那背包中也不会再有物品1

而在完全背包中,物品是可以放无限个,所以 即使空出物品1空间重量,那背包中也可能还有物品1,所以此时我们依然考虑放 物品0 和 物品1 的最大价值即:dp[1] [1], 而不是 dp[0] [1]

所以放物品1的情况 = dp[1] [1] + 物品1的价值

以上过程,抽象化如下:

  • 不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1] [j]。
  • 放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i] [j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i] [j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

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

(注意,完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别,01背包中是max(dp[i - 1] [j] , dp[i - 1] [j - weight[i]] + value[i]))

  1. dp数组如何初始化:

首先从dp[i] [j]的定义出发,如果背包容量j为0的话,即dp[i] [0],无论是选取哪些物品,背包价值总和一定为0。

再看其他情况。

状态转移方程dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。

dp[0] [j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。

那么很明显当j < weight[0]的时候,dp[0] [j] 应该是 0,因为背包容量比编号0的物品重量还小。

j >= weight[0]时,dp[0] [j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个

代码初始化如下:

for(inti=1;i<weight.size();i++){// 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略dp[i][0]=0;}// 正序遍历,如果能放下就一直装物品0for(intj=weight[0];j<=bagWeight;j++)dp[0][j]=dp[0][j-weight[0]]+value[0];

从递归公式: dp[i] [j] = max(dp[i - 1] [j], dp[i] [j - weight[i]] + value[i]); 可以看出dp[i] [j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。

但只不过一开始就统一把dp数组统一初始为0,更方便一些。

最后初始化代码如下:

// 初始化 dpvector<vector<int>>dp(weight.size(),vector<int>(bagweight+1,0));for(intj=weight[0];j<=bagWeight;j++){dp[0][j]=dp[0][j-weight[0]]+value[0];}
  1. 确定遍历顺序

01背包二维DP数组,先遍历物品还是先遍历背包都是可以的

因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。

既可以先遍历物品再遍历背包:

for(inti=1;i<n;i++){// 遍历物品for(intj=0;j<=bagWeight;j++){// 遍历背包容量if(j<weight[i])dp[i][j]=dp[i-1][j];elsedp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);}}

也可以先遍历背包再遍历物品:

for(intj=0;j<=bagWeight;j++){// 遍历背包容量for(inti=1;i<n;i++){// 遍历物品if(j<weight[i])dp[i][j]=dp[i-1][j];elsedp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+value[i]);}}
  1. 举例推导dp数组

【52.携带研究材料】
// 二维dp数组解法#include<iostream>#include<vector>usingnamespacestd;intmain(){intn,v;// n,v,分别表示研究材料的种类和行李所能承担的总重量cin>>n>>v;vector<int>weight(n,0);vector<int>values(n,0);for(inti=0;i<n;i++){cin>>weight[i]>>values[i];}vector<vector<int>>dp(n,vector<int>(v+1,0));// dp[i][j]表示从行李0~i中在背包容量为j的时候可携带的最大价值// 初始化for(intj=weight[0];j<=v;j++){dp[0][j]=dp[0][j-weight[0]]+values[0];}// 遍历for(inti=1;i<n;i++){for(intj=0;j<=v;j++){if(j<weight[i])dp[i][j]=dp[i-1][j];elsedp[i][j]=max(dp[i-1][j],dp[i][j-weight[i]]+values[i]);}}cout<<dp[n-1][v]<<endl;return0;}
一维dp数组

压缩成一维DP数组,也就是将上一层拷贝到当前层。

将上一层dp[i-1] 的那一层拷贝到 当前层 dp[i] ,那么 递推公式由:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])变成:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])

这里有录友想,这样拷贝的话, dp[i - 1][j] 的数值会不会 覆盖了 dp[i][j] 的数值呢?

并不会,因为 当前层 dp[i][j] 是空的,是没有计算过的。

变成dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])我们压缩成一维dp数组,去掉 i 层数维度。

即:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

遍历顺序是重点:

01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。

在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的

因为dp[j]是根据下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。

完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。

先遍历背包再遍历物品,代码如下:

for(intj=0;j<=bagWeight;j++){// 遍历背包容量for(inti=0;i<weight.size();i++){// 遍历物品if(j-weight[i]>=0)dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);}cout<<endl;}

先遍历物品再遍历背包:

for(inti=0;i<weight.size();i++){// 遍历物品for(intj=0;j<=bagWeight;j++){// 遍历背包容量if(j-weight[i]>=0)dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);}}

// 一维dp数组解法#include<iostream>#include<vector>usingnamespacestd;intmain(){intn,v;// n,v,分别表示研究材料的种类和行李所能承担的总重量cin>>n>>v;vector<int>weight(n,0);vector<int>values(n,0);for(inti=0;i<n;i++){cin>>weight[i]>>values[i];}vector<int>dp(v+1,0);// dp[i]表示背包重量为i的时候可以携带的最大价值for(inti=0;i<n;i++){// 遍历物品for(intj=1;j<=v;j++){// 遍历背包重量if(j>=weight[i])dp[j]=max(dp[j],dp[j-weight[i]]+values[i]);}}cout<<dp[v]<<endl;return0;}

注意:对于纯完全背包问题,其for循环的先后循环是可以颠倒的!

但如果题目稍稍有点变化,就会体现在遍历顺序上。

如果问装满背包有几种方式的话? 那么两个for循环的先后顺序就有很大区别了。

【518.零钱兑换II】

思路:

二维dp数组解法:
  1. 确定dp数组以及下标的含义:

定义二维dp数值 dp[i[j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。

  1. 确定递推公式:

本题和 494. 目标和 是一样的,唯一区别就是 494. 目标和 是 01背包,本题是完全背包。

在494. 目标和中详解讲解了装满背包有几种方法,二维DP数组的递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]

所以本题递推公式:**dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]**,区别依然是dp[i - 1][j - nums[i]]dp[i][j - nums[i]]

这个 ‘所以’ 省略了很多推导的内容,具体内容可在 494. 目标和 和 完全背包理论基础 中找寻。

  1. dp数组如何初始化:

最上行dp[0] [j] 如何初始化:

dp[0] [j]的含义:用物品0(即coins[0])装满背包容量为j的背包,有几种组合方法。

如果 j 可以整除 物品0,那么装满背包就有1种组合方法。

初始化代码:

for(intj=0;j<=bagSize;j++){if(j%coins[0]==0)dp[0][j]=1;}

最左列如何初始化:

dp[i] [0] 的含义:用物品i(即coins[i])装满容量为0的背包有几种组合方法。

都有一种方法,即不装。

所以 dp[i] [0] 都初始化为1。

  1. 确定遍历顺序

二维DP数组的完全背包的两个for循环先后顺序是无所谓的。

先遍历背包,还是先遍历物品都是可以的i

  1. 打印dp数组
// 二维dp数组classSolution{public:intchange(intamount,vector<int>&coins){intbagSize=amount;vector<vector<uint64_t>>dp(coins.size(),vector<uint64_t>(bagSize+1,0));// dp[i][j]表示从0~i中选有多少种方法凑到amount=jfor(inti=0;i<=bagSize;i++){if(i%coins[0]==0)dp[0][i]=1;}for(intj=0;j<coins.size();j++){dp[j][0]=1;}for(inti=1;i<coins.size();i++){for(intj=0;j<=bagSize;j++){if(j<coins[i])dp[i][j]=dp[i-1][j];elsedp[i][j]=dp[i-1][j]+dp[i][j-coins[i]];}}returndp[coins.size()-1][bagSize];}};
一维dp数组解法:
  1. 确定dp数组以及下标的含义

dp[j]:凑成总金额j的货币组合数为dp[j]

  1. 确定递推公式

本题二维dp 递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]

压缩成一维:dp[j] += dp[j - coins[i]]

  1. dp数组如何初始化

装满背包容量为0 的方法是1,即不放任何物品,dp[0] = 1

  1. 确定遍历顺序

在完全背包(一维DP)中讲解了完全背包的两个for循环的先后顺序都是可以的。

但本题就不行了!

因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行!

而本题要求凑成总和的组合数,元素之间明确要求没有顺序。

所以纯完全背包是能凑成总和就行,不用管怎么凑的。

本题是求凑出来的方案个数,且每个方案个数是组合数。

那么本题,两个for循环的先后顺序可就有说法了。

我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。

代码如下:

for(inti=0;i<coins.size();i++){// 遍历物品for(intj=coins[i];j<=amount;j++){// 遍历背包容量dp[j]+=dp[j-coins[i]];}}

假设:coins[0] = 1,coins[1] = 5。

那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。

所以这种遍历顺序中dp[j]里计算的是组合数!

如果把两个for交换顺序,代码如下:

for(intj=0;j<=amount;j++){// 遍历背包容量for(inti=0;i<coins.size();i++){// 遍历物品if(j-coins[i]>=0)dp[j]+=dp[j-coins[i]];}}

背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。

此时dp[j]里算出来的就是排列数!

  1. 举例推导dp数组

classSolution{public:intchange(intamount,vector<int>&coins){intbagSize=amount;vector<uint64_t>dp(bagSize+1,0);// dp[i]表示总数容量为i能凑出的方法数dp[0]=1;for(inti=0;i<coins.size();i++){for(intj=coins[i];j<=bagSize;j++){dp[j]+=dp[j-coins[i]];}}returndp[bagSize];}};
【377.组合总和Ⅳ】

思路:

和上一题一样,重点在遍历顺序:

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

// 一维dp数组classSolution{public:intcombinationSum4(vector<int>&nums,inttarget){vector<uint32_t>dp(target+1,0);//dp[i]表示target为i时能凑出的方法数dp[0]=1;for(inti=0;i<=target;i++){for(intj=0;j<nums.size();j++){if(i-nums[j]>=0&&dp[i]<=INT_MAX-dp[i-nums[j]]){dp[i]+=dp[i-nums[j]];}}}returndp[target];}};
【70.爬楼梯(进阶版)】

思路:

看出来是求排列数的完全背包即可。

#include<iostream>#include<vector>usingnamespacestd;intmain(){intn,m;cin>>n>>m;// n就是target,0~m就是每次完全背包可选择的数vector<int>dp(n+1,0);dp[0]=1;for(inti=1;i<=n;i++){// 遍历背包容量for(intj=1;j<=m;j++){// 遍历物品容量if(i>=j)dp[i]+=dp[i-j];}}cout<<dp[n]<<endl;return0;}
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/19 23:51:23

Pixel Sorter 4插件实战:用AE制作音频驱动像素故障艺术

如果你是一位视频创作者&#xff0c;是否曾遇到过这样的困境&#xff1a;精心剪辑的音乐MV或VJ表演视频&#xff0c;总感觉缺少一些“炸裂”的视觉冲击力&#xff1f;想要实现那些在顶级电音现场或赛博朋克风格影片中看到的、极具未来感的像素拉伸、排序故障效果&#xff0c;却…

作者头像 李华
网站建设 2026/8/19 23:48:07

Arduino轴测投影:在微控制器上实现3D图形渲染的轻量级方案

1. 从二维屏幕到三维世界&#xff1a;为什么要在Arduino上搞轴测投影&#xff1f;如果你玩过Arduino&#xff0c;大概率用它点亮过LED、驱动过舵机&#xff0c;或者做过一个温湿度计。但有没有想过&#xff0c;用这块小小的单片机&#xff0c;也能在屏幕上画出有立体感的3D图形…

作者头像 李华
网站建设 2026/8/19 23:47:07

CorelDRAW高效选择技巧:从底层逻辑到实战应用

1. 这篇文章真正要解决的问题如果你用过 CorelDRAW&#xff0c;大概率遇到过这样的场景&#xff1a;想选中一个被压在底层的对象&#xff0c;却总是点到它上层的元素&#xff1b;想框选几个特定对象&#xff0c;结果把背景和辅助线也一起选中了&#xff1b;或者想微调一个复杂图…

作者头像 李华
网站建设 2026/8/19 23:44:38

RT-Thread内核移植实战:空闲线程与钩子函数在iCore3上的深度应用

1. 项目缘起&#xff1a;为什么要在iCore3上移植RT-Thread内核&#xff1f; 最近在做一个基于STM32F4和FPGA的复杂嵌入式项目&#xff0c;用的是银杏科技的iCore3双核心板。项目里既有实时数据采集&#xff0c;又有复杂的算法处理&#xff0c;还有网络通信&#xff0c;裸机编程…

作者头像 李华
网站建设 2026/8/19 23:42:19

【2026年】教学实验室通风系统:兼顾安全与节能的人性化设计思路

一、教学实验室与科研实验室的区别教学实验室承担大量基础实验教学&#xff0c;特点是使用人数多、时段集中、操作频繁、学生经验不足。这给通风系统带来三个独特需求&#xff1a;安全要有更高冗余、操作要简洁直观、能耗要控制在合理范围——因为它们常处于"满载运行但满…

作者头像 李华