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

动态规划 - 二维费用的背包问题、似包非包、卡特兰数

相关动态规划问题

在算法题中,很多问题不会直接标注 “这是背包问题”,但只要出现以下几类关键 “字眼”,就大概率能和背包模型挂钩,帮我们快速锁定解题方向。结合之前提到的二维费用背包、“似包非包” 问题,我们可以按场景拆解这些核心特征:

一、基础背包问题的核心 “信号词”(通用识别点)

无论哪种背包,核心逻辑都是 “有限资源下的选择优化”,因此出现以下表述时,先往背包方向联想:

  1. 资源约束类:“容量”“重量”“体积”“时间”“次数”“数量上限”—— 这些是背包的 “约束维度”,比如 “背包容量为 V”“每件物品重量不超过 W”,直接对应经典背包的 “背包容量”“物品重量” 核心参数。
  2. 选择目标类:“最大价值”“最小消耗”“能否装满”“恰好达到某状态”—— 这是背包问题的 “优化目标”,比如 “选择物品使总价值最大”“能否用物品装满容量为 V 的背包”,完美匹配 0-1 背包、完全背包的经典目标。
  3. 物品属性类:“每件物品只能选一次”(0-1 背包)、“每件物品可选无限次”(完全背包)、“每件物品最多选 k 次”(多重背包)—— 这类 “选择次数限制” 的描述,是区分背包类型的关键,直接决定状态转移方程的写法。

二、二维费用背包的专属 “提示词”

当问题中出现两个独立的约束维度时,基本可判定为二维费用背包,常见组合如下:

  1. 双重资源约束:“重量不超过 W,体积不超过 V”“花费不超过 C,时间不超过 T”—— 比如 “每件物品有重量和体积,背包同时限制最大重量和最大体积,求最大价值”,这里 “重量 + 体积”“花费 + 时间” 就是典型的二维约束。
  2. 目标与约束结合:“在满足 A 条件(如总数量≤K)的前提下,最大化 B 目标(如总价值),同时满足 C 约束(如总重量≤W)”—— 当约束条件不止一个,且彼此独立时,优先考虑二维(或多维)费用背包模型。

三、“似包非包” 问题的隐藏 “线索词”

这类问题最隐蔽,但只要抓住 “选择的累积效应” 和 “状态依赖前序选择”,就能关联背包思维,常见表述有:

  1. 拆分 / 组合类:“将一个数拆成若干个数的和,求方案数”“从数组中选元素,使和为某值,求最小 / 最大代价”—— 比如 “拆分 n 为若干个不重复的正整数,求拆分方案数”,本质是 “物品为 1~n,背包容量为 n,每件物品选一次” 的 0-1 背包计数问题。
  2. 限制条件类:“选 k 个元素,使总和不超过 S,求最大价值”“完成任务需要满足两个条件(如 A≥x,B≥y),求最小成本”—— 这类 “多条件限制 + 选择优化” 的问题,往往可转化为 “背包容量为 k+S”“二维约束 A 和 B” 的背包模型,只是 “物品” 和 “容量” 的定义更灵活。

简单来说,只要题目中出现 “选择”“约束”“优化目标” 这三大核心要素,且约束是 “有限的资源”,目标是 “最大化 / 最小化某值” 或 “计数方案数”,就可以优先尝试用背包的思维去拆解 —— 先定义 “背包容量”(约束条件)、“物品”(可选择的元素 / 操作)、“价值”(优化目标),再推导状态转移方程,多数时候能迎刃而解。

题目练习

474. 一和零 - 力扣(LeetCode)

解法(动态规划):

算法思路:

先将问题转化成我们熟悉的题型。

  • ⅰ. 在一些物品中 「挑选」一些出来,然后在满足某个「限定条件」 下,解决一些问题,大概率是背包模型;

  • ⅱ. 由于每一个物品都只有 1 个,因此是一个 「01 背包问题」。

但是,我们发现这一道题里面有 「两个限制条件」。因此是一个「二维费用的 01 背包问题」。那么我们定义状态表示的时候,来一个三维 dp 表,把第二个限制条件加上即可。

1. 状态表示:

dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有的选法中,最大的长度。

2. 状态转移方程:

