LeetCode 2585 - Number of Ways to Earn Points
This problem is essentially asking how many distinct ways we can select questions from multiple types to reach exactly a target score. Each type of question has a fixed number of questions (counti) and a fixed number of points per question (marksi).
Difficulty: 🔴 Hard
Topics: Array, Dynamic Programming
Solution
Problem Understanding
This problem is essentially asking how many distinct ways we can select questions from multiple types to reach exactly a target score. Each type of question has a fixed number of questions (counti) and a fixed number of points per question (marksi). Questions of the same type are indistinguishable, which means selecting any subset of questions of the same type with the same quantity counts as one way.
The input consists of an integer target and a 2D array types where each subarray [counti, marksi] represents the number of questions and points per question for that type. The output is the total number of ways to reach exactly the target points, modulo 10^9 + 7.
The constraints indicate that target can be up to 1000, and there can be up to 50 types of questions, each with up to 50 questions. This suggests that naive combinatorial enumeration is infeasible because the number of combinations grows exponentially. Edge cases include situations where the target is smaller than the lowest possible combination, exactly equal to the sum of all maximum points, or when some types have more questions than needed.
Approaches
A brute-force approach would involve recursively trying all combinations of questions from each type. For each type, we could choose 0 up to counti questions and recursively attempt to achieve the remaining target with subsequent types. While correct, this approach is too slow since the number of recursive calls is exponential in both the number of types and the number of questions per type.
The optimal approach is dynamic programming. The key insight is to recognize this as a bounded knapsack problem: each question type can contribute multiple times to the total, but only up to its count. We can use a 1D DP array dp[j] where j represents a score from 0 to target. Initially, dp[0] = 1 because there is exactly one way to achieve zero points: select no questions. Then, for each type, we iterate from target down to 0, and for each possible number of questions of that type, update the ways to achieve a new score.
Using DP with careful iteration ensures we count all combinations without redundancy and stay within feasible time complexity.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((count1+1)(count2+1)...*(countn+1)) | O(target) | Exponentially many combinations; infeasible for constraints |
| Optimal | O(n * target * max_count) | O(target) | Bounded knapsack DP with rolling array optimization |
Algorithm Walkthrough
-
Initialize a DP array of size
target + 1withdp[0] = 1and all other values 0. This represents the number of ways to achieve each score. -
For each type
[counti, marksi]: -
Iterate
jfromtargetdown to 0. This reverse iteration prevents overcounting within the same type. -
For each
kfrom 1 tocounti: -
Compute
next_score = j + k * marksi. -
If
next_score > target, break because larger scores are not needed. -
Update
dp[next_score] = (dp[next_score] + dp[j]) % MOD. -
After processing all types,
dp[target]contains the total number of ways to reach the exact target score.
Why it works: This algorithm correctly maintains the invariant that dp[j] represents the total number of ways to achieve score j using the types processed so far. By iterating in reverse and considering all counts per type, we account for all valid combinations without overcounting.
Python Solution
from typing import List
class Solution:
def waysToReachTarget(self, target: int, types: List[List[int]]) -> int:
MOD = 10**9 + 7
dp = [0] * (target + 1)
dp[0] = 1
for count, marks in types:
for j in range(target, -1, -1):
for k in range(1, count + 1):
next_score = j + k * marks
if next_score > target:
break
dp[next_score] = (dp[next_score] + dp[j]) % MOD
return dp[target]
Explanation: The dp array keeps track of the number of ways to reach each possible score. For each type, we iterate in reverse to avoid using the same type multiple times in one iteration. The inner loop goes over possible counts for that type, updating all achievable scores. Modular arithmetic ensures we stay within the required numeric limits.
Go Solution
func waysToReachTarget(target int, types [][]int) int {
const MOD int = 1e9 + 7
dp := make([]int, target+1)
dp[0] = 1
for _, t := range types {
count, marks := t[0], t[1]
for j := target; j >= 0; j-- {
for k := 1; k <= count; k++ {
nextScore := j + k*marks
if nextScore > target {
break
}
dp[nextScore] = (dp[nextScore] + dp[j]) % MOD
}
}
}
return dp[target]
}
Explanation: The Go solution mirrors the Python approach, using a slice for the DP array and constants for modular arithmetic. Reverse iteration is used for correctness, and the inner loop breaks early when scores exceed the target.
Worked Examples
Example 1
target = 6, types = [[6,1],[3,2],[2,3]]
- Start with
dp = [1,0,0,0,0,0,0] - Process
[6,1]:
- Adding 1 question 1 point: dp[1] = 1
- Adding 2 questions 1 point each: dp[2] = 1
- … dp[6] = 1
- Process
[3,2]:
- Combining with previous dp values to update dp[2], dp[4], dp[6], etc.
- Process
[2,3]:
- Updates dp[3], dp[6] by adding new combinations.
- Final dp[6] = 7
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n * target * max_count) | n types, target scores, and at most max_count questions per type |
| Space | O(target) | Only a 1D array of size target + 1 is needed |
The reverse iteration and modular arithmetic keep updates efficient and prevent overcounting, keeping memory usage minimal.
Test Cases
# Provided examples
assert Solution().waysToReachTarget(6, [[6,1],[3,2],[2,3]]) == 7
assert Solution().waysToReachTarget(5, [[50,1],[50,2],[50,5]]) == 4
assert Solution().waysToReachTarget(18, [[6,1],[3,2],[2,3]]) == 1
# Edge cases
assert Solution().waysToReachTarget(1, [[1,1]]) == 1 # minimum target
assert Solution().waysToReachTarget(1000, [[50,20],[50,15]]) >= 0 # large target, many combinations
assert Solution().waysToReachTarget(5, [[1,1],[1,2],[1,3]]) == 1 # exact fit without excess
| Test | Why |
|---|---|
| target=6, multiple types | Validates standard combination counting |
| target=5, large counts | Validates algorithm handles large counts efficiently |
| target=18, exact sum | Validates exact fit with all questions |
| target=1, single question | Minimal input edge case |
| target=1000, large numbers | Stress test with large target |
| target=5, small counts | Ensures correct handling of limited options |
Edge Cases
- Minimum target and single type: If
target = 1andtypes = [[1,1]], the algorithm correctly identifies one way. This is important because DP initialization must handledp[0] = 1to ensure a base case exists. - Maximum target with multiple large types: When
targetis large and types have counts up to 50, nested loops could potentially be slow. The DP approach avoids exponential recursion and handles this efficiently within constraints. - Target not achievable: If no combination of questions can sum to
target, the DP array will leavedp[target] = 0. The algorithm correctly returns zero without needing explicit checks.
This approach guarantees correctness, efficiency, and handles edge cases gracefully.