Skip to content

Files

Latest commit

4d6c701 · Feb 21, 2024

History

History
This branch is 5 commits ahead of, 1648 commits behind doocs/leetcode:main.

0837.New 21 Game

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
Feb 21, 2024
Feb 21, 2024
May 2, 2023
Oct 31, 2023
May 2, 2023
May 2, 2023
Aug 31, 2023
Jan 13, 2024
Jan 13, 2024
Jan 13, 2024
Jan 13, 2024
Jan 13, 2024

English Version

题目描述

爱丽丝参与一个大致基于纸牌游戏 “21点” 规则的游戏,描述如下:

爱丽丝以 0 分开始,并在她的得分少于 k 分时抽取数字。 抽取时,她从 [1, maxPts] 的范围中随机获得一个整数作为分数进行累计,其中 maxPts 是一个整数。 每次抽取都是独立的,其结果具有相同的概率。

当爱丽丝获得 k或更多分 时,她就停止抽取数字。

爱丽丝的分数不超过 n 的概率是多少?

与实际答案误差不超过 10-5 的答案将被视为正确答案。

 

示例 1:

输入:n = 10, k = 1, maxPts = 10
输出:1.00000
解释:爱丽丝得到一张牌,然后停止。

示例 2:

输入:n = 6, k = 1, maxPts = 10
输出:0.60000
解释:爱丽丝得到一张牌,然后停止。 在 10 种可能性中的 6 种情况下,她的得分不超过 6 分。

示例 3:

输入:n = 21, k = 17, maxPts = 10
输出:0.73278

 

提示:

  • 0 <= k <= n <= 104
  • 1 <= maxPts <= 104

解法

方法一:记忆化搜索

我们设计一个函数 d f s ( i ) ,表示当前分数为 i 时,到最终停止抽取数字时,分数不超过 n 的概率。那么答案就是 d f s ( 0 )

函数 d f s ( i ) 的计算方法如下:

  • 如果 i k ,那么停止抽取数字,如果 i n ,返回 1 ,否则返回 0
  • 否则,可以在 [ 1 , . . m a x P t s ] 范围内抽取下一个数字 j ,那么 d f s ( i ) = 1 m a x P t s j = 1 m a x P t s d f s ( i + j )

这里我们可以使用记忆化搜索来加速计算。

以上方法的时间复杂度为 O ( k × m a x P t s ) ,会超出时间限制,我们需要优化一下。

i < k 时,以下等式成立:

d f s ( i ) = ( d f s ( i + 1 ) + d f s ( i + 2 ) + + d f s ( i + m a x P t s ) ) / m a x P t s ( 1 )

i < k 1 时,以下等式成立:

d f s ( i + 1 ) = ( d f s ( i + 2 ) + d f s ( i + 3 ) + + d f s ( i + m a x P t s + 1 ) ) / m a x P t s ( 2 )

因此,当 i < k 1 时,我们将等式 ( 1 ) 减去等式 ( 2 ) ,得到:

d f s ( i ) d f s ( i + 1 ) = ( d f s ( i + 1 ) d f s ( i + m a x P t s + 1 ) ) / m a x P t s

即:

d f s ( i ) = d f s ( i + 1 ) + ( d f s ( i + 1 ) d f s ( i + m a x P t s + 1 ) ) / m a x P t s

如果 i = k 1 ,有:

d f s ( i ) = d f s ( k 1 ) = d f s ( k ) + d f s ( k + 1 ) + + d f s ( k + m a x P t s 1 ) / m a x P t s ( 3 )

我们假设有 i 个数不超过 n ,那么 k + i 1 n ,又因为 i m a x P t s ,所以 i min ( n k + 1 , m a x P t s ) ,因此等式 ( 3 ) 可以写成:

d f s ( k 1 ) = min ( n k + 1 , m a x P t s ) / m a x P t s

综上所述,有以下状态转移方程:

