AlgoMaster Logo

New 21 Game

Last Updated: April 2, 2026

medium
4 min read

Understanding the Problem

Let's break this down carefully because the problem statement can be confusing at first.

Alice draws cards one at a time. Each card gives her a random number of points between 1 and maxPts, with equal probability (so each value has probability 1/maxPts). She keeps drawing as long as her total is strictly less than k. Once her total reaches k or more, she stops.

We need to find the probability that her final total is at most n.

Here is the key observation: Alice's final score can range from k (she was at k-1 and drew 1) up to k - 1 + maxPts (she was at k-1 and drew maxPts). Any score in the range [k, k - 1 + maxPts] is possible. We need the probability that this final score is at most n.

If k == 0, Alice never draws at all, so she stays at 0 points, and since 0 <= n, the probability is 1. If n >= k - 1 + maxPts, every possible ending score is at most n, so the probability is also 1.

This is fundamentally a probability DP problem where we build up the probability of reaching each score, then sum up the probabilities for all valid final scores.

Key Constraints:

  • 0 <= k <= n <= 10^4 --> Both k and n are at most 10,000. This means a DP table of size up to ~20,000 is feasible. An O(n * maxPts) approach would be 10^8 in the worst case, which is too slow.
  • 1 <= maxPts <= 10^4 --> The number of possible draws per turn is up to 10,000. We need a sliding window optimization to avoid an O(maxPts) inner loop at each step.

Approach 1: Recursive DP (Brute Force)

Intuition

The most natural way to think about this: define dp[i] as the probability that Alice ends up with exactly i points at some point during the game. Alice starts at 0, so dp[0] = 1. From any score j < k, Alice draws a card valued 1 through maxPts, each with probability 1/maxPts, and moves to score j + card.

So the probability of reaching score i is:

dp[i] = sum of dp[j] / maxPts for all valid j where j = i - maxPts to j = i - 1 and j >= 0 and j < k.

The condition j < k matters because if Alice is at score j >= k, she has already stopped and won't draw more cards.

The answer is the sum of dp[i] for all i from k to n.

Algorithm

  1. If k == 0 or n >= k + maxPts - 1, return 1.0 (all outcomes are valid).
  2. Create a dp array of size n + 1. Set dp[0] = 1.0.
  3. For each score i from 1 to n, compute dp[i] = sum(dp[j] for j in range(max(0, i - maxPts), min(i, k))) / maxPts.
  4. Return the sum of dp[i] for i from k to n.

Example Walkthrough

1Initialize: dp[0] = 1.0 (Alice starts at score 0)
0
1
start
1
0
2
0
3
0
4
0
5
0
6
0
1/5

Code

This approach works but the inner loop recomputes the same sum from scratch each time. Since the window shifts by one element per step, we can maintain a running sum instead.

Approach 2: DP with Sliding Window (Optimal)

Intuition

The recurrence for dp[i] involves summing a contiguous window of previous dp values:

dp[i] = (dp[i - maxPts] + dp[i - maxPts + 1] + ... + dp[i - 1]) / maxPts

But only including terms where the index is in the range [0, k-1] (since Alice stops drawing at score k or above).

When we move from computing dp[i] to dp[i+1], the window shifts by one position to the right. We add dp[i] to the window (if i < k) and remove dp[i - maxPts] from the window (if it falls out of range). This is a textbook sliding window optimization that brings the time from O(n * maxPts) down to O(n).

The crucial detail is that we only add dp[i] to the window when i < k. Once Alice reaches score k or above, she stops drawing, so those scores don't contribute to reaching higher scores.

Algorithm

  1. If k == 0 or n >= k + maxPts - 1, return 1.0.
  2. Create dp array of size n + 1. Set dp[0] = 1.0.
  3. Initialize windowSum = 1.0 (starts with just dp[0]).
  4. For each score i from 1 to n:
    • Set dp[i] = windowSum / maxPts.
    • If i < k, add dp[i] to windowSum (this score can contribute to future scores).
    • If i - maxPts >= 0, subtract dp[i - maxPts] from windowSum (this score falls out of the window).
  5. Sum dp[k] through dp[n] for the answer.

Example Walkthrough

1Initialize: dp[0]=1.0, windowSum=1.0
0
1
windowSum=1.0
1
0
2
0
3
0
4
0
5
0
6
0
1/6

Code