2318. Number of Distinct Roll Sequences
Problem Description
You need to simulate rolling a 6-sided dice n
times and count how many different valid sequences of rolls are possible.
A sequence of rolls is valid if it satisfies two conditions:
-
Adjacent rolls must be coprime: Any two consecutive dice values in the sequence must have a greatest common divisor (GCD) of 1. For example, if you roll a 2 followed by a 3, this is valid since
gcd(2,3) = 1
. But rolling a 2 followed by a 4 is invalid sincegcd(2,4) = 2
. -
Same values must be separated: If the same number appears multiple times in the sequence, there must be at least 2 other rolls between them. In other words, if position
i
has the same value as positionj
, then|i - j| > 2
. For example, the sequence[1, 2, 3, 1]
is valid (the two 1's are separated by 2 positions), but[1, 2, 1]
is invalid (the two 1's are only separated by 1 position).
The problem asks you to count all distinct valid sequences of length n
where each roll can be any value from 1 to 6. Since this count can be very large, return the result modulo 10^9 + 7
.
For example, if n = 1
, any single roll from 1 to 6 is valid, so the answer is 6. For larger values of n
, you need to consider both constraints when building valid sequences.
Intuition
When thinking about this problem, we need to track not just the current roll, but also the previous roll to ensure adjacent values are coprime, and potentially the roll before that to ensure we don't repeat a value too soon.
The key insight is that we need to know the last two rolls to determine if the next roll is valid. If we're at position k
:
- We need to know the roll at position
k-1
to check if the new roll is coprime with it - We need to know the roll at position
k-2
to ensure we're not repeating that value (violating the separation constraint)
This naturally leads to a dynamic programming approach where our state is defined by:
- The current position in the sequence (from 1 to
n
) - The value of the previous roll (position
k-1
) - The value of the roll before that (position
k-2
)
We can build up our answer using dp[k][i][j]
which represents the number of valid sequences of length k
where the last roll is i+1
and the second-to-last roll is j+1
(using 0-based indexing for the array).
Starting with sequences of length 2, we can initialize all valid pairs where two adjacent values are coprime and different. Then for each subsequent position, we try all possible previous values that would make a valid sequence:
- The new value must be coprime with the immediately previous value
- The new value must be different from both the previous value and the value two positions back
By building up from length 2 to length n
, we accumulate all possible valid sequences. The final answer is the sum of all dp[n][i][j]
values, representing all possible ways to end an n
-length sequence with any valid pair of final values.
Learn more about Memoization and Dynamic Programming patterns.
Solution Approach
The implementation uses a 3D dynamic programming array to track valid sequences:
Data Structure:
dp[k][i][j]
: A 3D array wheredp[k][i][j]
represents the number of valid sequences of lengthk
ending with dice valuei+1
at positionk
and dice valuej+1
at positionk-1
.
Algorithm Steps:
-
Handle Base Case: If
n = 1
, return 6 since any single dice roll (1-6) forms a valid sequence. -
Initialize DP Array: Create a 3D array of dimensions
(n+1) × 6 × 6
initialized with zeros. -
Initialize Length-2 Sequences:
for i in range(6): for j in range(6): if gcd(i + 1, j + 1) == 1 and i != j: dp[2][i][j] = 1
This sets
dp[2][i][j] = 1
for all valid 2-roll sequences where:- The two values are coprime (
gcd(i+1, j+1) == 1
) - The two values are different (
i != j
)
- The two values are coprime (
-
Build Longer Sequences: For each length
k
from 3 ton
:for k in range(3, n + 1): for i in range(6): # Current last roll for j in range(6): # Second-to-last roll if gcd(i + 1, j + 1) == 1 and i != j: for h in range(6): # Third-to-last roll if gcd(h + 1, i + 1) == 1 and h != i and h != j: dp[k][i][j] += dp[k - 1][h][i]
For each valid ending pair
(i, j)
:- Check if they are coprime and different
- Try all possible third-to-last values
h
- Validate that
h
is coprime withi
(the new second-to-last) - Ensure
h ≠ i
(adjacent values must differ) - Ensure
h ≠ j
(values must be separated by at least 2 positions) - Add the count from the previous state
dp[k-1][h][i]
-
Calculate Final Answer: Sum all valid sequences of length
n
:ans = 0 for i in range(6): for j in range(6): ans += dp[-1][i][j] return ans % mod
The solution uses modulo 10^9 + 7
to handle large numbers. The time complexity is O(n × 6³) = O(216n)
and space complexity is O(n × 36) = O(36n)
.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with n = 3
to find all valid 3-roll sequences.
Step 1: Initialize for length 2
First, we find all valid 2-roll sequences where values are coprime and different:
- (1,2): gcd(1,2)=1, different ✓ → dp[2][0][1] = 1
- (1,3): gcd(1,3)=1, different ✓ → dp[2][0][2] = 1
- (1,5): gcd(1,5)=1, different ✓ → dp[2][0][4] = 1
- (2,1): gcd(2,1)=1, different ✓ → dp[2][1][0] = 1
- (2,3): gcd(2,3)=1, different ✓ → dp[2][1][2] = 1
- (2,5): gcd(2,5)=1, different ✓ → dp[2][1][4] = 1
- (3,1): gcd(3,1)=1, different ✓ → dp[2][2][0] = 1
- (3,2): gcd(3,2)=1, different ✓ → dp[2][2][1] = 1
- (3,4): gcd(3,4)=1, different ✓ → dp[2][2][3] = 1
- (3,5): gcd(3,5)=1, different ✓ → dp[2][2][4] = 1
(Continuing this process for all pairs gives us 17 valid 2-roll sequences)
Step 2: Build length 3 sequences
For each valid ending pair (i,j), we try adding a third value h at the beginning:
Example: Building sequences ending with (2,3):
- Current last two rolls: j=2 (value 3), i=1 (value 2)
- Need dp[3][1][2] (sequences ending with 2,3)
- Try each possible h (third-to-last roll):
- h=0 (value 1): Check gcd(1,2)=1 ✓, 1≠2 ✓, 1≠3 ✓
- Look up dp[2][0][1] = 1 (sequences "1,2")
- Add to dp[3][1][2]: creates sequence [1,2,3]
- h=2 (value 3): Check gcd(3,2)=1 ✓, 3≠2 ✓, but 3=3 ✗
- Skip (violates separation rule)
- h=3 (value 4): Check gcd(4,2)=2 ✗
- Skip (not coprime)
- h=4 (value 5): Check gcd(5,2)=1 ✓, 5≠2 ✓, 5≠3 ✓
- Look up dp[2][4][1] = 1 (sequences "5,2")
- Add to dp[3][1][2]: creates sequence [5,2,3]
- h=0 (value 1): Check gcd(1,2)=1 ✓, 1≠2 ✓, 1≠3 ✓
So dp[3][1][2] = 2 (two valid sequences ending with 2,3)
Step 3: Sum all valid length-3 sequences
After processing all combinations:
- Valid sequences like [1,2,3], [1,2,5], [1,3,2], [1,3,4], [1,3,5], [1,5,2], [1,5,3], [1,5,4], [1,5,6]...
- Sum all dp[3][i][j] values where dp[3][i][j] > 0
- Total count = 48 valid sequences of length 3
The algorithm systematically builds longer sequences by extending shorter valid sequences, ensuring both the coprime constraint (through gcd checks) and the separation constraint (by tracking the last two values).
Solution Implementation
1from math import gcd
2
3class Solution:
4 def distinctSequences(self, n: int) -> int:
5 # Base case: if n is 1, we can roll any of the 6 dice values
6 if n == 1:
7 return 6
8
9 MOD = 10**9 + 7
10
11 # dp[length][last_die][second_last_die] = number of valid sequences
12 # where last_die is the last rolled die (0-indexed: 0 represents die value 1)
13 # and second_last_die is the second to last rolled die
14 dp = [[[0] * 6 for _ in range(6)] for _ in range(n + 1)]
15
16 # Initialize base case for sequences of length 2
17 # Two consecutive dice must have gcd = 1 and be different
18 for last_die in range(6):
19 for second_last_die in range(6):
20 # Convert 0-indexed to 1-indexed for gcd calculation
21 if gcd(last_die + 1, second_last_die + 1) == 1 and last_die != second_last_die:
22 dp[2][last_die][second_last_die] = 1
23
24 # Build up the dp table for sequences of length 3 to n
25 for length in range(3, n + 1):
26 for last_die in range(6):
27 for second_last_die in range(6):
28 # Check if last two dice are valid (coprime and different)
29 if gcd(last_die + 1, second_last_die + 1) == 1 and last_die != second_last_die:
30 # Try all possible third-to-last dice
31 for third_last_die in range(6):
32 # Check constraints:
33 # 1. third_last and second_last must be coprime
34 # 2. third_last must be different from second_last
35 # 3. third_last must be different from last (no same value at distance 2)
36 if (gcd(third_last_die + 1, second_last_die + 1) == 1 and
37 third_last_die != second_last_die and
38 third_last_die != last_die):
39 dp[length][last_die][second_last_die] += dp[length - 1][third_last_die][second_last_die]
40
41 # Sum up all valid sequences of length n ending with any valid pair
42 total_sequences = 0
43 for last_die in range(6):
44 for second_last_die in range(6):
45 total_sequences += dp[n][last_die][second_last_die]
46
47 return total_sequences % MOD
48
1class Solution {
2 /**
3 * Calculate the number of distinct sequences that can be created using n dice rolls
4 * following specific rules about consecutive values.
5 *
6 * @param n The number of dice rolls
7 * @return The number of distinct valid sequences modulo 10^9 + 7
8 */
9 public int distinctSequences(int n) {
10 // Base case: single roll can have any of 6 values
11 if (n == 1) {
12 return 6;
13 }
14
15 final int MOD = (int) 1e9 + 7;
16
17 // dp[length][lastValue][secondLastValue] represents the number of valid sequences
18 // of given length ending with lastValue and secondLastValue
19 // Values are 0-indexed (0 represents dice value 1, 5 represents dice value 6)
20 int[][][] dp = new int[n + 1][6][6];
21
22 // Initialize base case for sequences of length 2
23 // Two consecutive values must be coprime (GCD = 1) and different
24 for (int lastValue = 0; lastValue < 6; lastValue++) {
25 for (int secondLastValue = 0; secondLastValue < 6; secondLastValue++) {
26 if (gcd(lastValue + 1, secondLastValue + 1) == 1 && lastValue != secondLastValue) {
27 dp[2][lastValue][secondLastValue] = 1;
28 }
29 }
30 }
31
32 // Build up sequences from length 3 to n
33 for (int length = 3; length <= n; length++) {
34 for (int lastValue = 0; lastValue < 6; lastValue++) {
35 for (int secondLastValue = 0; secondLastValue < 6; secondLastValue++) {
36 // Check if last two values are valid (coprime and different)
37 if (gcd(lastValue + 1, secondLastValue + 1) == 1 && lastValue != secondLastValue) {
38 // Try all possible third-last values
39 for (int thirdLastValue = 0; thirdLastValue < 6; thirdLastValue++) {
40 // Third-last must be coprime with second-last, different from second-last,
41 // and different from last (to avoid same value at distance 2)
42 if (gcd(thirdLastValue + 1, lastValue + 1) == 1 &&
43 thirdLastValue != lastValue && thirdLastValue != secondLastValue) {
44 dp[length][lastValue][secondLastValue] =
45 (dp[length][lastValue][secondLastValue] + dp[length - 1][thirdLastValue][lastValue]) % MOD;
46 }
47 }
48 }
49 }
50 }
51 }
52
53 // Sum up all valid sequences of length n with any ending pair
54 int totalSequences = 0;
55 for (int lastValue = 0; lastValue < 6; lastValue++) {
56 for (int secondLastValue = 0; secondLastValue < 6; secondLastValue++) {
57 totalSequences = (totalSequences + dp[n][lastValue][secondLastValue]) % MOD;
58 }
59 }
60
61 return totalSequences;
62 }
63
64 /**
65 * Calculate the greatest common divisor of two numbers using Euclidean algorithm.
66 *
67 * @param a First number
68 * @param b Second number
69 * @return The GCD of a and b
70 */
71 private int gcd(int a, int b) {
72 return b == 0 ? a : gcd(b, a % b);
73 }
74}
75
1class Solution {
2public:
3 int distinctSequences(int n) {
4 // Base case: single die roll has 6 possibilities
5 if (n == 1) return 6;
6
7 const int MOD = 1e9 + 7;
8 const int DICE_FACES = 6;
9
10 // dp[length][last_die][second_last_die] = number of valid sequences
11 // of given length ending with these two dice values
12 vector<vector<vector<int>>> dp(n + 1,
13 vector<vector<int>>(DICE_FACES, vector<int>(DICE_FACES, 0)));
14
15 // Initialize sequences of length 2
16 // Two consecutive dice must be different and coprime
17 for (int lastDie = 0; lastDie < DICE_FACES; ++lastDie) {
18 for (int secondLastDie = 0; secondLastDie < DICE_FACES; ++secondLastDie) {
19 int lastValue = lastDie + 1;
20 int secondLastValue = secondLastDie + 1;
21
22 if (gcd(lastValue, secondLastValue) == 1 && lastDie != secondLastDie) {
23 dp[2][lastDie][secondLastDie] = 1;
24 }
25 }
26 }
27
28 // Build up sequences from length 3 to n
29 for (int length = 3; length <= n; ++length) {
30 for (int lastDie = 0; lastDie < DICE_FACES; ++lastDie) {
31 for (int secondLastDie = 0; secondLastDie < DICE_FACES; ++secondLastDie) {
32 int lastValue = lastDie + 1;
33 int secondLastValue = secondLastDie + 1;
34
35 // Check if last two dice are valid (different and coprime)
36 if (gcd(lastValue, secondLastValue) == 1 && lastDie != secondLastDie) {
37 // Try all possible third-last dice values
38 for (int thirdLastDie = 0; thirdLastDie < DICE_FACES; ++thirdLastDie) {
39 int thirdLastValue = thirdLastDie + 1;
40
41 // Ensure third-last die is valid:
42 // - Coprime with second-last
43 // - Different from second-last
44 // - Different from last (no same value at distance 2)
45 if (gcd(thirdLastValue, secondLastValue) == 1 &&
46 thirdLastDie != secondLastDie &&
47 thirdLastDie != lastDie) {
48 dp[length][lastDie][secondLastDie] =
49 (dp[length][lastDie][secondLastDie] +
50 dp[length - 1][thirdLastDie][secondLastDie]) % MOD;
51 }
52 }
53 }
54 }
55 }
56 }
57
58 // Sum all valid sequences of length n
59 int totalSequences = 0;
60 for (int lastDie = 0; lastDie < DICE_FACES; ++lastDie) {
61 for (int secondLastDie = 0; secondLastDie < DICE_FACES; ++secondLastDie) {
62 totalSequences = (totalSequences + dp[n][lastDie][secondLastDie]) % MOD;
63 }
64 }
65
66 return totalSequences;
67 }
68
69private:
70 // Calculate greatest common divisor using Euclidean algorithm
71 int gcd(int a, int b) {
72 return b == 0 ? a : gcd(b, a % b);
73 }
74};
75
1function distinctSequences(n: number): number {
2 // Base case: single die roll has 6 possibilities
3 if (n === 1) return 6;
4
5 const MOD = 1e9 + 7;
6 const DICE_FACES = 6;
7
8 // dp[length][lastDie][secondLastDie] = number of valid sequences
9 // of given length ending with these two dice values
10 const dp: number[][][] = Array(n + 1)
11 .fill(null)
12 .map(() => Array(DICE_FACES)
13 .fill(null)
14 .map(() => Array(DICE_FACES).fill(0)));
15
16 // Initialize sequences of length 2
17 // Two consecutive dice must be different and coprime
18 for (let lastDie = 0; lastDie < DICE_FACES; lastDie++) {
19 for (let secondLastDie = 0; secondLastDie < DICE_FACES; secondLastDie++) {
20 const lastValue = lastDie + 1;
21 const secondLastValue = secondLastDie + 1;
22
23 // Check if the two dice values are coprime and different
24 if (gcd(lastValue, secondLastValue) === 1 && lastDie !== secondLastDie) {
25 dp[2][lastDie][secondLastDie] = 1;
26 }
27 }
28 }
29
30 // Build up sequences from length 3 to n
31 for (let length = 3; length <= n; length++) {
32 for (let lastDie = 0; lastDie < DICE_FACES; lastDie++) {
33 for (let secondLastDie = 0; secondLastDie < DICE_FACES; secondLastDie++) {
34 const lastValue = lastDie + 1;
35 const secondLastValue = secondLastDie + 1;
36
37 // Check if last two dice are valid (different and coprime)
38 if (gcd(lastValue, secondLastValue) === 1 && lastDie !== secondLastDie) {
39 // Try all possible third-last dice values
40 for (let thirdLastDie = 0; thirdLastDie < DICE_FACES; thirdLastDie++) {
41 const thirdLastValue = thirdLastDie + 1;
42
43 // Ensure third-last die is valid:
44 // - Coprime with second-last
45 // - Different from second-last
46 // - Different from last (no same value at distance 2)
47 if (gcd(thirdLastValue, secondLastValue) === 1 &&
48 thirdLastDie !== secondLastDie &&
49 thirdLastDie !== lastDie) {
50 dp[length][lastDie][secondLastDie] =
51 (dp[length][lastDie][secondLastDie] +
52 dp[length - 1][secondLastDie][thirdLastDie]) % MOD;
53 }
54 }
55 }
56 }
57 }
58 }
59
60 // Sum all valid sequences of length n
61 let totalSequences = 0;
62 for (let lastDie = 0; lastDie < DICE_FACES; lastDie++) {
63 for (let secondLastDie = 0; secondLastDie < DICE_FACES; secondLastDie++) {
64 totalSequences = (totalSequences + dp[n][lastDie][secondLastDie]) % MOD;
65 }
66 }
67
68 return totalSequences;
69}
70
71// Calculate greatest common divisor using Euclidean algorithm
72function gcd(a: number, b: number): number {
73 return b === 0 ? a : gcd(b, a % b);
74}
75
Time and Space Complexity
Time Complexity: O(n * 6^3)
= O(216n)
= O(n)
The algorithm uses dynamic programming with three nested loops:
- The outermost loop iterates from 3 to n, contributing
O(n)
iterations - The middle two loops iterate through all possible pairs of dice values (i, j), each from 0 to 5, contributing
O(6^2)
=O(36)
iterations - The innermost loop iterates through all possible previous dice values (h), contributing
O(6)
iterations - Inside the loops, the GCD calculation and other operations are
O(1)
Therefore, the total time complexity is O(n * 6 * 6 * 6)
= O(216n)
= O(n)
.
Space Complexity: O(n * 6^2)
= O(36n)
= O(n)
The algorithm uses a 3D DP array:
- The first dimension has size
n + 1
- The second dimension has size 6 (representing the previous dice value)
- The third dimension has size 6 (representing the current dice value)
Therefore, the total space complexity is O((n + 1) * 6 * 6)
= O(36n)
= O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Index Mapping in the Recurrence Relation
The Pitfall:
When building sequences of length k
, the code uses dp[k-1][third_last_die][second_last_die]
in the transition. This is incorrect because:
- In
dp[k-1]
, the last position is at indexk-1
and second-to-last is atk-2
- When extending to length
k
, the old "last" (positionk-1
) becomes the new "second-to-last" - The old "second-to-last" (position
k-2
) becomes the new "third-to-last"
The current code incorrectly keeps second_last_die
in the same position when it should shift to represent the third-to-last position.
The Fix:
# Incorrect transition: dp[length][last_die][second_last_die] += dp[length - 1][third_last_die][second_last_die] # Correct transition: dp[length][last_die][second_last_die] += dp[length - 1][second_last_die][third_last_die]
When extending from length k-1
to k
:
- The new last die is
last_die
(positionk
) - The new second-to-last is
second_last_die
(positionk-1
) - The old configuration had
second_last_die
as last (positionk-1
) andthird_last_die
as second-to-last (positionk-2
)
2. Constraint Validation Confusion
The Pitfall: The constraint checking can be confusing because we're validating relationships between the wrong pairs. When extending the sequence, we need to ensure:
- The new die (
last_die
) is coprime with the previous die (second_last_die
) - The new die doesn't equal any die within distance 2
The Fix: Structure the validation more clearly:
for length in range(3, n + 1):
for last_die in range(6):
for second_last_die in range(6):
# First validate the new pair being added
if gcd(last_die + 1, second_last_die + 1) != 1 or last_die == second_last_die:
continue
# Then check all valid previous states
for third_last_die in range(6):
# Ensure no same value at distance 2
if third_last_die == last_die:
continue
# Add valid transitions from previous state
dp[length][last_die][second_last_die] += dp[length - 1][second_last_die][third_last_die]
3. Missing Modulo Operations
The Pitfall: The code only applies modulo at the final return, but intermediate values can overflow in languages with fixed integer sizes or cause performance issues with very large numbers.
The Fix: Apply modulo during accumulation:
dp[length][last_die][second_last_die] = (dp[length][last_die][second_last_die] + dp[length - 1][second_last_die][third_last_die]) % MOD
Complete Corrected Solution:
from math import gcd
class Solution:
def distinctSequences(self, n: int) -> int:
if n == 1:
return 6
MOD = 10**9 + 7
dp = [[[0] * 6 for _ in range(6)] for _ in range(n + 1)]
# Initialize length 2 sequences
for i in range(6):
for j in range(6):
if gcd(i + 1, j + 1) == 1 and i != j:
dp[2][i][j] = 1
# Build longer sequences
for length in range(3, n + 1):
for last_die in range(6):
for second_last_die in range(6):
if gcd(last_die + 1, second_last_die + 1) == 1 and last_die != second_last_die:
for third_last_die in range(6):
if third_last_die != last_die:
dp[length][last_die][second_last_die] = (
dp[length][last_die][second_last_die] +
dp[length - 1][second_last_die][third_last_die]
) % MOD
total = 0
for i in range(6):
for j in range(6):
total = (total + dp[n][i][j]) % MOD
return total
Which data structure is used to implement recursion?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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
Want a Structured Path to Master System Design Too? Don’t Miss This!