ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

LeetCode 799 Champagne Tower 香槟塔问题全解:从递归到空间优化的五重递进

LeetCode 799 Champagne Tower 香槟塔问题全解:从递归到空间优化的五重递进 LeetCode 799 Champagne Tower 香槟塔问题全解从递归到空间优化的五重递进【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode本文以 LeetCode 799「Champagne Tower香槟塔」为实战载体系统讲解一类经典的「溢出传播型」动态规划问题如何统计从顶部倒入香槟后任意一只杯子的实际盛酒量。通过递归、自顶向下 DP、自底向上 DP、滚动数组与单数组原地更新五种解法层层递进读者将完整掌握「带容量上限的流分配」建模技巧以及如何将二维 DP 逐步优化到 O(n) 空间。仓库在 java/0799-champagne-tower.java 与 kotlin/0799-champagne-tower.kt 中提供了可直接运行的参考实现可与本文五种解法相互印证。问题背景与前置知识香槟塔是一个按行排列的三角形玻璃杯堆第 0 行有 1 只杯子第 1 行有 2 只第 2 行有 3 只……当从顶部一次性倒入poured杯香槟后每只杯子容量为 1 单位。装满后溢出的香槟会等量均分给正下方的左右两只杯子每只各得一半。题目要求返回查询位置(query_row, query_glass)处杯子的盛酒量且结果不超过 1.0杯子最多是满的。在动手解题前建议先熟悉以下三个基础技能它们正是本问题五种解法的技术底座带记忆化的递归Recursion with Memoization通过缓存子问题结果避免重复计算动态规划Dynamic Programming从较小规模子问题自底向上构建答案空间优化Space Optimization利用「每行只依赖上一行」的递推特性用滚动数组把空间从 O(n²) 降到 O(n)。1. 纯递归解法从父杯子反推流量直觉Intuition每只杯子接收的香槟来自它正上方的两只「父杯子」——左父为(row-1, glass-1)右父为(row-1, glass)。当父杯子溢出时溢出的部分超出 1 单位的部分均分给两个子杯子。因此任意杯子的流入量可以递归地定义为其两个父杯子溢出量之和的一半递归的基例是顶部杯子(0, 0)它接收全部倒入的香槟。算法步骤定义递归函数返回流入(row, glass)的香槟总量基例越界row 0、glass 0或glass row返回 0位于(0, 0)时返回poured对一般位置分别递归求左父(row-1, glass-1)与右父(row-1, glass)的流入量每个父杯子只贡献其溢出量amount - 1若为正即max(0, amount - 1)最终答案取min(1, 查询杯子的流入量)——杯子不可能超过满杯。参考实现Pythonclass Solution: def champagneTower(self, poured: int, query_row: int, query_glass: int) - float: def rec(row, glass): if row 0 or glass 0 or glass row: return 0 if row 0 and glass 0: return poured left_parent max(0, rec(row - 1, glass - 1) - 1) right_parent max(0, rec(row - 1, glass) - 1) return (left_parent right_parent) / 2 return min(1, rec(query_row, query_glass))复杂度分析时间复杂度O(2ⁿ)其中 n 为query_row。递归树存在大量重叠子问题呈指数级增长空间复杂度O(n)即递归调用栈的深度。其中 n 为给定的queryRow。该解法思路最直观但因重复计算在行数较大时会超时仅适合理解问题结构。2. 动态规划自顶向下 / 记忆化缓存消除重复计算直觉Intuition递归解法中同一只杯子会被重复求值很多次。由于某杯的流入量只依赖其上方的杯子一旦算过就不会再变。用记忆化表缓存每个(row, glass)的结果即可把指数级时间压缩为多项式级。算法步骤建立 memo 表用于缓存每个(row, glass)位置的香槟流入量将memo[0][0]初始化为poured沿用递归逻辑但在计算前先查表命中则直接返回计算完成后把结果写回 memo 表再返回最终返回min(1, memo[query_row][query_glass])。参考实现Python含 memo 初始化细节class Solution: def champagneTower(self, poured: int, query_row: int, query_glass: int) - float: memo { (0, 0): poured } def rec(row, glass): if row 0 or glass 0 or glass row: return 0 if (row, glass) in memo: return memo[(row, glass)] left_parent max(0, rec(row - 1, glass - 1) - 1) right_parent max(0, rec(row - 1, glass) - 1) memo[(row, glass)] (left_parent right_parent) / 2 return memo[(row, glass)] return min(1, rec(query_row, query_glass))在类 C 语言如 Java/C中memo 常用二维数组实现并用-1作为「未计算」的哨兵值例如仓库 Java 风格实现中会预先以Arrays.fill(memo[i], -1)填充再以memo[row][glass] ! -1判断是否命中缓存参见 java/0799-champagne-tower.java 中prev_row逐行迭代的写法可反推其数组管理思路。复杂度分析时间复杂度O(n × m)空间复杂度O(n × m)。其中 n 为queryRowm 为queryGlass。自顶向下写法保留了「只计算查询路径所需状态」的优点但哈希表/二维数组仍占用较大空间。3. 动态规划自底向上模拟倒酒过程直觉Intuition与其从查询位置反向回溯不如直接从顶部开始正向模拟逐行记录每只杯子的流入量一旦某杯溢出流入量 1就把超出部分均分给下方左右两只杯子。这个思路最贴近物理过程也最容易调试。算法步骤创建二维数组dp[row][glass]表示该杯的总流入量令dp[0][0] poured遍历第 0 行到query_row - 1行对行内每只杯子若流入量超过 1计算溢出量excess把excess的一半分别加到dp[row1][glass]与dp[row1][glass1]返回min(1, dp[query_row][query_glass])。参考实现Python含 99 行上限细节class Solution: def champagneTower(self, poured: int, query_row: int, query_glass: int) - float: dp [[0] * (i 1) for i in range(query_row 5)] dp[0][0] poured for row in range(min(99, query_row 1)): for glass in range(row 1): excess (dp[row][glass] - 1.0) / 2.0 if excess 0: dp[row 1][glass] excess dp[row 1][glass 1] excess return min(1.0, dp[query_row][query_glass])值得注意的细节各语言实现中循环上限常写成min(99, query_row 1)。这源于题目数据范围的约束——行数最多为 1000 query_row 99因此只需推进到第 99 行即可覆盖所有合法查询query_row 5的数组尺寸则是为了给溢出分配留出缓冲避免dp[row1]访问越界。仓库中 kotlin/0799-champagne-tower.kt 采用滚动两行prevRow/nextRow的等价写法可对照阅读。复杂度分析时间复杂度O(n × m)空间复杂度O(n × m)。其中 n 为queryRowm 为queryGlass。二维表把所有行的状态都保留下来为下一步空间优化留下伏笔。4. 动态规划空间优化 / 滚动数组只保留两行直觉Intuition观察递推关系可以发现当前行只依赖紧邻的上一行。因此无需保存完整的二维表只需两个一维数组——prev_row存放上一行cur_row存放正在计算的行处理完一行后cur_row便成为下一轮的prev_row。算法步骤用prev_row初始化索引 0 处为poured从第 1 行迭代到query_row新建合适大小的cur_row数组遍历上一行的每只杯子若溢出把一半溢出量分别加到cur_row[i]与cur_row[i1]令prev_row cur_row进入下一轮迭代返回min(1, prev_row[query_glass])。参考实现Pythonclass Solution: def champagneTower(self, poured: int, query_row: int, query_glass: int) - float: prev_row [poured] # Flow for row in range(1, query_row 1): cur_row [0] * (row 1) for i in range(row): extra prev_row[i] - 1 if extra 0: cur_row[i] 0.5 * extra cur_row[i 1] 0.5 * extra prev_row cur_row return min(1, prev_row[query_glass])这一版本与仓库参考实现几乎一一对应例如 java/0799-champagne-tower.java 正是以double[] prev_row {poured}起步、double[] cur_row new double[row1]逐行推进、最后Math.min(1, prev_row[query_glass])收尾的滚动数组写法kotlin/0799-champagne-tower.kt 的prevRow/nextRow亦然。两份代码均可作为本题「空间优化 DP」的标准答案模板。复杂度分析时间复杂度O(n × m)空间复杂度O(n)。其中 n 为queryRowm 为queryGlass。空间从 O(n × m) 降至 O(n)这是面试中最常被追问的优化点。5. 动态规划最优 / 单数组原地更新一行空间逆序更新直觉Intuition滚动数组仍需要两个数组。进一步观察若在同一行内从右向左处理每个位置用自身溢出量的左半覆盖自己、右半加到下一位置就不会覆盖掉尚未使用的旧值——单数组即可完成整轮更新。这正是「自右向左 原地覆盖」的标准空间优化套路。算法步骤创建大小为query_row 1的一维数组dp索引 0 初始化为poured从第 1 行迭代到query_row从右向左遍历索引row-1递减到 0若dp[i] 1令dp[i] 0.5 * (dp[i] - 1)并把同样的量加到dp[i1]若dp[i] 1令dp[i] 0该杯未溢出不向下分配返回min(1, dp[query_glass])。参考实现Pythonclass Solution: def champagneTower(self, poured: int, query_row: int, query_glass: int) - float: dp [poured] [0] * query_row for row in range(1, query_row 1): for i in range(row - 1, -1, -1): extra dp[i] - 1 if extra 0: dp[i] 0.5 * extra dp[i 1] 0.5 * extra else: dp[i] 0 return min(1, dp[query_glass])逆序更新的正确性关键处理dp[i]时dp[i]与dp[i1]中存放的仍是上一行的旧值先改写dp[i]它不再被更左侧的位置读取再把溢出右半累加到dp[i1]它会在下一步作为dp[i]被处理。若改为正序遍历dp[i]被dp[i-1]累加的新值覆盖后其「旧值溢出量」将丢失结果即出错——这是单数组版本最容易踩的坑。复杂度分析时间复杂度O(n × m)空间复杂度O(n)。其中 n 为queryRowm 为queryGlass。时间无法再压缩每个杯子状态都要计算但空间已做到理论下限是本题最优雅的收尾版本。五种解法对比一览解法思路时间复杂度空间复杂度适用场景纯递归由父杯反推流量O(2ⁿ)O(n)理解问题结构数据量小时可用自顶向下 DP递归 记忆化缓存O(n × m)O(n × m)只关心单点查询、代码直观自底向上 DP正向模拟溢出分配O(n × m)O(n × m)需得到整座塔状态时滚动数组 DP双一维数组交替O(n × m)O(n)面试标准答案、兼顾实现与效率单数组最优右向左原地更新O(n × m)O(n)追求极限空间代码最简洁其中 n 为queryRowm 为queryGlass。常见陷阱与避坑指南陷阱一忘记把最终结果钳制到 1一只杯子可能收到远超 1 单位的香槟但题目只关心它的「满溢程度」最多为 1.0。直接返回原始流入量对会溢出的杯子会得到大于 1 的错误答案。# 错误返回溢出量 return dp[query_row][query_glass] # 正确钳制到 1.0杯子不可能超过满杯 return min(1, dp[query_row][query_glass])陷阱二把总流入量而非溢出量分给子杯每只杯子先装满 1 单位才溢出。常见的错误是把收到的全部香槟均分给子杯这会错误地「清空」本应保持满杯的杯子导致下方所有杯子结果偏差。# 错误把全部流量都分出去 dp[row 1][glass] dp[row][glass] / 2 # 正确只分配超出容量部分的溢出量 excess dp[row][glass] - 1 if excess 0: dp[row 1][glass] excess / 2陷阱三父杯子下标写错在递归/自顶向下版本中左父与右父的位置必须区分左父在(row-1, glass-1)右父在(row-1, glass)。把两个父杯都写成同一索引会破坏香槟塔的几何结构得到完全错误的分流结果。# 错误两个父杯子指向同一位置 left_parent rec(row - 1, glass) right_parent rec(row - 1, glass) # 正确左右父杯子在不同位置 left_parent rec(row - 1, glass - 1) right_parent rec(row - 1, glass)小结从香槟塔看一类 DP 的通用建模Champagne Tower 表面是模拟题本质是「带容量上限的流分配动态规划」状态(row, glass)的转移依赖两个父状态转移方程即flow(r,g) (max(0, flow(r-1,g-1)-1) max(0, flow(r-1,g)-1)) / 2。掌握「只分配溢出量」「结果钳制上限」「行间滚动、行内逆序」三个要点后同一套框架可直接迁移到杨辉三角、水流扩散、层级资源分配等同类问题。仓库中 java/0799-champagne-tower.java 与 kotlin/0799-champagne-tower.kt 提供了两种语言的滚动数组实现可作为刷题时的对照标准。【免费下载链接】leetcodeLeetcode solutions项目地址: https://gitcode.com/GitHub_Trending/leetcode1/leetcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表