Facebook Pixel

1223. Dice Roll Simulation

Problem Description

You have a special die simulator that generates random numbers from 1 to 6. However, this simulator has a constraint: for each number i (from 1 to 6), it cannot appear more than rollMax[i-1] times consecutively.

Given:

  • An integer n representing the number of times you'll roll the die
  • An array rollMax of length 6, where rollMax[i] represents the maximum consecutive times that number i+1 can appear

Your task is to find the total number of distinct valid sequences of length n that can be obtained. Since this number can be very large, return the result modulo 10^9 + 7.

For example, if rollMax = [1,1,2,2,2,3] and n = 2:

  • The sequence [1,1] is invalid because 1 can only appear at most 1 time consecutively
  • The sequence [1,2] is valid
  • The sequence [3,3] is valid because 3 can appear up to 2 times consecutively

Two sequences are considered different if they differ in at least one position.

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

Intuition

When we think about building valid sequences, we need to keep track of three key pieces of information at each step:

  1. How many rolls we've completed so far
  2. What number was rolled last
  3. How many times that number has appeared consecutively

This naturally leads to a recursive approach where we try all possible next rolls and check if they're valid based on our constraints.

For each position in our sequence, we have up to 6 choices (rolling 1 through 6). The validity of each choice depends on our current state:

  • If we roll a different number than the last one, we can always do it (resetting the consecutive count to 1)
  • If we roll the same number as the last one, we can only do it if we haven't exceeded the maximum consecutive limit for that number

The key insight is that we can use dynamic programming with memoization because the same state (position, last_number, consecutive_count) will be encountered multiple times through different paths. For instance, reaching position 3 with the last roll being 4 and having it appear twice consecutively could happen through sequences like [1,4,4,...] or [2,4,4,...]. The number of valid ways to complete the sequence from this state is the same regardless of how we got there.

