news 2026/10/26 17:37:54

LeetCode 1130题:动态规划与单调栈解法详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 1130题:动态规划与单调栈解法详解

1. 问题背景与核心理解

LeetCode 1130题"Minimum Cost Tree From Leaf Values"是一个典型的动态规划问题,要求我们根据给定的叶节点值数组构建满足特定条件的二叉树,并找到构建成本最小的方案。这道题在2023年频繁出现在各类算法面试中,尤其在互联网大厂的笔试环节被多次考察。

问题的核心在于:给定一个正整数数组arr,其中每个元素代表二叉树叶节点的值。我们需要将这些叶节点排列成非递减顺序,然后通过合并相邻节点构建内部节点,直到最终形成一棵完整的二叉树。每次合并两个相邻叶节点时,会产生一个内部节点,其值为这两个叶节点值的乘积,合并成本即为这个乘积值。我们的目标是找到所有可能的构建方案中,总成本最小的那一种。

关键提示:这个问题看似是二叉树构建问题,实则可以通过区间DP(动态规划)的思路高效解决。因为最终的树结构取决于叶节点的合并顺序,而这正好对应着动态规划中的子问题划分。

2. 动态规划解法详解

2.1 状态定义与转移方程

对于这类区间最优化问题,我们通常定义dp[i][j]表示处理arr数组中从第i个元素到第j个元素(闭区间)时的最小总成本。同时,为了计算乘积值,我们还需要记录每个区间内的最大值max_val[i][j]。

状态转移方程可以表示为: dp[i][j] = min(dp[i][k] + dp[k+1][j] + max_val[i][k]*max_val[k+1][j]) 其中k在[i,j-1]范围内遍历所有可能的分割点。

# 初始化 n = len(arr) dp = [[0]*n for _ in range(n)] max_val = [[0]*n for _ in range(n)] for i in range(n): max_val[i][i] = arr[i] for j in range(i+1, n): max_val[i][j] = max(max_val[i][j-1], arr[j]) # 动态规划填表 for length in range(1, n): # 子区间长度 for i in range(n-length): j = i + length dp[i][j] = float('inf') for k in range(i, j): dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + max_val[i][k]*max_val[k+1][j])

2.2 算法复杂度分析

上述解法的时间复杂度为O(n^3),因为有三重循环:

  1. 外层循环遍历子区间长度:O(n)
  2. 中层循环遍历区间起始点:O(n)
  3. 内层循环遍历分割点:O(n)

空间复杂度为O(n^2),用于存储dp和max_val两个二维数组。

2.3 优化思路与单调栈解法

虽然O(n^3)的解法在n较小时(通常n≤50)可以接受,但对于更大的数据规模,我们可以考虑使用单调栈将时间复杂度优化到O(n)。这种优化基于一个关键观察:每个元素最终只会与它左右两边第一个比它大的元素相乘。

def mctFromLeafValues(arr): res = 0 stack = [float('inf')] for num in arr: while stack[-1] <= num: mid = stack.pop() res += mid * min(stack[-1], num) stack.append(num) while len(stack) > 2: res += stack.pop() * stack[-1] return res

这个优化解法的核心思想是维护一个单调递减栈,当遇到比栈顶大的元素时,说明找到了一个"洼地",可以计算局部最小乘积值。这种方法将问题转化为寻找每个元素的左右边界问题,大大提高了效率。

3. 关键实现细节与注意事项

3.1 边界条件处理

在实际编码中,有几个边界条件需要特别注意:

  1. 当区间长度为1时(i==j),dp[i][j]应该为0,因为单个节点不需要合并
  2. 当数组长度为1时,直接返回0
  3. 在单调栈解法中,初始时栈中放入一个极大值(如float('inf'))作为哨兵

3.2 常见错误与调试技巧

很多初学者在解决这个问题时容易犯以下错误:

  1. 混淆了节点值和乘积值的概念,错误地将节点值相加而非相乘
  2. 在动态规划填表时,循环的顺序不正确,导致访问未计算的子问题
  3. 在单调栈解法中,忘记处理最后栈中剩余的元素

