LeetCode 837. 新21点 | Python
837. 新21点
题目来源:力扣(LeetCode)https://leetcode-cn.com/problems/new-21-game
题目
爱丽丝参与一个大致基于纸牌游戏 “21点” 规则的游戏,描述如下:
爱丽丝以 0 分开始,并在她的得分少于 K 分时抽取数字。 抽取时,她从 [1, W] 的范围中随机获得一个整数作为分数进行累计,其中 W 是整数。 每次抽取都是独立的,其结果具有相同的概率。
当爱丽丝获得不少于 K 分时,她就停止抽取数字。 爱丽丝的分数不超过 N 的概率是多少?
示例 1:
输入:N = 10, K = 1, W = 10
输出:1.00000
说明:爱丽丝得到一张卡,然后停止。
示例 2:
输入:N = 6, K = 1, W = 10
输出:0.60000
说明:爱丽丝得到一张卡,然后停止。
在 W = 10 的 6 种可能下,她的得分不超过 N = 6 分。
示例 3:
输入:N = 21, K = 17, W = 10
输出:0.73278
提示:
- 0 <= K <= N <= 10000
- 1 <= W <= 10000
- 如果答案与正确答案的误差不超过 10^-5,则该答案将被视为正确答案通过。
- 此问题的判断限制时间已经减少。
解题思路
思路:动态规划
题目中,提供了三个变量。N, K, W,这里先简单解释下三者分别是什么?
N:这里相当于一个界限,题目中要求的就是最终抽取数字和与 N 的比较判定输赢
K:这里表示一个可继续抽取数字的条件
W:数字面值,也就是抽取的数字,是在 1 ~ W 的范围内
现在先看示例 1:
N = 10, K = 1, W = 10
因为爱丽丝是以 0 开局的,此时 0 < K = 1,那么她现在可以继续抽数字,而数字面值范围在 [1, 10]。要求抽取数字和小于 N(N=10)的概率?这里能比较明显的看出来,概率是 1。因为面值在 [1, 10],无论抽取那个数字,都会大于 K,符合不在抽取的条件,而且最终的和也会落在 [1, 10] 之间,这里的结果肯定小于等于 N。所以概率是 1。
再看示例 2:
N = 6, K = 1, W = 10
这里改动的是 N 值。抽取的情形也如示例 1,在面值 [1, 10] 的范围内抽取数字,无论抽到是哪个数字都不能再次抽取,因为和大于 K,最终的和同样落在 [1, 10] 之间。但是这里的 N 值已经变为 6,这里抽取最终数字和只有落在 [1, 6] 这部分才符合不超过 N,这部分占总体部分 60%,所以概率为 0.6。
而示例 3,则比较复杂。也是我们主要需要分析的一种情况。
我们可以看到,前面例子 1, 2 中,爱丽丝以 0 开始抽取数字,之后与抽取的数字进行累加,然后跟 K 跟 W 比较,去确定是否可再次抽取,以及是否不超过 N?
其实这里可以看到,爱丽丝能够赢的概率其实是跟下一轮开始前的分数有关的。
现在令 dp[i]
表示从分数为 i 的情况下开始进行抽取数字能够赢的概率,那么最终要求的就是 dp[0]
的结果。
现在要求出状态转移方程,先看题目中所给的一些条件。
当分数超过 K 时,这个时候停止抽数字进行结算,当分数不超过 N 时则判定为胜利,否则失败。
现在,先看下最后一次能够抽取的最大分数。因为超过 K,不能够进行再次抽取。那么想要再次进行抽取,此时允许的最大分数即是 K - 1。那么再次抽取中可抽取最大的数字是 W,所以最大分数即是 K - 1 + W。
也就是说当 $K \leq i \leq min(N, K+W-1)$ 时,这个时候 dp[i]=1
,而 $i > min(N,K+W-1)$ 时,dp[i]=0
。
那么现在要求当 $0\leq i < K$ 时 dp[i] 的值,应该如何求?
其实当 $0\leq i < K$ 时,再进行抽取的时候,此时的概率是由后面抽取数字后累计分数是否超过 N 的概率总和。而前面题目说了,在 [1, W] 数字之间进行抽取的概率是相等的。那么状态转移方程则如下:
$$
dp[i] = \frac{dp[i+1]+dp[i+2]+...+dp[i+W]}{W}
$$
虽然状态转移方程已经求出来了,但是会发现将转移方程代入代码中会超时。下面进行优化:
在这里,我们先看 dp[i+1] 是怎样的?根据前面的状态转移方程:
$$
dp[i+1] = \frac{dp[i+2]+dp[i+3]+...+dp[i+W+1]}{W}
$$
不过这个时候 $0 \leq i < K-1$。
可以看到,其实 dp[i] 跟 dp[i+1] 中间有一大部分是相同的,那么 dp[i] 的值,可由 dp[i+1] 得到:
$$
dp[i]-dp[i+1] =\frac{dp[i+1]-dp[i+W+1]}{W}
$$
此时:
$$
dp[i] = dp[i+1] - \frac{dp[i+W+1]- dp[i+1]}{W}
$$
在这里,$i = K - 1$,不适用于上面的公式,那么将其代入最开始的转移方程中:
$$
dp[K-1] = \frac{dp[K]+dp[K+1]+dp[K+W-1]}{W}
$$
我们前面有个 $dp[i]=1$ 的条件,即是当 i 的取值范围在 $[K, min(N, K+W-1)]$。
此时,i 为 K-1 时再次抽取数字有可能赢的部分就落在 $min(N, K+W-1) - K + 1$ 次抽取数字上,那么结果为:
$$
dp[K-1]=\frac{min(N, K+W-1)-K+1}{W}=\frac{min(N-K+1, W)}{W}
$$
而其他的值则有新的转移方程进行求得。
具体的代码如下。
代码实现
class Solution:
def new21Game(self, N: int, K: int, W: int) -> float:
dp = [0.0] * (K+W)
# 先对概率为 1.0 的情况进行处理
# 就是 i 落在 [K, min(N, K+W-1)] 这部分
for i in range(K, min(N, K+W-1) + 1):
dp[i] = 1.0
# 这里先计算 K - 1 的情况
# 将推导的结果公式放在这里
dp[K-1] = float(min(N-K+1, W) / W)
# 这里开始从 K-2 计算到 dp[0] 的值
for i in range(K-2, -1, -1):
dp[i] = dp[i+1] - (dp[i+W+1]-dp[i+1]) / W
return dp[0]
实现结果
总结
- 题目中使用的思路是动态规划,这里主要的难点在于推导状态转移方程。
- 根据题意,推导的方向是由后面的情况往前进行推导。先处理最后一次抽取数字的情况,然后再往类推。
- 游戏是否能赢,跟下一次开始抽取前的分数是关联的。当前抽取能赢的概率其实是由后面抽取数字后累计分数是否超过 N 的概率总和。根据这个就能够求得状态转移方程。
- 因为最初的转移方程会超时,那么将其进行优化。根据相邻差分,简化状态转移方程。
如果觉得文章写得还可以,欢迎关注。公众号《书所集录》同步更新,同样欢迎关注。