爱丽丝参与一个大致基于纸牌游戏 “21点” 规则的游戏,描述如下:
爱丽丝以 0 分开始,并在她的得分少于 K 分时抽取数字。 抽取时,她从 [1, W] 的范围中随机获得一个整数作为分数进行累计,其中 W 是整数。 每次抽取都是独立的,其结果具有相同的概率。
当爱丽丝获得不少于 K 分时,她就停止抽取数字。 爱丽丝的分数不超过 N 的概率是多少?
提示条件:
0 <= K <= N <= 10000
1 <= W <= 10000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/new-21-game
很明显,看一眼就意识到很可能要使用动态规划。但是动归确实是我个人很长时间都没有搞明白的一种算法,今天尝试着边学边解一下吧。
先写在前面,动态规划这个算法呢,我一直属于那种看完题解能明白思路,但是自己面对一个新问题确实是一点思路都没有的那种。感觉动态规划很多时候难度都是在状态转移方程上面,代码实现反而不是很难。
参照官方解法的思路,首先取临界值梳理一下思路,就不难找到正确的状态转移方程了。最常见的21点的临界值是17点,如果没记错的话,去赌场玩21点,庄家在不够17点的时候是必须要抽下一张牌的,那么不妨就用这个最常见的情况进行边界值处理,那么每次摸牌的值是在1—13之间,若此时牌正好是16点,可以得到下面的表格:
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
当前值 | 胜利 | 胜利 | 胜利 | 胜利 | 胜利 | 爆炸 | 爆炸 | 爆炸 | 爆炸 | 爆炸 | 爆炸 | 爆炸 | 爆炸 |
表中已经表示的很明显了,也就是当摸牌摸到1—5的时候我们胜利,摸牌摸到6—K就自爆了。因此我们16点时摸牌胜利的概率是 5 13 \frac{5}{13} 135.稍微思考一下这个概率是怎么得到的,因为超过17点我们就不再摸牌了,那么不妨用下面的表格重新表示一下:
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
5 13 \frac{5}{13} 135 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
那么16点时获胜的概率 5 13 \frac{5}{13} 135是怎么得到的呢?当然是使用这样的公式得到的: p ( 17 ) + p ( 18 ) + p ( 19 ) + p ( 20 ) + p ( 21 ) + p ( 22 ) + p ( 23 ) + p ( 24 ) + p ( 25 ) + p ( 26 ) + p ( 27 ) + p ( 28 ) + p ( 29 ) 13 \frac{p(17) + p(18) + p(19) + p(20) + p(21) +p(22) + p(23) + p(24) + p(25) + p(26) +p(27) + p(28) + p(29) }{13} 13p(17)+p(18)+p(19)+p(20)+p(21)+p(22)+p(23)+p(24)+p(25)+p(26)+p(27)+p(28)+p(29) 反映在表格中就是:
5 13 = 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 ) 13 \frac{5}{13} = \frac{1 + 1 + 1 + 1 + 1 + 0+ 0+ 0+ 0+ 0+ 0+ 0+ 0) }{13} 135=131+1+1+1+1+0+0+0+0+0+0+0+0)
也就是说,16点时获胜的概率为:
p ( 16 ) = p ( 17 ) + p ( 18 ) + ⋯ + p ( 29 ) 13 p(16)=\frac{p(17) + p(18) +\cdots+ p(29)}{13} p(16)=13p(17)+p(18)+⋯+p(29)
根据题目,将这个公式用题目中的字母进行替换,就可以得到在某一个点数n情况下的概率公式:
p ( n ) = p ( n + 1 ) + p ( n + 2 ) + ⋯ + p ( n + W ) W p(n)=\frac{p(n+1) + p(n+2) +\cdots+ p(n+W)}{W} p(n)=Wp(n+1)+p(n+2)+⋯+p(n+W)
其实这就已经是我们的状态转移方程了,但是为了确保能看懂这个公式,我们向前推一步,看一看15点的情况下,如下表:
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
当前值 | 5 13 \frac{5}{13} 135 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
假设此时摸到Ace也就是1点,进入16点的情况,那么直接 1 13 × 5 13 \frac{1}{13} × \frac{5}{13} 131×135,再加上后续的概率,可以得到如下的计算公式:
p ( 15 ) = 1 13 × 5 13 + ( 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 ) 13 = 5 13 + ( 1 + 1 + 1 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 ) 13 p(15)= \frac{1}{13} × \frac{5}{13} + \frac{(1 + 1 + 1 + 1 + 1 + 0+ 0+ 0+ 0+ 0+ 0+ 0) }{13} = \frac{ \frac{5}{13} + (1 + 1 + 1 + 1 + 1 + 0+ 0+ 0+ 0+ 0+ 0+ 0) }{13} p(15)=131×135+13(1+1+1+1+1+0+0+0+0+0+0+0)=13135+(1+1+1+1+1+0+0+0+0+0+0+0)
可以看出,15点的时候确实是符合这个状态转移方程的,14点以及以前的点数也自然依次类推。
状态转移方程有了,代码实现也就水到渠成了。直接贴上个人的代码:
class Solution {
public:
double new21Game(int N, int K, int W) {
double *dp_array = new double[K + W + 1];
for (int i = K; i <= K + W; i++) {
i <= N ? dp_array[i] = 1 : dp_array[i] = 0;
}//初始化一下后面W个点数的概率值
for (int i = K - 1; i >= 0; i--) {
double total = 0;
for (int j = i + 1; j <= i + W; j++) {
total += dp_array[j];
}
dp_array[i] = total / (double)W;
}
return dp_array[0];
}
};
如果直接把这份代码提交,那么我们会惊奇的发现超出了时间限制。根据反馈的情况,在N,K和W分别为7687,6402和3302的时候,虽然能得到正确答案约为0.62165,但耗时达到了64ms,被判定为超时了。去继续看了下官方解答,确实还有很明显的优化空间。
以前面的p(15)为例,我们在计算时,一口气从p(16)加到了p(28),再除以13。但是事实上,p(16)就是p(17)加到了p(29)再除以13的结果。所以我们可以这样计算:
p ( 15 ) = p ( 16 ) + 1 13 × p ( 16 ) − 1 13 × p ( 29 ) p(15)= p(16) + \frac{1}{13} × p(16) - \frac{1}{13} × p(29) p(15)=p(16)+131×p(16)−131×p(29)
于是就得到了新的便于计算的状态转移方程:
p ( n ) = p ( n + 1 ) + p ( n + 1 ) W − p ( n + W ) W p(n)=p(n+1) + \frac{p(n+1)}{W} - \frac{p(n+W)}{W} p(n)=p(n+1)+Wp(n+1)−Wp(n+W)
再使用这个方程对代码进行优化,就可以得到下面的最终代码了:
class Solution {
public:
double new21Game(int N, int K, int W) {
double *dp_array = new double[K + W + 1];
for (int i = K; i <= K + W; i++) {
i <= N ? dp_array[i] = 1 : dp_array[i] = 0;
}//初始化一下后面W个点数的概率值
for (int i = K - 1; i >= 0; i--) {
if (i == K - 1) {
double total = 0;
for (int j = i + 1; j <= i + W; j++) {
total += dp_array[j];
}
dp_array[i] = total / (double)W;
continue;
}//注意要初始化数组中K-1里的这个值,其他的计算是基于它的基础之上
dp_array[i] = dp_array[i + 1] + dp_array[i + 1] / (double)W - dp_array[i + 1 + W] / (double)W;
}
return dp_array[0];
}
};
下面是在官方题解下面看到的一个对状态转移方程的补充回复,看完以后感觉也不太可能写的比原回答更全面更好了,而且公式比较多,写上来也很费时费力。所以直接贴出来链接:
作者:sour-e
链接:怎样得到官方题解中的状态转移方程
来源:力扣(LeetCode)