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, W], where W is an integer.  Each draw is independent and the outcomes have equal probabilities.

Alice stops drawing numbers when she gets K or more points.  What is the probability that she has N or less points?

Example 1:

Input: N = 10, K = 1, W = 10
Output: 1.00000
Explanation: Alice gets a single card, then stops.

Example 2:

Input: N = 6, K = 1, W = 10
Output: 0.60000
Explanation: Alice gets a single card, then stops.
In 6 out of W = 10 possibilities, she is at or below N = 6 points.

Example 3:

Input: N = 21, K = 17, W = 10
Output: 0.73278

Note:

  1. 0 <= K <= N <= 10000
  2. 1 <= W <= 10000
  3. Answers will be accepted as correct if they are within 10^-5 of the correct answer.
  4. The judging time limit has been reduced for this question.
 

这道题就是赌桌上经典的 21 点游戏了,想起了当年实习的游轮活动,就有21点游戏的赌桌。我当时还纳闷为啥庄家到了 17 点以后就不再要牌了,原来里面大有学问啊,因为再多拿牌,增大了爆的概率,而如果小于 17 就不拿牌的话,会增大玩家赢的概率,估计是经过精心计算,用这个阈值庄家赢的概率最大吧,想着当时庄家每拿一张牌,大家都一起在喊“爆,爆,爆。。”的情景,还是觉得非常搞笑。但当时有一位同期实习的大神,可以根据分析台面上已经出现的牌,来推出最合理的策略,因为庄家的规则是不变的,只要过了 17 就坚决不拿牌,但是大神却可以根据已出现的牌来制定自己的最优策略,经常能赢庄家。据大神称他去赌场经常都能赢上个小二百刀,给跪了有木有?!

好,来解题吧。这道题说的是有 [1, W] 范围内的牌,问我们当拿到不少于K点的时候就停止摸牌,最终点数能不超过的N点的概率。那么我们先来分析下,拿到 [1, W] 范围内的任意一张牌的概率是 1/W,因为是随机取的,所以拿到任意张牌的概率都是相等的。那么点数大于W的时候,概率怎么算呢,比如 W = 10, 我们拿到15点的概率是多少呢?这时候肯定不止拿一张牌了,那么我们分析最后一张牌,可以取1到 10,那么能拿到 15 点就有十种情况,之前共拿5点且最后一张拿10,之前共拿6点且最后一张拿9,之前拿共7点且最后一张拿8,...,之前共拿 14 点且最后一张拿1。那么拿 15 点的概率就是把这十种的概率都加起来。这道题给的假设是每次取牌都是等概率的,不管什么时候拿到 [1, 10] 内的任意张牌的概率都是十分之一,但是现实情况肯定不是这样的,已经取出了的牌,不会放回了,所以现实情况要更加复杂。不用管它,反正我们拿最后一张牌的概率都是 1/W,由于是‘且’的关系,所以是概率相乘,可以将 1/W 提取出来,那么对于拿到x点的概率就可以归纳出下面的等式:

P(x) = 1/W * (P(x-1) + P(x-2) + P(x-W))

= 1/W * sumP(x-W, x-1)

这里的x是有范围限制的,必须在 [W, K] 之间,因为小于等于W的点数概率都是 1/W,而大于等于K的时候,就不会再拿牌了。现在回过头来看看这道题要我们求什么,要求的是拿到不少于K点的时候就停止摸牌,最终点数能不超过的N点的概率,即 P(<=N | >= K)。那么现在我们就要引入强大的条件概率公式了,传说中的贝叶斯公式就是由其推导出来的:

P(A | B) = P(AB) / P(B)

意思就是在事件B发生的条件下发生事件A的概率,等于事件A和B同时发生的概率除以事件B单独发生的概率。那么带入本题的环境,就可以得到下列等式:

P(<=N | >=K) = P(<=N && >=K) / P(>=K)

就是说拿到不小于K点的前提下,还能不超过N点的概率,等于拿到不小于K点且不超过N点的概率除以拿到不小于K点的概率。这样,我们只要分别求出 P(<=N && >=K) 和 P(>=K) 就可以了:

P(<=N && >=K) = P(K) + P(K+1) + ... + P(N) = sumP(K, N)

P(>=K) = sumP(K, +∞) = sumP(K, K+W-1)