By defining our state as dfs(i, j, x) where:

  • i = current position (how many dice we've rolled)
  • j = the last number rolled
  • x = consecutive count of number j

We can recursively compute all valid sequences and use memoization to avoid recalculating the same states. The base case is when we've rolled all n dice (i >= n), which represents one valid complete sequence.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses dynamic programming with memoization through a recursive function dfs(i, j, x) that represents:

  • i: the current position (number of dice rolls completed)
  • j: the last number rolled (0 initially, 1-6 for actual rolls)
  • x: the consecutive count of number j

Implementation Details:

  1. Base Case: When i >= n, we've completed all n rolls successfully, so we return 1 to count this valid sequence.

  2. Recursive Case: For each position, we try rolling each number from 1 to 6:

    • If the new number k is different from the last rolled number j:
      • We can always roll it, calling dfs(i + 1, k, 1)
      • The consecutive count resets to 1 since we're starting a new number
    • If the new number k equals the last rolled number j:
      • We check if x < rollMax[j - 1] (haven't exceeded the limit)
      • If valid, we call dfs(i + 1, j, x + 1) with incremented consecutive count
      • If not valid, we skip this choice
  3. Memoization: The @cache decorator automatically memoizes the function results, preventing redundant calculations for the same state (i, j, x).

  4. Initial Call: We start with dfs(0, 0, 0) where:

    • Position 0 (no rolls yet)
    • Last number 0 (special value indicating no previous roll)
    • Consecutive count 0
  5. Modulo Operation: Since the answer can be very large, we take modulo 10^9 + 7 before returning from each recursive call.

The time complexity is O(n × 6 × max(rollMax)) since we have at most that many unique states, and the space complexity is the same for storing the memoization cache.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example with n = 3 and rollMax = [1, 1, 2, 2, 2, 3].

We want to find all valid sequences of length 3 where:

  • Number 1 can appear at most 1 time consecutively
  • Number 2 can appear at most 1 time consecutively
  • Number 3 can appear at most 2 times consecutively
  • Number 4 can appear at most 2 times consecutively
  • Number 5 can appear at most 2 times consecutively
  • Number 6 can appear at most 3 times consecutively

Starting with dfs(0, 0, 0) (position 0, no previous number, count 0):

First Roll (position 0): Since there's no previous number (j=0), we can roll any number 1-6:

  • Roll 1: → dfs(1, 1, 1)
  • Roll 2: → dfs(1, 2, 1)
  • Roll 3: → dfs(1, 3, 1)
  • Roll 4: → dfs(1, 4, 1)
  • Roll 5: → dfs(1, 5, 1)
  • Roll 6: → dfs(1, 6, 1)

Second Roll (position 1): Let's trace the path starting with rolling 3: From dfs(1, 3, 1) (last roll was 3, appearing once):

  • Roll 1: Different from 3 → dfs(2, 1, 1)
  • Roll 2: Different from 3 → dfs(2, 2, 1)
  • Roll 3: Same as last, check if 1 < rollMax[2]=2 → Yes → dfs(2, 3, 2)
  • Roll 4: Different from 3 → dfs(2, 4, 1)
  • Roll 5: Different from 3 → dfs(2, 5, 1)
  • Roll 6: Different from 3 → dfs(2, 6, 1)

Third Roll (position 2): Let's continue with the path [3, 3]: From dfs(2, 3, 2) (last roll was 3, appearing twice consecutively):

  • Roll 1: Different from 3 → dfs(3, 1, 1) → Base case: return 1 ✓
  • Roll 2: Different from 3 → dfs(3, 2, 1) → Base case: return 1 ✓
  • Roll 3: Same as last, check if 2 < rollMax[2]=2 → No → Skip ✗
  • Roll 4: Different from 3 → dfs(3, 4, 1) → Base case: return 1 ✓
  • Roll 5: Different from 3 → dfs(3, 5, 1) → Base case: return 1 ✓
  • Roll 6: Different from 3 → dfs(3, 6, 1) → Base case: return 1 ✓

So from state dfs(2, 3, 2), we get 5 valid sequences:

  • [3, 3, 1]
  • [3, 3, 2]
  • [3, 3, 4]
  • [3, 3, 5]
  • [3, 3, 6]

Note that [3, 3, 3] is invalid because 3 cannot appear more than 2 times consecutively.

Similarly, sequences like [1, 1, 1] and [2, 2, 2] would be invalid because numbers 1 and 2 can only appear once consecutively.

The memoization ensures that when we encounter the same state (like reaching position 2 with last roll 4 and count 1) through different paths, we only compute it once and reuse the result.

Solution Implementation

1class Solution:
2    def dieSimulator(self, n: int, rollMax: List[int]) -> int:
3        MOD = 10**9 + 7
4      
5        @cache
6        def dfs(roll_index: int, last_number: int, consecutive_count: int) -> int:
7            """
8            Dynamic programming function to count valid dice roll sequences.
9          
10            Args:
11                roll_index: Current position in the sequence (0 to n-1)
12                last_number: The last rolled number (1-6, or 0 if no previous roll)
13                consecutive_count: How many times the last_number appeared consecutively
14          
15            Returns:
16                Number of valid sequences from this state
17            """
18            # Base case: completed all n rolls
19            if roll_index >= n:
20                return 1
21          
22            total_sequences = 0
23          
24            # Try rolling each die face (1 through 6)
25            for die_face in range(1, 7):
26                if die_face != last_number:
27                    # Rolling a different number - reset consecutive count to 1
28                    total_sequences += dfs(roll_index + 1, die_face, 1)
29                elif consecutive_count < rollMax[last_number - 1]:
30                    # Rolling the same number - check if within rollMax limit
31                    # Note: rollMax is 0-indexed, so we use last_number - 1
32                    total_sequences += dfs(roll_index + 1, last_number, consecutive_count + 1)
33          
34            return total_sequences % MOD
35      
36        # Start with roll_index=0, last_number=0 (no previous roll), consecutive_count=0
37        return dfs(0, 0, 0)
38
1class Solution {
2    // Memoization cache: [rollIndex][lastDieFace][consecutiveCount]
3    private Integer[][][] memo;
4    // Maximum consecutive rolls allowed for each die face
5    private int[] rollMax;
6    // Modulo value for the result
7    private static final int MOD = 1000000007;
8  
9    public int dieSimulator(int n, int[] rollMax) {
10        // Initialize memoization array
11        // n: number of rolls
12        // 7: die faces (1-6) + 0 for initial state
13        // 16: maximum possible consecutive count (given constraint)
14        this.memo = new Integer[n][7][16];
15        this.rollMax = rollMax;
16      
17        // Start DFS with initial state: no rolls yet, no last face, no consecutive count
18        return dfs(0, 0, 0);
19    }
20  
21    /**
22     * DFS to calculate number of valid dice roll sequences
23     * @param currentRoll - current roll index (0 to n-1)
24     * @param lastFace - the face value of the previous roll (0 if no previous roll, 1-6 for die faces)
25     * @param consecutiveCount - number of consecutive occurrences of lastFace
26     * @return number of valid sequences from this state
27     */
28    private int dfs(int currentRoll, int lastFace, int consecutiveCount) {
29        // Base case: all rolls completed successfully
30        if (currentRoll >= memo.length) {
31            return 1;
32        }
33      
34        // Check memoization cache
35        if (memo[currentRoll][lastFace][consecutiveCount] != null) {
36            return memo[currentRoll][lastFace][consecutiveCount];
37        }
38      
39        long totalWays = 0;
40      
41        // Try rolling each die face (1 to 6)
42        for (int nextFace = 1; nextFace <= 6; nextFace++) {
43            if (nextFace != lastFace) {
44                // Different face than last roll - start new consecutive sequence
45                totalWays += dfs(currentRoll + 1, nextFace, 1);
46            } else if (consecutiveCount < rollMax[lastFace - 1]) {
47                // Same face as last roll - check if we can continue the sequence
48                // rollMax is 0-indexed, so lastFace-1 gives the correct index
49                totalWays += dfs(currentRoll + 1, lastFace, consecutiveCount + 1);
50            }
51            // If same face but reached max consecutive limit, skip this option
52        }
53      
54        // Apply modulo to prevent overflow
55        totalWays %= MOD;
56      
57        // Store result in cache and return
58        memo[currentRoll][lastFace][consecutiveCount] = (int) totalWays;
59        return memo[currentRoll][lastFace][consecutiveCount];
60    }
61}
62
1class Solution {
2public:
3    int dieSimulator(int n, vector<int>& rollMax) {
4        // dp[rollIndex][lastNumber][consecutiveCount] stores the number of valid sequences
5        // rollIndex: current roll position (0 to n-1)
6        // lastNumber: the last rolled number (0-6, where 0 means no previous roll)
7        // consecutiveCount: how many times the last number appeared consecutively (0-15)
8        int dp[n][7][16];
9        memset(dp, 0, sizeof(dp));
10      
11        const int MOD = 1e9 + 7;
12      
13        // DFS with memoization to calculate number of valid dice sequences
14        function<int(int, int, int)> calculateSequences = [&](int rollIndex, int lastNumber, int consecutiveCount) -> int {
15            // Base case: if we've made all n rolls, we have one valid sequence
16            if (rollIndex >= n) {
17                return 1;
18            }
19          
20            // Return memoized result if already calculated
21            if (dp[rollIndex][lastNumber][consecutiveCount]) {
22                return dp[rollIndex][lastNumber][consecutiveCount];
23            }
24          
25            long totalSequences = 0;
26          
27            // Try rolling each die face (1 to 6)
28            for (int currentNumber = 1; currentNumber <= 6; ++currentNumber) {
29                if (currentNumber != lastNumber) {
30                    // Different number rolled, reset consecutive count to 1
31                    totalSequences += calculateSequences(rollIndex + 1, currentNumber, 1);
32                } else if (consecutiveCount < rollMax[lastNumber - 1]) {
33                    // Same number rolled, but within the allowed consecutive limit
34                    totalSequences += calculateSequences(rollIndex + 1, lastNumber, consecutiveCount + 1);
35                }
36                // If same number but exceeds limit, skip this possibility
37            }
38          
39            // Apply modulo to prevent overflow
40            totalSequences %= MOD;
41          
42            // Memoize and return the result
43            return dp[rollIndex][lastNumber][consecutiveCount] = totalSequences;
44        };
45      
46        // Start with roll 0, no previous number (0), and 0 consecutive count
47        return calculateSequences(0, 0, 0);
48    }
49};
50
1function dieSimulator(n: number, rollMax: number[]): number {
2    // dp[rollIndex][lastNumber][consecutiveCount] stores the number of valid sequences
3    // rollIndex: current roll position (0 to n-1)
4    // lastNumber: the last rolled number (0-6, where 0 means no previous roll)
5    // consecutiveCount: how many times the last number appeared consecutively (0-15)
6    const dp: number[][][] = Array(n)
7        .fill(null)
8        .map(() => 
9            Array(7)
10                .fill(null)
11                .map(() => Array(16).fill(0))
12        );
13  
14    const MOD = 1e9 + 7;
15  
16    /**
17     * DFS with memoization to calculate number of valid dice sequences
18     * @param rollIndex - Current roll position (0 to n-1)
19     * @param lastNumber - The last rolled number (0-6, where 0 means no previous roll)
20     * @param consecutiveCount - How many times the last number appeared consecutively
21     * @returns Number of valid sequences from this state
22     */
23    const calculateSequences = (rollIndex: number, lastNumber: number, consecutiveCount: number): number => {
24        // Base case: if we've made all n rolls, we have one valid sequence
25        if (rollIndex >= n) {
26            return 1;
27        }
28      
29        // Return memoized result if already calculated
30        if (dp[rollIndex][lastNumber][consecutiveCount] !== 0) {
31            return dp[rollIndex][lastNumber][consecutiveCount];
32        }
33      
34        let totalSequences = 0;
35      
36        // Try rolling each die face (1 to 6)
37        for (let currentNumber = 1; currentNumber <= 6; currentNumber++) {
38            if (currentNumber !== lastNumber) {
39                // Different number rolled, reset consecutive count to 1
40                totalSequences += calculateSequences(rollIndex + 1, currentNumber, 1);
41            } else if (consecutiveCount < rollMax[lastNumber - 1]) {
42                // Same number rolled, but within the allowed consecutive limit
43                totalSequences += calculateSequences(rollIndex + 1, lastNumber, consecutiveCount + 1);
44            }
45            // If same number but exceeds limit, skip this possibility
46        }
47      
48        // Apply modulo to prevent overflow
49        totalSequences %= MOD;
50      
51        // Memoize and return the result
52        dp[rollIndex][lastNumber][consecutiveCount] = totalSequences;
53        return totalSequences;
54    };
55  
56    // Start with roll 0, no previous number (0), and 0 consecutive count
57    return calculateSequences(0, 0, 0);
58}
59

Time and Space Complexity

The time complexity is O(n × k × M), where:

  • n is the number of dice rolls
  • k = 6 is the number of faces on the die (constant)
  • M is the maximum value in rollMax array

The space complexity is O(n × k × M) due to the memoization cache.

Analysis:

For the time complexity:

  • The recursive function dfs(i, j, x) has three parameters:
    • i ranges from 0 to n-1 (n possible values)
    • j ranges from 0 to 6 (7 possible values, including the initial 0)
    • x ranges from 0 to at most max(rollMax) (M possible values)
  • Due to the @cache decorator, each unique state (i, j, x) is computed only once
  • Total number of unique states: n × 7 × M
  • For each state, we iterate through 6 possible dice values (constant work)
  • Therefore, time complexity is O(n × 7 × M) which simplifies to O(n × k × M) where k represents the dice range

For the space complexity:

  • The memoization cache stores all computed states
  • Maximum number of states stored: n × 7 × M
  • Additionally, the recursion stack depth can go up to n
  • The dominant factor is the cache storage: O(n × k × M)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Index Mapping for rollMax Array

One of the most common mistakes is confusion about the indexing between die faces (1-6) and the rollMax array (0-indexed). When checking the constraint for die face k, you need to use rollMax[k-1] not rollMax[k].

Incorrect:

elif consecutive_count < rollMax[last_number]:  # Wrong! Array out of bounds when last_number=6

Correct:

elif consecutive_count < rollMax[last_number - 1]:  # Correct 0-based indexing

2. Forgetting to Apply Modulo Operation

The problem requires the answer modulo 10^9 + 7, but it's easy to forget applying it, especially if you're accumulating results in a loop.

Incorrect:

def dfs(roll_index, last_number, consecutive_count):
    if roll_index >= n:
        return 1
    total_sequences = 0
    for die_face in range(1, 7):
        # ... logic ...
        total_sequences += dfs(...)
    return total_sequences  # Missing modulo!

Correct:

return total_sequences % MOD

3. Improper Initial State Handling

Starting with wrong initial values can lead to incorrect logic. A common mistake is starting with last_number=1 or consecutive_count=1.

Incorrect:

return dfs(0, 1, 1)  # Wrong! This assumes we already rolled a 1

Correct:

return dfs(0, 0, 0)  # Start with no previous roll (last_number=0)

4. Not Handling the First Roll Correctly

When last_number=0 (initial state), the logic for checking consecutive counts shouldn't apply. The current solution handles this correctly because die_face != last_number will always be true for the first roll.

Potential Bug (if logic is restructured):

# If someone tries to optimize by checking rollMax first
if consecutive_count >= rollMax[die_face - 1]:
    continue  # This would incorrectly skip valid first rolls

5. Memory Limit Issues with Large Inputs

While @cache is convenient, for very large inputs, the memoization table might consume too much memory. In such cases, consider using iterative DP with space optimization.

Alternative Approach for Memory Optimization:

def dieSimulator(self, n: int, rollMax: List[int]) -> int:
    MOD = 10**9 + 7
    # dp[i][j] = number of ways to roll i times with last number being j+1
    # and considering all valid consecutive counts
    dp = [[0] * 6 for _ in range(n + 1)]
    # Additional tracking for consecutive counts can be added if needed
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the minimum element can be found in:


Recommended Readings

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

Load More