线性 dp 状态转移方程分析方式,一般都是 「根据最后一步」 的状况,来分情况讨论。为了方便叙述,我们记第 i 个字符中,字符 0 的个数为 a,字符 1 的个数为 b

  • ⅰ. 不选第 i 个字符串:相当于就是去前 i - 1 个字符串中挑选,并且字符 0 的个数不超过 j,字符 1 的个数不超过 k。此时的最大长度为 dp[i][j][k] = dp[i - 1][j][k]

  • ⅱ. 选择第 i 个字符串:那么接下来我仅需在前 i - 1 个字符串里面,挑选出来字符 0 的个数不超过 j - a,字符 1 的个数不超过 k - b 的最长长度,然后在这个长度后面加上字符串 i 即可。此时 dp[i][j][k] = dp[i - 1][j - a][k - b] + 1。但是这种状态不一定存在,因此需要特判一下。

综上,状态转移方程为:dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1)

3. 初始化:

当没有字符串的时候,没有长度,因此初始化为 0 即可。

4. 填表顺序:

保证第一维的循环 **「从小到大」** 即可。

5. 返回值:

根据 「状态表示」,我们返回 dp[len][m][n]

其中 len 表示字符串数组的长度。

class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));for(int i = 1; i <= len; ++i){int a = 0, b = 0;for(auto ch : strs[i - 1])  if(ch == '0') ++a;else ++b;for(int j = 0; j <= m; ++j)for(int k = 0; k <= n; ++k){dp[i][j][k] = dp[i - 1][j][k];if(j >= a && k >= b) dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);}}return dp[len][m][n];}
};

6. 空间优化:

所有的 **「背包问题」**,都可以进行空间上的优化。

对于 **「二维费用的 01 背包」** 类型的,我们的优化策略是:

  • ⅰ. 删掉第一维;
  • ⅱ. 修改第二层以及第三层循环的遍历顺序即可。
class Solution {
public:int findMaxForm(vector<string>& strs, int m, int n) {int len = strs.size();vector<vector<int>> dp(m + 1, vector<int>(n + 1));for(int i = 0; i < len; ++i){int a = 0, b = 0;for(auto ch : strs[i])  if(ch == '0') ++a;else ++b;for(int j = m; j >= a; j--)for(int k = n; k >= b; k--){dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);}}return dp[m][n];}
};

879. 盈利计划 - 力扣(LeetCode)

解法(动态规划):

算法思路:

这道题目非常难读懂,但是如果结合例子多读几遍,你就会发现是一个经典的 「二维费用的背包问题」。因此我们可以仿照「二维费用的背包」 来定义状态表示。

1. 状态表示:

dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j,总利润至少为 k,一共有多少种选法。

注意注意注意,这道题里面出现了一个 「至少」,和我们之前做过的背包问题不一样。因此,我们在分析「状态转移方程」 的时候要结合实际情况考虑一下。

2. 状态转移方程:

老规矩,根据 「最后一个位置」的元素,结合题目的要求,我们有「选择」最后一个元素或者「不选择」 最后一个元素两种策略:

  • ⅰ. 不选 i 位置的计划:那我们只能去前 i - 1 个计划中挑选,总人数不超过 j,总利润至少为 k。此时一共有 dp[i - 1][j][k] 种选法;

  • ⅱ. 选择 i 位置的计划:那我们在前 i - 1 个计划中挑选的时候,限制就变成了,总人数不超过 j - g[i],总利润至少为 k - p[i]。此时一共有 dp[i - 1][j - g[i]][k - p[i]]

第二种情况下有两个细节需要注意:

  1. j - g[i] < 0:此时说明 g[i] 过大,也就是人数过多。因为我们的状态表示要求人数是不能超过 j 的,因此这个状态是不合法的,需要舍去。

  2. k - p[i] < 0:此时说明 p[i] 过大,也就是利润太高。但是利润高,不正是我们想要的嘛?所以这个状态 「不能舍去」。但是问题来了,我们的 dp 表是没有负数的下标的,这就意味着这些状态我们无法表示。其实,根本不需要负的下标,我们根据实际情况来看,如果这个任务的利润已经能够达标了,我们仅需在之前的任务中,挑选出来的利润至少为 0 就可以了。因为实际情况不允许我们是负利润,那么负利润就等价于利润至少为 0 的情况。所以说这种情况就等价于 dp[i][j][0],我们可以对 k - p[i] 的结果与 0 取一个 max

综上,我们的状态转移方程为:dp[i][j][k] = dp[i - 1][j][k] + dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])]

3. 初始化:

当没有任务的时候,我们的利润为 0,此时无论人数限制为多少,我们都能找到一个 「空集」 的方案。

