Facebook Pixel

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).

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Outer loop (i from 1 to n): Iterates through the number of dice being used.

  2. Middle loop (j from 1 to min(i*k, target)): Iterates through possible sums. The upper bound min(i*k, target) is an optimization - with i dice, the maximum possible sum is i*k, so we don't need to compute beyond that or beyond our target.

  3. Inner loop (h from 1 to min(j, k)): Represents the value shown on the current (i-th) die. We can't roll higher than k (the number of faces) or higher than j (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 Evaluator

Example 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, need f[0][0] = 1 way
  • f[1][2]: Die shows 2, need f[0][0] = 1 way
  • f[1][3]: Die shows 3, need f[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
  • 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
  • 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

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 most target times
  • The inner loop iterates up to min(j, k) times, which is at most k 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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More