【阿里笔试题】【三维dp优化二维dp】【hard】力扣879. 盈利计划

集团里有 n 名员工,他们可以完成各种各样的工作创造利润。

第 i 种工作会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。如果成员参与了其中一项工作,就不能参与另一项工作。

工作的任何至少产生 minProfit 利润的子集称为 盈利计划 。并且工作的成员总数最多为 n 。

有多少种计划可以选择?因为答案很大,所以 返回结果模 10^9 + 7 的值。

示例 1:
输入:n = 5, minProfit = 3, group = [2,2], profit = [2,3]
输出:2
解释:至少产生 3 的利润,该集团可以完成工作 0 和工作 1 ,或仅完成工作 1 。
总的来说,有两种计划。

示例 2:
输入:n = 10, minProfit = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:至少产生 5 的利润,只要完成其中一种工作就行,所以该集团可以完成任何工作。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。

提示:
1 <= n <= 100
0 <= minProfit <= 100
1 <= group.length <= 100
1 <= group[i] <= 100
profit.length == group.length
0 <= profit[i] <= 100


这道题有三维dp和优化后的二维dp两种解法,但是需要注意:
两种方法中dp[i][j][k]定义不同了。 第一种方法中表示为j个工人需要全部用到,所以最后需要根据j累加最后的结果, 第二种方法中表示为j个工人可以不全用到, 所以dp[n][minProfit]就是最终结果, 直接返回即可。 在代码中的差别就在于是否对i=0时的dp[0][j][0]赋值为1。

三维dp

//两种方法中dp[i][j][k]定义不同了。 第一种方法中表示为j个工人需要全部用到,所以最后需要根据j累加最后的结果, 第二种方法中表示为j个工人可以不全用到, 所以dp[n][minProfit]就是最终结果, 直接返回即可。 在代码中的差别就在于是否对i=0时的dp[0][j][0]赋值为1

// 当 j 和 k 从 0 开始时:
// j = 0:没有成员可以分配任务,唯一的方案是什么任务都不做。
// k = 0:没有利润要求时,任何成员分配只要合法都是有效的方案。
class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size(), MOD = (int)1e9 + 7;
        vector<vector<vector<int>>> dp(len + 1, vector<vector<int>>(n + 1, vector<int>(minProfit + 1)));
        dp[0][0][0] = 1;
        for(int i = 1; i <= len; i++){
            int member = group[i-1], earn = profit[i-1];
            for(int j = 0; j <= n; j++){
                //工作利润至少为k
                for(int k = 0; k <= minProfit; k++){
                    if(j >= member){
                        dp[i][j][k] = (dp[i-1][j][k] + dp[i-1][j-member][max(0, k-earn)]) % MOD;
                    }else{
                        dp[i][j][k] = dp[i-1][j][k] % MOD;
                    }
                }
            }
        }

        int sum = 0;
        for(int j = 0; j <= n; j++){
            sum = (sum + dp[len][j][minProfit]) % MOD;
        }
        return sum;
    }
};

维护一个三维的dp,定义是选择前i个工作,然后使用刚好j个员工,能够创造的最低利润是多少。首先遍历每一个工作,同时定义两个整型来记录需要的员工和能够创造的利润。接着同样遍历j和k,分配的员工数量能够开展当前工作的时候,才会考虑选不选这个工作。首先不选的话,那么就和前i-1个工作且分配j个人,然后创造的最少利润为k的选择一样。如果选择这个工作,那么为了保证member数量的人来完成这个工作,然后剩下的j-member的人去做的工作在前i-1个项目中最少可以创造k-earn的利润,其中要注意的是dp中储存的不是恰好k利润,而是最少k利润,且题目利润没有负数,那么当k-earn小于负数的时候,为了防止数组索引出现负数越界,所以把他定义为0即可。所以我们列出动态转换方程 dp[i][j][k] = (dp[i-1][j][k] + dp[i-1][j-member][max(0, k-earn)]) % MOD;。然后当能分配人数连当前工作都分配不了,就不考虑选择这工作,列出动态转移方程dp[i][j][k] = dp[i-1][j][k] % MOD;

由于在法一三维dp中我们的j是恰好j个员工,那么就要枚举出前len个项目中,最小利润为minProfit的所有情况,也就是把j的所有情况遍历,然后加到sum中储存。最后返回sum,也就是可选择的盈利计划。

优化:二维dp

class Solution {
public:
    int profitableSchemes(int n, int minProfit, vector<int>& group, vector<int>& profit) {
        int len = group.size(), MOD = (int)1e9 + 7;
        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 <= len; i++){
            int member = group[i-1], earn = profit[i-1];
            for(int j = n; j >= member; j--){
                //工作利润至少为k
                for(int k = minProfit; k >= 0; k--){                    
                    dp[j][k] = (dp[j][k] + dp[j-member][max(0, k-earn)]) % MOD;
                }
            }
        }   
        return dp[n][minProfit];
    }
};

利用滚动数组倒序来减少dp维度不少见,这个方法容易疑惑的地方实际上是在法2中,j的含义是最多可以使用j个员工,可以不全部使用。造成这种差异的本质原因有两点:
一个是三维dp每个维度独立表示工作数、人数和利润,递推过程中每一维的控制非常细致,可以确保你确切知道用了多少人、获得了多少利润,而二维dp通过二维数组进行存储,节省了空间,但同时也丧失了精确控制人数的能力。

第二个是在初始化中,由于二维dp这种初始化,dp[j][0] = 1 表示当利润至少为 0 时,无论是用 0 个人、1 个人,还是用 j 个人,这样的方案都被认为是有效的方案。
而在三维dp中,dp[0][0][0] = 1:这意味着在没有选择任何工作的情况下,使用 0 个人,获得 0 的利润时,有一种合法的方案——即不做任何事情。在计算后dp[i][0][0] = 1 对于所有 i 都成立,因为这是空方案的状态。而dp[0][j][0]都初始化为0,意味着没有工作可以让你恰好使用j个人去完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值