LeetCode-437-路径总和3

从一次错误实现看懂前缀和解法:Path Sum III 深度解析

有些算法你以为自己会了,

直到它在几个边界条件上毫不留情地反咬你一口。

这篇文章不是从"标准答案"开始,而是从一次真实的错误实现出发,一步步拆解:

  • 为什么思路是对的

  • 为什么结果却是错的

  • 错在哪里

  • 正确解法为什么必须"长这样"

如果你已经刷过 LeetCode 437(Path Sum III),那这篇文章会把你从"会做"推到"真正理解"


一、问题回顾:Path Sum III

题目要求:

给定一棵二叉树和一个目标值 targetSum

统计树中 路径和等于 targetSum 的路径数量

路径可以从任意节点开始,到任意节点结束,

但必须是 向下的连续路径

这道题最容易想到的做法是:

  • 枚举起点

  • 枚举终点

但时间复杂度是 O(n²),在大树上直接超时。

于是我们引出了真正的主角:前缀和(Prefix Sum)


二、正确但危险的思路:树上的前缀和

1. 核心思想

在数组中,前缀和的经典公式是:

复制代码
sum(i, j) = prefix[j] - prefix[i-1]

在树中,路径是从根向下的,因此我们可以把:

  • 从根到当前节点的路径和

  • 看作一个"树上的前缀和"

于是,对任意一个节点:

以该节点为结尾、和为 targetSum 的路径数量 =
当前前缀和 - targetSum 出现过的次数

这一步,是整道题的灵魂。


三、第一次实现:思路对了,结果却错了

很多人(包括我)第一次写出来的代码,大致结构是这样的:

  • DFS 遍历整棵树

  • unordered_map 统计前缀和出现次数

  • 每到一个节点,就尝试统计答案

但实际运行后,你会发现:

  • 有的用例少算

  • 有的用例多算

  • 有的用例看起来"随机错"

这说明:

错误不在思路,而在细节顺序和初始化上。

下面是三个最典型、也最容易被忽略的坑。


四、错误一:忘记初始化 prefixSum = 0

现象

从根节点开始、恰好等于 targetSum 的路径,永远统计不到

原因分析

前缀和解法默认一个前提:

在遍历开始前,
存在一个前缀和为 0 的"虚拟起点"

如果不手动加入:

复制代码
counts[0] = 1;

那么:

  • curSum == targetSum

  • 你实际在查的是 counts[0]

  • 但 map 里根本没有这个 0

本质原因

前缀和不是从第一个节点开始的,而是从"路径开始之前"开始的。


五、错误二:统计顺序写反了(这是最致命的)

错误写法

复制代码
curSum += node->val;
counts[curSum]++;
res += counts[curSum - targetSum];

看似合理,实际很危险

你在做两件事:

  1. 把当前前缀和加入 map

  2. 用 map 来统计答案

但问题是:

你把"当前节点"也当成了"之前的路径"

在某些情况下,会导致:

  • 多统计一条路径

  • 或者产生幽灵答案

正确顺序(必须背下来)

复制代码
curSum += node->val;

// 先统计
res += counts[curSum - targetSum];

// 再记录当前前缀和
counts[curSum]++;

一句话记忆法:

先用历史,再写历史。


六、错误三:unordered_map::operator[] 的隐形副作用

常见写法

复制代码
if (counts[x] > 0) {
    res += counts[x];
}

实际发生了什么?

  • 如果 x 不存在

  • operator[]自动插入一个 key,值为 0

结果:

  • map 里出现大量无意义的 key

  • 调试难度陡增

  • 思维模型被悄悄污染

更干净的写法

复制代码
res += counts[curSum - targetSum];

或者使用 find


七、完整正确解法(回溯是关键)

把上面三个问题全部修正后,完整逻辑是:

  1. DFS 遍历

  2. 维护当前路径前缀和

  3. 用 map 统计历史前缀和

  4. 回溯时撤销当前节点的影响

回溯这一步非常重要:

前缀和只对"当前路径"有效,

离开节点后必须撤销。


八、为什么这道题值得反复咀嚼?

因为它暴露了一个很残酷、也很真实的事实:

算法题的难点,往往不在公式,
而在状态的生命周期管理。

你不是不会前缀和,

你只是还没完全掌控:

  • 状态什么时候生效

  • 什么时候失效

  • 哪些状态属于"历史"


九、总结一句话(送给正在刷题的你)

真正的算法能力,

不是"我知道这道题用什么",

而是:

"我知道每一行代码,
为什么必须写在这里。"

如果你能把这道题讲清楚,

二叉树 + 前缀和这一类问题,

基本就被你拿下了。

复制代码
/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    int targetSum;
    int res;
    std::unordered_map<double, int> counts;
    int pathSum(TreeNode* root, int tS) {
        targetSum = tS;
        res = 0;
        counts[0]=1;
        traverse(root, 0.0);
        return res;

    }
    void traverse(TreeNode* root,  double curSum){
        if(root == nullptr){
            return;
        }
        curSum += root->val;
        if(counts[curSum-targetSum] > 0){
            res += counts[curSum-targetSum];
        }
        ++counts[curSum];
        traverse(root->left, curSum);
        traverse(root->right, curSum);
        --counts[curSum];
    }
};

如果你愿意继续深入,这道题还能引出:

  • 树上状态压缩

  • 前缀和的可迁移模板

  • 为什么高性能代码里要极力避免隐式副作用

这些,才是真正"工程级"的算法理解。

相关推荐
范纹杉想快点毕业几秒前
状态机设计与嵌入式系统开发完整指南从面向过程到面向对象,从理论到实践的全面解析
linux·服务器·数据库·c++·算法·mongodb·mfc
fish-man4 分钟前
测试加粗效果
算法
晓131316 分钟前
第二章 【C语言篇:入门】 C 语言基础入门
c语言·算法
yong999027 分钟前
MATLAB面波频散曲线反演程序
开发语言·算法·matlab
JicasdC123asd42 分钟前
【工业检测】基于YOLO13-C3k2-EIEM的铸造缺陷检测与分类系统_1
人工智能·算法·分类
Not Dr.Wang4221 小时前
自动控制系统稳定性研究及判据分析
算法
VT.馒头1 小时前
【力扣】2722. 根据 ID 合并两个数组
javascript·算法·leetcode·职场和发展·typescript
ffqws_1 小时前
A*算法:P5507 机关 题解
算法
执着2592 小时前
力扣hot100 - 108、将有序数组转换为二叉搜索树
算法·leetcode·职场和发展
2501_901147832 小时前
学习笔记:单调递增数字求解的迭代优化与工程实践
linux·服务器·笔记·学习·算法