Leetcode 799.香槟塔:动态规划+递归

香槟塔:动态规划+递归

题目来源:Leetcode 22/11/20每日一题:799.香槟塔
https://leetcode.cn/problems/champagne-tower

我们把玻璃杯摆成金字塔的形状,其中 第一层 有 1 个玻璃杯, 第二层 有 2 个,依次类推到第 100 层,每个玻璃杯 (250ml) 将盛有香槟。
从顶层的第一个玻璃杯开始倾倒一些香槟,当顶层的杯子满了,任何溢出的香槟都会立刻等流量的流向左右两侧的玻璃杯。当左右两边的杯子也满了,就会等流量的流向它们左右两边的杯子,依次类推。(当最底层的玻璃杯满了,香槟会流到地板上)
例如,在倾倒一杯香槟后,最顶层的玻璃杯满了。倾倒了两杯香槟后,第二层的两个玻璃杯各自盛放一半的香槟。在倒三杯香槟后,第二层的香槟满了 - 此时总共有三个满的玻璃杯。在倒第四杯后,第三层中间的玻璃杯盛放了一半的香槟,他两边的玻璃杯各自盛放了四分之一的香槟。
现在当倾倒了非负整数杯香槟后,返回第 i 行 j 个玻璃杯所盛放的香槟占玻璃杯容积的比例( i 和 j 都从0开始)。

示例 1:
输入: poured(倾倒香槟总杯数) = 1, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.00000
解释: 我们在顶层(下标是(0,0))倒了一杯香槟后,没有溢出,因此所有在顶层以下的玻璃杯都是空的。

示例 2:
输入: poured(倾倒香槟总杯数) = 2, query_glass(杯子的位置数) = 1, query_row(行数) = 1
输出: 0.50000
解释: 我们在顶层(下标是(0,0)倒了两杯香槟后,有一杯量的香槟将从顶层溢出,位于(1,0)的玻璃杯和(1,1)的玻璃杯平分了这一杯香槟,所以每个玻璃杯有一半的香槟。
示例 3:

输入: poured = 100000009, query_row = 33, query_glass = 17
输出: 1.00000

解释

因为下层杯子中的量是从上层溢出的,即与上层有关的。很自然的会想到dp。

如何定义dp数组?只需一个一维数组即可,长度为0~要查询的杯子的位置。要查询的杯子的位置是什么?观察香槟塔的摆放方式,可以看到,第一层1个杯子,第二层2个杯子...第n层n个杯子,每层杯子数量构成了一个等差数列,那么计算要查询的杯子位置,只需计算它所在层数前的所有层的杯子总数+它在本层中所在的位置即可:

dp[i],0<=i<=(query_row(query_row+1)/2+query_glass+1)

下面,只需要按照层级来进行dfs即可,边界条件是:

  • 当前层<最大层(即目标杯子所在的层):递归
  • 当前层=最大层:结束递归

对于每一层,需要从这一层的杯子开始位置,一直遍历到杯子结束的位置。某一层杯子开始位置是:curStart=cur(cur+1)/2,每一层的杯子个数是:cur+1,其中cur表示当前所在的层数,cur从0开始。

对于某一层的某一个杯子,假设其所在层数的起始坐标为curStart,它位于所在层的第i个位置(i从0开始),进行如下处理:

  • 如果dp[curStart+i]>1.0,那么该杯子中的香槟会溢出,此时记录溢出值:more=dp[curStart+i]1.0,并将该杯子的香槟量设置为1。
  • 对于溢出的香槟more,应该平分给其下层的两个“孩子”。

如何确定两个孩子的坐标?观察可得,如果一个杯子在某一层的第i个位置,那么它的孩子在其下一层的第i个位置和第i+1个位置。

如下图,以2号为例,其孩子为4号和5号,2号位于第1层(层数从0开始)的第1个(位置从0开始)位置,而4号位于第2层的第1个位置,5号位于第2层的第2个位置。

image.png

那么数值上就容易确定了,假设当前节点位于第cur层的第i个,那么其两个孩子的坐标分别为:
leftChild=(cur+2)(cur+1)/2+i
rightChild=leftChild+1

确定了两个孩子坐标后,只需要将more平分给两个孩子即可。别忘记判断孩子坐标是否越界。

实现

代码如下:

/**
 * 时间2ms,超过98.42%
 * 空间41.6MB,超过93.69%
 */
class Solution {
    public double champagneTower(int poured, int query_row, int query_glass) {
        int n = query_row * (query_row + 1) / 2 + query_glass + 1;
        double[] dp = new double[n];
        dp[0] = poured;
        cal(dp, query_row, 0);
        return Math.min(dp[n-1], 1.0);
    }

    public void cal(double[] dp, int max, int cur) {
        if(cur < max) {
            int curStart = cur * (cur + 1) / 2;
            for(int i = 0; i <= cur; ++i) {
                if(dp[i + curStart] > 1.0) {
                    double more = dp[i + curStart] - 1.0;
                    dp[i + curStart] = 1.0;
                    int leftChild = (cur + 2) * (cur + 1) / 2 + i;
                    int rightChild = leftChild + 1;
                    if(leftChild < dp.length) dp[leftChild] += more / 2;
                    if(rightChild < dp.length) dp[rightChild] += more / 2;
                }
            }
            cal(dp, max, cur + 1);
        }
    }
}

复杂度分析

时间复杂度O(n),需要遍历一遍杯子;空间复杂度O(n),需要一个dp数组和递归所用到的栈。

posted @   KindBrave  阅读(113)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示