前言数字三角形是一道经典的动态规划dynamic programming, DP入门题给定一个由数字组成的三角形从顶部出发每次只能走到下一行相邻的两个位置之一求到达底部时路径上数字之和的最大值。它的规模不大暴力枚举所有路径也能做但路径数量随层数指数增长层数稍多就跑不动。DP 的价值在于把「指数级的路径枚举」压缩成「每层每个位置一个最优值」。最常见的误解是「自顶向下递推时每个位置只保留最大值就够了」。这在求和问题上成立因为加法满足最优子结构到达某位置的最大和等于该位置的值加上到达其上方两个父位置的最大和中的较大者。但如果题目改成「求和的最小值」或者「记录路径」状态定义与转移就要跟着变不能照搬。另一个误解是忽略负数的存在。很多数字三角形的样例全是正数于是有人把「最优值」初始化成 0遇到全负的输入就得到错误答案。本文分别给出自顶向下与自底向上两种写法并说明空间优化与路径还原的处理方式。一、状态定义与转移方程把三角形存成二维数组a[i][j]其中0 j i n。定义dp[i][j]为「从顶部走到第 i 行第 j 列时能获得的最大和」。由于每一步只能从左上(i-1, j-1)或正上(i-1, j)过来转移方程是dp[i][j] a[i][j] max(dp[i-1][j-1], dp[i-1][j])边界要单独处理最左列j 0只能从正上方来最右列j i只能从左上方来顶点dp[0][0] a[0][0]。最终答案是最后一行的最大值而不是dp[n-1][n-1]——终点可以是底部的任意一列。这套定义的时间复杂度是 O(n²)空间复杂度也是 O(n²)。由于每一行的状态只依赖上一行空间可以压缩到 O(n)从右往左更新一维数组避免覆盖还未使用的值。二、自底向上更简洁的写法自底向上的思路是反过来定义状态dp[j]表示「从第 i 行第 j 列出发走到最底部能得到的最大和」。转移变成dp[j] a[i][j] max(dp[j], dp[j1])这里dp[j]是下一行同列的值dp[j1]是下一行右邻列的值。初值是最后一行的元素本身。因为每次只用到下一行一维数组就够而且不需要处理左右边界的特判——j的范围是0..ij1最大取到i1恰好是下一行的有效下标。// 适用于 C17#include algorithm#include cstddef#include iostream#include vectorlong long max_path_sum(const std::vectorstd::vectorint tri) {if (tri.empty()) {return 0;}std::vectorlong long dp(tri.back().begin(), tri.back().end());for (int i static_castint(tri.size()) - 2; i 0; --i) {for (int j 0; j i; j) {dp[static_caststd::size_t(j)] static_castlong long(tri[static_caststd::size_t(i)][static_caststd::size_t(j)]) std::max(dp[static_caststd::size_t(j)],dp[static_caststd::size_t(j) 1]);}}return dp[0];}这段代码有几个点需要核对。i用int并从size() - 2开始是因为size()是std::size_t直接减 2 在空三角或只有一行时会下溢函数开头已经处理了空输入但「只有一行」时size() - 2仍然会回绕所以先转成int再减。dp用long long是因为路径和可能超出int范围而有符号整数溢出是未定义行为undefined behavior, UB不是简单的「变成负数」。tri.back()复制到dp时std::vectorlong long的迭代器范围构造会把每个int转成long long这是合法的隐式转换。若三角形为空tri.back()是 UB所以必须先判断empty()。三、完整示例与路径还原如果只需要最大值上面的函数就够了。如果还需要输出具体路径就得额外记录「每个位置是从左边还是上边来的」。一种做法是再开一个n × n的char表存 0 表示从左上来、1 表示从正上来另一种做法是在自底向上时记录每步选择了哪一侧。// 适用于 C17#include algorithm#include cstddef#include iostream#include vectorint main() {std::vectorstd::vectorint tri {{7},{3, 8},{8, 1, 0},{2, 7, 4, 4},{4, 5, 2, 6, 5}};const int n static_castint(tri.size());// best[i][j]从 (i, j) 到底部的最大和std::vectorstd::vectorlong long best(static_caststd::size_t(n),std::vectorlong long(static_caststd::size_t(n), 0));for (int j 0; j n; j) {best[static_caststd::size_t(n - 1)][static_caststd::size_t(j)] tri[static_caststd::size_t(n - 1)][static_caststd::size_t(j)];}for (int i n - 2; i 0; --i) {for (int j 0; j i; j) {best[static_caststd::size_t(i)][static_caststd::size_t(j)] tri[static_caststd::size_t(i)][static_caststd::size_t(j)] std::max(best[static_caststd::size_t(i 1)][static_caststd::size_t(j)],best[static_caststd::size_t(i 1)][static_caststd::size_t(j) 1]);}}std::cout max sum: best[0][0] \n;int j 0;for (int i 0; i n; i) {std::cout tri[static_caststd::size_t(i)][static_caststd::size_t(j)];if (i 1 n) {const long long left best[static_caststd::size_t(i 1)][static_caststd::size_t(j)];const long long right best[static_caststd::size_t(i 1)][static_caststd::size_t(j) 1];std::cout - ;if (right left) {j;}}}std::cout \n;return 0;}路径还原的关键是站在(i, j)时比较下一行(i1, j)与(i1, j1)的best值谁大就往谁走。若两者相等走哪边都能得到同样的最大和代码里固定走左边right left才右移保证输出确定。打印箭头与选路判断分开写避免把「输出什么」和「走到哪」混在同一个表达式里。用样例手工推演底部是4 5 2 6 5倒数第二行2 7 4 4对应2max(4,5)7、7max(5,2)12、4max(2,6)10、4max(6,5)10继续向上最终顶点值应当是 30路径为7 - 3 - 8 - 7 - 5。这个结果可以逐层核对用来检查实现对不对。四、复杂度与变体写法时间空间特点暴力枚举所有路径O(2ⁿ)O(n) 递归栈只适合极小规模自顶向下 记忆化O(n²)O(n²)思路直观需处理边界自底向上二维O(n²)O(n²)便于还原路径自底向上一维O(n²)O(n)只求值时的最优选择几个常见变体值得注意。若把「最大」改成「最小」转移里的max换成min但初始化不能用 0要用足够大的哨兵值或直接以底行为初值。若允许从底部任意位置出发、到顶部结束把三角形上下翻转即可。若移动规则改成「可以走到下一行任意列」问题就退化成「每行取最大值再求和」不再需要 DP。若数字范围很大路径和要用long long甚至在数值超过 64 位时改用高精度。还有一个容易忽略的点三角形的存储不一定要用vectorvectorint。用一维数组按行优先展开a[i * (i 1) / 2 j]定位缓存局部性更好内存分配次数也更少。对于竞赛类输入这种写法很常见对于可读性优先的业务代码二维结构更清楚。常见坑点坑点一把答案取成最后一行的最后一个元素。❌return dp[n - 1][n - 1];终点可以是底部任意一列。✅ 对最后一行取最大值或在自底向上写法中直接返回dp[0]。坑点二自顶向下时不处理左右边界。❌ 统一写max(dp[i-1][j-1], dp[i-1][j])最左列访问dp[i-1][-1]越界。✅ 最左列只取正上方最右列只取左上方或把数组整体右移一列留出哨兵。坑点三用 0 初始化最优值。❌ 全负数三角形时max结果被 0 拉高得到错误答案。✅ 用底行元素本身作为初值或用负无穷哨兵。坑点四路径和用int累加。❌ 层数多、数值大时溢出属于 UB。✅ 用long long保存dp值输入转换时显式提升。坑点五size() - 2在短输入上下溢。❌for (std::size_t i tri.size() - 2; i 0; --i)空或单行时回绕成巨大值。✅ 先转int并在函数开头处理空输入。坑点六还原路径时只看当前层不看子问题的值。❌ 比较a[i1][j]与a[i1][j1]的大小来决定走向。✅ 比较best[i1][j]与best[i1][j1]即两条子路径的最优值。坑点七认为 DP 一定比暴力快。❌ 在 n 只有 5 时也套一层二维表代码更长且没有实际收益。✅ 理解 DP 的收益来自消除重叠子问题规模极小时暴力反而更简单。总结要点结论状态从某位置出发到底部的最大和转移当前值加下方两个子问题中的较大者方向自底向上无需边界特判空间一维数组滚动O(n)路径还原记录选择或反查子问题的值数值用long long防溢出数字三角形的 DP 之所以经典是因为它把「最优子结构」和「重叠子问题」两件事同时展示了出来每个位置的最优值只由下一层的两个位置决定而这两个位置又被上层大量复用。把状态定义从「从顶部到此处」换成「从此处到底部」边界处理立刻简化这也是解题时值得先想一想的切入点。
企业数字化 ERP 产品动态
相关推荐
美赛C题Wordle建模:ARIMA预测、PLS回归与K-means分档 简介:这份2023年美赛C题M奖论文以Wordle游戏数据为研究对象,面向美赛参赛者、数据分析学习者及需要毕业设计参考的读者,提供一套从数据预处理到建模评估的完整研究范例。全文围绕时间序列预测、不同尝试次数得分百分比预测、单词词性与难度分… · 2026/11/5 12:59:38
多因变量无标签小样本:ARIMA、PLS与K-means建模 简介:面向美赛参赛者与数据分析学习者的2023年美赛C题M论文,聚焦Wordle游戏数据的分析与预测,可作为竞赛论文写作、时间序列与多变量建模的学习参考,也适合毕业设计中的案例借鉴。全文围绕数据清洗与标准化、基于9个单词属性的Spe… · 2026/11/5 12:59:38
一物一码防伪溯源系统:扫码验真+红包发放+积分到账全闭环实现 简介:本资源是一款面向企业防伪系统开发者与电商技术运营人员的‘一物一码’防伪溯源解决方案,聚焦商品真伪验证、消费者互动激励与全流程追溯三大核心需求,适用于快消、医药、酒类等高仿风险行业。压缩包共153个文件,含37个PHP后… · 2026/11/5 12:59:38
C++数字三角形问题与dp算法 前言
数字三角形是一道经典的动态规划(dynamic programming, DP)入门题:给定一个由数字组成的三角形,从顶部出发,每次只能走到下一行相邻的两个位置之一,求到达底部时路径上数字之和的最大值。它的规模不大… · 2026/11/5 12:59:38
美赛C题Wordle建模:ARIMA预测、PLS回归与K-means分档 简介:这份2023年美赛C题M奖论文以Wordle游戏数据为研究对象,面向美赛参赛者、数据分析学习者及需要毕业设计参考的读者,提供一套从数据预处理到建模评估的完整研究范例。全文围绕时间序列预测、不同尝试次数得分百分比预测、单词词性与难度分… · 2026/11/5 12:59:38
多因变量无标签小样本:ARIMA、PLS与K-means建模 简介:面向美赛参赛者与数据分析学习者的2023年美赛C题M论文,聚焦Wordle游戏数据的分析与预测,可作为竞赛论文写作、时间序列与多变量建模的学习参考,也适合毕业设计中的案例借鉴。全文围绕数据清洗与标准化、基于9个单词属性的Spe… · 2026/11/5 12:59:38
一物一码防伪溯源系统:扫码验真+红包发放+积分到账全闭环实现 简介:本资源是一款面向企业防伪系统开发者与电商技术运营人员的‘一物一码’防伪溯源解决方案,聚焦商品真伪验证、消费者互动激励与全流程追溯三大核心需求,适用于快消、医药、酒类等高仿风险行业。压缩包共153个文件,含37个PHP后… · 2026/11/5 12:59:38
美赛C题建模全链路:数据预处理、ARIMA预测与PLS-Kmeans分档 简介:围绕2023年美赛C题Wordle数据建模的M论文,以单份PDF形式呈现,适合备战美赛、需要完整赛题解题框架的参赛者,以及做数据分析方向毕业设计的读者参考。压缩包仅含1个PDF文件,大小约2.18MB,篇幅集中&… · 2026/11/5 12:59:38
Text to SQL系统第三层实现:复杂查询意图识别与优化 1. 项目概述Text to SQL系统作为自然语言处理与数据库技术交叉领域的重要应用,正在深刻改变着非技术人员与数据库交互的方式。这个看似简单的"输入问题,输出SQL"功能背后,隐藏着复杂的实现逻辑和技术挑战。今天我们就来拆解这个系统… · 2026/11/5 12:26:29
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07
多智能体协作系统架构设计与实践优化 1. 多智能体协作系统的时代背景2017年Transformer架构的诞生彻底改变了人工智能的发展轨迹。当行业还沉浸在单一大模型带来的性能突破时,Anthropic的研究团队已经将目光投向了更前沿的领域——如何让多个智能体通过协作产生超越单体能力的系统智能。这就像交响乐团中… · 2026/11/5 0:00:39
大模型系统性入门实战指南:从原理到部署微调的全链路路线 提到大模型的系统性入门,我先说个结论:这事真不难,难的是资料太散、路线太多,今天这个框架明天那个工具,新人很容易在“收藏夹吃灰”和“教程看到一半放弃”之间反复横跳。市面上关于大模型的内容少说有几十万篇&#… · 2026/11/5 0:00:39
鸿蒙文件系统沙箱机制解析与开发实践 1. 鸿蒙文件系统沙箱机制深度解析作为一名从Android转向鸿蒙开发的工程师,我深刻体会到文件系统访问是迁移过程中最先遇到的"拦路虎"。在Android上运行良好的文件读写代码,在鸿蒙真机环境却频频出现文件写入后神秘消失或直接抛出权限拒绝错误的… · 2026/11/5 0:00:39
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:07