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

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

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

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:

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:

  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 数组如何定义以及怎么推导状态转移方程吧。

首先来看分配黑帮资源时候都需要考虑哪些因素,总共有三点,要干几票买卖,要用多少人,能挣多少钱。所以我们需要一个三维的 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是表示最多干的买卖数,可能上并没有干到这么多,所以只需要累加人数这个维度即可,参见代码如下:

解法一:

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 题目讲解汇总(持续更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)

[LeetCode] 879. Profitable Schemes 盈利方案的更多相关文章

  1. 879. Profitable Schemes

    There are G people in a gang, and a list of various crimes they could commit. The i-th crime generat ...

  2. [Swift]LeetCode879. 盈利计划 | Profitable Schemes

    There are G people in a gang, and a list of various crimes they could commit. The i-th crime generat ...

  3. All LeetCode Questions List 题目汇总

    All LeetCode Questions List(Part of Answers, still updating) 题目汇总及部分答案(持续更新中) Leetcode problems clas ...

  4. leetcode hard

    # Title Solution Acceptance Difficulty Frequency     4 Median of Two Sorted Arrays       27.2% Hard ...

  5. Swift LeetCode 目录 | Catalog

    请点击页面左上角 -> Fork me on Github 或直接访问本项目Github地址:LeetCode Solution by Swift    说明:题目中含有$符号则为付费题目. 如 ...

  6. 【LeetCode】动态规划(下篇共39题)

    [600] Non-negative Integers without Consecutive Ones [629] K Inverse Pairs Array [638] Shopping Offe ...

  7. 【Leetcode周赛】从contest-91开始。(一般是10个contest写一篇文章)

    Contest 91 (2018年10月24日,周三) 链接:https://leetcode.com/contest/weekly-contest-91/ 模拟比赛情况记录:第一题柠檬摊的那题6分钟 ...

  8. Sublime Text 3最好的功能、插件和设置(转)

    Sublime Text 3 是一个了不起的软件.首先,它是一个干净,实用,可以快速的编写代码编辑器.它不仅具有令人难以置信的内置功能(多行编辑和VIM模式),而且还支持插件,代码片段和其他许多东西. ...

  9. 简谈HTML5与APP技术应用

    HTML5到底能给企业带来些什么? HTML5是近年来互联网行业的热门词汇,火的很.微软IE产品总经理发文: 未来的网络属于HTML5.乔布斯生前也在公开信<Flash之我见>中预言:像H ...

随机推荐

  1. 大话设计模式Python实现-原型模式

    原型模式(Prototype Pattern):用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象 一个原型模式的简单demo: #!/usr/bin/env python # -*- c ...

  2. 海边拾贝-G-若干有用的文章(乱序,经常更新)

    若干有用的文章,乱序版本.会经常性修改.     若干Python模块的介绍不错 https://www.cnblogs.com/sui776265233/category/1239819.html ...

  3. JVM内存溢出分析java.lang.OutOfMemoryError: Java heap space

    JVM内存溢出查询java.lang.OutOfMemoryError: Java heap space查出具体原因分为几个预备步骤 1.在运行java程序是必须设置jvm -XX:+HeapDump ...

  4. 云原生生态周报 Vol.9| K8s v1.15 版本发布

    本周作者 | 衷源.心贵 业界要闻 1.Kubernetes Release v1.15 版本发布,新版本的两个主题是持续性改进和可扩展性.(https://github.com/kubernetes ...

  5. pytest-Mark数据驱动

    数据驱动 import pytest @pytest.mark.parametrize(("a", "b", "expected"), [ ...

  6. 《深入理解Java虚拟机》并发(第12~13章)笔记

    volatile关键字的作用 所有变量的可见性--仅仅是修改后的值的可见性,不保证并发修改时新值和预期一致.即只保证读,不保证写. 禁止指令重排序--修饰的变量,读写不会指令重排.如变量isReady ...

  7. SAP 公司间STO场景中外向交货单过账后自动触发内向交货单功能的实现

    SAP 公司间STO场景中外向交货单过账后自动触发内向交货单功能的实现 如下STO,是从公司代码SZSP转入CSAS, 如下图示的内向交货单180018660.该内向交货单是在外向交货单8001632 ...

  8. 高性能TcpServer(C#) - 4.文件通道(处理:文件分包,支持断点续传)

    高性能TcpServer(C#) - 1.网络通信协议 高性能TcpServer(C#) - 2.创建高性能Socket服务器SocketAsyncEventArgs的实现(IOCP) 高性能TcpS ...

  9. SSH开发模式——Struts2进阶

    在之前我有写过关于struts2框架的博客,好像是写了三篇,但是之前写的内容仅仅是struts2的一些基础知识而已,struts2还有很多有趣的内容等待着我们去发掘.我准备再写几篇关于struts2的 ...

  10. Android Studio Gradle被墙bug总结

    1 Unknown host 'd29vzk4ow07wi7.cloudfront.net'. You may need to adjust the proxy settings in Gradle ...