集团里有 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个人去完成。