Alice plays the following game, loosely based on the card game "21".
Alice starts with 0 points and draws numbers while she has less than k points. During each draw, she gains an integer number of points randomly from the range [1, maxPts], where maxPts is an integer. Each draw is independent and the outcomes have equal probabilities.
Alice stops drawing numbers when she gets k or more points.
Return the probability that Alice has n or fewer points.
Answers within (10^(-5)) of the actual answer are considered accepted.
Example 1:
Input: n = 10, k = 1, maxPts = 10
Output: 1.00000Explanation: Alice gets a single card, then stops.
Example 2:
Input: n = 6, k = 1, maxPts = 10
Output: 0.60000Explanation: Alice gets a single card, then stops.
In 6 out of 10 possibilities, she is at or below 6 points.
Example 3:
Input: n = 21, k = 17, maxPts = 10
Output: 0.73278Constraints:
0 <= k <= n <= 10,0001 <= maxPts <= 10,000Before attempting this problem, you should be comfortable with:
This problem asks for the probability of ending with a score between k and n (inclusive). Alice draws cards with values 1 to maxPts with equal probability until her score reaches k or more.
We can think recursively: from a given score, what is the probability of eventually ending at or below n? If our score is already >= k, we stop drawing. The outcome is successful (probability 1) if score <= n, and failed (probability 0) if score > n.
For scores below k, we draw one of maxPts values with equal probability 1/maxPts, so the probability at score s is the average of probabilities at scores s+1, s+2, ..., s+maxPts.
dfs(score) that returns the probability of ending with a final score <= n.score >= k, return 1 if score <= n, else return 0.score < k, sum up dfs(score + i) for i from 1 to maxPts, then divide by maxPts.dfs(0).class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
cache = {}
def dfs(score):
if score >= k:
return 1 if score <= n else 0
if score in cache:
return cache[score]
prob = 0
for i in range(1, maxPts + 1):
prob += dfs(score + i)
cache[score] = prob / maxPts
return cache[score]
return dfs(0)Where is the threshold score, is the maximum points per draw and is the upper bound on score.
The basic top-down approach sums over maxPts values at each state, leading to O(k * maxPts) time. We can optimize by noticing that consecutive states have overlapping sums.
The probability at score s equals the sum of probabilities from s+1 to s+maxPts divided by maxPts. The probability at s can be expressed in terms of s+1's probability using a sliding window technique: dp[s] = dp[s+1] minus the difference caused by the window shifting.
The boundary at k-1 needs special handling since it is the last score where we still draw cards.
score is k-1, directly compute probability based on how many outcomes land within n.score > n, return 0. If score >= k and score <= n, return 1.dp[score] = dp[score+1] minus (dp[score+1+maxPts] - dp[score+1]) / maxPts.dp[0].class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
cache = {}
def dfs(score):
if score == k - 1:
return min(n - k + 1, maxPts) / maxPts
if score > n:
return 0
if score >= k:
return 1.0
if score in cache:
return cache[score]
cache[score] = dfs(score + 1)
cache[score] -= (dfs(score + 1 + maxPts) - dfs(score + 1)) / maxPts
return cache[score]
return dfs(0)Where is the threshold score, is the maximum points per draw and is the upper bound on score.
Instead of computing probabilities of success from each score, we compute the probability of reaching each score. dp[score] represents the probability of landing on exactly that score at some point during the game.
We start at score 0 with probability 1. For each score from 0 to k-1 (the scores where we keep drawing), we distribute probability to scores reachable by drawing 1 to maxPts. Each draw has probability 1/maxPts.
The answer is the sum of dp[score] for all scores from k to n (valid ending scores).
dp array of size n+1, set dp[0] = 1.1 to n:draw value from 1 to maxPts:score - draw is in range [0, k-1], add dp[score - draw] / maxPts to dp[score].dp[k] through dp[n] and return the result.class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
dp = [0] * (n + 1)
dp[0] = 1.0
for score in range(1, n + 1):
for draw in range(1, maxPts + 1):
if score - draw >= 0 and score - draw < k:
dp[score] += dp[score - draw] / maxPts
return sum(dp[k:n + 1])Where is the threshold score, is the maximum points per draw and is the upper bound on score.
We can optimize the bottom-up approach using a sliding window. Notice that dp[i] depends on the sum of dp values from dp[i-maxPts] to dp[i-1], but only those in the range [0, k-1]. Instead of recomputing this sum each time, we maintain a running window sum.
Working backwards from k-1 to 0, we compute dp[i] as windowSum / maxPts. The window initially covers scores from k to k+maxPts-1. For scores >= k and <= n, the probability of success is 1 (we already stopped and the score is valid). As we move the window, we add the newly computed dp value and remove the value that slides out.
k is 0, return 1.0 (already done, score 0 is valid).windowSum as the count of scores from k to k+maxPts-1 that are <= n.k-1 down to 0:dp[i] = windowSum / maxPts.windowSum: add dp[i] and subtract the value sliding out of the window (if it exists and is <= n).dp[0].class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
if k == 0:
return 1.0
windowSum = 0
for i in range(k, k + maxPts):
windowSum += 1 if i <= n else 0
dp = {}
for i in range(k - 1, -1, -1):
dp[i] = windowSum / maxPts
remove = 0
if i + maxPts <= n:
remove = dp.get(i + maxPts, 1)
windowSum += dp[i] - remove
return dp[0]Where is the threshold score, is the maximum points per draw and is the upper bound on score.
When k equals 0, Alice never draws any cards and stays at score 0, which is always less than or equal to n. The probability is exactly 1.0 in this case. Failing to handle this edge case leads to division by zero or incorrect recursion that never terminates.
When the score is at least k, Alice stops drawing. The probability of success is 1 if the score is also at most n, and 0 otherwise. Some solutions incorrectly return 1 for all scores >= k, forgetting that scores above n are losing outcomes.
The sliding window optimization maintains a running sum of probabilities. Accumulated floating point errors from many additions and subtractions can cause the final probability to slightly exceed 1 or become negative. While typically not causing wrong answers on test cases, this can be addressed by clamping results to [0, 1].