Skip to content
← All posts

Dice Roll Simulation: Constrained DP

6 min read
leetcodeproblemharddynamic-programming

You have a die with 6 faces numbered 1 through 6. You are given n (the number of rolls) and an array rollMax of length 6 where rollMax[i] is the maximum number of consecutive times face i + 1 can appear. Return the number of distinct sequences you can roll, modulo 10^9 + 7.

This is LeetCode 1223: Dice Roll Simulation. For example, with n = 2 and rollMax = [1,1,2,2,2,3], face 1 cannot appear twice in a row, but face 3 can appear up to twice consecutively. The answer is 34.

Dice faces with rollMax constraints1max 12max 13max 24max 25max 26max 3After rolling face 3 once (1 consecutive), face 3 can be rolled again10/120/131/240/250/260/3AvailableAt limitNot yet rolled
rollMax = [1,1,2,2,2,3]. Face 1 can appear at most 1 time in a row, face 3 up to 2 times, and face 6 up to 3 times.

Why this problem matters

Most DP problems have a simple state: the current index, the remaining capacity, or the last element. Dice Roll Simulation forces you to track an extra dimension, the consecutive count. This "constrained state" pattern appears in many harder DP problems. Once you learn to add a third dimension for tracking how many times in a row something has happened, you unlock a whole family of problems that would otherwise feel impossible to model.

The constraint here is not about what you can pick. You can always pick any face. The constraint is about how many times in a row you can pick the same face. That subtle distinction means the DP state needs to encode not just what happened last, but how long the current streak has been going.

The key insight

A naive 2D DP where dp[i][j] counts sequences of length i ending with face j is not enough. Consider face 3 with rollMax = 2. If the last two rolls were both 3, you cannot roll 3 again. But if only the last roll was 3, you can. The 2D table cannot distinguish these cases.

The fix is to add a third dimension. Define dp[i][j][k] as the number of valid sequences of length i that end with face j having been rolled k consecutive times. Now the state carries exactly the information you need to decide whether rolling face j again is legal.

The transitions are:

  • Switch face: if the new face j differs from the previous face, consecutive count resets to 1. dp[i][j][1] += dp[i-1][f][k] for all f != j and all valid k.
  • Extend streak: if you roll the same face j again and k + 1 is within rollMax[j-1], the count increments. dp[i][j][k+1] += dp[i-1][j][k].

The solution

def dieSimulator(n, rollMax):
    MOD = 10**9 + 7
    max_consec = max(rollMax)
    dp = [[[0] * (max_consec + 1) for _ in range(6)] for _ in range(n + 1)]

    for j in range(6):
        dp[1][j][1] = 1

    for i in range(2, n + 1):
        for j in range(6):
            for f in range(6):
                if f != j:
                    for k in range(1, rollMax[f] + 1):
                        dp[i][j][1] = (dp[i][j][1] + dp[i - 1][f][k]) % MOD
            for k in range(2, rollMax[j] + 1):
                dp[i][j][k] = dp[i - 1][j][k - 1]

    result = 0
    for j in range(6):
        for k in range(1, rollMax[j] + 1):
            result = (result + dp[n][j][k]) % MOD
    return result

The outer loop iterates over sequence lengths from 2 to n. For each position, you fill every (face, consecutive count) pair. The "switch" transition sums over all other faces and their valid consecutive counts. The "extend" transition simply copies from the previous consecutive count.

You can optimize the switch transition. Instead of summing over all other faces, precompute the total across all faces and consecutive counts for roll i-1, then subtract the contribution of face j itself. This reduces the inner loop from O(6 * max(rollMax)) to O(max(rollMax)) per cell.

Visual walkthrough

Let's trace through n = 2 and rollMax = [1,1,2,2,2,3] step by step. The key question at each roll is: which (face, consecutive count) pairs are valid, and how do they combine?

Step 1: Define the state. dp[i][j][k] = sequences of length i ending with face j rolled k times consecutively.

dp[i][j][k] where 1 <= i <= n, 1 <= j <= 6, 1 <= k <= rollMax[j-1]

Three dimensions: roll number i, last face j (1-6), and consecutive count k (1 to rollMax[j]).

Step 2: Base case (i=1). Each face can appear once, with consecutive count 1.

i = 1k=1face 1face 2face 3face 4face 5face 6111111

dp[1][j][1] = 1 for all faces j. Every single-roll sequence is valid. Total sequences of length 1: 6.

Step 3: Transition for i=2. For each face, either switch from a different face (k resets to 1) or extend the same face (k increments).

dp[2][j][1] = total(i=1) - sum_k(dp[1][j][k])

dp[2][j][k] = dp[1][j][k-1] (if k <= rollMax[j])