调试时可以:

  1. 打印出dp表格,检查每个单元格的值是否符合预期
  2. 对于小规模测试用例(如[6,2,4]),手动计算预期结果并与程序输出对比
  3. 在单调栈解法中,打印栈的变化过程,观察元素进出栈的顺序

4. 问题变种与扩展思考

4.1 类似问题对比

这个问题与矩阵链乘问题非常相似,都是通过不同的合并顺序来寻找最优解。区别在于:

  1. 矩阵链乘的cost计算方式是三个维度相乘
  2. 本问题的cost是左右子树最大叶节点值的乘积

其他类似问题还包括:

  • 多边形三角剖分问题
  • 石子合并问题
  • 最优二叉搜索树问题

4.2 实际应用场景

虽然这个问题看起来是纯理论性的,但它的一些变种在实际中有重要应用:

  1. 查询优化:数据库执行计划生成时,需要确定多个表连接的最佳顺序
  2. 图像处理:图像金字塔构建时的最优合并策略
  3. 编译器优化:表达式求值顺序的选择

5. 不同语言实现要点

5.1 Python实现特点

Python实现时可以利用列表推导式简化二维数组初始化:

dp = [[0]*n for _ in range(n)]

注意不要使用[[0]*n]*n这种方式,这会导致内部列表是同一个对象的引用。

5.2 C++实现优化

在C++中可以使用vector容器,但要注意预分配空间以避免频繁扩容:

vector<vector<int>> dp(n, vector<int>(n, 0)); vector<vector<int>> max_val(n, vector<int>(n, 0));

5.3 Java实现注意点

Java中二维数组初始化为:

int[][] dp = new int[n][n]; int[][] maxVal = new int[n][n];

注意Java会默认初始化为0,不需要显式初始化。

6. 测试用例设计与验证

为了确保代码的正确性,应该设计多种类型的测试用例:

  1. 基础用例:[6,2,4] → 32
  2. 升序数组:[1,2,3,4] → 20
  3. 降序数组:[4,3,2,1] → 20
  4. 随机数组:[5,4,3,6,2] → 74
  5. 边界用例:[1] → 0

在LeetCode上提交前,建议先在本地运行这些测试用例,特别是要测试n=1和n=2的情况。

7. 性能优化实战技巧

7.1 记忆化搜索实现

除了自底向上的动态规划,我们还可以使用记忆化搜索(递归+缓存)的方式实现:

from functools import lru_cache @lru_cache(maxsize=None) def dfs(i, j): if i == j: return (0, arr[i]) min_cost = float('inf') max_val = 0 for k in range(i, j): left_cost, left_max = dfs(i, k) right_cost, right_max = dfs(k+1, j) total = left_cost + right_cost + left_max * right_max if total < min_cost: min_cost = total max_val = max(left_max, right_max) return (min_cost, max_val)

这种方法代码更直观,但可能因为递归深度问题在大数据量时出现栈溢出。

7.2 空间优化技巧

观察到dp[i][j]只依赖于长度更短的子区间,因此可以优化空间复杂度。不过对于这个问题,由于需要同时维护max_val数组,空间优化效果有限。

8. 面试中的考察重点

在技术面试中,面试官通常会关注:

  1. 能否正确识别出这是一个区间DP问题
  2. 状态定义是否合理,转移方程是否正确
  3. 边界条件处理是否完善
  4. 是否能从O(n^3)的解法进一步优化
  5. 代码实现的整洁度和可读性

建议在面试中:

  1. 先明确问题要求,确认理解正确
  2. 从暴力解法开始,逐步优化
  3. 讨论时间空间复杂度
  4. 提出可能的优化方向
  5. 编写代码时注意变量命名和注释

9. 学习资源推荐

