Before attempting this problem, you should be comfortable with:
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.
dfs(i, n, p) where:i is the current crime index.n is the remaining number of members available.p is the profit accumulated so far.i equals the number of crimes, return 1 if p >= minProfit, else return 0.dfs(i + 1, n, p).dfs(i + 1, n - group[i], p + profit[i]).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)Where is the size of the array.
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.
dfs(i, n, p) with the same logic as the recursive approach.minProfit to reduce state space.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)Where is the size of the array, is the given minimum profit, and is the number of group members.
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.
dp[i][j][p] representing the count of schemes considering crimes from index i onward, using at most j members, with current profit p.j, set dp[N][j][minProfit] = 1 (reached the goal).i from N-1 down to 0, j from 0 to n, and p from 0 to minProfit:dp[i+1][j][p].minProfit).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]Where is the size of the array, is the given minimum profit, and is the number of group members.
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.
dp[j][p] for member count j and profit p.j, set dp[j][minProfit] = 1.i from N-1 down to 0:j from n down to 0 (reverse to avoid using updated values).p from 0 to minProfit: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]Where is the size of the array, is the given minimum profit, and is the number of group members.
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).
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.
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).
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.
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.