【动态规划算法】(一文讲透二维费用的背包问题)


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

在背包问题中,我们最熟悉的往往是"一个容量限制下如何选择物品",比如在固定背包容量内获得最大价值.但在实际问题中,限制条件常常不止一个:既要控制体积,又要控制重量;既要消耗时间,又要消耗成本;既要满足容量约束,还要兼顾数量限制.这类同时受到两个费用约束的问题,就是典型的二维费用背包问题.二维费用背包可以看作 0-1 背包的扩展:普通背包只有一个容量维度,而二维费用背包需要同时维护两个容量维度.因此,状态定义、状态转移和遍历顺序都会发生变化.如果没有理解清楚,很容易在数组维度设计、边界初始化或循环方向上出错.本文将围绕"二维费用的背包问题"展开,从问题模型出发,逐步讲解状态定义、转移方程、空间优化与代码实现,并结合典型例题分析解题思路.通过本文,你将掌握如何把"双重约束"转化为动态规划模型,理解二维费用背包的核心套路,并能灵活应对相关变形题.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

1.二维费用的背包问题背景介绍

在经典背包问题中,我们通常只考虑一个限制条件,例如背包容量不能超过 W,每个物品有对应的重量和价值,目标是在容量范围内选择物品,使总价值最大.这类问题可以很好地描述"单一资源约束"下的最优选择.

但在很多实际场景中,约束条件往往不止一个.比如:

在购物时,不仅要考虑预算,还要考虑背包空间;

在项目排期中,不仅要考虑人力成本,还要考虑时间成本;

在服务器资源分配中,不仅要考虑 CPU 使用量,还要考虑内存占用;

在学习计划安排中,不仅要考虑学习时间,还要考虑精力消耗.

这类问题的共同特点是:每个物品或任务都会同时消耗两种资源,而我们需要在两种资源都不超限的前提下,获得最大收益.这就是二维费用的背包问题.

与普通 0-1 背包相比,二维费用背包多了一个费用维度.普通背包只需要考虑"当前容量是否够用",而二维费用背包需要同时判断两个条件是否满足.例如,一个物品可能同时占用 cost1cost2 两种资源,只有当两个维度的剩余容量都足够时,才能选择该物品.

因此,二维费用背包的状态通常需要设计为:
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]].

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

  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 表示字符串数组的长度.

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;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容【动态规划算法】(似包非包以及卡特兰数问题深入解析)


每日心灵鸡汤:你的努力,时间都看得见
在这个快节奏的时代,我们总是急于看到努力的结果.今天付出了,就希望明天有回报;这个月努力了,就希望下个月能成功.但很多时候,努力并不会立刻得到回报,它需要时间的沉淀.你的努力,时间都看得见.就像农民种庄稼,春天播种,夏天耕耘,秋天才能收获.你不能因为看不到果实,就放弃耕耘.同样,你不能因为暂时看不到成果,就放弃努力.那些看似没有回报的努力,其实都在为你未来的成功积累能量.那些成功的人,往往都是在别人看不到的地方默默努力.他们会在深夜里读书,会在清晨里锻炼,会在别人玩乐的时候学习.他们知道,时间是最公平的裁判,它会奖励那些坚持不懈的人.不要急于求成,不要害怕努力没有回报.只要你坚持下去,时间会给你最好的答案.你的努力,也许不会立刻让你成功,但它会让你变得越来越好,让你在未来遇到更好的自己.

相关推荐
S1998_1997111609•X1 小时前
论述情况盀导致系统应用通信通讯协议被恶意注入污染蜜罐开元盀用于非法侵入爬虫植入ssd的通用技术原理
网络·网络协议·百度·哈希算法·开闭原则
知识分享小能手1 小时前
R语言入门学习教程,从入门到精通,初识R语言(1)
开发语言·学习·r语言
2301_815279521 小时前
鸿蒙原生开发的“硬核通道”:ArkTS 与 C/C++ 高性能互操作全栈指南 —— FFI 机制深度解析与实战精要
c语言·c++·harmonyos
Zevalin爱灰灰7 小时前
现代密码学 第二章——流密码【下】
算法·密码学
飞Link9 小时前
大模型长文本的“救命稻草”:深度解析 TurboQuant 与 KV Cache 压缩技术
算法
郝学胜-神的一滴10 小时前
深度学习优化核心:梯度下降与网络训练全解析
数据结构·人工智能·python·深度学习·算法·机器学习
Je1lyfish11 小时前
CMU15-445 (2025 Fall/2026 Spring) Project#3 - QueryExecution
linux·c语言·开发语言·数据结构·数据库·c++·算法
许彰午11 小时前
03-二叉树——从递归遍历到非递归实现
java·算法
Brilliantwxx11 小时前
【C++】 vector(代码实现+坑点讲解)
开发语言·c++·笔记·算法