需要注意的是,一旦大于等于 K+W了,那么概率就为0了,所以边界就从正无穷降到 K+W-1 了。既然说到了边界,那么就来处理一下 corner case 吧,当 K=0 时,由于题目中说当前点数大于等于K,不能摸牌,那么一开始就不能摸牌了,而 K <= N,所以永远不会超过N,概率返回1。还有就是当 N >= K+W 的时候,当我们大于等于K的时候,不能摸牌,此时不会超过N。当刚好为 K-1 的时候,此时还有一次摸牌机会,但最大也就摸个W,总共为 K-1+W,还是小于N,所以返回概率为1。

根据上面的条件概率公式推导,P(>=K) 的边界降到了 K+W-1, 所以我们只要更新到这个边界就都用了,因为 P(<=N && >=K) 的范围是 [K, N],而 N 是要小于 K+W 的。我们新建一个大小为 K+W 的一维数组 sums,其中 sum[i] 表示获得范围 [0, i] 内的点数的概率综合,初始化 sum[0] 为 1.0。下面来推导状态转移方程吧 ,通常来说,我们要更新 sum[i],那么只要知道了 sum[i-1],就只要算出 P[i],就行了,因为 sum[i] = sum[i-1] + P[i]。但这道题的更新其实比较复杂,要考虑两个关键的位置,K和W,我们还是用经典的21点游戏来举例说明吧,N=21, K=17, W=10。先来说一下当点数不超过 10 的更新方法,这个其实比较简单,比如拿到七点的概率 P[7],根据我们上面对于 P(x) 的求法,我们知道可以拆分为下列多种情况:先拿到六点的概率 (P[6]) 乘以再拿一个1点的概率 (1/W),先拿到五点的概率 (P[5]) 乘以再拿一个2点的概率 (1/W),...,先拿到一点的概率 (P[1]) 乘以再拿一个六点的概率 (1/W),直接拿个七点的概率 (1/W),那么统统加起来,就是:

P[7] = 1/W * (P[6] + p[5] + ... + P[1] + P[0]) = 1/W * sum[6]

那么归纳一下,对于 i <= W 的情况下:

P[i] = 1/W * sum[i-1]

sum[i] = sum[i-1] + P[i] = sum[i-1] + sum[i-1] / W     (when i <= W)

那么当 i > W 的时候,情况是不一样的,比如要求得到 15 点的概率 P[15],那么还是根据上面求 P(x) 的方法,拆分为下面多种情况:先拿到 14 点的概率 (P[14]) 乘以再拿一个1点的概率 (1/W),先拿到 13 点的概率 (P[13]) 乘以再拿一个2点的概率 (1/W),...,先拿到五点的概率 (P[5]) 乘以再拿一个 10 点的概率 (1/W),那么统统加起来就是:

P[15] = 1/W * (P[14] + P[13] + ... + P[5]) = 1/W * (sum[14] - sum[4])

那么归纳一下,对于 i > W 的情况下:

P[i] = 1/W * (sum[i-1] - sum[i-W-1])

sum[i] = sum[i-1] + P[i] = sum[i-1] + (sum[i-1] - sum[i-W-1]) / W     (when i > W)

到这里,你以为就大功告成了吗?图样图森破,嘛哒得斯。还有一个K呢,更新K以内的P值,和更新大于K的P值是稍有不同的,比如当 K=17 时,我们要更新 P[15],那么跟上面分析的一样,同时还得考虑W的情况,归纳一下:

P[i] = 1/W * sum[i-1]     (when i <= K && i <= W)

P[i] = 1/W * (sum[i-1] - sum[i-W-1])    (when i <= K && i > W)

但是对于大于K的值,比如 P[20] 的更新方法就有所不同了,为啥呢?这要分析 20 点是怎么得来的,由于超过了 17 点就不能再摸牌了,所以 20 点只能由下列情况组成:先拿到 16 点的概率 (P[16]) 再拿到一个4点的概率 (1/W),先拿到 15 点的概率 (P[15]) 再拿到一个5点的概率 (1/W),...,先拿到 10 点的概率 (P[10]) 再拿到一个 10 点的概率 (1/W),那么统统加起来就是:

P[20] = 1/W * (P[16] + P[15] + P[14] + ... + P[10]) = 1/W * (sum[16] - sum[9])

那么我们归纳一下,就有:

P[i] = 1/W * sum[K-1]     (when i > K && i <= W)

P[i] = 1/W * (sum[K-1] - sum[i-W-1])    (when i > K && i > W)

