爱丽丝参与一个大致基于纸牌游戏 “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 分。
来源:力扣(LeetCode)
微信搜索关注公众号“编程猿来如此”,获取最新文章。
题意分析
过程模拟:抽一个数字累计到当前的总积分 i ,然后做判断,如果 i 小于 K 则继续抽下一个数字,直到累计积分大于等于 K 则停止整个过程。
当前的积分 i 是在上一步的基础上抽取一个数字得到的,有以下几种情况:
- 上一步积分是 i-1,然后抽取数字 1 后得到 i,概率为: 【得到 i-1 分的概率】*1/W
- 上一步积分是 i-2,然后抽取数字 2 后得到 i,概率为: 【得到 i-2 分的概率】*1/W
- 上一步积分是 i-3,然后抽取数字 3 后得到 i,概率为: 【得到 i-3 分的概率】*1/W
- …
- 上一步积分是 i-W,然后抽取数字 W 后得到 i,概率为: 【得到 i-3 分的概率】*1/W
因此得到当前积分 i 的概率为以上所有情况的概率之和。不超过N的概率即为从 K 到 N 各概况之和。
动态规划
当前的状态是由之前已经发生的状态值决定的,因此可以使用动态规划来解决问题:
- dp[i] 表示得到积分 i 的概率,根据题意 i 最大值为 N,因此 dp 长度为 N+1;
- dp[i] = ( dp[i-1]+dp[i-2]+…dp[i-W] ) /W ;
然后从 K 到 N 进行dp[i] 求和即为本题的结果。
代码实现
class Solution {
public:
double new21Game(int N, int K, int W) {
vector<double> dp(N+1, 0);//初始化dp
dp[0] = 1;// 积分 0 的概率为1
for(int i = 1;i <= N;i++ )
{
// [i-W,i-1]求和,除以 W
for( int j = 1;j<= W ;j++)
{
//到 K 就停止了,因此区间不能超出 K
if( i -j < K && i- j >= 0)
{
dp[i] += 1.00000 / W * dp[i - j];
}
}
}
double res = 0;
for(int i = K;i<=N;i++)//[K,N]求和
{
res+=dp[i];
}
return res;
}
};
优化重复过程
在求 dp[i] 的过程中,每个值都计算了 i-W到 i-1 的和,存在重复的计算过程,可以进行优化。区间 [i-W,i-1] 就是一个固定大小为 W 的窗口,每次向右移动一步后,区间和是在之前的基础上右侧减少一个元素,左侧增加一个元素。
class Solution {
public:
double new21Game(int N, int K, int W) {
vector<double> dp(N+1, 0);
dp[0] = 1;
double presum = 0;//表示之前 W 个元素的概率和
for(int i = 1;i <= N;i++ )
{
if( i - W -1 >= 0 )
presum -= dp[ i-W -1];//右侧移除一个元素
if( i -1 < K)
presum += dp[ i-1];//左侧加入一个元素
dp[i] = presum * 1.00000/W;
}
double res = 0;
for(int i = K;i<=N;i++)
{
res+=dp[i];
}
return res;
}
};