Alice plays the following game, loosely based on the card game "21".html
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.git
Alice stops drawing numbers when she gets K
or more points. What is the probability that she has N
or less points?github
Example 1:数组
Input: N = 10, K = 1, W = 10 Output: 1.00000 Explanation: Alice gets a single card, then stops.
Example 2:less
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:dom
Input: N = 21, K = 17, W = 10 Output: 0.73278
Note:post
0 <= K <= N <= 10000
1 <= W <= 10000
10^-5
of the correct answer.这道题就是赌桌上经典的 21 点游戏了,想起了当年实习的游轮活动,就有21点游戏的赌桌。我当时还纳闷为啥庄家到了 17 点之后就再也不要牌了,原来里面大有学问啊,由于再多拿牌,增大了爆的几率,而若是小于 17 就不拿牌的话,会增大玩家赢的几率,估计是通过精心计算,用这个阈值庄家赢的几率最大吧,想着当时庄家每拿一张牌,你们都一块儿在喊“爆,爆,爆。。”的情景,仍是以为很是搞笑。但当时有一位同期实习的大神,能够根据分析台面上已经出现的牌,来推出最合理的策略,由于庄家的规则是不变的,只要过了 17 就坚定不拿牌,可是大神却能够根据已出现的牌来制定本身的最优策略,常常能赢庄家。据大神称他去赌场常常都能赢上个小二百刀,给跪了有木有?!this
好,来解题吧。这道题说的是有 [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点的几率就能够概括出下面的等式:url
P(x) = 1/W * (P(x-1) + P(x-2) + P(x-W))spa
= 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 == 0 || N >= K + W) return 1.0; vector<double> sum(K + W); sum[0] = 1.0; for (int i = 1; i < K + W; ++i) { int t = min(i - 1, K - 1); if (i <= W) sum[i] = sum[i - 1] + sum[t] / W; else sum[i] = sum[i - 1] + (sum[t] - sum[i - W - 1]) / W; } return (sum[N] - sum[K - 1]) / (sum[K + W - 1] - sum[K - 1]); } };
下面这种解法跟上面的解法没有啥本质的区别,这里的 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 == 0 || N >= K + W) return 1.0; vector<double> dp(K + W); dp[0] = 1.0; for (int i = 1; i < K + W; ++i) { dp[i] = dp[i - 1]; if (i <= W) dp[i] += dp[i - 1] / W; else dp[i] += (dp[i - 1] - dp[i - W - 1]) / W; if (i > K) dp[i] -= (dp[i - 1] - dp[K - 1]) / W; } return dp[N] - dp[K - 1]; } };
下面这种解法仍是大同小异,吃透了解法一的讲解,看这些变形基本都比较好理解。这里的 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 == 0 || N >= K + W) return 1.0; vector<double> dp(N + 1); dp[0] = 1.0; double sumW = 1.0, res = 0.0; for (int i = 1; i <= N; ++i) { dp[i] = sumW / W; if (i < K) sumW += dp[i]; else res += dp[i]; if (i - W >= 0) 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)