当前位置: 首页 > news >正文

背包dp——动态规划

文章目录

  • 一、0-1背包
  • 二、完全背包
  • 三、二维费用背包
  • 四、似包非包

  背包dp,全称是背包类动态规划,是动态规划问题中非常经典的一类问题。它的基本模型来源于一个非常形象的场景——一个容量有限的背包,和一组物品。每个物品都有自己的重量(或体积)和价值。目标是选择一些物品装入背包,使得在不超过背包总容量的前提下,装入背包的物品的总价值最大。

  这个看似简单的模型,却可以衍生出许多变种,广泛应用于资源分配、投资决策、项目选择等现实问题中。

  根据物品的特点,背包问题还可以进一步细分。如果每种物品只有一个,可以选择将之放入或不放入背包,那么可以将这类问题称为0-1背包问题。0-1背包问题是最基本的背包问题,其他背包问题通常可以转化为0-1背包问题。

  如果第iii种物品最多有MiM_iMi个(Mi≥1M_i\geq1Mi1),即每种物品的数量有限,这类背包问题称为有界背包问题(也常称为多重背包问题)。如果每种物品的数量都是无限的,那么这类背包问题称为无界背包问题(也可以称为完全背包问题)。

  二维费用背包问题是传统背包问题的扩展,其核心在于背包的限制条件从一个维度增加到两个维度(例如同时考虑总重量和总体积),除此之外还有混合背包,分组背包等,在此章我们只会讲解0-1背包、完全背包和二维费用背包。

下面通过几个典型的题目来分析如何使用动态规划解决背包问题。

一、0-1背包

0-1背包(模板题)
在这里插入图片描述
题目解析

有一个背包,最多能容纳的体积是VVV,有nnn个物品,体积为viv_ivi,价值wiw_iwi

  • 问1:最多能装的最大价值的物品?(背包不必装满)
  • 问2:背包恰好装满,最多能装多大价值的物品

在这里插入图片描述
如上示例:一个体积为5的背包,提供选择的有三个的物品。
  选择物品 1、2、3 或 1、2 均无法全部装入,退一步选择物品 1 和 3 为最优,总价值 14;此时背包虽未装满,但已无合适物品可补充。
  而第二问要求背包恰好装满,因此上述组合均不满足条件,仅选择物品 2 和 3(总体积 5)符合要求。
而背包装满的情况也可能不存在,如下:
在这里插入图片描述

算法原理
第一问

  1. 状态表示

  该题本质线性dpdpdp,因为可以从左往右走,每一个元素有选和不选两种情况,在它们当中选最优。所以状态表示可以是:

  • dp[i]dp[i]dp[i]表示:从前i个物品中选,所有选法中能挑选出来的最大价值。

  值得注意的是背包容量是有限的,在做状态转移方程时,我们无法知道当前选法占背包容量多少,是否还能再选。无法推出状态转移方程。所以需要在加一个维度用来记录占用背包容量情况,即:

  • dp[i][j]dp[i][j]dp[i][j]:从前iii个物品中挑选,总体积不超过jjj,所有选法中能挑选出来的最大价值。
  1. 状态转移方程

分两种情况:

  • 不选i物品:等价于从000i−1i-1i1中挑的最优价值,所以结果就是dp[i−1][j]dp[i-1][j]dp[i1][j]
  • 选i商品:等价于从000i−0i-0i0中挑的最优价值加上当前i物品的价值。但要注意此时容量要增加v(i)v(i)v(i),根据状态表示,我们需要从dp[i−1][j−v[i]]dp[i-1][j-v[i]]dp[i1][jv[i]]选法的基础上添加iii物品才是dp[i][j]dp[i][j]dp[i][j]的最终结果。注意j−v[i]j-v[i]jv[i]要大于等于000

最后在两种情况中最大价值。
状态转移方程:

