[概率 DP]808. 分汤
808. 分汤
808. 分汤 - 力扣(LeetCode)
二、寻找子问题
定义目标事件:汤 A 先于 B 耗尽,或者两种汤在同一回合耗尽(此时只计一半的权重)。
比如 n=200:
从汤 A 取 100 毫升,从汤 B 取 0 毫升。问题变成两种汤的剩余量分别为 100 毫升和 200 毫升时,目标事件发生的概率。
从汤 A 取 75 毫升,从汤 B 取 25 毫升。问题变成两种汤的剩余量分别为 125 毫升和 175 毫升时,目标事件发生的概率。
从汤 A 取 50 毫升,从汤 B 取 50 毫升。问题变成两种汤的剩余量分别为 150 毫升和 150 毫升时,目标事件发生的概率。
从汤 A 取 25 毫升,从汤 B 取 75 毫升。问题变成两种汤的剩余量分别为 175 毫升和 125 毫升时,目标事件发生的概率。
这些问题都是和原问题相似的、规模更小的子问题,可以用递归解决。
三、状态定义与状态转移方程
根据「寻找子问题」,定义 dfs(a,b) 表示两种汤的剩余量分别为 a 毫升和 b 毫升时,目标事件发生的概率。
在当前回合,我们等概率地选择以下四种操作中的一种执行:
从汤 A 取 100 毫升,从汤 B 取 0 毫升。问题变成两种汤的剩余量分别为 a−100 毫升和 b 毫升时,目标事件发生的概率,即 dfs(a−100,b)。
从汤 A 取 75 毫升,从汤 B 取 25 毫升。问题变成两种汤的剩余量分别为 a−75 毫升和 b−25 毫升时,目标事件发生的概率,即 dfs(a−75,b−25)。
从汤 A 取 50 毫升,从汤 B 取 50 毫升。问题变成两种汤的剩余量分别为 a−50 毫升和 b−50 毫升时,目标事件发生的概率,即 dfs(a−50,b−50)。
从汤 A 取 25 毫升,从汤 B 取 75 毫升。问题变成两种汤的剩余量分别为 a−25 毫升和 b−75 毫升时,目标事件发生的概率,即 dfs(a−25,b−75)。
在当前回合,上述四种操作互斥且等概率地被选择,因此目标事件发生的概率为四个后续状态值的平均值,即
dfs(a,b)=41[dfs(a−100,b)+dfs(a−75,b−25)+dfs(a−50,b−50)+dfs(a−25,b−75)]
递归边界:
四、递归搜索 + 保存递归返回值 = 记忆化搜索
执行一次操作 2 和一次操作 4,会让 a 和 b 均减少 100;执行两次操作 3,也会让 a 和 b 均减少 100。这两种方式都会递归到 dfs(a−100,b−100),这会导致我们重复计算同一个状态。
考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:
如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 memo 数组中。
如果一个状态不是第一次遇到(memo 中保存的结果不等于 memo 的初始值),那么可以直接返回 memo 中保存的结果。
class Solution:def soupServings(self, n: int) -> float:if n >= 4451:return 1@cachedef dfs(a,b):if a<=0 and b <= 0:return 0.5if a <= 0:return 1.0if b <= 0:return 0.0return (dfs(a-100,b)+dfs(a-75,b-25)+dfs(a-50,b-50)+dfs(a-25,b-75))/4return dfs(n,n)