879. Profitable Schemes - Explanation
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.