[LeetCode] 879. Profitable Schemes 盈利方案



There are G people in a gang, and a list of various crimes they could commit.html

The i-th crime generates a profit[i] and requires group[i] gang members to participate.git

If a gang member participates in one crime, that member can't participate in another crime.github

Let's call a profitable scheme any subset of these crimes that generates at least P profit, and the total number of gang members participating in that subset of crimes is at most G.数组

How many schemes can be chosen?  Since the answer may be very large, return it modulo 10^9 + 7.函数

Example 1:优化

Input: G = 5, P = 3, group = [2,2], profit = [2,3]
Output: 2
Explanation:
To make a profit of at least 3, the gang could either commit crimes 0 and 1, or just crime 1.
In total, there are 2 schemes.

Example 2:ui

Input: G = 10, P = 5, group = [2,3,5], profit = [6,7,8]
Output: 7
Explanation:
To make a profit of at least 5, the gang could commit any crimes, as long as they commit one.
There are 7 possible schemes: (0), (1), (2), (0,1), (0,2), (1,2), and (0,1,2).

Note:3d

  1. 1 <= G <= 100
  2. 0 <= P <= 100
  3. 1 <= group[i] <= 100
  4. 0 <= profit[i] <= 100
  5. 1 <= group.length = profit.length <= 100


这道题说的是黑帮如何合理分配资源,从而实现利润最大化的问题,感受这年头连黑帮也得合理分配资源,还必须得懂动态规划,我也是醉了。这个题目的背景设定这么叼,不怕教坏小盆友么。说是黑帮中总共有G我的,如今有好几票生意,每票买卖须要的人手不一样,分别放在数组 group 中,对应的每票生意能赚的利润放在了数组 profit 中。假如如今黑帮老大设定了一个绩效指标P,帮里这G我的随便用,任务随便作,只要能赚到很多于P的利润便可,惟一的限制就是一个弟兄不能作多个任务(可能由于危险度很高,弟兄可能无法活着回来),问有多少种作任务的方式。这实际上是一道多重背包问题 Knapsack,改天有时间了博主想专门作一期背包问题的总结帖,敬请期待~ 好,回到题目来,题目中说告终果可能很是大,要对一个超大数取余,看到这里,咱们也就该明白为了避免爆栈,只能用动态规划 Dynamic Programming 来作,LeetCode 里有好多题都是要对这个 1e9+7 取余,不知道为啥都是对这个数取余。Anyway,who cares,仍是来想一想 dp 数组如何定义以及怎么推导状态转移方程吧。code

首先来看分配黑帮资源时候都须要考虑哪些因素,总共有三点,要干几票买卖,要用多少人,能挣多少钱。因此咱们须要一个三维的 dp 数组,其中 dp[k][i][j] 表示最多干k票买卖,总共用了i我的,得到利润为j的状况下分配方案的总数,初始化 dp[0][0][0] 为1。如今来推导状态转移方程,整个规划的核心是买卖,总共买卖的个数是固定的,每多干一票买卖,可能的分配方法就可能增长,但不可能减小的,由于假如当前已经算出来作 k-1 次买卖的分配方法总数,再作一次买卖,以前的分配方法不会减小,顶可能是人数不够,作不成当前这票买卖而已,因此咱们的 dp[k][i][j] 能够先更新为 dp[k-1][i][j],而后再来看这第k个买卖还能不能作,咱们知道假设这第k个买卖须要g我的,能得到利润p,只有当咱们如今的人数i大于等于g的时候,才有可能作这个任务,咱们要用g我的来作任务k的话,那么其他的 k-1 个任务只能由 i-g 我的来作了,并且因为整个须要产生利润j,第k个任务能产生利润p,因此其他的 k-1 个任务须要产生利润 j-p,因为利润不能是负值,因此咱们还须要跟0比较,取两者的最大值,综上所述,若咱们选择作任务k,则能新产生的分配方案的个数为 dp[k-1][i-g][max(0,j-p)],记得每次累加完要对超大数取余。最终咱们须要将 dp[n][i][P] ( 0 <= i <= G ) 累加起来,由于咱们不必定要所有使用G我的,只要能产生P的利润,用几我的都不要紧,而k是表示最多干的买卖数,可能上并无干到这么多,因此只须要累加人数这个维度便可,参见代码以下:htm



解法一:

