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 <= G <= 1000 <= P <= 1001 <= group[i] <= 1000 <= profit[i] <= 1001 <= 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)