Switch: dp[2][j][1] = sum of all dp[1][f][k] where f != j. Extend: dp[2][j][k] = dp[1][j][k-1] if k is within rollMax[j].

Step 4: Fill i=2 table. Faces 1,2 (rollMax=1) cannot repeat. Faces 3-6 can extend.

i = 2k=1k=2k=3face 1face 2face 3face 4face 5face 65--5--51-51-51-51-

Face 1: only k=1 is allowed (rollMax=1), so dp[2][1][1] = 5. Face 3: dp[2][3][1] = 5 (switch), dp[2][3][2] = 1 (extend from k=1).

Step 5: Sum it up. Total valid sequences of length 2: 5 + 5 + 6 + 6 + 6 + 6 = 34.

i = 2 (final)k=1k=2k=3face 1face 2face 3face 4face 5face 65--5--51-51-51-51-

Faces with rollMax=1 contribute 5 each (no repeat allowed). Faces with rollMax >= 2 contribute 6 each (5 switches + 1 extend). Answer: 34.

Notice how faces 1 and 2 contribute fewer sequences because their rollMax is 1. They can never extend a streak, so the only way to end on face 1 or 2 after two rolls is to switch from something else. Faces 3 through 6 get an extra sequence each from the extend transition.

Complexity analysis

ApproachTimeSpace
3D DPO(n * 6 * max(rollMax))O(6 * max(rollMax))

The time complexity has three nested factors: n rolls, 6 faces, and up to max(rollMax) consecutive counts. The switch transition adds another factor of 6 in the worst case, but the optimized version (precomputing totals) brings it back down. For the space, you only need two layers of the DP table (current roll and previous roll), giving O(6 * max(rollMax)). Since rollMax[i] is at most 15 per the constraints, the table fits comfortably in memory.

The building blocks

1. State definition with consecutive count

dp = [[[0] * (max_consec + 1) for _ in range(6)] for _ in range(n + 1)]

for j in range(6):
    dp[1][j][1] = 1

The third dimension k tracks how deep you are into a streak. Base case: after one roll, every face has been rolled exactly once consecutively. This pattern of adding a dimension to encode "how many times in a row" is the core of constrained state DP. You will see the same idea in problems like Count Vowels Permutation, where the constraint is about which transitions are allowed based on the current state.

2. Transition with constraint checking

for j in range(6):
    for f in range(6):
        if f != j:
            for k in range(1, rollMax[f] + 1):
                dp[i][j][1] = (dp[i][j][1] + dp[i - 1][f][k]) % MOD
    for k in range(2, rollMax[j] + 1):
        dp[i][j][k] = dp[i - 1][j][k - 1]

Two cases, cleanly separated. Switching face resets consecutive count to 1 and draws from every valid state of every other face. Extending a streak increments the count by 1 and only applies when the new count is still within the limit. The constraint check is implicit in the loop bound: k only goes up to rollMax[j], so you never create an invalid state.

Edge cases

  • All rollMax values are 1: no face can repeat consecutively. The problem reduces to counting permutations where no two adjacent elements are the same. For n = 1 the answer is 6. For n = 2 the answer is 30 (6 choices for first, 5 for second).
  • n = 1: the answer is always 6, regardless of rollMax, since a single roll cannot violate any consecutive constraint.
  • One rollMax is very large: if rollMax[0] = 15 (the max allowed), face 1 can appear up to 15 times in a row. The DP table grows in the k dimension but the logic stays the same.
  • All rollMax values equal n: no constraint is ever binding. Every sequence of length n is valid, giving 6^n mod (10^9 + 7).
  • Modular arithmetic: the counts grow fast. Always take mod at every addition to avoid overflow in languages with fixed-size integers.

From understanding to recall

You now understand why a third DP dimension is necessary, how the switch and extend transitions work, and how the rollMax constraint gets enforced through loop bounds. But understanding the logic once is not the same as reproducing it under pressure. In an interview, you need to remember that the state is (roll, face, consecutive), that switching resets k to 1, and that extending increments k by 1.

The constrained state DP pattern shows up in many problems beyond dice. Count Vowels Permutation constrains which letters can follow which. Student Attendance Record II constrains how many consecutive absences are allowed. Every time you see "at most X in a row," you should think of adding a consecutive-count dimension. Drilling this pattern with spaced repetition until the state definition and transitions are automatic is the fastest way to own this category of problems.

Related posts

The constrained state DP pattern is one of roughly 60 reusable building blocks in the CodeBricks system. You practice it with spaced repetition until the three-dimensional state definition and the switch/extend transitions are second nature. That kind of fluency is what separates "I can solve it if I think hard enough" from "I see the pattern instantly."