讲到这里,是不是头晕又眼花,哈哈,博主也快绕晕了,最重要的四个式子已经加粗显示了,K和W的大小关系其实是不知道的,不过我们可以把二者揉在一起,我们每次使用 i-1 和 K-1 中的较小值来算 P[i] 即可,这样就完美把K融到了W的分类情况中,当 sum 数组计算完成之后,我们就直接按照上面的条件概率公式来算 P(<=N | >=K) = P(<=N && >=K) / P(>=K) = sumP(K, N) / sumP(K, K+W-1) 就行了,写的累s博主了,听个《青鸟》缓解一下吧,跟博主一起唱~阿欧伊,阿欧伊,阿弄嗖啦~

解法一:

class Solution {
public:
double new21Game(int N, int K, int W) {
if (K == || N >= K + W) return 1.0;
vector<double> sum(K + W);
sum[] = 1.0;
for (int i = ; i < K + W; ++i) {
int t = min(i - , K - );
if (i <= W) sum[i] = sum[i - ] + sum[t] / W;
else sum[i] = sum[i - ] + (sum[t] - sum[i - W - ]) / W;
}
return (sum[N] - sum[K - ]) / (sum[K + W - ] - sum[K - ]);
}
};

下面这种解法跟上面的解法没有啥本质的区别,这里的 dp 数组跟上面的 sum 数组表达的意思是完全一样的,dp[i] 表示获得范围 [0, i] 内的点数的概率综合,初始化 dp[0] 为 1.0。希望博主在上面已经解释清楚了,我们可以看到,这里并没有将K融合到W的分类中,而是多加了 (K, i] 区间的部分,所以当 i > K 时就要将这部分多加的减去,从而符合题意。还有一点让博主惊奇的地方是,这道题的条件概率和联合概率是相同的,根据之前的条件概率公式:

P(<=N | >=K) = P(<=N && >=K) / P(>=K)

就是说拿到不小于K点的前提下,还能不超过N点的概率,等于拿到不小于K点且不超过N点的概率除以拿到不小于K点的概率。但是实际上这道题 P(<=N | >=K) = P(<=N && >=K),即拿到不小于K点的前提下,还能不超过N点的概率,等于拿到不小于K点且不超过N点的概率。那么就是说拿到不小于K点的概率的总是为1,想想也是阿,只有在拿到不少K点的时候才停止摸牌,这样肯定点数不少于K点阿,单独计算这个概率简直是多此一举啊,参见代码如下:

解法二:

class Solution {
public:
double new21Game(int N, int K, int W) {
if (K == || N >= K + W) return 1.0;
vector<double> dp(K + W);
dp[] = 1.0;
for (int i = ; i < K + W; ++i) {
dp[i] = dp[i - ];
if (i <= W) dp[i] += dp[i - ] / W;
else dp[i] += (dp[i - ] - dp[i - W - ]) / W;
if (i > K) dp[i] -= (dp[i - ] - dp[K - ]) / W;
}
return dp[N] - dp[K - ];
}
};

下面这种解法还是大同小异,吃透了解法一的讲解,看这些变形基本都比较好理解。这里的 dp 数组意义跟上面的一样,但是并没有初始化大小为 K+W,而是只初始化为了 N+1,为啥呢,根据解法二的讲解,我们知道了这道题的条件概率和联合概率是相等的,所以只要求出 P(<=N && >=K),即 dp[N] - dp[K-1],而这题不是更新完整个dp数组后再求联合概率,而是在更新的过程中就累加到了结果 res,当 i >= K 的时候,正好可以将概率加入到结果 res,而且此时不用再累加 sumW,这里的 sumW 是保存的到目前为止的概率和,相当于之前的 dp[i-1],还需要判断的是当 i >= W 时,要减去多加的概率 dp[i-W],参见代码如下:

解法三:

class Solution {
public:
double new21Game(int N, int K, int W) {
if (K == || N >= K + W) return 1.0;
vector<double> dp(N + );
dp[] = 1.0;
double sumW = 1.0, res = 0.0;
for (int i = ; i <= N; ++i) {
dp[i] = sumW / W;
if (i < K) sumW += dp[i];
else res += dp[i];
if (i - W >= ) sumW -= dp[i - W];
}
return res;
}
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/837

参考资料:

https://leetcode.com/problems/new-21-game/

https://leetcode.com/problems/new-21-game/discuss/132334/One-Pass-DP-O(N)

https://leetcode.com/problems/new-21-game/discuss/132478/C%2B%2B-12ms-O(K%2BW)-solution-with-explanation

https://leetcode.com/problems/new-21-game/discuss/132358/Java-O(K-%2B-W)-DP-solution-with-explanation

LeetCode All in One 题目讲解汇总(持续更新中...)

[LeetCode] New 21 Game 新二十一点游戏的更多相关文章

  1. LeetCode 95 | 构造出所有二叉搜索树

