Skip to content

837. New 21 Game

DifficultyMedium

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 <= 104
  • 1 <= maxPts <= 104

Solutions

Thinking

We add a uniform integer in \([1,\textit{maxPts}]\) until the total reaches \(k\), and want the probability it is still \(\le n\). Expanding every draw is impossible for \(k,n\le 10^4\); memoize on the current total.

\(\textit{dfs}(i)\) is the success probability from total \(i\). For \(i\ge k\) we stop and test \(i\le n\); \(i=k-1\) has a closed form. Other states use a difference of neighbors so each transition is \(O(1)\) instead of \(O(\textit{maxPts})\).

We design a function \(\textit{dfs}(i)\), which represents the probability that when the current score is \(i\), the final score does not exceed \(n\) when we stop drawing numbers. The answer is \(\textit{dfs}(0)\).

The calculation method of function \(\textit{dfs}(i)\) is as follows:

  • If \(i \ge k\), then we stop drawing numbers. If \(i \le n\), return \(1\), otherwise return \(0\);
  • Otherwise, we can draw the next number \(j\) in the range \([1,..\textit{maxPts}]\), then \(\textit{dfs}(i) = \frac{1}{\textit{maxPts}} \sum_{j=1}^{\textit{maxPts}} \textit{dfs}(i+j)\).

Here we can use memoized search to accelerate the calculation.

The time complexity of the above method is \(O(k \textit{maxPts})\), which will exceed the time limit, so we need to optimize it.

When \(i \lt k\), the following equation holds:

\[ \begin{aligned} \textit{dfs}(i) &= (\textit{dfs}(i + 1) + \textit{dfs}(i + 2) + \cdots + \textit{dfs}(i + \textit{maxPts})) / \textit{maxPts} & (1) \end{aligned} \]

When \(i \lt k - 1\), the following equation holds:

\[ \begin{aligned} \textit{dfs}(i+1) &= (\textit{dfs}(i + 2) + \textit{dfs}(i + 3) + \cdots + \textit{dfs}(i + \textit{maxPts} + 1)) / \textit{maxPts} & (2) \end{aligned} \]

Therefore, when \(i \lt k-1\), we subtract equation \((2)\) from equation \((1)\) to get:

\[ \begin{aligned} \textit{dfs}(i) - \textit{dfs}(i+1) &= (\textit{dfs}(i + 1) - \textit{dfs}(i + \textit{maxPts} + 1)) / \textit{maxPts} \end{aligned} \]

That is:

\[ \begin{aligned} \textit{dfs}(i) &= \textit{dfs}(i + 1) + (\textit{dfs}(i + 1) - \textit{dfs}(i + \textit{maxPts} + 1)) / \textit{maxPts} \end{aligned} \]

If \(i=k-1\), we have:

\[ \begin{aligned} \textit{dfs}(i) &= \textit{dfs}(k - 1) = (\textit{dfs}(k) + \textit{dfs}(k + 1) + \cdots + \textit{dfs}(k + \textit{maxPts} - 1)) / \textit{maxPts} & (3) \end{aligned} \]

We assume there are \(i\) numbers not exceeding \(n\), then \(k+i-1 \leq n\), and since \(i\leq \textit{maxPts}\), we have \(i \leq \min(n-k+1, \textit{maxPts})\), so equation \((3)\) can be written as:

\[ \begin{aligned} \textit{dfs}(k-1) &= \min(n-k+1, \textit{maxPts}) / \textit{maxPts} \end{aligned} \]

In summary, we have the following state transition equation:

\[ \begin{aligned} \textit{dfs}(i) &= \begin{cases} 1, & i \geq k, i \leq n \\ 0, & i \geq k, i \gt n \\ \min(n-k+1, \textit{maxPts}) / \textit{maxPts}, & i = k - 1 \\ \textit{dfs}(i + 1) + (\textit{dfs}(i + 1) - \textit{dfs}(i + \textit{maxPts} + 1)) / \textit{maxPts}, & i < k - 1 \end{cases} \end{aligned} \]