d f s ( i ) = { 1 , i k , i n 0 , i k , i > n min ( n k + 1 , m a x P t s ) / m a x P t s , i = k 1 d f s ( i + 1 ) + ( d f s ( i + 1 ) d f s ( i + m a x P t s + 1 ) ) / m a x P t s , i < k 1

时间复杂度 O ( k + m a x P t s ) ,空间复杂度 O ( k + m a x P t s ) 。其中 k 为最大分数。

class Solution:
    def new21Game(self, n: int, k: int, maxPts: int) -> float:
        @cache
        def dfs(i: int) -> float:
            if i >= k:
                return int(i <= n)
            if i == k - 1:
                return min(n - k + 1, maxPts) / maxPts
            return dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts

        return dfs(0)
class Solution {
    private double[] f;
    private int n, k, maxPts;

    public double new21Game(int n, int k, int maxPts) {
        f = new double[k];
        this.n = n;
        this.k = k;
        this.maxPts = maxPts;
        return dfs(0);
    }

    private double dfs(int i) {
        if (i >= k) {
            return i <= n ? 1 : 0;
        }
        if (i == k - 1) {
            return Math.min(n - k + 1, maxPts) * 1.0 / maxPts;
        }
        if (f[i] != 0) {
            return f[i];
        }
        return f[i] = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts;
    }
}
class Solution {
public:
    double new21Game(int n, int k, int maxPts) {
        vector<double> f(k);
        function<double(int)> dfs = [&](int i) -> double {
            if (i >= k) {
                return i <= n ? 1 : 0;
            }
            if (i == k - 1) {
                return min(n - k + 1, maxPts) * 1.0 / maxPts;
            }
            if (f[i]) {
                return f[i];
            }
            return f[i] = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts;
        };
        return dfs(0);
    }
};
func new21Game(n int, k int, maxPts int) float64 {
	f := make([]float64, k)
	var dfs func(int) float64
	dfs = func(i int) float64 {
		if i >= k {
			if i <= n {
				return 1
			}
			return 0
		}
		if i == k-1 {
			return float64(min(n-k+1, maxPts)) / float64(maxPts)
		}
		if f[i] > 0 {
			return f[i]
		}
		f[i] = dfs(i+1) + (dfs(i+1)-dfs(i+maxPts+1))/float64(maxPts)
		return f[i]
	}
	return dfs(0)
}
function new21Game(n: number, k: number, maxPts: number): number {
    const f = new Array(k).fill(0);
    const dfs = (i: number): number => {
        if (i >= k) {
            return i <= n ? 1 : 0;
        }
        if (i === k - 1) {
            return Math.min(n - k + 1, maxPts) / maxPts;
        }
        if (f[i] !== 0) {
            return f[i];
        }
        return (f[i] = dfs(i + 1) + (dfs(i + 1) - dfs(i + maxPts + 1)) / maxPts);
    };
    return dfs(0);
}

方法二:动态规划

我们可以将方法一中的记忆化搜索改成动态规划。

定义 f [ i ] 表示当前分数为 i 时,到最终停止抽取数字时,分数不超过 n 的概率。那么答案就是 f [ 0 ]

k i min ( n , k + m a x P t s 1 ) 时,有 f [ i ] = 1

i = k 1 时,有 f [ i ] = min ( n k + 1 , m a x P t s ) / m a x P t s

i < k 1 时,有 f [ i ] = f [ i + 1 ] + ( f [ i + 1 ] f [ i + m a x P t s + 1 ] ) / m a x P t s

时间复杂度 O ( k + m a x P t s ) ,空间复杂度 O ( k + m a x P t s ) 。其中 k 为最大分数。

class Solution:
    def new21Game(self, n: int, k: int, maxPts: int) -> float:
        f = [0] * (k + maxPts)
        for i in range(k, min(n + 1, k + maxPts)):
            f[i] = 1
        f[k - 1] = min(n - k + 1, maxPts) / maxPts
        for i in range(k - 2, -1, -1):
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts
        return f[0]
class Solution {
    public double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double[] f = new double[k + maxPts];
        for (int i = k; i < Math.min(n + 1, k + maxPts); ++i) {
            f[i] = 1;
        }
        f[k - 1] = Math.min(n - k + 1, maxPts) * 1.0 / maxPts;
        for (int i = k - 2; i >= 0; --i) {
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
        }
        return f[0];
    }
}
class Solution {
public:
    double new21Game(int n, int k, int maxPts) {
        if (k == 0) {
            return 1.0;
        }
        double f[k + maxPts];
        memset(f, 0, sizeof(f));
        for (int i = k; i < min(n + 1, k + maxPts); ++i) {
            f[i] = 1;
        }
        f[k - 1] = min(n - k + 1, maxPts) * 1.0 / maxPts;
        for (int i = k - 2; i >= 0; --i) {
            f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
        }
        return f[0];
    }
};
func new21Game(n int, k int, maxPts int) float64 {
	if k == 0 {
		return 1
	}
	f := make([]float64, k+maxPts)
	for i := k; i < min(n+1, k+maxPts); i++ {
		f[i] = 1
	}
	f[k-1] = float64(min(n-k+1, maxPts)) / float64(maxPts)
	for i := k - 2; i >= 0; i-- {
		f[i] = f[i+1] + (f[i+1]-f[i+maxPts+1])/float64(maxPts)
	}
	return f[0]
}
function new21Game(n: number, k: number, maxPts: number): number {
    if (k === 0) {
        return 1;
    }
    const f = new Array(k + maxPts).fill(0);
    for (let i = k; i < Math.min(n + 1, k + maxPts); ++i) {
        f[i] = 1;
    }
    f[k - 1] = Math.min(n - k + 1, maxPts) / maxPts;
    for (let i = k - 2; i >= 0; --i) {
        f[i] = f[i + 1] + (f[i + 1] - f[i + maxPts + 1]) / maxPts;
    }
    return f[0];
}