
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在背包问题中,我们最熟悉的往往是"一个容量限制下如何选择物品",比如在固定背包容量内获得最大价值.但在实际问题中,限制条件常常不止一个:既要控制体积,又要控制重量;既要消耗时间,又要消耗成本;既要满足容量约束,还要兼顾数量限制.这类同时受到两个费用约束的问题,就是典型的二维费用背包问题.二维费用背包可以看作 0-1 背包的扩展:普通背包只有一个容量维度,而二维费用背包需要同时维护两个容量维度.因此,状态定义、状态转移和遍历顺序都会发生变化.如果没有理解清楚,很容易在数组维度设计、边界初始化或循环方向上出错.本文将围绕"二维费用的背包问题"展开,从问题模型出发,逐步讲解状态定义、转移方程、空间优化与代码实现,并结合典型例题分析解题思路.通过本文,你将掌握如何把"双重约束"转化为动态规划模型,理解二维费用背包的核心套路,并能灵活应对相关变形题.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
1.二维费用的背包问题背景介绍
在经典背包问题中,我们通常只考虑一个限制条件,例如背包容量不能超过 W,每个物品有对应的重量和价值,目标是在容量范围内选择物品,使总价值最大.这类问题可以很好地描述"单一资源约束"下的最优选择.
但在很多实际场景中,约束条件往往不止一个.比如:
在购物时,不仅要考虑预算,还要考虑背包空间;
在项目排期中,不仅要考虑人力成本,还要考虑时间成本;
在服务器资源分配中,不仅要考虑 CPU 使用量,还要考虑内存占用;
在学习计划安排中,不仅要考虑学习时间,还要考虑精力消耗.
这类问题的共同特点是:每个物品或任务都会同时消耗两种资源,而我们需要在两种资源都不超限的前提下,获得最大收益.这就是二维费用的背包问题.
与普通 0-1 背包相比,二维费用背包多了一个费用维度.普通背包只需要考虑"当前容量是否够用",而二维费用背包需要同时判断两个条件是否满足.例如,一个物品可能同时占用 cost1 和 cost2 两种资源,只有当两个维度的剩余容量都足够时,才能选择该物品.
因此,二维费用背包的状态通常需要设计为:
dp[i][j]
其中,i 表示第一种费用的容量限制,j 表示第二种费用的容量限制,dp[i][j] 表示在这两个限制条件下能够获得的最大价值.
本质上,二维费用背包就是在普通背包的基础上,把"一维容量限制"扩展成了"二维容量限制".它考查的不只是背包模型本身,还考查我们对状态设计、状态转移和遍历顺序的理解.掌握它之后,很多带有"双重约束"的动态规划问题都会变得更加清晰.
2.一和零(OJ题)

算法思路:解法(动态规划):
先将问题转化成我们熟悉的题型.
i. 在一些物品中挑选一些出来,然后在满足某个限定条件下,解决一些问题,大概率是背包模型;
ii. 由于每一个物品都只有 1 个,因此是一个01 背包问题.
但是,我们发现这一道题里面有两个限制条件.因此是一个二维费用的 01 背包问题.那么我们定义状态表示的时候,来一个三维 dp 表,把第二个限制条件加上即可.
1.状态表示:
dp[i][j][k] 表示:从前 i 个字符串中挑选,字符 0 的个数不超过 j,字符 1 的个数不超过 k,所有的选法中,最大的长度.
2.状态转移方程:
线性 dp 状态转移方程分析方式,一般都是根据最后一步的状况,来分情况讨论.为了方便叙述,我们记第 i 个字符中,字符 0 的个数为 a,字符 1 的个数为 b:
i. 不选第 i 个字符串:相当于就是去前 i - 1 个字符串中挑选,并且字符 0 的个数不超过 j,字符 1 的个数不超过 k.此时的最大长度为 dp[i][j][k] = dp[i - 1][j][k];
ii. 选择第 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 表示字符串数组的长度.
6.空间优化:
所有的背包问题,都可以进行空间上的优化.
对于二维费用的 01 背包类型的,我们的优化策略是:
i. 删掉第一维;
ii. 修改第二层以及第三层循环的遍历顺序即可.






