1155. Number of Dice Rolls With Target Sum
Problem Description
You have n
dice, and each die has k
faces numbered from 1
to k
.
Given three integers n
, k
, and target
, you need to find the number of possible ways to roll all the dice such that the sum of the face-up numbers equals exactly target
.
Since there are k^n
total possible outcomes when rolling n
dice (each die can show one of k
faces), you're counting how many of these outcomes sum to the target value.
For example, if you have 2 dice with 6 faces each and want a sum of 7, the valid combinations would be: (1,6), (2,5), (3,4), (4,3), (5,2), (6,1), giving us 6 possible ways.
The answer could be very large, so return the result modulo 10^9 + 7
.
The solution uses dynamic programming where f[i][j]
represents the number of ways to achieve a sum of j
using exactly i
dice. The state transition works by considering each possible value the current die can show (from 1 to k), and adding up the ways to achieve the remaining sum with one fewer die. The formula is:
f[i][j] = Σ(h=1 to min(j,k)) f[i-1][j-h]
This means for each die i
and target sum j
, we sum up all the ways to achieve j-h
with i-1
dice, where h
is the value shown on the current die (ranging from 1 to the minimum of j
and k
).
Intuition
When faced with counting problems involving multiple choices that build upon each other, dynamic programming often provides an elegant solution. Let's think about this problem step by step.
If we only had 1 die, the problem would be trivial - we can only achieve sums from 1 to k
, each with exactly one way. But what happens when we add a second die?
To get a sum of j
with 2 dice, we need the first die to show some value h
(between 1 and k
), and the second die must show j - h
. If we already knew how many ways to get each possible sum with 1 die, we could calculate the ways for 2 dice by considering all valid splits.
This observation leads to a key insight: the number of ways to achieve a sum with i
dice depends only on the number of ways to achieve related sums with i-1
dice. This is a classic dynamic programming pattern - we can build up the solution incrementally.
Think of it like this: when rolling the i
-th die, it can show any value from 1 to k
. If it shows value h
, then the remaining i-1
dice must sum to j-h
to achieve our target sum j
. Since we're counting all possibilities, we sum over all valid values of h
.
The constraint min(j, k)
in our iteration ensures we don't consider impossible die values - we can't roll higher than k
, and we also can't roll higher than j
(since that would make achieving sum j
impossible with non-negative contributions from other dice).
By building up from 0 dice (base case: one way to get sum 0) to n
dice, we systematically count all valid combinations without repetition or omission. The modulo operation keeps our numbers manageable throughout the computation.
Learn more about Dynamic Programming patterns.
Solution Approach
The solution implements a bottom-up dynamic programming approach using a 2D array to store intermediate results.
Data Structure Setup:
We create a 2D array f
where f[i][j]
represents the number of ways to achieve sum j
using exactly i
dice. The array dimensions are (n+1) × (target+1)
to handle all possible states from 0 to n
dice and sums from 0 to target
.
Base Case:
We initialize f[0][0] = 1
, meaning there's exactly one way to achieve sum 0 with 0 dice (by doing nothing). All other entries start at 0.
Main Algorithm: The solution uses three nested loops:
-
Outer loop (
i
from 1 ton
): Iterates through the number of dice being used. -
Middle loop (
j
from 1 tomin(i*k, target)
): Iterates through possible sums. The upper boundmin(i*k, target)
is an optimization - withi
dice, the maximum possible sum isi*k
, so we don't need to compute beyond that or beyond our target. -
Inner loop (
h
from 1 tomin(j, k)
): Represents the value shown on the current (i-th) die. We can't roll higher thank
(the number of faces) or higher thanj
(the current sum we're trying to achieve).
State Transition:
For each state f[i][j]
, we accumulate contributions from all possible previous states:
f[i][j] = (f[i][j] + f[i-1][j-h]) % mod
This means: "The ways to get sum j
with i
dice equals the sum of ways to get sum j-h
with i-1
dice, for all valid die values h
."
Modulo Operation:
Since the result can be very large, we apply modulo 10^9 + 7
at each addition to prevent integer overflow and meet the problem's requirements.
Final Result:
After filling the entire table, f[n][target]
contains our answer - the number of ways to achieve the target sum using all n
dice.
The time complexity is O(n × k × target)
due to the three nested loops, and the space complexity is O(n × target)
for the DP table.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with n = 2
dice, k = 3
faces (numbered 1-3), and target = 4
.
Step 1: Initialize DP Table
We create a table f[i][j]
where i
ranges from 0 to 2 (number of dice) and j
ranges from 0 to 4 (target sum).
j: 0 1 2 3 4 i=0: 1 0 0 0 0 i=1: 0 0 0 0 0 i=2: 0 0 0 0 0
Base case: f[0][0] = 1
(one way to get sum 0 with 0 dice).
Step 2: Fill row for i=1 (using 1 die)
For each sum j
from 1 to 3 (max possible with 1 die), we consider what the die can show:
f[1][1]
: Die shows 1, needf[0][0]
= 1 wayf[1][2]
: Die shows 2, needf[0][0]
= 1 wayf[1][3]
: Die shows 3, needf[0][0]
= 1 way
j: 0 1 2 3 4 i=0: 1 0 0 0 0 i=1: 0 1 1 1 0 i=2: 0 0 0 0 0
Step 3: Fill row for i=2 (using 2 dice)
For each sum j
from 2 to 4:
-
f[2][2]
: Current die can show 1 (need sum 1 with 1 die) or 2 (need sum 0 with 1 die)- Die shows 1:
f[1][1]
= 1 - Die shows 2:
f[1][0]
= 0 - Total: 1 way
- Die shows 1:
-
f[2][3]
: Current die can show 1, 2, or 3- Die shows 1:
f[1][2]
= 1 - Die shows 2:
f[1][1]
= 1 - Die shows 3:
f[1][0]
= 0 - Total: 2 ways
- Die shows 1:
-
f[2][4]
: Current die can show 1, 2, or 3- Die shows 1:
f[1][3]
= 1 - Die shows 2:
f[1][2]
= 1 - Die shows 3:
f[1][1]
= 1 - Total: 3 ways
- Die shows 1:
Final Table:
j: 0 1 2 3 4 i=0: 1 0 0 0 0 i=1: 0 1 1 1 0 i=2: 0 0 1 2 3
Answer: f[2][4] = 3
The three ways to get sum 4 with 2 dice (each with 3 faces) are:
- (1, 3) - first die shows 1, second shows 3
- (2, 2) - both dice show 2
- (3, 1) - first die shows 3, second shows 1
This matches our DP calculation!
Solution Implementation
1class Solution:
2 def numRollsToTarget(self, n: int, k: int, target: int) -> int:
3 # dp[i][j] represents the number of ways to roll i dice to get sum j
4 dp = [[0] * (target + 1) for _ in range(n + 1)]
5
6 # Base case: 0 dice with sum 0 has exactly 1 way (empty set)
7 dp[0][0] = 1
8
9 # Modulo value for the result
10 MOD = 10**9 + 7
11
12 # Iterate through each die
13 for dice_count in range(1, n + 1):
14 # Iterate through all possible sums up to the maximum achievable
15 # Maximum sum with dice_count dice is dice_count * k
16 for current_sum in range(1, min(dice_count * k, target) + 1):
17 # Try all possible values for the current die (1 to k)
18 for die_value in range(1, min(current_sum, k) + 1):
19 # Add the number of ways to achieve (current_sum - die_value)
20 # with (dice_count - 1) dice
21 dp[dice_count][current_sum] = (
22 dp[dice_count][current_sum] +
23 dp[dice_count - 1][current_sum - die_value]
24 ) % MOD
25
26 # Return the number of ways to achieve target sum with n dice
27 return dp[n][target]
28
1class Solution {
2 public int numRollsToTarget(int n, int k, int target) {
3 // Modulo value for preventing integer overflow
4 final int MOD = (int) 1e9 + 7;
5
6 // dp[i][j] represents the number of ways to get sum j using i dice
7 int[][] dp = new int[n + 1][target + 1];
8
9 // Base case: 0 dice with sum 0 has exactly 1 way (empty selection)
10 dp[0][0] = 1;
11
12 // Iterate through each die
13 for (int diceCount = 1; diceCount <= n; diceCount++) {
14 // Iterate through possible sums
15 // Maximum possible sum with diceCount dice is diceCount * k
16 for (int currentSum = 1; currentSum <= Math.min(target, diceCount * k); currentSum++) {
17 // Try all possible face values for the current die
18 for (int faceValue = 1; faceValue <= Math.min(currentSum, k); faceValue++) {
19 // Add the number of ways to achieve (currentSum - faceValue)
20 // with (diceCount - 1) dice
21 dp[diceCount][currentSum] = (dp[diceCount][currentSum] +
22 dp[diceCount - 1][currentSum - faceValue]) % MOD;
23 }
24 }
25 }
26
27 // Return the number of ways to achieve target sum with n dice
28 return dp[n][target];
29 }
30}
31
1class Solution {
2public:
3 int numRollsToTarget(int n, int k, int target) {
4 const int MOD = 1e9 + 7;
5
6 // dp[i][j] represents the number of ways to get sum j using i dice
7 int dp[n + 1][target + 1];
8 memset(dp, 0, sizeof(dp));
9
10 // Base case: 0 dice with sum 0 has exactly 1 way
11 dp[0][0] = 1;
12
13 // Iterate through each die
14 for (int diceCount = 1; diceCount <= n; ++diceCount) {
15 // Iterate through each possible sum
16 // Maximum sum with diceCount dice is diceCount * k
17 for (int sum = 1; sum <= min(target, diceCount * k); ++sum) {
18 // Try each possible face value for the current die
19 for (int faceValue = 1; faceValue <= min(sum, k); ++faceValue) {
20 // Add the number of ways to achieve (sum - faceValue) with (diceCount - 1) dice
21 dp[diceCount][sum] = (dp[diceCount][sum] + dp[diceCount - 1][sum - faceValue]) % MOD;
22 }
23 }
24 }
25
26 // Return the number of ways to get target sum using n dice
27 return dp[n][target];
28 }
29};
30
1/**
2 * Calculate the number of ways to roll n dice with k faces to get a target sum
3 * @param n - Number of dice to roll
4 * @param k - Number of faces on each die (numbered from 1 to k)
5 * @param target - Target sum to achieve
6 * @returns Number of possible ways modulo 10^9 + 7
7 */
8function numRollsToTarget(n: number, k: number, target: number): number {
9 // dp[i][j] represents the number of ways to roll i dice to get sum j
10 const dp: number[][] = Array.from(
11 { length: n + 1 },
12 () => Array(target + 1).fill(0)
13 );
14
15 // Base case: 0 dice with sum 0 has exactly 1 way (no dice rolled)
16 dp[0][0] = 1;
17
18 // Modulo value to prevent integer overflow
19 const MOD: number = 1e9 + 7;
20
21 // Iterate through each die
22 for (let diceCount = 1; diceCount <= n; diceCount++) {
23 // Iterate through possible sums (maximum possible sum with diceCount dice is diceCount * k)
24 for (let currentSum = 1; currentSum <= Math.min(diceCount * k, target); currentSum++) {
25 // Try all possible face values for the current die
26 for (let faceValue = 1; faceValue <= Math.min(currentSum, k); faceValue++) {
27 // Add the number of ways from the previous state
28 // Previous state: (diceCount - 1) dice with sum (currentSum - faceValue)
29 dp[diceCount][currentSum] = (dp[diceCount][currentSum] + dp[diceCount - 1][currentSum - faceValue]) % MOD;
30 }
31 }
32 }
33
34 // Return the number of ways to roll n dice to get the target sum
35 return dp[n][target];
36}
37
Time and Space Complexity
Time Complexity: O(n * target * k)
The algorithm uses three nested loops:
- The outer loop iterates
n
times (number of dice) - The middle loop iterates up to
min(i * k, target)
times, which is at mosttarget
times - The inner loop iterates up to
min(j, k)
times, which is at mostk
times
Therefore, the overall time complexity is O(n * target * k)
.
Space Complexity: O(n * target)
The current implementation uses a 2D array f
with dimensions (n + 1) × (target + 1)
to store all intermediate states, resulting in O(n * target)
space complexity.
Space Optimization Potential: As mentioned in the reference answer, since f[i][j]
only depends on the previous row f[i-1][]
, we can optimize the space complexity to O(target)
by using a rolling array technique. This would involve using only two 1D arrays of size target + 1
(or even just one array with careful updating) instead of the full 2D array.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Loop Bounds for Sum Iteration
One of the most common mistakes is setting incorrect bounds for the middle loop that iterates through possible sums. Developers often write:
Incorrect:
for current_sum in range(1, target + 1): # Wrong!
Why it's wrong: This attempts to calculate sums that are impossible to achieve with the current number of dice. For example, with 2 dice having 6 faces each, the maximum possible sum is 12, but this loop would try to calculate sums up to target
(which could be much larger).
Solution:
for current_sum in range(1, min(dice_count * k, target) + 1):
This ensures we only compute achievable sums, improving efficiency and avoiding unnecessary calculations.
2. Integer Overflow from Delayed Modulo Operation
Another frequent error is applying the modulo operation too late:
Incorrect:
for die_value in range(1, min(current_sum, k) + 1):
dp[dice_count][current_sum] += dp[dice_count - 1][current_sum - die_value]
dp[dice_count][current_sum] %= MOD # Modulo applied after all additions - Wrong!
Why it's wrong: The intermediate sum can exceed integer limits before the modulo is applied, especially in languages with fixed integer sizes. Even in Python (which has arbitrary precision), this approach is inefficient for large numbers.
Solution:
for die_value in range(1, min(current_sum, k) + 1):
dp[dice_count][current_sum] = (
dp[dice_count][current_sum] +
dp[dice_count - 1][current_sum - die_value]
) % MOD # Apply modulo at each step
3. Space Optimization Pitfall
While not necessarily incorrect, many developers miss the opportunity to optimize space by using a 1D array instead of 2D:
Space-Optimized Solution:
def numRollsToTarget(self, n: int, k: int, target: int) -> int:
MOD = 10**9 + 7
prev = [0] * (target + 1)
prev[0] = 1
for dice_count in range(1, n + 1):
curr = [0] * (target + 1)
for current_sum in range(1, min(dice_count * k, target) + 1):
for die_value in range(1, min(current_sum, k) + 1):
curr[current_sum] = (curr[current_sum] + prev[current_sum - die_value]) % MOD
prev = curr
return prev[target]
This reduces space complexity from O(n × target) to O(target), which can be crucial for large inputs.
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!