class Solution {
public:
    int profitableSchemes(int G, int P, vector<int>& group, vector<int>& profit) {
        int n = group.size(), res = 0, M = 1e9 + 7;
        vector<vector<vector<int>>> dp(n + 1, vector<vector<int>>(G + 1, vector<int>(P + 1)));
        dp[0][0][0] = 1;
        for (int k = 1; k <= n; ++k) {
            int g = group[k - 1], p = profit[k - 1];
            for (int i = 0; i <= G; ++i) {
                for (int j = 0; j <= P; ++j) {
                    dp[k][i][j] = dp[k - 1][i][j];
                    if (i >= g) {
                        dp[k][i][j] = (dp[k][i][j] + dp[k - 1][i - g][max(0, j - p)]) % M;
                    }
                }
            }
        }
        for (int i = 0; i <= G; ++i) {
            res = (res + dp[n][i][P]) % M;
        }
        return res;
    }
};



咱们也可优化一下空间复杂度,由于当前作的第k个任务,只跟前 k-1 个任务的分配方案有关,因此并不须要保存全部的任务个数的分配方式。这样咱们就节省了一个维度,可是须要注意的是,更新的时候i和j只能从大到小更新,这个其实也不难理解,由于此时 dp[i][j] 存的是前 k-1 个任务的分配方式,因此更新第k个任务的时候,必定要从后面开始覆盖,由于用到了前面的值,若从前面的值开始更新的话,就不能保证用到的都是前 k-1 个任务的分配方式,有可能用到的是已经更新过的值,就会出错,参见代码以下:



解法二:

class Solution {
public:
    int profitableSchemes(int G, int P, vector<int>& group, vector<int>& profit) {
        int n = group.size(), res = 0, M = 1e9 + 7;
        vector<vector<int>> dp(G + 1, vector<int>(P + 1));
        dp[0][0] = 1;
        for (int k = 1; k <= n; ++k) {
            int g = group[k - 1], p = profit[k - 1];
            for (int i = G; i >= g; --i) {
                for (int j = P; j >= 0; --j) {
                    dp[i][j] = (dp[i][j] + dp[i - g][max(0, j - p)]) % M;
                }
            }
        }
        for (int i = 0; i <= G; ++i) {
            res = (res + dp[i][P]) % M;
        }
        return res;
    }
};



咱们也能够用递归加记忆数组来作,基本思想跟解法一没有太大的区别,递归的记忆数组其实跟迭代形式的 dp 数组没有太大的区别,做用都是保存中间状态从而减小大量的重复计算。这里稍稍须要注意下的就是递归函数中的 corner case,当 k=0 时,则根据j的值来返回0或1,当j小于等于0,返回1,不然返回0,至关于修改了初始化值(以前都初始化为了整型最小值),而后当j小于0时,则j赋值为0,由于利润不能为负值。而后就看若当前的 memo[k][i][j] 已经计算过了,则直接返回便可,参见代码以下:



解法三:

class Solution {
public:
    int profitableSchemes(int G, int P, vector<int>& group, vector<int>& profit) {
        vector<vector<vector<int>>> memo(group.size() + 1, vector<vector<int>>(G + 1, vector<int>(P + 1, INT_MIN)));
        return helper(group.size(), G, P, group, profit, memo);
    }
    int helper(int k, int i, int j, vector<int>& group, vector<int>& profit, vector<vector<vector<int>>>& memo) {
        if (k == 0) return j <= 0;
        if (j < 0) j = 0;
        if (memo[k][i][j] != INT_MIN) return memo[k][i][j];
        int g = group[k - 1], p = profit[k - 1], M = 1e9 + 7;
        int res = helper(k - 1, i, j, group, profit, memo);
        if (i >= group[k - 1]) {
            res = (res + helper(k - 1, i - g, j - p, group, profit, memo)) % M;
        }
        return memo[k][i][j] = res;
    }
};



Github 同步地址:

https://github.com/grandyang/leetcode/issues/879



参考资料:

https://leetcode.com/problems/profitable-schemes/

https://leetcode.com/problems/profitable-schemes/discuss/154617/C%2B%2BJavaPython-DP

https://leetcode.com/problems/profitable-schemes/discuss/157099/Java-original-3d-to-2d-DP-solution

https://leetcode.com/problems/profitable-schemes/discuss/154636/C%2B%2B-O(PGn)-top-down-DP-solution



LeetCode All in One 题目讲解汇总(持续更新中...)

相关文章
相关标签/搜索