要深入掌握这类动态规划问题,推荐以下资源:

  1. 《算法导论》动态规划章节
  2. LeetCode动态规划专题
  3. 经典论文《The Art of Dynamic Programming》
  4. 在线课程:MIT 6.006 Introduction to Algorithms
  5. 竞赛选手的解题报告(如Codeforces、Topcoder)

10. 个人解题心得

在实际解决这个问题时,我最初尝试了贪心算法但发现无法得到全局最优解。后来通过分析问题特性,意识到这是一个典型的区间划分问题,适合用动态规划解决。在实现过程中,最大的收获是理解了如何将看似复杂的树构建问题转化为区间DP模型。

一个实用的调试技巧是:对于小规模测试用例,手动绘制所有可能的树结构并计算成本,然后与程序输出对比。这能帮助快速发现状态转移方程中的错误。

最后,单调栈的优化解法让我认识到,很多DP问题都有其特殊的性质可以被利用来优化。这提醒我在解决问题时,不仅要掌握通用解法,还要注意观察问题的特殊结构。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 17:37:33

千寻智能十年技术演进:从CNN到AGI的AI架构升级

1. 项目概述&#xff1a;千寻智能的十年技术演进之路 2015年那个春天&#xff0c;我和团队在杭州一间不足50平米的办公室里&#xff0c;用三台二手服务器搭建了最初的千寻智能原型系统。当时没人能想到&#xff0c;这个从图像识别起家的创业项目&#xff0c;会在十年间经历四次…

作者头像 李华
网站建设 2026/10/26 17:35:01

融合古典管理智慧的Vibe Coding多Agent系统设计

1. 项目概述&#xff1a;当Agent管理遇上Vibe Coding最近在开发一个多Agent协作系统时&#xff0c;突然意识到传统的管理方式已经跟不上Vibe Coding的工作节奏。那种在咖啡厅里戴着降噪耳机&#xff0c;随着音乐节奏敲代码的状态下&#xff0c;传统的Agent管理框架显得格格不入…

作者头像 李华
网站建设 2026/10/26 17:34:58

AI辅助初始处方获批:医疗AI落地的关键切口

1. 这不是“AI医生”&#xff0c;而是监管框架下的一次精准切口 “Nolla Health 获监管批准让 AI 开出初始处方”——这句话乍看像科幻片开场&#xff0c;实则是一记沉稳、克制、经过千锤百炼的临床落地信号。它背后没有“取代医生”的喧嚣&#xff0c;也没有“全自动诊疗”的幻…

作者头像 李华
网站建设 2026/10/26 17:33:51

红蓝对抗实战指南:构建企业网络安全防御体系

1. 红蓝对抗的本质与价值红蓝对抗本质上是一场精心设计的网络安全实战演练。红队扮演攻击方&#xff0c;模拟真实威胁行为&#xff1b;蓝队作为防守方&#xff0c;负责检测和响应。这种对抗模式最早源于军事领域的作战演习&#xff0c;后被引入到网络安全领域。为什么企业需要红…

作者头像 李华
网站建设 2026/10/26 17:33:50

基于CNN的玻璃破碎检测技术实践与优化

1. 项目背景与核心价值玻璃破碎检测在工业质检、安防监控和智能家居领域有着广泛的应用需求。传统人工检测方式效率低下且容易漏检&#xff0c;而基于规则算法的计算机视觉方案又难以应对复杂环境下的检测场景。这正是我们选择卷积神经网络(CNN)来解决这个问题的根本原因——它…

作者头像 李华
网站建设 2026/10/26 17:33:03

多端短剧管理后台:Spring Boot与Vue3的高效架构实践

1. 项目背景与核心价值在移动互联网内容爆发式增长的今天&#xff0c;短剧作为一种新兴的内容形式正在快速崛起。作为内容运营团队&#xff0c;我们经常面临一个典型困境&#xff1a;同一部短剧需要在APP、微信小程序、H5页面等多个渠道同步发布&#xff0c;但每个平台的后台管…

作者头像 李华