A clear explanation of the New 21 Game problem using probability dynamic programming and a sliding window sum.
Problem Restatement
Alice starts with 0 points.
While her score is less than k, she keeps drawing a random number from 1 to maxPts.
Each draw is independent, and every number from 1 to maxPts has the same probability.
Alice stops drawing as soon as her score is at least k.
Return the probability that Alice ends with n or fewer points.
Answers within 10^-5 of the correct answer are accepted.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integers n, k, and maxPts |
| Output | Probability that Alice ends with score <= n |
| Draw range | Each draw gives a number from 1 to maxPts |
| Stop rule | Alice stops once score is at least k |
| Accepted error | 10^-5 |
Function shape:
class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
...Examples
Example 1:
n = 10
k = 1
maxPts = 10Alice draws exactly once, because after the first draw her score is at least 1.
The draw is between 1 and 10.
Every possible final score is at most 10.
So the answer is:
1.0Example 2:
n = 6
k = 1
maxPts = 10Alice draws exactly once.
The final score is one of:
1, 2, 3, 4, 5, 6, 7, 8, 9, 10Only 1 through 6 are valid.
So the probability is:
6 / 10 = 0.6Example 3:
n = 21
k = 17
maxPts = 10The answer is approximately:
0.73278First Thought: Recursive Probability
Let:
dp[x]mean the probability of eventually ending with score at most n, starting from score x.
If x >= k, Alice stops immediately.
So:
dp[x] = 1, if x <= n
dp[x] = 0, if x > nIf x < k, Alice draws a number from 1 to maxPts.
So:
dp[x] = (dp[x + 1] + dp[x + 2] + ... + dp[x + maxPts]) / maxPtsThis is correct, but computing every sum directly is too slow.
Problem With Direct DP
For every score x, direct DP may sum maxPts future states.
That gives:
O(k * maxPts)This can be too large.
We need to reuse the sum of nearby DP values.
Key Insight
The recurrence uses a moving window:
dp[x] = average of dp[x + 1] through dp[x + maxPts]When moving from one x to the next, most of the window stays the same.
So we can maintain a sliding sum instead of recomputing the whole sum.
There is also a useful forward version.
Let:
dp[i]be the probability that Alice reaches exactly score i.
Initially:
dp[0] = 1A score i can be reached from previous scores:
i - 1, i - 2, ..., i - maxPtsbut only previous scores less than k can draw again.
So we maintain:
window_sum = sum of probabilities of active previous scoresThen:
dp[i] = window_sum / maxPtsAlgorithm
Handle the easy winning case first.
If:
k == 0Alice never draws, so her score is 0, and the answer is 1.
Also, the maximum possible final score is:
k - 1 + maxPtsbecause Alice can draw only while her score is at most k - 1, then she may draw up to maxPts.
If:
n >= k - 1 + maxPtsthen every possible final score is at most n, so the answer is 1.
Otherwise:
- Create
dpwheredp[i]is the probability of reaching scorei. - Set
dp[0] = 1. - Set
window_sum = 1. - For each score
ifrom1ton:- Set
dp[i] = window_sum / maxPts. - If
i < k, adddp[i]towindow_sum. - If
i - maxPts >= 0, removedp[i - maxPts]fromwindow_sum. - If
i >= k, adddp[i]to the answer.
- Set
Walkthrough
Use:
n = 6
k = 1
maxPts = 10Alice starts at 0.
Since 0 < k, she draws once.
Each result from 1 to 10 has probability:
1 / 10Valid final scores are:
1, 2, 3, 4, 5, 6There are 6 valid scores, each with probability 0.1.
So:
answer = 6 * 0.1 = 0.6The DP computes the same value.
For i = 1 through 6, each dp[i] receives:
dp[0] / 10 = 0.1Since all these scores are at least k, they are final scores.
The answer becomes:
0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.6Correctness
The value dp[i] represents the probability that Alice reaches score i.
The initial state is correct because Alice starts at score 0, so dp[0] = 1.
A score i can be reached only from a previous score j where:
1 <= i - j <= maxPtsand Alice must still be drawing at j, which requires:
j < kEach active previous score contributes:
dp[j] / maxPtsto dp[i].
The sliding window stores exactly the sum of these active previous probabilities. Therefore, assigning:
dp[i] = window_sum / maxPtscomputes the correct probability of reaching score i.
When i < k, Alice may draw again from score i, so dp[i] is added to the active window.
When i >= k, Alice stops at score i, so dp[i] is added to the answer instead.
The window also removes scores more than maxPts behind, because they can no longer reach the current or future score with one draw.
Thus every final score from k through n is counted once, and no non-final or too-large score is counted. The returned sum is exactly the probability that Alice ends with score at most n.
Complexity
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | We compute each dp[i] once |
| Space | O(n) | We store the probability array |
Implementation
class Solution:
def new21Game(self, n: int, k: int, maxPts: int) -> float:
if k == 0 or n >= k - 1 + maxPts:
return 1.0
dp = [0.0] * (n + 1)
dp[0] = 1.0
window_sum = 1.0
answer = 0.0
for score in range(1, n + 1):
dp[score] = window_sum / maxPts
if score < k:
window_sum += dp[score]
else:
answer += dp[score]
if score - maxPts >= 0:
window_sum -= dp[score - maxPts]
return answerCode Explanation
The early return handles cases where Alice always wins:
if k == 0 or n >= k - 1 + maxPts:
return 1.0dp[score] stores the probability of reaching exactly score:
dp = [0.0] * (n + 1)
dp[0] = 1.0The sliding sum starts with score 0, because Alice can draw from score 0:
window_sum = 1.0For each score:
dp[score] = window_sum / maxPtsThis distributes probability from all active previous scores.
If score < k, Alice can still draw from this score:
window_sum += dp[score]If score >= k, this is a stopping score:
answer += dp[score]Finally, remove scores that are too far behind:
if score - maxPts >= 0:
window_sum -= dp[score - maxPts]Testing
def almost_equal(a: float, b: float, eps: float = 1e-5) -> bool:
return abs(a - b) <= eps
def run_tests():
s = Solution()
assert almost_equal(s.new21Game(10, 1, 10), 1.0)
assert almost_equal(s.new21Game(6, 1, 10), 0.6)
assert almost_equal(s.new21Game(21, 17, 10), 0.73278)
assert almost_equal(s.new21Game(0, 0, 1), 1.0)
assert almost_equal(s.new21Game(1, 0, 1), 1.0)
assert almost_equal(s.new21Game(5, 2, 3), 1.0)
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
(10, 1, 10) | All one-draw outcomes are valid |
(6, 1, 10) | Only six of ten one-draw outcomes are valid |
(21, 17, 10) | Standard larger probability case |
k = 0 | Alice never draws |
| Small guaranteed win | Confirms early return |
Maximum final score within n | Confirms always-win case |