    今天是LeetCode专题第61篇文章,我们一起来看的是LeetCode95题,Unique Binary Search Trees II(不同的二叉搜索树II). 这道题的官方难度是Medium,点 ...

  2. 【LeetCode算法-21】Merge Two Sorted Lists

    LeetCode第21题 Merge two sorted linked lists and return it as a new list. The new list should be made ...

  3. 推荐21款最佳 HTML5 网页游戏

    尽管 HTML5 的完全实现还有很长的路要走,但 HTML5 正在改变 Web,未来 HTML5 将把 Web 带入一个更加成熟和开放的应用平台.现在,越来越多的人尝试用 HTML5 来制作网页游戏等 ...

  4. cocos2dx新研发的游戏,手机运行时非常热的解决方案

    cocos2dx新研发的游戏,手机运行时非常热,有需要的朋友可以参考下. cocos2dx新研发的游戏,手机上运行时导致手机非常热,后来听其他项目组分享时得知,可以通过降帧解决这个问题,原来是coco ...

  5. C# leetcode 之 096 不同的二叉搜索树

    C# leetcode 之 096 不同的二叉搜索树 题目描述 给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种? 二叉搜索树定义 左子树上所有节点的值小于根节点, 右子树上左右 ...

  6. Leetcode:96. 不同的二叉搜索树

    Leetcode:96. 不同的二叉搜索树 Leetcode:96. 不同的二叉搜索树 题目在链接中,点进去看看吧! 先介绍一个名词:卡特兰数 卡特兰数 卡特兰数Cn满足以下递推关系: \[ C_{n ...

  7. Leetcode:1305. 两棵二叉搜索树中的所有元素

    Leetcode:1305. 两棵二叉搜索树中的所有元素 Leetcode:1305. 两棵二叉搜索树中的所有元素 思路 BST树中序历遍有序. 利用双指针法可以在\(O(n)\)的复杂度内完成排序. ...

  8. 【leetcode】Jump Game I, II 跳跃游戏一和二

    题目: Jump Game I: Given an array of non-negative integers, you are initially positioned at the first ...

  9. [LeetCode] Design Snake Game 设计贪吃蛇游戏

    Design a Snake game that is played on a device with screen size = width x height. Play the game onli ...

随机推荐

  1. 论文笔记:Learning wrapped guidance for blind face restoration

    这篇论文主要是讲人脸修复的,所谓人脸修复,其实就是将低清的,或者经过压缩等操作的人脸图像进行高清复原.这可以近似为针对人脸的图像修复工作.在图像修复中,我们都会假设退化的图像是高清图像经过某种函数映射 ...

  2. 百度杯”CTF比赛 2017 二月场 没错!就是文件包含漏洞。

    题目源码: 文件包含漏洞的话,看一下 你么可以使用php://input 伪协议,执行代码(参考了大佬WP)这里使用了POSTMAN, 目录下还有一个dle345aae.php文件,呢么用cat命令打 ...

  3. js性能的进阶

    为了说明js性能方面的差异用一个简单的例子说明下, <style> #ul1{ padding: 5px; overflow: hidden; } #ul1 li{ list-style: ...

  4. js数据结构与算法——二叉树

    function BinaryTree(){ var Node = function(key){ this.key = key; //值 this.left = null; //左箭头 this.ri ...

  5. PHP 两数组循环组合

  6. wxpy使用

    一 简介 wxpy基于itchat,使用了 Web 微信的通讯协议,,通过大量接口优化提升了模块的易用性,并进行丰富的功能扩展.实现了微信登录.收发消息.搜索好友.数据统计等功能. 总而言之,可用来实 ...

  7. bootstrap-typeahead 自动补全简单的使用教程

    参考链接: 参考1 : https://segmentfault.com/a/1190000006036166参考2 : https://blog.csdn.net/u010174173/articl ...

  8. [转] Vue中异步错误处理

    一般在一个项目开始之前,我们一般会对现有的框架做一定功能上的丰富,比如对ajax请求功能的二次封装,封装的功能可能包含了:通用错误处理,请求过滤,响应过滤等等.如果我们封装的函数叫request,那么 ...

  9. pip命令

    安装包 pip install django   #最新版本 pip install django==1.11.7 #指定版本 pip install django>=1.11.7 #最小版本 ...

  10. vs2013下配置x64版c++

    最近在ddctf的比赛遇到了x64版的逆向,一大堆寄存器调试的头昏,然后比赛结束后在自己电脑上配置下x64版的c++环境记录下: 首先我们需要新建项目不再废话,然后选择:debug->配置管理器 ...