香槟塔 图解题解
这道题到底在问什么
- 输入
- poured=2, row=1, glass=1
- 输出
- 0.5(顶杯溢出 1 杯,平分给下面两杯各 0.5)
- 输入
- poured=8, row=3, glass=2
- 输出
- 0.875(本动画演示的实例)
最优解:为什么这么做
一句话答案:LeetCode 799 香槟塔逐行模拟:每杯满 1 后溢出的部分平分给正下方左右两杯,用滚动数组推到查询行,结果取 min(1, 该杯量)。时间 O(row²)、空间 O(row)。
查询那一杯,最终接住多少香槟
香槟从最顶端杯子倒进去,总量 poured 杯。每杯只能装满 1,多出的部分均匀流进正下方左右相邻的两杯。给你行号 query_row 和该行第几个杯子 query_glass,问倒完后这杯里有多少香槟,上限 1。poured=2 查第 1 行第 1 杯:顶杯满 1 剩 1 杯溢出、平分给下面两杯各 0.5,答案 0.5。
为什么盯着目标杯往上倒推会指数爆炸
只算目标那一杯:某杯的量 = 左上、右上两父杯溢出量之和,父杯又依赖它头上两杯。写成递归,每往上追一层就分叉成两支,追到第 query_row 行约 O(2^row) 次,中间父杯反复重算。既然每杯只由上一行相邻两杯决定,反过来从顶杯往下逐行填、每杯只算一次就够。
从顶杯逐行往下淌,一维数组存当前行
用一维数组 row 表示当前行的各杯,起手只有顶杯 row = [poured]。往下走一行,就新建一个比它长 1 的数组存下一行,把当前行每杯该溢的量灌进去,再把它当成新的当前行往下推到 query_row。数组里存的是「流经这杯的总量」,可能远大于 1——它得先超过 1 才有余量往下溢。每行算完就覆盖成下一行,这叫滚动数组(只留当前一行、循环覆盖旧值),空间从存整座塔 O(row²) 压到 O(row)。
溢出为什么除 2、下层为什么用累加
溢出量 over = max(0, x - 1) / 2:减 1 是它自己要先留满一杯,除 2 是溢出要平分给左右两杯、每个只得一半,忘了除 2 会让香槟凭空翻倍。
灌进下一行写 next[i] += over、next[i+1] += over,都用累加:下一行一杯会同时接到左上、右上两父杯的溢出,两股要加起来,直接赋值会覆盖先到那份、少算一半。
拿 poured=8 查第 3 行第 2 杯逐行算一遍
第 0 行只有顶杯 [8]。推第 1 行:顶杯 over=(8-1)/2=3.5 灌进下一行两杯,得 [3.5, 3.5]。推第 2 行:第 0 杯 over=(3.5-1)/2=1.25 加到第 0、1 杯,第 1 杯 over=1.25 加到第 1、2 杯;第 1 杯接到左右两股累计 1.25+1.25=2.5,得 [1.25, 2.5, 1.25]。
推第 3 行:第 0 杯 over=(1.25-1)/2=0.125、第 1 杯 over=(2.5-1)/2=0.75、第 2 杯 over=0.125,各加给相邻两杯,得 [0.125, 0.875, 0.875, 0.125]。读第 2 杯,min(1, 0.875)=0.875 即答案。
推到第几行要多少次,除 2 和 min 别丢
从第 0 行推到第 query_row 行,各行杯子数累加约 row²/2 次,时间 O(row²);滚动数组只存一行,空间 O(row)。poured=0 时全塔为 0、查询返回 0;query_row=0 还没溢出,答案就是 min(1, poured)——这两处别漏。收尾 min 也不能提前做——途中若把超过 1 的杯子压到 1,它就没余量往下溢、下层会少算,只有读答案那刻才 min。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记住口诀:「满 1 才溢,溢出平分给下面两杯」,下面逐杯套它。
- 4先把全部 8 杯香槟倒进顶杯:第 0 行只有 1 个杯子,里面是 8(紫)。它远远超过 1,接下来会大量溢出。其余行暂时都是空的(0)。
- 5现在看第 0 行(蓝)的每个杯子。凡是超过 1 的,多出来的部分要平分流进第 1 行紧挨它的左右两杯。第 1 行此刻还是空的,等着接香槟。
- 6看第 0 行第 0 个杯子(紫),里面有 8,超过 1。多出来的 8 - 1 = 7 平分成两份,每份 3.5,分别流向下一行第 0、第 1 个杯子。
- 7把 3.5 同时灌进第 1 行的第 0 杯和第 1 杯(绿,它们现在分别累计到 3.5 和 3.5)。同一个下层杯子可能同时接到左上、右上两个杯子的溢出,所以用累加。
- 8第 0 行所有杯子都处理过了。第 1 行现在是 [3.5, 3.5](绿)。继续往下一行递推。
- 9现在看第 1 行(蓝)的每个杯子。凡是超过 1 的,多出来的部分要平分流进第 2 行紧挨它的左右两杯。第 2 行此刻还是空的,等着接香槟。
- 10看第 1 行第 0 个杯子(紫),里面有 3.5,超过 1。多出来的 3.5 - 1 = 2.5 平分成两份,每份 1.25,分别流向下一行第 0、第 1 个杯子。
- 11把 1.25 同时灌进第 2 行的第 0 杯和第 1 杯(绿,它们现在分别累计到 1.25 和 1.25)。同一个下层杯子可能同时接到左上、右上两个杯子的溢出,所以用累加。
- 12看第 1 行第 1 个杯子(紫),里面有 3.5,超过 1。多出来的 3.5 - 1 = 2.5 平分成两份,每份 1.25,分别流向下一行第 1、第 2 个杯子。
- 13把 1.25 同时灌进第 2 行的第 1 杯和第 2 杯(绿,它们现在分别累计到 2.5 和 1.25)。同一个下层杯子可能同时接到左上、右上两个杯子的溢出,所以用累加。
- 14第 1 行所有杯子都处理过了。第 2 行现在是 [1.25, 2.5, 1.25](绿)。继续往下一行递推。
- 15现在看第 2 行(蓝)的每个杯子。凡是超过 1 的,多出来的部分要平分流进第 3 行紧挨它的左右两杯。第 3 行此刻还是空的,等着接香槟。
- 16看第 2 行第 0 个杯子(紫),里面有 1.25,超过 1。多出来的 1.25 - 1 = 0.25 平分成两份,每份 0.125,分别流向下一行第 0、第 1 个杯子。
- 17把 0.125 同时灌进第 3 行的第 0 杯和第 1 杯(绿,它们现在分别累计到 0.125 和 0.125)。同一个下层杯子可能同时接到左上、右上两个杯子的溢出,所以用累加。
- 18看第 2 行第 1 个杯子(紫),里面有 2.5,超过 1。多出来的 2.5 - 1 = 1.5 平分成两份,每份 0.75,分别流向下一行第 1、第 2 个杯子。
- 19把 0.75 同时灌进第 3 行的第 1 杯和第 2 杯(绿,它们现在分别累计到 0.875 和 0.75)。同一个下层杯子可能同时接到左上、右上两个杯子的溢出,所以用累加。
- 20看第 2 行第 2 个杯子(紫),里面有 1.25,超过 1。多出来的 1.25 - 1 = 0.25 平分成两份,每份 0.125,分别流向下一行第 2、第 3 个杯子。
- 21把 0.125 同时灌进第 3 行的第 2 杯和第 3 杯(绿,它们现在分别累计到 0.875 和 0.125)。同一个下层杯子可能同时接到左上、右上两个杯子的溢出,所以用累加。
- 22第 2 行所有杯子都处理过了。第 3 行现在是 [0.125, 0.875, 0.875, 0.125](绿)。已经推到查询的第 3 行,接下来直接读取第 2 个杯子并对 1 取 min。
- 23推到查询的第 3 行,看第 2 个杯子(紫):里面累计到 0.875。杯子最多装 1 杯,所以取 min(1, 0.875) = 0.875。这就是最终答案。
- 24回顾整条链:8 杯从顶杯一路溢下来,每层「满 1 才溢、溢出平分给下面两杯」。查询的杯子(紫)最终是 0.875。整道题的核心就是从上到下逐行模拟这一个溢出规则。
⚠️ 容易写错的地方
✗ 错:溢出量忘记除以 2
✓ 对:over = max(0, x - 1) / 2
溢出的香槟要平分给左右两个杯子,每个只得一半;不除 2 会让总量翻倍
✗ 错:下一行杯子用赋值而不是累加
✓ 对:next[i] += over; next[i+1] += over
下层一个杯子会同时接到左上、右上两个杯子的溢出,必须累加,直接赋值会丢掉其中一份
✗ 错:忘了最后对 1 取 min
✓ 对:return min(1, row[query_glass])
杯子最多装满 1 杯,中间累计值可能超过 1(它会继续往下溢),但该杯实际持有量上限是 1
完整代码(Python / C++ / Java)
Python
class Solution:
def champagneTower(self, poured: int, query_row: int, query_glass: int) -> float:
row = [float(poured)]
for _ in range(query_row):
nxt = [0.0] * (len(row) + 1)
for i, x in enumerate(row):
over = max(0.0, x - 1.0) / 2.0
nxt[i] += over
nxt[i + 1] += over
row = nxt
return min(1.0, row[query_glass])C++
#include <vector>
using namespace std;
class Solution {
public:
double champagneTower(int poured, int query_row, int query_glass) {
vector<double> row{(double)poured};
for (int r = 0; r < query_row; ++r) {
vector<double> nxt(row.size() + 1);
for (int i = 0; i < (int)row.size(); ++i) {
double over = max(0.0, row[i] - 1.0) / 2.0;
nxt[i] += over; nxt[i + 1] += over;
}
row.swap(nxt);
}
return min(1.0, row[query_glass]);
}
};Java
import java.util.*;
class Solution {
public double champagneTower(int poured, int query_row, int query_glass) {
double[] row = {poured};
for (int r = 0; r < query_row; r++) {
double[] next = new double[row.length + 1];
for (int i = 0; i < row.length; i++) {
double over = Math.max(0.0, row[i] - 1.0) / 2.0;
next[i] += over; next[i + 1] += over;
}
row = next;
}
return Math.min(1.0, row[query_glass]);
}
}复杂度
时间
O(row²)
从第 0 行推到第 query_row 行,第 r 行有 r+1 个杯子,累加是 1+2+…+row 个等差,约 row² / 2
空间
O(row)
滚动数组只存当前行,最长就是查询行的 row+1 个杯子
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 香槟塔 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么可以只用一维滚动数组,不必把整座塔都存下来?+
因为第 r+1 行的每个杯子只依赖第 r 行相邻两杯的溢出,递推时手里只需要「上一行」。所以维护一个一维数组、每行算完就覆盖成下一行即可,空间从 O(row²) 降到 O(row)。反过来说,如果题目要对同一次倒入反复查询很多不同的行列,倒不如一次性把整座塔(二维)算到最深行缓存起来复用,省去重复模拟。每行只由上一行相邻两杯决定,这种逐行递推和杨辉三角同属一类。
中间某个杯子的累计值超过 1,会不会把下面的量算错?+
不会,反而必须让它超过 1。数组里存的是「流经这个杯子的总量」,它要先超过 1 才能继续往下溢出 (x-1)/2。所以模拟过程中绝不能提前对 1 取 min,只有在最后读取目标杯时才取。若中途就把超过 1 的值压到 1,等于掐断了它往下淌的余量,下层会少算应得的香槟。
为什么下一行的杯子要用 += 累加,不能直接赋值?+
因为下一行的一个杯子往往同时接到左上、右上两个父杯的溢出——它既是左边父杯的右下方,又是右边父杯的左下方。处理左父杯时给它加一份、处理右父杯时再加一份,两股必须叠加。如果写成赋值,后处理的那个父杯会把先到的那份覆盖掉,这个杯子就凭空少了一半香槟,往下的递推也跟着全错。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 香槟塔 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。