The time complexity is \(O(k + \textit{maxPts})\), and the space complexity is \(O(k + \textit{maxPts})\), where \(k\) is the maximum score.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Solution:
    def new21Game(self, n: int, k: int, maxPts: int) -> float:
        @cache
        def dfs(i: int) -> float:
            if i >= k:
                return int(i <= n)
            if i == k - 1:
                return min(n - k + 1, maxPts) / maxPts
            return dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts

        return dfs(0)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Solution {
    private double[] f;
    private int n, k, maxPts;

    public double new21Game(int n, int k, int maxPts) {
        f = new double[k];
        this.n = n;
        this.k = k;
        this.maxPts = maxPts;
        return dfs(0);
    }

    private double dfs(int i) {
        if (i >= k) {
            return i <= n ? 1 : 0;
        }
        if (i == k - 1) {
            return Math.min(n - k + 1, maxPts) * 1.0 / maxPts;
        }
        if (f[i] != 0) {
            return f[i];
        }
        return f[i] = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts;
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
    double new21Game(int n, int k, int maxPts) {
        vector<double> f(k);
        auto dfs = [&](this auto&& dfs, int i) -> double {
            if (i >= k) {
                return i <= n ? 1 : 0;
            }
            if (i == k - 1) {
                return min(n - k + 1, maxPts) * 1.0 / maxPts;
            }
            if (f[i]) {
                return f[i];
            }
            return f[i] = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts;
        };
        return dfs(0);
    }
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func new21Game(n int, k int, maxPts int) float64 {
    f := make([]float64, k)
    var dfs func(int) float64
    dfs = func(i int) float64 {
        if i >= k {
            if i <= n {
                return 1
            }
            return 0
        }
        if i == k-1 {
            return float64(min(n-k+1, maxPts)) / float64(maxPts)
        }
        if f[i] > 0 {
            return f[i]
        }
        f[i] = dfs(i+1) + (dfs(i+1)-dfs(i+maxPts+1))/float64(maxPts)
        return f[i]
    }
    return dfs(0)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function new21Game(n: number, k: number, maxPts: number): number {
    const f: number[] = Array(k).fill(0);
    const dfs = (i: number): number => {
        if (i >= k) {
            return i <= n ? 1 : 0;
        }
        if (i === k - 1) {
            return Math.min(n - k + 1, maxPts) / maxPts;
        }
        if (f[i] !== 0) {
            return f[i];
        }
        return (f[i] = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts);
    };
    return dfs(0);
}

Solution 2: Dynamic Programming

Thinking

Solution 1 already fills the probabilities from the high scores downward. Each \(f[i]\) comes from \(f[i+1]\) and the window end \(f[i+\textit{maxPts}+1]\).

Terminals in \([k,\min(n,k+\textit{maxPts}))\) are \(1\). The answer is \(f[0]\). Time stays linear.

Solution 1 already fills this table from the high scores downward. The transitions below are the same ones.

Define \(f[i]\) to represent the probability that when the current score is \(i\), the final score does not exceed \(n\) when we stop drawing numbers. The answer is \(f[0]\).

When \(k \leq i \leq \min(n, k + \textit{maxPts} - 1)\), we have \(f[i] = 1\).

When \(i = k - 1\), we have \(f[i] = \min(n-k+1, \textit{maxPts}) / \textit{maxPts}\).

When \(i \lt k - 1\), we have \(f[i] = f[i + 1] + (f[i + 1] - f[i + \textit{maxPts} + 1]) / \textit{maxPts}\).

The time complexity is \(O(k + \textit{maxPts})\), and the space complexity is \(O(k + \textit{maxPts})\), where \(k\) is the maximum score.

1
2
3
4
5
6
7
8
9
class Solution:
    def new21Game(self, n: int, k: int, maxPts: int) -> float:
        f = [0] * (k + maxPts)
        for i in range(k, min(n + 1, k + maxPts)):
            f[i] = 1
        f[k - 1] = min(n - k + 1, maxPts) / maxPts
        for i in range(k - 2, -1, -1):
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts
        return f[0]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
    public double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double[] f = new double[k + maxPts];
        for (int i = k; i < Math.min(n + 1, k + maxPts); ++i) {
            f[i] = 1;
        }
        f[k - 1] = Math.min(n - k + 1, maxPts) * 1.0 / maxPts;
        for (int i = k - 2; i >= 0; --i) {
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
        }
        return f[0];
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
public:
    double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double f[k + maxPts];
        memset(f, 0, sizeof(f));
        for (int i = k; i < min(n + 1, k + maxPts); ++i) {
            f[i] = 1;
        }
        f[k - 1] = min(n - k + 1, maxPts) * 1.0 / maxPts;
        for (int i = k - 2; i >= 0; --i) {
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
        }
        return f[0];
    }
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func new21Game(n int, k int, maxPts int) float64 {
    if k == 0 {
        return 1
    }
    f := make([]float64, k+maxPts)
    for i := k; i < min(n+1, k+maxPts); i++ {
        f[i] = 1
    }
    f[k-1] = float64(min(n-k+1, maxPts)) / float64(maxPts)
    for i := k - 2; i >= 0; i-- {
        f[i] = f[i+1] + (f[i+1]-f[i+maxPts+1])/float64(maxPts)
    }
    return f[0]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function new21Game(n: number, k: number, maxPts: number): number {
    if (k === 0) {
        return 1;
    }
    const f: number[] = Array(k + maxPts).fill(0);
    for (let i = k; i < Math.min(n + 1, k + maxPts); ++i) {
        f[i] = 1;
    }
    f[k - 1] = Math.min(n - k + 1, maxPts) / maxPts;
    for (let i = k - 2; i >= 0; --i) {
        f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
    }
    return f[0];
}

Solution 3: Dynamic Programming

Thinking

We add a uniform integer in \([1,\textit{maxPts}]\) until the total reaches \(k\), and want the probability it is still \(\le n\). \(k\) can be \(10^4\), so expanding every draw is impossible.

After the one-step difference, every state except the closed form at \(k-1\) reads a larger score first. The chain from \(0\) to \(k\) has length \(k\) and overflows the stack.

Larger scores are known if we walk downward. Let \(f[i]\) be the success probability from total \(i\), write the terminal window as \(1\), and fill from \(k-2\) down to \(0\).

Let \(f[i]\) be the probability that a current score of \(i\) ends at most \(n\). The answer is \(f[0]\).

The value of \(f[i]\) is as follows:

  • If \(i \ge k\), drawing stops. The value is \(1\) when \(i \le n\), and \(0\) otherwise;
  • Otherwise the next draw \(j\) lies in \([1,..\textit{maxPts}]\), so \(f[i] = \frac{1}{\textit{maxPts}} \sum_{j=1}^{\textit{maxPts}} f[i+j]\).

Every state depends only on a larger score, so fill from \(k - 2\) down to \(0\). When \(k = 0\) no draw has started, and the answer is \(1\).

The time complexity of the above method is \(O(k \textit{maxPts})\), which will exceed the time limit, so we need to optimize it.

When \(i \lt k\), the following equation holds:

\[ \begin{aligned} f[i] &= (f[i + 1] + f[i + 2] + \cdots + f[i + \textit{maxPts}]) / \textit{maxPts} & (1) \end{aligned} \]

When \(i \lt k - 1\), the following equation holds:

\[ \begin{aligned} f[i+1] &= (f[i + 2] + f[i + 3] + \cdots + f[i + \textit{maxPts} + 1]) / \textit{maxPts} & (2) \end{aligned} \]

Therefore, when \(i \lt k-1\), we subtract equation \((2)\) from equation \((1)\) to get:

\[ \begin{aligned} f[i] - f[i+1] &= (f[i + 1] - f[i + \textit{maxPts} + 1]) / \textit{maxPts} \end{aligned} \]

That is:

\[ \begin{aligned} f[i] &= f[i + 1] + (f[i + 1] - f[i + \textit{maxPts} + 1]) / \textit{maxPts} \end{aligned} \]

If \(i=k-1\), we have:

\[ \begin{aligned} f[i] &= f[k - 1] = (f[k] + f[k + 1] + \cdots + f[k + \textit{maxPts} - 1]) / \textit{maxPts} & (3) \end{aligned} \]

We assume there are \(i\) numbers not exceeding \(n\), then \(k+i-1 \leq n\), and since \(i\leq \textit{maxPts}\), we have \(i \leq \min(n-k+1, \textit{maxPts})\), so equation \((3)\) can be written as:

\[ \begin{aligned} f[k-1] &= \min(n-k+1, \textit{maxPts}) / \textit{maxPts} \end{aligned} \]

In summary, we have the following state transition equation:

\[ \begin{aligned} f[i] &= \begin{cases} 1, & i \geq k, i \leq n \\ 0, & i \geq k, i \gt n \\ \min(n-k+1, \textit{maxPts}) / \textit{maxPts}, & i = k - 1 \\ f[i + 1] + (f[i + 1] - f[i + \textit{maxPts} + 1]) / \textit{maxPts}, & i < k - 1 \end{cases} \end{aligned} \]

The time complexity is \(O(k + \textit{maxPts})\), and the space complexity is \(O(k + \textit{maxPts})\), where \(k\) is the maximum score.

1
2
3
4
5
6
7
8
9
class Solution:
    def new21Game(self, n: int, k: int, maxPts: int) -> float:
        f = [0] * (k + maxPts)
        for i in range(k, min(n + 1, k + maxPts)):
            f[i] = 1
        f[k - 1] = min(n - k + 1, maxPts) / maxPts
        for i in range(k - 2, -1, -1):
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts
        return f[0]
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Solution {
    public double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double[] f = new double[k + maxPts];
        for (int i = k; i < Math.min(n + 1, k + maxPts); ++i) {
            f[i] = 1;
        }
        f[k - 1] = Math.min(n - k + 1, maxPts) * 1.0 / maxPts;
        for (int i = k - 2; i >= 0; --i) {
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
        }
        return f[0];
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Solution {
public:
    double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double f[k + maxPts];
        memset(f, 0, sizeof(f));
        for (int i = k; i < min(n + 1, k + maxPts); ++i) {
            f[i] = 1;
        }
        f[k - 1] = min(n - k + 1, maxPts) * 1.0 / maxPts;
        for (int i = k - 2; i >= 0; --i) {
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
        }
        return f[0];
    }
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func new21Game(n int, k int, maxPts int) float64 {
    if k == 0 {
        return 1
    }
    f := make([]float64, k+maxPts)
    for i := k; i < min(n+1, k+maxPts); i++ {
        f[i] = 1
    }
    f[k-1] = float64(min(n-k+1, maxPts)) / float64(maxPts)
    for i := k - 2; i >= 0; i-- {
        f[i] = f[i+1] + (f[i+1]-f[i+maxPts+1])/float64(maxPts)
    }
    return f[0]
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function new21Game(n: number, k: number, maxPts: number): number {
    if (k === 0) {
        return 1;
    }
    const f: number[] = Array(k + maxPts).fill(0);
    for (let i = k; i < Math.min(n + 1, k + maxPts); ++i) {
        f[i] = 1;
    }
    f[k - 1] = Math.min(n - k + 1, maxPts) / maxPts;
    for (let i = k - 2; i >= 0; --i) {
        f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
    }
    return f[0];
}

Comments