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 <= G <= 100
0 <= P <= 100
1 <= group[i] <= 100
0 <= profit[i] <= 100
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