dp[i][j]={dp[i−1][j],其他max(dp[i−1][j],w[i]+dp[i−1][j−v[i]]),(j−v[i])≥0dp[i][j]=\begin{cases} dp[i-1][j] & \text{},其他 \\ max(dp[i-1][j],w[i]+dp[i-1][j-v[i]]) & \text{},(j-v[i])\geq0 \end{cases} dp[i][j]={dp[i1][j]max(dp[i1][j],w[i]+dp[i1][jv[i]]),其他,(jv[i])0

3. 初始化
为了避免繁琐的边界判断,我们在dpdpdp表上加一行一列。然后对其初始化:
在这里插入图片描述

  • 因为容量不超过0时,怎么选都是最优价值为0。物品为0时容量多大最优价值都是0。
  • 添加行和列后注意数据映射关系。(整体下标减1)
  1. 填表顺序

  从状态转移方程来看,填写dp[i][j]时需要知道,左边,左上方的数据,所以需要从上往下,从左往右填写。

  1. 返回值
    返回选完所有物品,体积不超过背包体积的情况,即dp[n][v]dp[n][v]dp[n][v]

第二问

  1. 状态表示
    根据题目要求,我们需要把状态表示改为总体积正好等于j的状态,即:
  • dp[i][j]:从前iii个物品中挑选,总体积等于jjj,所有选法中能挑选出来的最大价值。
  1. 状态转移方程

  与问题一类似需要注意的是体积刚好等于jjj的情况可能不存在。所以有些dpdpdp位置无法完成填写。我们把它填入特殊元素-1。

  • 不选i物品:等价于从0到i-1中挑的最优价值,所以结果就是dp[i−1][j]dp[i-1][j]dp[i1][j],尽管是总体积等于jjj的情况不存在,在dp[i−1][j]dp[i-1][j]dp[i1][j]中会做处理。
  • 选i商品:在问题一j−v[i]≥0j-v[i]\geq0jv[i]0的条件下还要满足dp[i−1][j−v[i]]dp[i-1][j-v[i]]dp[i1][jv[i]]的情况存在。即:(j−v[i])≥0&&dp[i−1][j−v[i]]!=−1(j-v[i])\geq0\&\&dp[i-1][j-v[i]]!=-1(jv[i])0&&dp[i1][jv[i]]!=1

所以状态表示:

dp[i][j]={dp[i−1][j],其他max(dp[i−1][j],w[i]+dp[i−1][j−v[i]]),(j−v[i])≥0&&dp[i−1][j−v[i]]!=−1dp[i][j]=\begin{cases} dp[i-1][j] & \text{},其他 \\ max(dp[i-1][j],w[i]+dp[i-1][j-v[i]]) & \text{},(j-v[i])\geq0\&\&dp[i-1][j-v[i]]!=-1 \end{cases} dp[i][j]={dp[i1][j]max(dp[i1][j],w[i]+dp[i1][jv[i]]),其他,(jv[i])0&&dp[i1][jv[i]]!=1

  1. 初始化
    在这里插入图片描述
    当总容量为0时,怎么选最优价值都是0,当可选物品为 0 但总容量不为 0 时,无法凑出该容量,因此初始化为 - 1。

  2. 填表顺序
    从上往下,从左往右

  3. 返回值
    如果dp[n][v]dp[n][v]dp[n][v]为-1则返回0,如果不是则返回dp[n][v]dp[n][v]dp[n][v]

代码编写

#include<iostream>
#include<vector>
using namespace std;
int main()
{int n,v;cin>>n>>v;vector<int> vl(n+1);auto wl=vl;vector<vector<int>> dp1(n+1,vector<int>(v+1));auto dp2=dp1;//获取数据for(int i=1;i<=n;i++){int tv,tw;cin>>tv>>tw;vl[i]=tv,wl[i]=tw;}//初始化for(int i=1;i<=v;i++) dp2[0][i]=-1;//dp表填写for(int i=1;i<=n;i++){for(int j=1;j<=v;j++){//第一问if(j-vl[i]>=0) dp1[i][j]=max(dp1[i-1][j],wl[i]+dp1[i-1][j-vl[i]]);elsedp1[i][j]=dp1[i-1][j];//第二问if(j-vl[i]>=0&&dp2[i-1][j-vl[i]]!=-1) dp2[i][j]=max(dp2[i-1][j],wl[i]+dp2[i-1][j-vl[i]]);elsedp2[i][j]=dp2[i-1][j];}}cout<<dp1[n][v]<<endl;if(dp2[n][v]==-1)cout<<0;else cout<<dp2[n][v];return 0;
}

优化
  初步优化(滚动数组):填写dp[i][j]dp[i][j]dp[i][j]只需要左边,上边,左上角的值,往上两行后的值就没有利用价值了,而往下几行的空间也暂时用不到。所以只需要用两行就能完成dpdpdp表的填写,需要两行循环往复的利用。
  进一步优化:我们尝试把优化成一行,可以发现,当dpdpdp表填写完dp[i][j]dp[i][j]dp[i][j]后,只有dp[i−1][j]dp[i-1][j]dp[i1][j]这个位置及往右的元素是有用的,而dp[i][j]dp[i][j]dp[i][j]往右的空间暂时是空闲的,所以可以把原本需要填到上面的元素填在该行后面不用的空间就可以省下一行的空间。
  更方便理解的做法是我们使用一个数组,从后往前填写,因为我们用到的是前面的数据,这样做就不会使新数据覆盖前面的有效数据。

#include<iostream>
#include<vector>
using namespace std;
int main()
{int n,v;cin>>n>>v;vector<int> vl(n+1),dp1(v+1);auto wl=vl,dp2=dp1;for(int i=1;i<=n;i++){int tv,tw;cin>>tv>>tw;vl[i]=tv,wl[i]=tw;}for(int i=1;i<=v;i++) dp2[i]=-1;for(int i=1;i<=n;i++){for(int j=v;j>=vl[i];j--){  dp1[j]=max(dp1[j],wl[i]+dp1[j-vl[i]]);if(dp2[j-vl[i]]!=-1) dp2[j]=max(dp2[j],wl[i]+dp2[j-vl[i]]);}}cout<<dp1[v]<<endl;if(dp2[v]==-1)cout<<0;else cout<<dp2[v];return 0;
}

注意:

  • 这是一个模板题,该题的分析思路可以,运用到很多题里面。
  • 需强行纠结优化后一维数组的状态表示字面含义,核心是理解‘从后往前遍历’避免覆盖有效数据的逻辑,其本质是二维 DP 的空间压缩。

分割等和子集
在这里插入图片描述
题目解析

  • 该题要求将数组分割为两个子集,使得两个子集的元素和相等。
  • 只要存在任意一种分割方式满足条件,返回 true;否则返回 false。

算法原理
  由题意可知确定一个数组后是可以知道它分成子集后的和的。它就等于这个数组总和的一半,如果数组总和除以2还有余数呢?这说明该数组无法分成和相同的两个子集。
在这里插入图片描述

  所以我们可以把问题转化为能否抽出数组的一部分元素使得它们的和为sum/2sum/2sum/2。这样就成了一个0-1背包问题。
照搬上题分析思路

  1. 状态表示
  • dp[i][j]dp[i][j]dp[i][j]:表示从前iii个数中选,所有选法中能否凑成jjj这个数。是一个bool类型
  1. 状态转移方程

在面对一个元素时我们都有选和不选两种情况,只要任意一种情况为true,则为true

  • 不选:等价于考虑000i−1i-1i1中是否能选出元素和为j的子集。
  • 选:等价与在000i−1i-1i1是否能选出元素和为j−nums[i]j-nums[i]jnums[i]的子集。要满足j−nums[i]≥0j-nums[i]\geq0jnums[i]0

所以状态转移方程为:

dp[i][j]={dp[i−1][j],其他dp[i−1][j]∣∣dp[i−1][j−nums[i]],(j−nums[i])≥0dp[i][j]=\begin{cases} dp[i-1][j] & \text{},其他 \\ dp[i-1][j]||dp[i-1][j-nums[i]] & \text{},(j-nums[i])\geq0 \end{cases} dp[i][j]={dp[i1][j]dp[i1][j]∣∣dp[i1][jnums[i]],其他,(jnums[i])0

  1. 初始化
    同样的为了不做繁琐的边界判断,我们添加一行一列。
    在这里插入图片描述
    凑成0的情况为true,在元素个数为0时凑成非零数时为false

  2. 填表顺序
    从上往下,左右无所谓

  3. 返回值
    返回dp[n][sum/2]dp[n][sum/2]dp[n][sum/2]
    代码编写

class Solution {
public:bool canPartition(vector<int>& nums){int n=nums.size(),sum=0;for(int i=0;i<n;i++) sum+=nums[i];if(sum%2) return false;vector<vector<bool>> dp(n+1,vector<bool>(sum/2+1));for(int i=0;i<=n;i++) dp[i][0]=true;for(int i=1;i<=n;i++){for(int j=1;j<=sum/2;j++){if(j-nums[i-1]>=0) dp[i][j]=dp[i-1][j]||dp[i-1][j-nums[i-1]];elsedp[i][j]=dp[i-1][j];}}return dp[n][sum/2];    }
};

优化

class Solution {
public:bool canPartition(vector<int>& nums){int n=nums.size(),sum=0;for(int i=0;i<n;i++) sum+=nums[i];if(sum%2) return false;vector<bool> dp(sum/2+1);dp[0]=true;for(int i=1;i<=n;++i)for(int j=sum/2;j>=nums[i-1];--j)dp[j]=dp[j]||dp[j-nums[i-1]];return dp[sum/2];    }
};

二、完全背包

完全背包(模板题)
在这里插入图片描述

  • 0-1背包:每件物品要么选要么不选(0或1)。
  • 完全背包:每件物品可以选0次、1次、2次…直到背包装不下为止。
    与 0-1 背包类似,完全背包也分为两类:背包必须装满、背包不必装满。

题目解析
在这里插入图片描述

算法原理

第一问

  1. 状态表示

与0-1背包相同:

  • dp[i][j]dp[i][j]dp[i][j]:从前iii个物品中选,总体积不超过jjj,所有选法中最大的价值。
  1. 状态转移方程

{不选,dp[i−1][j]选1个,dp[i−1][j−v[i]]+w[i]选2个,dp[i−1][j−2v[i]]+2w[i]选3个,dp[i−1][j−3v[i]]+3w[i]......\begin{cases} 不选,dp[i-1][j] \\ 选1个,dp[i-1][j-v[i]]+w[i] \\ 选2个,dp[i-1][j-2v[i]]+2w[i] \\ 选3个,dp[i-1][j-3v[i]]+3w[i] \\ ...... \\ \end{cases} 不选,dp[i1][j]1个,dp[i1][jv[i]]+w[i]2个,dp[i1][j2v[i]]+2w[i]3个,dp[i1][j3v[i]]+3w[i]......
  直到选到j−kv[i]<0j−kv[i]<0jkv[i]<0kkk为选择第iii种物品的个数)为止,然后选择价值最大的一种方案。
  但这会使我们增加一层循环,时间复杂度为O(n3)O(n^3)O(n3),可以试着用几个状态来表示这些结果优化复杂度。尝试用数学公式推导,如下:

  • dp[i][j]=max(dp[i−1][j],dp[i−1][j−v[i]]+w[i],dp[i−1][j−2v[i]]+2w[i]......dp[i−1][j−kv[i]]+kw[i])dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]]+w[i], dp[i-1][j-2v[i]]+2w[i]......dp[i-1][j-kv[i]]+kw[i])dp[i][j]=max(dp[i1][j],dp[i1][jv[i]]+w[i],dp[i1][j2v[i]]+2w[i]......dp[i1][jkv[i]]+kw[i])

将上式的j替换为j−v[i]j-v[i]jv[i]得到:

  • dp[i][j−v[i]]=max(dp[i−1][j−v[i]],dp[i−1][j−2v[i]]+w[i],dp[i−1][j−3v[i]]+2w[i]......dp[i−1][j−xv[i]]+(x−1)w[i])dp[i][j-v[i]] = max(dp[i-1][j-v[i]], dp[i-1][j-2v[i]]+w[i], dp[i-1][j-3v[i]]+2w[i]......dp[i-1][j-xv[i]]+(x-1)w[i])dp[i][jv[i]]=max(dp[i1][jv[i]],dp[i1][j2v[i]]+w[i],dp[i1][j3v[i]]+2w[i]......dp[i1][jxv[i]]+(x1)w[i])

结合两式得:

  • dp[i][j]=max(dp[i−1][j],dp[i][j−v[i]]+w[i])dp[i][j]=max(dp[i-1][j],dp[i][j-v[i]]+w[i])dp[i][j]=max(dp[i1][j],dp[i][jv[i]]+w[i]),其中j−v[i]>=0j-v[i]>=0jv[i]>=0

或许这个推导会唤醒你尘封已久的记忆,即高中数学的错位相减法。

最终的状态表示:

dp[i][j]={max(dp[i−1][j],dp[i][j−v[i]]+w[i]),j−v[i]≥0dp[i−1][j],其他dp[i][j]=\begin{cases} max(dp[i-1][j],dp[i][j-v[i]]+w[i]),j-v[i]\geq0 \\ dp[i-1][j],其他 \end{cases} dp[i][j]={max(dp[i1][j],dp[i][jv[i]]+w[i])jv[i]0dp[i1][j],其他

  1. 初始化
    添加一行添加一列,初始化为全0即可

  2. 填表顺序
    从上往下,从左往右

  3. 返回值
    返回dp[n][v]dp[n][v]dp[n][v]

第二问

  1. 状态转移方程
  • dp[i][j]dp[i][j]dp[i][j]:从前iii个物品中选,总体积正好为jjj,所有选法中最大的价值。
  1. 状态转移方程

  与0-1背包类似,只需要让无法选出体积正好为j的情况填写为-1,在填dpdpdp表时添加判断条件。
状态转移方程:
dp[i][j]={max(dp[i−1][j],dp[i][j−v[i]]+w[i]),j−v[i]≥0&&dp[i][j−v[i]]!=−1dp[i−1][j],其他dp[i][j]=\begin{cases} max(dp[i-1][j],dp[i][j-v[i]]+w[i]),j-v[i]\geq0\&\&dp[i][j-v[i]]!=-1 \\ dp[i-1][j],其他 \end{cases} dp[i][j]={max(dp[i1][j],dp[i][jv[i]]+w[i])jv[i]0&&dp[i][jv[i]]!=1dp[i1][j],其他
3. 初始化

  初始化时添加一行一列(避免边界判断):可选物品为 0 时,仅总体积j=0j=0j=0可行(价值 0),其余非零容量均不可行,因此第一行除j=0j=0j=0外均初始化为−1。

  1. 填表顺序
    从上往下,从左往右
  2. 返回值
    dp[n][v]dp[n][v]dp[n][v]−1-11返回000,否则返回dp[n][v]dp[n][v]dp[n][v]

代码编写

int main()
{int n,m;cin>>n>>m;vector<int> v(n),w(n);for(int i=0;i<n;i++) cin>>v[i]>>w[i];vector<vector<int>> dp1(n+1,vector<int>(m+1));auto dp2=dp1;for(int j=1;j<=m;j++) dp2[0][j]=-1;for(int i=1;i<=n;i++){for(int j=0;j<=m;j++){dp1[i][j]=dp1[i-1][j];dp2[i][j]=dp2[i-1][j];if(j-v[i-1]>=0)dp1[i][j]=max(dp1[i][j],dp1[i][j-v[i-1]]+w[i-1]);if(j-v[i-1]>=0&&dp2[i][j-v[i-1]]!=-1)dp2[i][j]=max(dp2[i][j],dp2[i][j-v[i-1]]+w[i-1]);}}cout<<dp1[n][m]<<endl;if(dp2[n][m]==-1) cout<<0;else cout<<dp2[n][m];return 0;
}

空间优化
  与0-1背包不同的是,在填写dp[i][j]dp[i][j]dp[i][j]时所需要的数据就在本行,而且是在它的左边,所以在填写降维后的dpdpdp表时需要从左往右填写。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{int n,m;cin>>n>>m;vector<int> v(n),w(n);for(int i=0;i<n;i++) cin>>v[i]>>w[i];vector<int> dp1(m+1);auto dp2=dp1;for(int j=1;j<=m;j++) dp2[j]=-0x3f3f3f3f;for(int i=1;i<=n;i++){for(int j=v[i-1];j<=m;j++){dp1[j]=max(dp1[j],dp1[j-v[i-1]]+w[i-1]);dp2[j]=max(dp2[j],dp2[j-v[i-1]]+w[i-1]);}}cout<<dp1[m]<<endl;if(dp2[m]<0) cout<<0;else cout<<dp2[m];return 0;
}

三、二维费用背包

盈利计划
在这里插入图片描述
题目解析

  给两个数组,用i表示下标,那么profit[i]表示i任务的利润,group[i]则表示完成i任务需要的人数。要求从这些任务中挑选若干个,满足总利润≥minProfit总利润\geq minProfit总利润minProfit总人数≤n总人数\le n总人数n,返回有多少种满足条件的方案。
示例1解析:
人数为n=5n=5n=5个,利润需要超过333,任务分别需要的人数[2,2][2, 2][2,2],任务分别能得到的利润[2,3][2, 3][2,3]

  • 挑选0任务:消耗2人<5,利润=2<3,不满足要求
  • 挑选1任务:消耗2人<5,利润=3=3,满足要求
  • 挑选0,1任务:消耗4人<5,利润=2+3>3,满足要求

所以有两种计划。

算法原理

  1. 状态表示

  同样的该题也是在数组元素上做选择,只有选和不选两种情况,0-1背包有背包容量限制,而这里有两个限制条件总利润≥minProfit总利润\geq minProfit总利润minProfit总人数≤n总人数\le n总人数n,因为有两个限制条件,所以是二维费用的0-1背包问题。
参考0-1背包的分析思路,该题只需要在此基础上多加一个维度。即:

  • dp[i][j][k]dp[i][j][k]dp[i][j][k]:从前iii个任务中挑选,总人数不超过jjj,总利润至少为kkk,一共有多少种选法
  1. 状态转移方程

对于一个iii任务有两种情况:

  • 不选:等价于从前i−1i-1i1个任务中挑选,总人数不超过jjj,总利润至少为kkk,选法个数,即dp[i−1][j][k]dp[i-1][j][k]dp[i1][j][k]
  • 选:等价于从前i−1i-1i1个任务中挑选,总人数不超过j−group[i]j-group[i]jgroup[i],总利润至少为k−profit[i]k-profit[i]kprofit[i],选法个数,注意这里j−group[i]j-group[i]jgroup[i]不能小于0,如果小于也就是group[i]>jgroup[i]>jgroup[i]>j不满足状态表示中人数不超过jjj的条件,而如果k−profit[i]k-profit[i]kprofit[i]小于0是可以的,也就是profit[i]>kprofit[i]>kprofit[i]>k,满足状态表示中总利润至少为kkk的条件。

因为需要统计所有满足要求的选法总数,所以将两种情况相加,即:

dp[i][j][k]={dp[i−1][j][k],其他dp[i−1][j][k]+dp[i−1][j−group[i]][k−profit[i]],j−group[i]≥0dp[i][j][k]=\begin{cases} dp[i-1][j][k],其他\\ dp[i-1][j][k]+dp[i-1][j-group[i]][k-profit[i]],j-group[i]\geq0 \end{cases} dp[i][j][k]={dp[i1][j][k],其他dp[i1][j][k]+dp[i1][jgroup[i]][kprofit[i]]jgroup[i]0

  1. 初始化

  添加一行一列防止越界,因为当任务为0时和最少产生利润为0时,无论有多少人都能满足要求,所以dp[0][j][0]=1dp[0][j][0]=1dp[0][j][0]=1(将所有的j初始化为1)

  1. 填表顺序
    因为填写dp[i][j][k]dp[i][j][k]dp[i][j][k]时需要的是上一个二维dpdpdp表的信息,所以iii000依次往后填写,jjjkkk可随意。
  2. 返回值
    返回dp[group.size()][n][minProfit]dp[group.size()][n][minProfit]dp[group.size()][n][minProfit]

代码编写

class Solution {
public:int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit){int m=group.size();vector<vector<vector<int>>> dp(m+1,vector<vector<int>>(n+1,vector<int>(minProfit+1)));for(int i=0;i<=n;i++) dp[0][i][0]=1;for(int i=1;i<=m;i++){for(int j=0;j<=n;j++){for(int k=0;k<=minProfit;k++){if(j-group[i-1]>=0)dp[i][j][k]=dp[i-1][j][k]+dp[i-1][j-group[i-1]][max(0,k-profit[i-1])];elsedp[i][j][k]=dp[i-1][j][k];dp[i][j][k]%=1000000007;}}}return dp[m][n][minProfit];}
};

空间优化

class Solution {
public:int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit){int m=group.size();vector<vector<int>> dp(n+1,vector<int>(minProfit+1));for(int i=0;i<=n;i++) dp[i][0]=1;for(int i=1;i<=m;i++){for(int j=n;j>=group[i-1];j--)for(int k=minProfit;k>=0;k--){dp[j][k]+=dp[j-group[i-1]][max(0,k-profit[i-1])];dp[j][k]%=1000000007;}}return dp[n][minProfit];}
};

四、似包非包

组合总和IV
在这里插入图片描述
题目解析
  给一个数组和一个目标值,要求从数组中选出一些元素使得元素之和等于目标值。从如上示例中可以看出一个元素可以选无限个,而且一组元素做不同排列也算满足条件。从数学定义来看,组合不考虑元素顺序,排列则强调顺序。本题要求“不同排列算不同方案”,本质是求排列数,题目名称中的“组合”容易产生误导。
  “似包非包”顾名思义就是看上去像背包问题,事实上并不是。该题像完全背包问题,但背包问题通常都是“组合”问题,不考虑元素的顺序性,不能用背包dpdpdp的解题思路来分析该题。

算法原理

  1. 状态表示

  我们试着用最原始的动态规划分析思路进行讲解——通过将复杂问题分解为更小的子问题来解决的算法思想,尤其适用于具有重叠子问题和最优子结构的优化问题。其核心目标是避免重复计算,通过存储中间结果(记忆化)来提升效率。
  这样去想就好办得多,把它划分成子问题,先得到和为target−1、target−2......target-1、target-2......target1target2......的组合数。再想办法从前面的结果信息得到最后的答案。
状态表示:

  • dp[i]:凑成总和为i,一共有多少种排列数

注意:若按传统背包“前 i 个元素”的状态套路设计,无法体现“排列顺序”的差异,因此不适用。

  1. 状态转移方程
  • dp[i]=∑j=0ndp[i−nums[j]]dp[i]=\sum_{j=0}^{n}dp[i-nums[j]]dp[i]=j=0ndp[inums[j]]

填到每个目标值时去循环把所有选法都遍历一遍(遍历numsnumsnums),并把每种情况都累加。

  • 注意:虽然每个元素都能选多个,但在dp[i−nums[j]]dp[i-nums[j]]dp[inums[j]]中已经把选则多个nums[j]nums[j]nums[j]的情况考虑进去了。
  1. 初始化

  为使后面的填表正确,需要把0下标初始化为1。也可以强行理解为和为0的排列也就是什么也没有,即什么也不选,所以有一种情况。

  1. 填表顺序
    从左往右

  2. 返回值
    返回dp[target]dp[target]dp[target]

代码编写

class Solution {
public:int combinationSum4(vector<int>& nums, int target) {sort(nums.begin(), nums.end());vector<unsigned int> dp(target + 1, 0);dp[0] = 1;for (int i = 1; i <= target; ++i){for (int val : nums){if (val <= i) dp[i] += dp[i - val];else break;}}return dp[target];}
};

  此题的解法有很多,如果本章不是动态规划,更让人容易想到的可能是递归或记忆递归,接下来我依次给出代码:

递归(超时)

class Solution
{
public:int count=0;int combinationSum4(vector<int>& nums, int target){sort(nums.begin(),nums.end());int sum=0;dfs(nums,target,sum);return count;}void dfs(vector<int>& nums, int target ,int sum){if(sum>=target){if(sum==target) count++;return;}for(int i=0;i<nums.size();i++){dfs(nums,target,sum+nums[i]);}}
};

记忆递归

class Solution {
public:unordered_map<int, int> memo;int combinationSum4(std::vector<int>& nums, int target){sort(nums.begin(), nums.end());return dfs(nums, target);}int dfs(std::vector<int>& nums, int target){if (target == 0) return 1;if (memo.find(target)!= memo.end()) return memo[target];int count = 0;for (int num : nums){if (num <= target) count += dfs(nums, target - num);}memo[target] = count;return count;}
};

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!在这里插入图片描述

http://www.dtcms.com/a/523556.html

相关文章:

  • 找做柜子的网站中国芯片制造最新消息
  • 甘肃省临夏州建设局网站wordpress 未分类
  • 用 Excalidraw+cpolar 做会议协作,像素级还原实体白板体验
  • 使用C++开发Android .so库的优势与实践指南
  • Spring AOP:注解配置与XML配置双实战
  • 基于YOLO11深度学习的半导体晶圆外观缺陷检测系统【Python源码+Pyqt5界面+数据集+安装使用教程+训练代码】【附下载链接】
  • 笔记本电脑待机、睡眠与休眠模式的技术差异解析
  • 2025丨时间很快,又来到1024
  • 基于python人脸识别系统 人脸检测 实时检测 深度学习 Dlib库 ResNet深度卷积神经网络 pyqt设计 大数据(源码)✅
  • 【C + +】unordered_set 和 unordered_map 的用法、区别、性能全解析
  • 《刚刚问世》系列初窥篇-Java+Playwright自动化测试-31- 操作日历时间控件-上篇(详细教程)
  • 电子商城网站建设与维护怎么建设淘客自己的网站_
  • 基于 Vue3 + WebSocket 实现的平板控制端与大屏展示端联动方案
  • 提高自己的网站网站 利润
  • 外贸seo软文发布平台上海百度推广优化排名
  • Qt 图像与定时器实战:实现动态图片轮播效果
  • C++ 模板初阶:从函数重载到泛型编程的优雅过渡
  • 第 01 天:Linux 是什么?内核、发行版及其生态系统
  • Docker 安装 MongoDB 完整指南:从入门到实战
  • Docker 离线安装
  • CUDA和cuDNN安装
  • 一篇初识什么是容器,引出 Docker
  • HTML 理论笔记
  • 《Linux系统编程之入门基础》【权限管理】
  • ELK(Elasticsearch + Logstash + Kibana + Filebeat)采集方案
  • 网站建设金手指排名霸屏主机类型wordpress
  • uniapp微信小程序简单表格展示
  • 【html】每日打卡页面
  • Server 15 ,VMware ESXi 实战指南:Ubuntu 20.04.6 版本虚拟机静态 IP 配置、分辨率固定及远程访问实践
  • 吴恩达深度学习课程一:神经网络和深度学习 第三周:浅层神经网络(三)