核心代码
cpp
class Solution {
public:
//strs 二进制字符串数组
// m 最多可以使用的 0 的数量
//n 最多可以使用的 1 的数量
int findMaxForm(vector<string>& strs, int m, int n) {
//获取字符串数组的长度
int len = strs.size();
//三维dp数组:dp[i][j][k]
//表示:前 i 个字符串,使用 j 个 0、k 个 1 时,能组成的最大子集长度
vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(m + 1, vector<int>(n + 1)));
//遍历每一个字符串(i从1开始,对应前i个物品)
for(int i = 1; i <= len; i++)
{
//统计当前字符串中 0 的个数 a,1 的个数 b
int a = 0, b = 0;
for(auto ch : strs[i - 1]) //strs[i-1] 是第i个字符串(数组下标从0开始)
if(ch == '0') a++;
else b++;
//逆序遍历 0 的可用数量(01背包,逆序防止重复选取)
for(int j = m; j >= 0; j--)
//逆序遍历 1 的可用数量
for(int k = n; k >= 0; k--)
{
//状态1:不选当前字符串,最大长度等于前i-1个字符串的结果
dp[i][j][k] = dp[i - 1][j][k];
//状态2:如果剩余的0和1足够选当前字符串
if(j >= a && k >= b)
//选当前字符串:取「不选」和「选」的最大值,选的话长度+1
dp[i][j][k] = max(dp[i][j][k], dp[i - 1][j - a][k - b] + 1);
}
}
//最终结果:所有字符串,最多用m个0、n个1的最大子集长度
return dp[len][m][n];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
class Solution {
public:
// strs 二进制字符串数组
// m 最多可以使用的 0 的数量
// n 最多可以使用的 1 的数量
int findMaxForm(vector<string>& strs, int m, int n) {
// 获取字符串数组的长度
int len = strs.size();
// 三维 dp 数组:dp[i][j][k]
// 表示:前 i 个字符串,使用 j 个 0、k 个 1 时,能组成的最大子集长度
vector<vector<vector<int>>> dp(
len + 1,
vector<vector<int>>(m + 1, vector<int>(n + 1, 0))
);
// 遍历每一个字符串
for (int i = 1; i <= len; i++)
{
// 统计当前字符串中 0 的个数 a,1 的个数 b
int a = 0, b = 0;
for (auto ch : strs[i - 1])
{
if (ch == '0')
{
a++;
}
else
{
b++;
}
}
// 遍历 0 的可用数量
for (int j = m; j >= 0; j--)
{
// 遍历 1 的可用数量
for (int k = n; k >= 0; k--)
{
// 状态1:不选当前字符串
dp[i][j][k] = dp[i - 1][j][k];
// 状态2:选当前字符串
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];
}
};
void printVector(const vector<string>& strs)
{
cout << "[";
for (int i = 0; i < strs.size(); i++)
{
cout << "\"" << strs[i] << "\"";
if (i != strs.size() - 1)
{
cout << ", ";
}
}
cout << "]";
}
void runTest(Solution& solution, vector<string> strs, int m, int n, int expected)
{
int result = solution.findMaxForm(strs, m, n);
cout << "strs = ";
printVector(strs);
cout << endl;
cout << "m = " << m << ",n = " << n << endl;
cout << "最大子集长度 = " << result << endl;
cout << "期望结果 = " << expected << endl;
if (result == expected)
{
cout << "测试通过" << endl;
}
else
{
cout << "测试失败" << endl;
}
cout << "------------------------" << endl;
}
int main()
{
Solution solution;
// 测试用例1:经典示例
// 可以选择 ["10", "0001", "1", "0"]
// 总共使用 5 个 0,3 个 1
runTest(
solution,
{"10", "0001", "111001", "1", "0"},
5,
3,
4
);
// 测试用例2:经典示例
// 可以选择 ["0", "1"],最大长度为 2
runTest(
solution,
{"10", "0", "1"},
1,
1,
2
);
// 测试用例3:资源充足,可以全部选择
runTest(
solution,
{"0", "1", "10", "01"},
4,
4,
4
);
// 测试用例4:没有 0 的额度,只能选择全是 1 的字符串
runTest(
solution,
{"0", "1", "11", "01"},
0,
3,
2
);
// 测试用例5:没有 1 的额度,只能选择全是 0 的字符串
runTest(
solution,
{"0", "00", "1", "10"},
3,
0,
2
);
// 测试用例6:m 和 n 都为 0,不能选择任何非空字符串
runTest(
solution,
{"0", "1", "10"},
0,
0,
0
);
// 测试用例7:字符串数组为空
runTest(
solution,
{},
5,
5,
0
);
// 测试用例8:多个字符串竞争资源
// 最优可以选择 "10", "0", "1",长度为 3
runTest(
solution,
{"10", "000", "111", "0", "1"},
2,
2,
3
);
// 测试用例9:每个字符串都超过资源限制
runTest(
solution,
{"000", "111", "0011"},
1,
1,
0
);
// 测试用例10:重复字符串
runTest(
solution,
{"10", "10", "10"},
2,
2,
2
);
return 0;
}

核心代码
cpp
//空间优化后
class Solution
{
public:
//strs 二进制字符串数组
//m最多允许使用的 0 的数量
//n最多允许使用的 1 的数量
int findMaxForm(vector<string>& strs, int m, int n)
{
//获取字符串数组的总长度
int len = strs.size();
//【空间优化】二维dp数组:dp[j][k]
//含义:最多使用 j 个 0、k 个 1 时,能组成的最大子集长度
//省略了物品维度,直接用二维数组滚动更新,节省空间
vector<vector<int>> dp(m + 1, vector<int>(n + 1));
//遍历每一个字符串(逐个处理背包中的物品)
for(int i = 1; i <= len; i++)
{
//统计当前字符串中 0 的个数 a,1 的个数 b
int a = 0, b = 0;
//strs[i-1]:数组下标从0开始,对应第i个字符串
for(auto ch : strs[i - 1])
if(ch == '0') a++;
else b++;
//【核心】逆序遍历 0 的数量(从大到小)
//01背包必须逆序,保证每个字符串只被选一次,避免重复选取
//直接从 a 开始遍历:小于a个0无法选当前字符串,优化循环次数
for(int j = m; j >= a; j--)
//逆序遍历 1 的数量,同理
for(int k = n; k >= b; k--)
//状态转移方程:
//1.不选当前字符串:保持 dp[j][k] 原值
//2.选当前字符串:dp[j-a][k-b] + 1(消耗a个0、b个1,子集长度+1)
//取两种情况的最大值,更新dp数组
dp[j][k] = max(dp[j][k], dp[j - a][k - b] + 1);
}
//最终结果:最多使用m个0、n个1的最大子集长度
return dp[m][n];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
// 空间优化后
class Solution
{
public:
// strs 二进制字符串数组
// m 最多允许使用的 0 的数量
// n 最多允许使用的 1 的数量
int findMaxForm(vector<string>& strs, int m, int n)
{
// 获取字符串数组的总长度
int len = strs.size();
// 【空间优化】二维 dp 数组:dp[j][k]
// 含义:最多使用 j 个 0、k 个 1 时,能组成的最大子集长度
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// 遍历每一个字符串
for (int i = 1; i <= len; i++)
{
// 统计当前字符串中 0 的个数 a,1 的个数 b
int a = 0, b = 0;
for (auto ch : strs[i - 1])
{
if (ch == '0')
{
a++;
}
else
{
b++;
}
}
// 01 背包必须逆序遍历,保证每个字符串只被选一次
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];
}
};
void printVector(const vector<string>& strs)
{
cout << "[";
for (int i = 0; i < strs.size(); i++)
{
cout << "\"" << strs[i] << "\"";
if (i != strs.size() - 1)
{
cout << ", ";
}
}
cout << "]";
}
void runTest(Solution& solution, vector<string> strs, int m, int n, int expected)
{
int result = solution.findMaxForm(strs, m, n);
cout << "strs = ";
printVector(strs);
cout << endl;
cout << "m = " << m << ",n = " << n << endl;
cout << "最大子集长度 = " << result << endl;
cout << "期望结果 = " << expected << endl;
if (result == expected)
{
cout << "测试通过" << endl;
}
else
{
cout << "测试失败" << endl;
}
cout << "------------------------" << endl;
}
int main()
{
Solution solution;
// 测试用例1:经典示例
// 可以选择 ["10", "0001", "1", "0"]
// 总共使用 5 个 0,3 个 1
runTest(
solution,
{"10", "0001", "111001", "1", "0"},
5,
3,
4
);
// 测试用例2:经典示例
// 可以选择 ["0", "1"],最大长度为 2
runTest(
solution,
{"10", "0", "1"},
1,
1,
2
);
// 测试用例3:资源充足,可以全部选择
runTest(
solution,
{"0", "1", "10", "01"},
4,
4,
4
);
// 测试用例4:没有 0 的额度,只能选择全是 1 的字符串
// 可以选择 "1" 和 "11",共 3 个 1,最大长度为 2
runTest(
solution,
{"0", "1", "11", "01"},
0,
3,
2
);
// 测试用例5:没有 1 的额度,只能选择全是 0 的字符串
// 可以选择 "0" 和 "00",共 3 个 0,最大长度为 2
runTest(
solution,
{"0", "00", "1", "10"},
3,
0,
2
);
// 测试用例6:m 和 n 都为 0,不能选择任何非空字符串
runTest(
solution,
{"0", "1", "10"},
0,
0,
0
);
// 测试用例7:字符串数组为空
runTest(
solution,
{},
5,
5,
0
);
// 测试用例8:多个字符串竞争资源
// 最优可以选择 "10", "0", "1",长度为 3
runTest(
solution,
{"10", "000", "111", "0", "1"},
2,
2,
3
);
// 测试用例9:每个字符串都超过资源限制
runTest(
solution,
{"000", "111", "0011"},
1,
1,
0
);
// 测试用例10:重复字符串
// 只能选择两个 "10",共使用 2 个 0 和 2 个 1
runTest(
solution,
{"10", "10", "10"},
2,
2,
2
);
// 测试用例11:只含 0 的字符串
runTest(
solution,
{"0", "00", "000"},
3,
5,
2
);
// 测试用例12:只含 1 的字符串
runTest(
solution,
{"1", "11", "111"},
5,
3,
2
);
return 0;
}

3.盈利计划(OJ题)

算法思路:解法(动态规划):
这道题目非常难读懂,但是如果结合例子多读几遍,你就会发现是一个经典的二维费用的背包问题.因此我们可以仿照二维费用的背包来定义状态表示.
1.状态表示:
dp[i][j][k] 表示:从前 i 个计划中挑选,总人数不超过 j,总利润至少为 k,一共有多少种选法.
注意注意注意,这道题里面出现了一个至少,和我们之前做过的背包问题不一样.因此,我们在分析状态转移方程的时候要结合实际情况考虑一下.
2.状态转移方程:
老规矩,根据最后一个位置的元素,结合题目的要求,我们有选择最后一个元素或者不选择最后一个元素两种策略:
i. 不选 i 位置的计划:那我们只能去前 i - 1 个计划中挑选,总人数不超过 j,总利润至少为 k.此时一共有 dp[i - 1][j][k] 种选法;
ii. 选择 i 位置的计划:那我们在前 i - 1 个计划中挑选的时候,限制就变成了,总人数不超过 j - g[i],总利润至少为 k - p[i].此时一共有 dp[i - 1][j - g[i]][k - p[i]].
第二种情况下有两个细节需要注意:
-
j - g[i] < 0:此时说明g[i]过大,也就是人数过多.因为我们的状态表示要求人数是不能超过j的,因此这个状态是不合法的,需要舍去. -
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 表示字符串数组的长度.
6.空间优化:
所有的背包问题,都可以进行空间上的优化.
对于二维费用的01背包类型的,我们的优化策略是:
i. 删掉第一维;
ii. 修改第二层以及第三层循环的遍历顺序即可.






核心代码
cpp
class Solution {
public:
//n员工总人数
//m最低盈利目标
//g每个工作需要的员工数数组
//p每个工作产生的利润数组
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) {
// 取模常量,防止数值溢出
const int MOD = 1e9 + 7;
// 工作的总数量
int len = g.size();
// 三维dp数组:dp[i][j][k]
// 状态定义:前 i 个工作,使用 j 个员工,总利润**至少**为 k 时的方案总数
vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(m + 1)));
// 初始化:0个工作,任意员工数,利润至少为0的方案数 = 1(空方案,不选任何工作)
for(int j = 0; j <= n; j++)
dp[0][j][0] = 1;
// 遍历每一个工作(i从1开始,对应前i个工作)
for(int i = 1; i <= len; i++)
// 遍历所有可用员工数 j
for(int j = 0; j <= n; j++)
// 遍历所有目标利润 k
for(int k = 0; k <= m; k++)
{
// 状态1:不选第 i 个工作
// 方案数 = 前 i-1 个工作,j个员工,利润k 的方案数
dp[i][j][k] = dp[i - 1][j][k];
// 状态2:选第 i 个工作(前提:员工数足够)
// g[i-1]:第i个工作需要的员工数(数组下标从0开始)
if(j >= g[i - 1])
{
// p[i-1]:第i个工作的利润
// max(0, k - p[i-1]):利润超过目标m时,统一按0计算(核心优化)
dp[i][j][k] += dp[i - 1][j - g[i - 1]][max(0, k - p[i - 1])];
}
// 每次累加后取模,避免整数溢出
dp[i][j][k] %= MOD;
}
// 最终结果:所有工作,最多n个员工,利润至少m的方案总数
return dp[len][n][m];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
// n 员工总人数
// m 最低盈利目标
// g 每个工作需要的员工数数组
// p 每个工作产生的利润数组
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p) {
// 取模常量,防止数值溢出
const int MOD = 1e9 + 7;
// 工作的总数量
int len = g.size();
// 三维 dp 数组:dp[i][j][k]
// 状态定义:
// 前 i 个工作,使用 j 个员工,总利润至少为 k 时的方案总数
vector<vector<vector<int>>> dp(
len + 1,
vector<vector<int>>(n + 1, vector<int>(m + 1, 0))
);
// 初始化:
// 0 个工作,任意员工数,利润至少为 0 的方案数 = 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++)
{
// 状态1:不选第 i 个工作
dp[i][j][k] = dp[i - 1][j][k];
// 状态2:选第 i 个工作,前提是员工数足够
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;
}
}
}
// 最终结果:
// 所有工作,最多 n 个员工,利润至少 m 的方案总数
return dp[len][n][m];
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (int i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
{
cout << ", ";
}
}
cout << "]";
}
void runTest(
Solution& solution,
int n,
int m,
vector<int> g,
vector<int> p,
int expected
)
{
int result = solution.profitableSchemes(n, m, g, p);
cout << "员工总数 n = " << n << endl;
cout << "最低盈利目标 m = " << m << endl;
cout << "每个工作需要的员工数 g = ";
printVector(g);
cout << endl;
cout << "每个工作产生的利润 p = ";
printVector(p);
cout << endl;
cout << "盈利计划方案数 = " << result << endl;
cout << "期望结果 = " << expected << endl;
if (result == expected)
{
cout << "测试通过" << endl;
}
else
{
cout << "测试失败" << endl;
}
cout << "------------------------" << endl;
}
int main()
{
Solution solution;
// 测试用例1:经典示例
// 可选方案:
// 选择第1个工作:员工2,利润2,不达标
// 选择第2个工作:员工2,利润3,达标
// 两个都选:员工4,利润5,达标
// 共2种
runTest(
solution,
5,
3,
{2, 2},
{2, 3},
2
);
// 测试用例2:经典示例
// n = 10, minProfit = 5
// group = [2, 3, 5]
// profit = [6, 7, 8]
// 任意单个工作都能达到利润5,并且部分组合也可以
// 共7种
runTest(
solution,
10,
5,
{2, 3, 5},
{6, 7, 8},
7
);
// 测试用例3:最低盈利目标为0
// 只要员工数不超过n,所有选择都算有效
// 工作数量为2,总方案数为:
// 不选、选第1个、选第2个、两个都选,共4种
runTest(
solution,
5,
0,
{2, 3},
{1, 2},
4
);
// 测试用例4:员工不够,无法选择任何工作
// 只有空方案,但利润目标为1,所以不达标
runTest(
solution,
1,
1,
{2, 3},
{2, 3},
0
);
// 测试用例5:员工刚好够选择一个工作
// 只有选择该工作可以达标
runTest(
solution,
2,
2,
{2},
{2},
1
);
// 测试用例6:没有工作,利润目标为0
// 空方案算一种
runTest(
solution,
5,
0,
{},
{},
1
);
// 测试用例7:没有工作,利润目标大于0
// 无法达到盈利目标
runTest(
solution,
5,
3,
{},
{},
0
);
// 测试用例8:多个工作,但只能选择部分
// 工作:
// 1号:员工2,利润2
// 2号:员工2,利润2
// 3号:员工3,利润3
// n=4,m=4
// 达标方案:
// 选1+2:员工4,利润4
// 共1种
runTest(
solution,
4,
4,
{2, 2, 3},
{2, 2, 3},
1
);
// 测试用例9:利润超过目标时统一按目标处理
// 选择任意一个利润为10的工作都达标
// 选择两个也达标
// 三个工作中,每个需要1人,n=2
// 可选达标方案:
// 单选3种,双选3种,共6种
runTest(
solution,
2,
5,
{1, 1, 1},
{10, 10, 10},
6
);
// 测试用例10:所有工作都可以选择
// n=6,m=6
// group=[1,2,3], profit=[1,2,3]
// 达标方案只有选择全部三个工作:
// 员工6,利润6
runTest(
solution,
6,
6,
{1, 2, 3},
{1, 2, 3},
1
);
return 0;
}

核心代码
cpp
//空间优化后
class Solution {
public:
//n可用的员工总人数
//m需要达到的最低盈利目标
//g每个工作所需的员工数量数组
//每个工作产生的利润数组
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p)
{
//取模常量,防止数值溢出,题目要求结果对 10^9+7 取模
const int MOD = 1e9 + 7;
//工作的总数量
int len = g.size();
//【空间优化】二维滚动dp数组:dp[j][k]
//状态定义:使用 j 个员工,总利润**至少**为 k 时的合法方案总数
//省略了工作维度,通过逆序遍历实现滚动更新,大幅节省内存
vector<vector<int>> dp(n + 1, vector<int>(m + 1));
//初始化:任意数量的员工,利润至少为 0 的方案数 = 1(空方案:不选择任何工作)
for(int j = 0; j <= n; j++)
dp[j][0] = 1;
//遍历每一个工作(逐个处理背包物品)
for(int i = 1; i <= len; i++)
//【核心】逆序遍历员工数 j(01背包必须逆序,保证每个工作只选一次)
//从 n 遍历到 g[i-1]:员工数不足时无法选择当前工作,直接跳过优化效率
for(int j = n; j >= g[i - 1]; j--)
//逆序遍历目标利润 k(滚动数组要求逆序更新)
for(int k = m; k >= 0; k--)
{
//状态转移:选择当前工作
//g[i-1]:第i个工作需要的员工数;p[i-1]:第i个工作的利润
//max(0, k - p[i-1]):利润超过目标m时,统一按0计算(简化状态)
dp[j][k] += dp[j - g[i - 1]][max(0, k - p[i - 1])];
//每次累加后取模,避免整数溢出
dp[j][k] %= MOD;
}
//最终结果:最多使用n个员工,利润至少为m的总方案数
return dp[n][m];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 空间优化后
class Solution {
public:
// n 可用的员工总人数
// m 需要达到的最低盈利目标
// g 每个工作所需的员工数量数组
// p 每个工作产生的利润数组
int profitableSchemes(int n, int m, vector<int>& g, vector<int>& p)
{
// 取模常量,防止数值溢出,题目要求结果对 10^9 + 7 取模
const int MOD = 1e9 + 7;
// 工作的总数量
int len = g.size();
// 【空间优化】二维滚动 dp 数组:dp[j][k]
// 状态定义:
// 使用 j 个员工,总利润至少为 k 时的合法方案总数
vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
// 初始化:
// 任意数量的员工,利润至少为 0 的方案数 = 1
// 空方案:不选择任何工作
for (int j = 0; j <= n; j++)
{
dp[j][0] = 1;
}
// 遍历每一个工作
for (int i = 1; i <= len; i++)
{
// 01 背包:员工数必须逆序遍历
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;
}
}
}
// 最终结果:
// 最多使用 n 个员工,利润至少为 m 的总方案数
return dp[n][m];
}
};
void printVector(const vector<int>& nums)
{
cout << "[";
for (int i = 0; i < nums.size(); i++)
{
cout << nums[i];
if (i != nums.size() - 1)
{
cout << ", ";
}
}
cout << "]";
}
void runTest(
Solution& solution,
int n,
int m,
vector<int> g,
vector<int> p,
int expected
)
{
int result = solution.profitableSchemes(n, m, g, p);
cout << "员工总数 n = " << n << endl;
cout << "最低盈利目标 m = " << m << endl;
cout << "每个工作需要的员工数 g = ";
printVector(g);
cout << endl;
cout << "每个工作产生的利润 p = ";
printVector(p);
cout << endl;
cout << "盈利计划方案数 = " << result << endl;
cout << "期望结果 = " << expected << endl;
if (result == expected)
{
cout << "测试通过" << endl;
}
else
{
cout << "测试失败" << endl;
}
cout << "------------------------" << endl;
}
int main()
{
Solution solution;
// 测试用例1:经典示例
// n = 5, minProfit = 3
// group = [2, 2], profit = [2, 3]
// 达标方案:
// 选择第2个工作:员工2,利润3
// 选择两个工作:员工4,利润5
// 共2种
runTest(
solution,
5,
3,
{2, 2},
{2, 3},
2
);
// 测试用例2:经典示例
// n = 10, minProfit = 5
// group = [2, 3, 5], profit = [6, 7, 8]
// 所有非空组合都能达标,且员工数不超过10
// 一共 2^3 - 1 = 7 种
runTest(
solution,
10,
5,
{2, 3, 5},
{6, 7, 8},
7
);
// 测试用例3:最低盈利目标为0
// 所有员工数不超过n的选择都合法
// 两个工作都可以选或不选,共4种方案
runTest(
solution,
5,
0,
{2, 3},
{1, 2},
4
);
// 测试用例4:员工不够,无法选择任何工作
// 目标利润大于0,空方案不达标
runTest(
solution,
1,
1,
{2, 3},
{2, 3},
0
);
// 测试用例5:员工刚好够选择一个工作
runTest(
solution,
2,
2,
{2},
{2},
1
);
// 测试用例6:没有工作,利润目标为0
// 空方案算一种
runTest(
solution,
5,
0,
{},
{},
1
);
// 测试用例7:没有工作,利润目标大于0
// 无法达到目标利润
runTest(
solution,
5,
3,
{},
{},
0
);
// 测试用例8:多个工作,但只能选择部分
// 工作:
// 1号:员工2,利润2
// 2号:员工2,利润2
// 3号:员工3,利润3
// n = 4,m = 4
// 达标方案只有:选择1号 + 2号
runTest(
solution,
4,
4,
{2, 2, 3},
{2, 2, 3},
1
);
// 测试用例9:利润超过目标时统一按目标处理
// 三个工作,每个需要1人,利润10
// n = 2,m = 5
// 单选3种,双选3种,共6种
runTest(
solution,
2,
5,
{1, 1, 1},
{10, 10, 10},
6
);
// 测试用例10:所有工作都可以选择,但只有全选才能达标
// n = 6,m = 6
// group = [1, 2, 3], profit = [1, 2, 3]
// 只有选择全部三个工作时,利润为6
runTest(
solution,
6,
6,
{1, 2, 3},
{1, 2, 3},
1
);
// 测试用例11:部分组合达标
// 工作:
// 1号:员工1,利润1
// 2号:员工1,利润2
// 3号:员工2,利润3
// n = 3,m = 3
// 达标方案:
// 选3号
// 选1号 + 2号
// 选1号 + 3号
// 选2号 + 3号
// 共4种
runTest(
solution,
3,
3,
{1, 1, 2},
{1, 2, 3},
4
);
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容【动态规划算法】(似包非包以及卡特兰数问题深入解析)
每日心灵鸡汤:你的努力,时间都看得见
在这个快节奏的时代,我们总是急于看到努力的结果.今天付出了,就希望明天有回报;这个月努力了,就希望下个月能成功.但很多时候,努力并不会立刻得到回报,它需要时间的沉淀.你的努力,时间都看得见.就像农民种庄稼,春天播种,夏天耕耘,秋天才能收获.你不能因为看不到果实,就放弃耕耘.同样,你不能因为暂时看不到成果,就放弃努力.那些看似没有回报的努力,其实都在为你未来的成功积累能量.那些成功的人,往往都是在别人看不到的地方默默努力.他们会在深夜里读书,会在清晨里锻炼,会在别人玩乐的时候学习.他们知道,时间是最公平的裁判,它会奖励那些坚持不懈的人.不要急于求成,不要害怕努力没有回报.只要你坚持下去,时间会给你最好的答案.你的努力,也许不会立刻让你成功,但它会让你变得越来越好,让你在未来遇到更好的自己.