因此初始化 dp[0][j][0] 的位置为 1,其中 0 <= j <= n。

4. 填表顺序:

根据 「状态转移方程」,我们保证 i 从小到大即可。

5. 返回值:

根据 「状态表示」,我们返回 dp[len][m][n]

其中 len 表示字符串数组的长度。

class Solution {
public:const int MOD = 1e9 + 7;int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) {int len = g.size();vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(m + 1)));for(int j = 0; j <= n; ++j) dp[0][j][0] = 1;for(int i = 1; i <= len; ++i)for(int j = 0; j <= n; ++j)for(int k = 0; k <= m; ++k){dp[i][j][k] = dp[i - 1][j][k];if(j >= g[i - 1]) dp[i][j][k] += dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])];dp[i][j][k] %= MOD;}return dp[len][n][m];}
};

6. 空间优化:

所有的 「背包问题」,都可以进行空间上的优化。

对于 「二维费用的 01 背包」 类型的,我们的优化策略是:

  • ⅰ. 删掉第一维;
  • ⅱ. 修改第二层以及第三层循环的遍历顺序即可。
class Solution {
public:const int MOD = 1e9 + 7;int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) {int len = g.size();vector<vector<int>> dp(n + 1, vector<int>(m + 1));for(int j = 0; j <= n; ++j) dp[j][0] = 1;for(int i = 1; i <= len; ++i)for(int j = n; j >= g[i - 1]; --j)for(int k = m; k >= 0; --k){dp[j][k] += dp[j - g[i - 1]][max(0, k - p[i - 1])];dp[j][k] %= MOD;}return dp[n][m];}
};

377. 组合总和 Ⅳ - 力扣(LeetCode)

解法(动态规划):

算法思路:

一定要注意,我们的背包问题本质上求的是 「组合」数问题,而这一道题求的是「排列数」 问题。因此我们不能被这道题给迷惑,还是用常规的 dp 思想来解决这道题。

1. 状态表示:

这道题的状态表示就是根据 「拆分出相同子问题」 的方式,抽象出来一个状态表示:

当我们在求 target 这个数一共有几种排列方式的时候,对于最后一个位置,如果我们拿出数组中的一个数 x,接下来就是去找 target - x 一共有多少种排列方式。

因此我们可以抽象出来一个状态表示:

dp[i] 表示:总和为 i 的时候,一共有多少种排列方案。

2. 状态转移方程:

对于 dp[i],我们根据 「最后一个位置」 划分,我们可以选择数组中的任意一个数 nums[j],其中 0 <= j <= n - 1。

当 nums[j] <= target 的时候,此时的排列数等于我们先找到 target - nums[j] 的方案数,然后在每一个方案后面加上一个数字 nums[j] 即可。

因为有很多个 j 符合情况,因此我们的状态转移方程为:dp[i] += dp[target - nums[j]],其中 0 <= j <= n - 1。

3. 初始化:

当和为 0 的时候,我们可以什么都不选,「空集」 一种方案,因此 dp[0] = 1

4. 填表顺序:

根据 「状态转移方程」易得「从左往右」。

5. 返回值:

根据 「状态表示」,我们要返回的是 dp[target] 的值。

class Solution {
public:int combinationSum4(vector<int>& nums, int target) {vector<double> dp(target + 1);dp[0] = 1;for(int i = 1; i <= target; ++i)for(auto x : nums)if(i >= x) dp[i] += dp[i - x];return dp[target];}
};

96. 不同的二叉搜索树 - 力扣(LeetCode)(卡特兰数 --- 不是背包问题)

解法(动态规划):

算法思路:

这道题属于 「卡特兰数」的一个应用,同样能解决的问题还有「合法的进出栈序列」「括号匹配的括号序列」「电影购票」等等。如果感兴趣的同学可以「百度」 搜索卡特兰数,会有很多详细的介绍。

1. 状态表示:

这道题的状态表示就是根据 「拆分出相同子问题」 的方式,抽象出来一个状态表示:

当我们在求个数为 n 的 BST 的个数的时候,当确定一个根节点之后,左右子树的结点 「个数」 也确定了。此时左右子树就会变成相同的子问题,因此我们可以这样定义状态表示:

dp[i] 表示:当结点的数量为 i 个的时候,一共有多少颗 BST

难的是如何推导状态转移方程,因为它跟我们之前常见的状态转移方程不是很像。

2. 状态转移方程:

