879. Profitable Schemes - Explanation
Description
You have n available members and a list of jobs. Job i needs group[i] members and earns profit[i] profit. A member may participate in at most one selected job.
A scheme is a subset of jobs whose total required members is at most n and whose total profit is at least minProfit. Return the number of such subsets, modulo 1000000007. Each job can be selected at most once. Jobs at different indices are distinct even when their requirements and profits match. The empty subset counts when minProfit is zero.
Example 1:
Input: n = 4
minProfit = 4
group = [2,3,1]
profit = [3,4,1]
Output: 3Explanation: Selecting only job 1 earns 4 profit with 3 members. Selecting jobs 0 and 2 earns 4 with 3 members, and selecting jobs 1 and 2 earns 5 with 4 members. These are the three valid subsets.
Example 2:
Input: n = 2
minProfit = 0
group = [1,2]
profit = [0,0]
Output: 3Explanation: The empty subset and either single job meet the zero-profit target. Selecting both jobs would require 3 members, exceeding the limit of 2. The answer is 3.
Example 3:
Input: n = 3
minProfit = 5
group = [4,2]
profit = [8,4]
Output: 0Explanation: Job 0 requires more members than are available. Job 1 fits but earns only 4 profit, below the target of 5. No subset qualifies, so return 0.
Constraints:
1 <= n <= 1000 <= minProfit <= 1001 <= group.length <= 100profit.length == group.length1 <= group[i] <= 1000 <= profit[i] <= 100
Topics
Prerequisites
Before attempting this problem, you should be comfortable with:
- 0/1 Knapsack Problem - This is a variation with two constraints (members and profit)
- Recursion - Building decision trees where each item can be included or excluded
- Memoization - Caching results to avoid redundant computation in overlapping subproblems
- Dynamic Programming - Converting recursive solutions to iterative bottom-up approaches
- Modular Arithmetic - Handling large results with modulo operations to prevent overflow
1. Recursion
Intuition
This is a variation of the 0/1 knapsack problem with two constraints: number of members and minimum profit. For each crime, we decide whether to include it or skip it. If we include a crime, we use its required members and gain its profit. At the end, we count paths where total profit meets the threshold and total members used does not exceed the limit.
Algorithm
- Define a recursive function
dfs(i, n, p)where:iis the current crime index.nis the remaining number of members available.pis the profit accumulated so far.
- Base case: if
iequals the number of crimes, return1ifp >= minProfit, else return0. - For the current crime:
- Option 1: skip it and recurse with
dfs(i + 1, n, p). - Option 2: if we have enough members, include it and recurse with
dfs(i + 1, n - group[i], p + profit[i]).
- Option 1: skip it and recurse with
- Sum both options and return the result modulo
10^9 + 7.
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
mod = 10**9 + 7
def dfs(i, n, p):
if i == len(group):
return 1 if p >= minProfit else 0
res = dfs(i + 1, n, p)
if n - group[i] >= 0:
res = (res + dfs(i + 1, n - group[i], p + profit[i])) % mod
return res
return dfs(0, n, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution has overlapping subproblems because the same state (crime index, remaining members, current profit) can be reached through different paths. By memoizing results, we avoid redundant computation. We also cap profit at minProfit since any profit beyond the threshold is equivalent for counting purposes.
Algorithm
- Create a 3D memoization table indexed by (crime index, remaining members, current profit).
- Define
dfs(i, n, p)with the same logic as the recursive approach. - Before computing, check if the state is already in the cache.
- When including a crime, cap the new profit at
minProfitto reduce state space. - Store and return the result modulo
10^9 + 7.
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
mod = 10**9 + 7
dp = {}
def dfs(i, n, p):
if i == len(group):
return 1 if p >= minProfit else 0
if (i, n, p) in dp:
return dp[(i, n, p)]
res = dfs(i + 1, n, p)
if n - group[i] >= 0:
nxtP = min(p + profit[i], minProfit)
res = (res + dfs(i + 1, n - group[i], nxtP)) % mod
dp[(i, n, p)] = res
return res
return dfs(0, n, 0)Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array, is the given minimum profit, and is the number of group members.
3. Dynamic Programming (Bottom-Up)
Intuition
We can convert the top-down solution to bottom-up by iterating through crimes in reverse order. We build up the answer by considering what happens if we include or exclude each crime, starting from the last crime and working backward to the first.
Algorithm
- Create a 3D DP table
dp[i][j][p]representing the count of schemes considering crimes from indexionward, using at mostjmembers, with current profitp. - Initialize: for all member counts
j, setdp[N][j][minProfit] = 1(reached the goal). - Iterate
ifromN-1down to 0,jfrom 0 ton, andpfrom 0 tominProfit:- Start with the count from skipping this crime:
dp[i+1][j][p]. - If we have enough members, add the count from including this crime with updated profit (capped at
minProfit).
- Start with the count from skipping this crime:
- Return
dp[0][n][0].
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
mod = 10**9 + 7
N = len(group)
dp = [[[0] * (minProfit + 1) for j in range(n + 2)] for i in range(N + 1)]
for j in range(n + 1):
dp[N][j][minProfit] = 1
for i in range(N - 1, -1, -1):
for j in range(n + 1):
for p in range(minProfit + 1):
res = dp[i + 1][j][p]
if j >= group[i]:
nxtP = min(profit[i] + p, minProfit)
res = (res + dp[i + 1][j - group[i]][nxtP]) % mod
dp[i][j][p] = res
return dp[0][n][0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array, is the given minimum profit, and is the number of group members.
4. Dynamic Programming (Space Optimized)
Intuition
Since each crime's DP state only depends on the next crime's state, we can reduce the 3D table to 2D. We iterate through crimes and update the table in-place. By processing member counts in reverse order, we ensure we use the previous iteration's values before overwriting them.
Algorithm
- Create a 2D DP table
dp[j][p]for member countjand profitp. - Initialize: for all
j, setdp[j][minProfit] = 1. - Iterate
ifromN-1down to 0:- Iterate
jfromndown to 0 (reverse to avoid using updated values). - For each profit
pfrom 0 tominProfit:- Add the contribution from including the crime if we have enough members.
- Iterate
- Return
dp[n][0].
class Solution:
def profitableSchemes(self, n: int, minProfit: int, group: List[int], profit: List[int]) -> int:
mod = 10**9 + 7
N = len(group)
dp = [[0] * (minProfit + 1) for j in range(n + 2)]
for j in range(n + 1):
dp[j][minProfit] = 1
for i in range(N - 1, -1, -1):
for j in range(n, -1, -1):
for p in range(minProfit + 1):
res = dp[j][p]
if j >= group[i]:
nxtP = min(profit[i] + p, minProfit)
res = (res + dp[j - group[i]][nxtP]) % mod
dp[j][p] = res
return dp[n][0]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the array, is the given minimum profit, and is the number of group members.
Common Pitfalls
Not Capping Profit at minProfit
When accumulating profit in the DP state, tracking values beyond minProfit wastes memory and causes state explosion. Any profit exceeding the threshold is functionally equivalent for counting valid schemes. Always cap the profit dimension at minProfit to reduce the state space from potentially unbounded to O(minProfit).
Forgetting the Modulo Operation
The problem requires results modulo 10^9 + 7 due to potentially huge answer values. Forgetting to apply modulo when summing the include and skip options leads to integer overflow. Apply modulo after every addition operation, not just at the end.
Incorrect Base Case Logic
The base case must return 1 when all crimes are considered AND the accumulated profit meets the threshold. A common mistake is returning 1 unconditionally or checking the wrong condition. The check p >= minProfit must happen only when i == len(group).
Off-by-One in Member Count Check
When deciding whether to include a crime, the condition should be n >= group[i] or equivalently n - group[i] >= 0. Using strict inequality n > group[i] incorrectly excludes cases where exactly enough members are available.
Iterating in Wrong Direction for Space Optimization
In the space-optimized bottom-up solution, iterating j (member count) in the wrong direction causes using updated values from the current iteration instead of the previous one. Process members from high to low to ensure correct dependency ordering.
Sign in to join the discussion