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

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

  • 树上状态压缩

  • 前缀和的可迁移模板

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

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

相关推荐
Queenie_Charlie21 小时前
二分匹配
c++·算法·二分匹配
历程里程碑21 小时前
链表--排序链表
大数据·数据结构·算法·elasticsearch·链表·搜索引擎·排序算法
IT猿手21 小时前
基于动态三维环境下的Q-Learning算法无人机自主避障路径规划研究,MATLAB代码
算法·matlab·无人机·动态路径规划·多无人机动态避障路径规划
美式请加冰21 小时前
栈的介绍和使用(算法)
数据结构·算法·leetcode
不染尘.21 小时前
排序算法详解2
数据结构·c++·算法·排序算法
cm65432021 小时前
C++代码切片分析
开发语言·c++·算法
重生之我是Java开发战士21 小时前
【递归、搜索与回溯】FloodFill算法:图像渲染,岛屿数量,岛屿的最大面积,被围绕的区域,太平洋大西洋水流问题,扫雷游戏,衣橱整理
算法·leetcode·深度优先
YUANQIANG202421 小时前
PPO算法典型思路
算法·机器学习
twc82921 小时前
大模型评估指标简要说明
算法·大模型·bleu
淀粉肠kk21 小时前
【C++】C++11可变参数模板和emplace系列接口
算法