对于 dp[i],此时我们已经有 i 个结点了,为了方便叙述,我们将这 i 个结点排好序,并且编上 1,2,3,4,5......i 的编号。

那么,对于所有不同的 BST,我们可以按照下面的划分规则,分成不同的 i 类:「按照不同的头结点来分类」。分类结果就是:

  • ⅰ. 头结点为 1 号结点的所有 BST

  • ⅱ. 头结点为 2 号结点的所有 BST

  • ⅲ. ……

如果我们能求出 「每一类中的 BST 的数量」,将所有类的 BST 数量累加在一起,就是最后结果。

接下来选择 「头结点为 j 号」 的结点,来分析这 i 类 BST 的通用求法。

如果选择 「j 号结点来作为头结点」,根据 BST 的定义:

  • ⅰ. j 号结点的 「左子树」的结点编号应该在 [1, j - 1] 之间,一共有 j - 1 个结点。那么 j 号结点作为头结点的话,它的「左子树的种类」 就有 dp[j - 1] 种(回顾一下我们 dp 数组的定义哈);

  • ⅱ. j 号结点的 「右子树」的结点编号应该在 [j + 1, i] 之间,一共有 i - j 个结点。那么 j 号结点作为头结点的话,它的「右子树的种类」 就有 dp[i - j] 种;

根据 「排列组合」 的原理可得:j 号结点作为头结点的 BST 的种类一共有 dp[j - 1] * dp[i - j] 种!

因此,我们只要把 「不同头结点的 BST 数量」 累加在一起,就能得到 dp[i] 的值:dp[i] += dp[j - 1] * dp[i - j](\(1 <= j <= i\))。「注意用的是 +=,并且 j 从 1 变化到 i」。

3. 初始化:

我们注意到,每一个状态转移里面的 j - 1 和 i - j 都是小于 i 的,并且可能会用到前一个的状态(当 i = 1,j = 1 的时候,要用到 dp[0] 的数据)。因此要先把第一个元素初始化。

当 i = 0 的时候,表示一颗空树,「空树也是一颗二叉搜索树」,因此 dp[0] = 1

4. 填表顺序:

根据 「状态转移方程」,易得「从左往右」。

5. 返回值:

根据 「状态表示」,我们要返回的是 dp[n] 的值。

class Solution {
public:int numTrees(int n) {vector<int> dp(n + 1);dp[0] = 1;for(int i = 1;i <= n; ++i)for(int j = 1; j <= i; ++j){dp[i] += dp[j - 1] * dp[i - j];}return dp[n];}
};

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

相关文章:

  • JAVA·方法的使用
  • 做rap的网站国内做网站网站风险大吗
  • 【代码随想录算法训练营——Day33】动态规划——62.不同路径、63.不同路径II、343.整数拆分、96.不同的二叉搜索树
  • 基于单片机的元胞自动机仿真系统设计
  • 星座运势网站技术解析:从零打造现代化Web应用
  • Asp.net core 跨域配置
  • Java学习之旅第二季-18:转型
  • 建筑物孪生模型:重构空间数字化格局,赋能智慧城市
  • Claude code、codex、gemini cli开启全自动(yolo)模式,无需审批
  • wordpress账号和站内网建网站需要注册公司吗
  • 24软件测试计划主要工作和确定测试资源
  • 【每天一个知识点】[特殊字符] 大数据的定义及单位
  • ICT 数字测试原理 17 - -VCL中的预处理
  • 领码方案|微服务与SOA的世纪对话(7):运营降本增效——智能架构时代的成本与服务管理
  • YOLO v1:目标检测领域的单阶段革命之作
  • 河北建设厅八大员报名网站中国网库网站介绍
  • 基于RuoYi框架+Mysql的汽车进销存后台管理系统
  • 网站底部导航制作制作视频特效
  • 南宁网站建设索王道下拉建设网站的法律声明
  • Java中Mock的写法
  • 在JavaScript / HTML中,调整div的边框
  • 关于margin:auto的注意点
  • 23种设计模式——责任链模式(Chain of Responsibility Pattern)
  • istio 为什么在主机上抓不到15001和15006的流量
  • 怎么建设电子邮箱网站wordpress国外空间
  • 网站内容页怎么设计模板网络建设与维护公司
  • 网页版的点名/抽奖程序
  • 学做课件的网站商丘seo快速排名
  • 海康相机拍照与上传图像识别系统
  • Oracle Database 23ai新特性之INSERT语句增强