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];
    }
};

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

  • 树上状态压缩

  • 前缀和的可迁移模板

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

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

相关推荐
jidaowansui19 分钟前
B3643 图的存储———>数组模拟邻接矩阵
算法
y1su30 分钟前
Leetcode 二分模板
java·数据结构·算法·leetcode·排序算法
youshi666837 分钟前
平衡车——PID算法优化
stm32·单片机·嵌入式硬件·算法·pid·平衡车
聪明蛋子哟2 小时前
不仅仅是向量检索:结合知识图谱与Tool Calling的混合增强生成(Hybrid RAG)方案
人工智能·算法·知识图谱
Pniubi2 小时前
力扣55跳跃游戏(贪心)
算法·leetcode·游戏
jyyyx的算法博客2 小时前
传球游戏【思维】
算法
FL16238631292 小时前
关于2026年南昌大学811信号与系统最后一题电路系统解题思路
算法
殷色玫瑰2 小时前
C++ STL:map 与 set 详解——从底层红黑树到实际应用
c语言·数据结构·c++·算法·visualstudio·rpc
行者全栈架构师2 小时前
【鸿蒙心迹】鸿蒙网络请求架构实战——@ohos.net.http 到 Axios 封装、拦截器与统一错误处理(HarmonyOS 7.x)
前端·算法·架构
小宋10213 小时前
OpenTelemetry GenAI可观测性实战:串起模型、工具、Token与错误
java·人工智能·算法·贪心算法