837. New 21 Game - Explanation

Problem Link

Description

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.00000

Explanation: Alice gets a single card, then stops.

Example 2:

Input: n = 6, k = 1, maxPts = 10

Output: 0.60000

Explanation: 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.73278

Constraints:

  • 0 <= k <= n <= 10,000
  • 1 <= maxPts <= 10,000


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Dynamic Programming (Memoization) - Top-down recursion with caching to avoid redundant calculations
  • Dynamic Programming (Tabulation) - Bottom-up iterative approach for building solutions
  • Probability Basics - Understanding how probabilities combine and average
  • Sliding Window Technique - Optimizing sum calculations over a moving range of values

1. Dynamic Programming (Top-Down)

Intuition

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.

Algorithm

  1. Define dfs(score) that returns the probability of ending with a final score <= n.
  2. Base case: if score >= k, return 1 if score <= n, else return 0.
  3. For score < k, sum up dfs(score + i) for i from 1 to maxPts, then divide by maxPts.
  4. Use memoization to cache computed values.
  5. Return 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)

Time & Space Complexity

  • Time complexity: O(km)O(k * m)
  • Space complexity: O(k)O(k)

Where kk is the threshold score, mm is the maximum points per draw and nn is the upper bound on score.


2. Dynamic Programming (Top-Down Optimized)

Intuition

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.

Algorithm

  1. Handle edge case: if score is k-1, directly compute probability based on how many outcomes land within n.
  2. If score > n, return 0. If score >= k and score <= n, return 1.
  3. Use the recurrence: dp[score] = dp[score+1] minus (dp[score+1+maxPts] - dp[score+1]) / maxPts.
  4. Memoize results and return 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)

Time & Space Complexity

  • Time complexity: O(k+m)O(k + m)
  • Space complexity: O(n)O(n)

Where kk is the threshold score, mm is the maximum points per draw and nn is the upper bound on score.


3. Dynamic Programming (Bottom-Up)

Intuition

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).

Algorithm

  1. Create dp array of size n+1, set dp[0] = 1.
  2. For each score from 1 to n:
    • For each draw value from 1 to maxPts:
      • If score - draw is in range [0, k-1], add dp[score - draw] / maxPts to dp[score].
  3. Sum 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])

Time & Space Complexity

  • Time complexity: O(nm)O(n * m)
  • Space complexity: O(n)O(n)

Where kk is the threshold score, mm is the maximum points per draw and nn is the upper bound on score.


4. Dynamic Programming (Sliding Window)

Intuition

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.

Algorithm

  1. If k is 0, return 1.0 (already done, score 0 is valid).
  2. Initialize windowSum as the count of scores from k to k+maxPts-1 that are <= n.
  3. Iterate from k-1 down to 0:
    • Set dp[i] = windowSum / maxPts.
    • Update windowSum: add dp[i] and subtract the value sliding out of the window (if it exists and is <= n).
  4. Return 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]

Time & Space Complexity

  • Time complexity: O(k+m)O(k + m)
  • Space complexity: O(n)O(n)

Where kk is the threshold score, mm is the maximum points per draw and nn is the upper bound on score.


Common Pitfalls

Missing Edge Case When k is Zero

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.

Incorrect Probability for Terminal States

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.

Floating Point Precision in Window Sum

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].