837. New 21 Game
Problem Description
Alice is playing a game similar to "21" (Blackjack). The game works as follows:
- Alice starts with
0
points - She keeps drawing numbers as long as she has less than
k
points - Each draw gives her a random integer between
1
andmaxPts
(inclusive), with all outcomes being equally likely - Each draw is independent of previous draws
- Alice stops drawing immediately when she reaches
k
or more points
Given three parameters:
n
: a target score thresholdk
: the minimum score at which Alice stops drawingmaxPts
: the maximum points that can be gained in a single draw
You need to calculate the probability that Alice's final score is n
or less.
For example, if k = 10
, maxPts = 5
, and n = 15
:
- Alice keeps drawing while her score is less than 10
- Each draw adds 1 to 5 points randomly
- Once she reaches 10 or more, she stops
- We want the probability that her final score is at most 15
The answer should be accurate within 10^-5
of the actual probability.
Intuition
The key insight is to think about this problem recursively. We can define dfs(i)
as the probability of ending with a score ≤ n
when starting from score i
.
Starting from any score i < k
, Alice must draw another card. The next score will be i + j
where j
is between 1
and maxPts
. Since each outcome is equally likely, we have:
dfs(i) = (1/maxPts) × Σ[j=1 to maxPts] dfs(i+j)
This means the probability from score i
is the average of probabilities from all possible next scores.
However, computing this directly for each state would be too slow. The clever optimization comes from recognizing a pattern. If we expand dfs(i)
and dfs(i+1)
:
dfs(i) = (dfs(i+1) + dfs(i+2) + ... + dfs(i+maxPts)) / maxPts
dfs(i+1) = (dfs(i+2) + dfs(i+3) + ... + dfs(i+maxPts+1)) / maxPts
Notice that these two sums overlap significantly! The only difference is that dfs(i)
includes dfs(i+1)
but not dfs(i+maxPts+1)
, while dfs(i+1)
includes dfs(i+maxPts+1)
but not dfs(i+1)
itself.
By subtracting the second equation from the first, we get:
dfs(i) - dfs(i+1) = (dfs(i+1) - dfs(i+maxPts+1)) / maxPts
Rearranging:
dfs(i) = dfs(i+1) + (dfs(i+1) - dfs(i+maxPts+1)) / maxPts
This transforms our O(maxPts) calculation per state into O(1)!
The special case is when i = k-1
. From this score, Alice draws one more card and immediately stops (since she'll reach k
or more). The probability of ending ≤ n
is simply the fraction of outcomes that land in the valid range [k, n]
. This gives us:
dfs(k-1) = min(n-k+1, maxPts) / maxPts
Learn more about Math, Dynamic Programming and Sliding Window patterns.
Solution Approach
The solution uses memoized recursion (dynamic programming with memoization) to efficiently calculate the probability. Here's how the implementation works:
State Definition:
We define dfs(i)
as the probability that starting from score i
, Alice ends with a final score ≤ n
.
Base Cases:
- If
i >= k
: Alice stops drawing. Return1
ifi <= n
(success), otherwise return0
(failure). - If
i == k - 1
: This is a special case. From scorek-1
, Alice draws exactly one more card and stops. The probability of success is the fraction of outcomes that result in a score ≤n
:- Valid outcomes are scores in range
[k, min(n, k + maxPts - 1)]
- Number of valid outcomes =
min(n - k + 1, maxPts)
- Probability =
min(n - k + 1, maxPts) / maxPts
- Valid outcomes are scores in range
Recursive Case (when i < k - 1
):
Using the optimization derived earlier:
dfs(i) = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts
This formula allows us to calculate dfs(i)
in O(1) time given the values of dfs(i + 1)
and dfs(i + maxPts + 1)
.
Implementation Details:
- The
@cache
decorator is used for memoization, storing already computed values ofdfs(i)
to avoid redundant calculations - The recursion starts from
dfs(0)
(Alice's initial score) - The function works backwards from higher scores to lower scores due to the recursive nature
Time Complexity: O(n) where n is the maximum score we need to consider. Each state is computed at most once due to memoization.
Space Complexity: O(n) for the memoization cache storing the computed probabilities.
The key insight that makes this solution efficient is transforming the naive O(k × maxPts) approach into an O(n) solution by recognizing the mathematical relationship between consecutive states and using the sliding window property of the probability calculations.
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 = 6
, k = 4
, and maxPts = 3
.
Alice starts at score 0 and keeps drawing while her score is less than 4. Each draw adds 1, 2, or 3 points with equal probability (1/3 each). She stops when reaching 4 or more points.
Step 1: Identify possible final scores
- From score 0: Drawing 1,2,3 → reach scores 1,2,3 (continue drawing)
- From score 1: Drawing 1,2,3 → reach scores 2,3,4 (continue from 2,3; stop at 4)
- From score 2: Drawing 1,2,3 → reach scores 3,4,5 (continue from 3; stop at 4,5)
- From score 3: Drawing 1,2,3 → reach scores 4,5,6 (stop at all)
Possible final scores: 4, 5, 6
Step 2: Calculate probabilities using our formula
Start with base cases (scores ≥ k):
dfs(6) = 1
(6 ≤ n = 6, success)dfs(5) = 1
(5 ≤ n = 6, success)dfs(4) = 1
(4 ≤ n = 6, success)
Special case at k-1 = 3:
- From score 3, Alice draws once more and stops
- Valid outcomes: reaching 4, 5, or 6 (all ≤ n = 6)
dfs(3) = min(6-4+1, 3) / 3 = 3/3 = 1.0
Working backwards using our optimized formula:
-
dfs(2) = dfs(3) + (dfs(3) - dfs(6)) / 3
-
dfs(2) = 1.0 + (1.0 - 1.0) / 3 = 1.0
-
dfs(1) = dfs(2) + (dfs(2) - dfs(5)) / 3
-
dfs(1) = 1.0 + (1.0 - 1.0) / 3 = 1.0
-
dfs(0) = dfs(1) + (dfs(1) - dfs(4)) / 3
-
dfs(0) = 1.0 + (1.0 - 1.0) / 3 = 1.0
Result: The probability is 1.0, meaning Alice always ends with a score ≤ 6.
This makes sense because the maximum possible score is 6 (reaching score 3, then drawing 3), which equals our target n. Therefore, Alice can never exceed n, giving us probability 1.0.
Solution Implementation
1from functools import cache
2from typing import Optional
3
4class Solution:
5 def new21Game(self, n: int, k: int, maxPts: int) -> float:
6 """
7 Calculate the probability of getting at most n points in the New 21 Game.
8
9 Args:
10 n: Maximum points we want to stay under or equal to
11 k: We stop drawing when we have k or more points
12 maxPts: Each draw gives us points between 1 and maxPts (inclusive)
13
14 Returns:
15 Probability of ending with n or fewer points
16 """
17
18 @cache
19 def calculate_probability(current_points: int) -> float:
20 """
21 Calculate probability of winning starting from current_points.
22
23 Args:
24 current_points: Current total points
25
26 Returns:
27 Probability of ending with n or fewer points from this state
28 """
29 # Base case: if we've reached k or more points, we stop drawing
30 if current_points >= k:
31 # Return 1.0 if we're at or below n, else 0.0
32 return 1.0 if current_points <= n else 0.0
33
34 # Special optimization: when we're at k-1 points
35 # We draw exactly one more card
36 if current_points == k - 1:
37 # Count how many outcomes lead to <= n points
38 # We can draw 1 to maxPts, landing on k to k-1+maxPts
39 # Valid outcomes are those landing on k to n
40 favorable_outcomes = min(n - k + 1, maxPts)
41 return favorable_outcomes / maxPts
42
43 # General case: use sliding window technique
44 # Probability = average of probabilities from all possible next states
45 # P(i) = (P(i+1) + P(i+2) + ... + P(i+maxPts)) / maxPts
46 # Using sliding window optimization:
47 # P(i) = P(i+1) + (P(i+1) - P(i+maxPts+1)) / maxPts
48 next_probability = calculate_probability(current_points + 1)
49 window_difference = (calculate_probability(current_points + 1) -
50 calculate_probability(current_points + maxPts + 1))
51
52 return next_probability + window_difference / maxPts
53
54 # Start the game with 0 points
55 return calculate_probability(0)
56
1class Solution {
2 // Memoization array to store probability values for each score
3 private double[] memo;
4 // Maximum score limit
5 private int maxScore;
6 // Score threshold to stop drawing
7 private int stopThreshold;
8 // Maximum points that can be drawn in one turn
9 private int maxPointsPerDraw;
10
11 public double new21Game(int n, int k, int maxPts) {
12 // Initialize memoization array for scores from 0 to k-1
13 memo = new double[k];
14 this.maxScore = n;
15 this.stopThreshold = k;
16 this.maxPointsPerDraw = maxPts;
17
18 // Calculate probability starting from score 0
19 return calculateProbability(0);
20 }
21
22 private double calculateProbability(int currentScore) {
23 // Base case: if current score >= k, we stop drawing
24 if (currentScore >= stopThreshold) {
25 // Return 1 if we're within the winning range, 0 otherwise
26 return currentScore <= maxScore ? 1.0 : 0.0;
27 }
28
29 // Special case optimization: when at k-1, we draw exactly once more
30 if (currentScore == stopThreshold - 1) {
31 // Count how many draws lead to a winning score (score <= n)
32 // We can draw 1 to maxPts points, landing on scores k to k+maxPts-1
33 // Winning scores are those <= n, so we count from k to min(n, k+maxPts-1)
34 return Math.min(maxScore - stopThreshold + 1, maxPointsPerDraw) * 1.0 / maxPointsPerDraw;
35 }
36
37 // Return memoized result if already calculated
38 if (memo[currentScore] != 0) {
39 return memo[currentScore];
40 }
41
42 // Recursive formula using sliding window technique
43 // P(i) = P(i+1) + (P(i+1) - P(i+maxPts+1)) / maxPts
44 // This efficiently calculates the average of P(i+1) through P(i+maxPts)
45 double nextProbability = calculateProbability(currentScore + 1);
46 double beyondWindowProbability = calculateProbability(currentScore + maxPointsPerDraw + 1);
47
48 // Store and return the calculated probability
49 memo[currentScore] = nextProbability +
50 (nextProbability - beyondWindowProbability) / maxPointsPerDraw;
51
52 return memo[currentScore];
53 }
54}
55
1class Solution {
2public:
3 double new21Game(int n, int k, int maxPts) {
4 // dp[i] represents the probability of getting n or less points starting from i points
5 vector<double> dp(k);
6
7 // Recursive function to calculate probability
8 // currentPoints: current accumulated points
9 // Returns: probability of ending with n or less points
10 function<double(int)> calculateProbability = [&](int currentPoints) -> double {
11 // Base case: if we've already reached k points, we stop drawing
12 if (currentPoints >= k) {
13 // If current points <= n, we win (probability = 1)
14 // Otherwise, we lose (probability = 0)
15 return currentPoints <= n ? 1.0 : 0.0;
16 }
17
18 // Special case optimization: when at k-1, we draw exactly one more card
19 if (currentPoints == k - 1) {
20 // Count how many cards would result in a winning score
21 // We can draw cards valued 1 to maxPts
22 // Winning cards are those that give us a total <= n
23 int winningCards = min(n - k + 1, maxPts);
24 return static_cast<double>(winningCards) / maxPts;
25 }
26
27 // Return memoized result if already calculated
28 if (dp[currentPoints] != 0.0) {
29 return dp[currentPoints];
30 }
31
32 // Calculate probability using sliding window technique
33 // P(i) = P(i+1) + [P(i+1) - P(i+maxPts+1)] / maxPts
34 // This formula efficiently computes the average of probabilities
35 // for all possible next states (i+1 to i+maxPts)
36 double nextProb = calculateProbability(currentPoints + 1);
37 double windowEndProb = calculateProbability(currentPoints + maxPts + 1);
38 dp[currentPoints] = nextProb + (nextProb - windowEndProb) / maxPts;
39
40 return dp[currentPoints];
41 };
42
43 // Start calculation from 0 points
44 return calculateProbability(0);
45 }
46};
47
1/**
2 * Calculates the probability of getting N or fewer points in the New 21 Game
3 * @param n - Maximum score to stay under or equal to
4 * @param k - Score threshold to stop drawing cards
5 * @param maxPts - Maximum points that can be drawn from a single card
6 * @returns Probability of getting N or fewer points
7 */
8function new21Game(n: number, k: number, maxPts: number): number {
9 // Memoization array to store probabilities for each score from 0 to k-1
10 const memo: number[] = Array(k).fill(0);
11
12 /**
13 * Recursive function to calculate probability starting from score i
14 * @param currentScore - Current accumulated score
15 * @returns Probability of winning starting from currentScore
16 */
17 const calculateProbability = (currentScore: number): number => {
18 // Base case: if score >= k, we stop drawing
19 if (currentScore >= k) {
20 // Win if final score <= n, lose otherwise
21 return currentScore <= n ? 1 : 0;
22 }
23
24 // Special case optimization: when at k-1, next draw determines outcome
25 if (currentScore === k - 1) {
26 // Count winning outcomes (cards that keep us <= n) divided by total possibilities
27 return Math.min(n - k + 1, maxPts) / maxPts;
28 }
29
30 // Return memoized result if already calculated
31 if (memo[currentScore] !== 0) {
32 return memo[currentScore];
33 }
34
35 // Calculate probability using sliding window technique
36 // P(i) = P(i+1) + [P(i+1) - P(i+maxPts+1)] / maxPts
37 // This formula leverages the relationship between consecutive probabilities
38 const probability = calculateProbability(currentScore + 1) +
39 (calculateProbability(currentScore + 1) -
40 calculateProbability(currentScore + maxPts + 1)) / maxPts;
41
42 // Store and return the calculated probability
43 memo[currentScore] = probability;
44 return probability;
45 };
46
47 // Start calculation from score 0
48 return calculateProbability(0);
49}
50
Time and Space Complexity
The time complexity is O(k + maxPts)
and the space complexity is O(k + maxPts)
.
Time Complexity Analysis:
The function uses memoization with @cache
decorator. Each state i
from 0
to at most k + maxPts
is computed at most once due to caching. The recursive calls explore states from i = 0
up to potentially i = k + maxPts - 1
(since when we're at state k-1
and draw maxPts
, we reach k - 1 + maxPts
). Each memoized computation takes O(1)
time as it only performs constant operations and makes recursive calls that are either already cached or will be cached. Therefore, the total time complexity is O(k + maxPts)
.
Space Complexity Analysis: The space complexity comes from two sources:
- The recursion stack depth, which can go up to
O(k + maxPts)
in the worst case - The cache storage for memoization, which stores results for states from
0
to potentiallyk + maxPts - 1
Both contribute to a space complexity of O(k + maxPts)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Base Case Handling
A common mistake is not properly handling the boundary between when Alice continues drawing and when she stops. The critical insight is that Alice stops drawing immediately when her score reaches k
or more.
Pitfall Example:
# WRONG: This incorrectly continues drawing even at score k if current_points > k: # Should be >= k return 1.0 if current_points <= n else 0.0
Solution: Always use current_points >= k
to check if Alice has stopped drawing.
2. Misunderstanding the k-1 Special Case
When Alice has exactly k-1
points, she will draw exactly one more card and stop. Many implementations fail to optimize this case or calculate it incorrectly.
Pitfall Example:
# WRONG: Incorrectly counting favorable outcomes if current_points == k - 1: # This doesn't account for the upper bound correctly favorable_outcomes = n - k + 1 # Missing min() with maxPts return favorable_outcomes / maxPts
Solution: The favorable outcomes should be min(n - k + 1, maxPts)
because:
- Alice can only draw values from 1 to maxPts
- She needs to land in the range [k, n] to win
- The number of winning draws is limited by both constraints
3. Stack Overflow with Deep Recursion
For large values of n
and k
, the recursive solution without memoization can cause stack overflow or timeout.
Pitfall Example:
# WRONG: No memoization leads to exponential time complexity
def calculate_probability(current_points):
if current_points >= k:
return 1.0 if current_points <= n else 0.0
# Recursive calls without caching results
return sum(calculate_probability(current_points + i)
for i in range(1, maxPts + 1)) / maxPts
Solution: Always use memoization (@cache
decorator or manual dictionary) to store computed results and avoid recalculating the same states.
4. Floating Point Precision Issues
When dealing with probabilities, accumulating small floating-point errors can lead to results outside the required accuracy of 10^-5
.
Pitfall Example:
# WRONG: Accumulating errors through repeated additions
total = 0
for i in range(1, maxPts + 1):
total += calculate_probability(current_points + i)
return total / maxPts # Small errors can accumulate
Solution: Use the sliding window optimization formula which minimizes the number of floating-point operations:
return next_probability + (next_probability - future_probability) / maxPts
5. Edge Case: When k > n
If k > n
, Alice needs more points to stop drawing than the maximum allowed score. In this case, the probability should be 0, but some implementations might not handle this correctly.
Solution: The base case if current_points >= k: return 1.0 if current_points <= n else 0.0
naturally handles this, but it's important to verify this edge case works correctly.
In a binary min heap, the maximum element can be found in:
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Want a Structured Path to Master System Design Too? Don’t Miss This!