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

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

  • 树上状态压缩

  • 前缀和的可迁移模板

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

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

相关推荐
鱼跃鹰飞2 小时前
Leetcode尊享面试100题:1060. 有序数组中的缺失元素
算法·leetcode·面试
啊我不会诶2 小时前
AtCoder Beginner Contest 438 vp补题
算法
computersciencer2 小时前
用最小二乘法求解一元一次方程模型的参数
算法·机器学习·最小二乘法
mit6.8242 小时前
扫描线|离散化|seg+二分|卡常
算法
不穿格子的程序员2 小时前
从零开始写算法——二叉树篇6:二叉树的右视图 + 二叉树展开为链表
java·算法·链表
大志若愚YYZ2 小时前
ROS2学习 C++中的this指针
c++·学习·算法
AI科技星2 小时前
光子的几何起源与量子本质:一个源于时空本底运动的统一模型
服务器·人工智能·线性代数·算法·机器学习
源代码•宸2 小时前
Golang原理剖析(map面试与分析)
开发语言·后端·算法·面试·职场和发展·golang·map
CodeByV2 小时前
【算法题】栈
算法