【力扣100题】38.路径总和 III

题目描述

给定一个二叉树的根节点 root,和一个整数 targetSum,求该二叉树里节点值之和等于 targetSum 的路径的数目。

  • 路径不需要从根节点开始
  • 路径不需要在叶子节点结束
  • 路径方向必须是向下的(只能从父节点到子节点)

示例 1:

复制代码
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3

树结构:
        10
       /  \
      5   -3
     / \    \
    3   2   11
   / \      \
  3  -2      1

三条路径:
1. 5 → 3 (5+3=8)
2. 5 → 2 → 1 (5+2+1=8)
3. -3 → 11 (-3+11=8)

示例 2:

复制代码
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3

树结构:
        5
       / \
      4   8
     /   / \
    11  13  4
     \      /
      7    2
     /    /
    2    1

三条路径:
1. 5 → 4 → 11 → 2 (5+4+11+2=22)
2. 5 → 4 → 8 → 4 → 1 (5+4+8+4+1=22)
3. 4 → 11 → 7 (4+11+7=22)

提示:

  • 二叉树的节点个数的范围是 [0, 1000]
  • -10^9 <= Node.val <= 10^9
  • -1000 <= targetSum <= 1000

解题思路总览

方法 思路 时间复杂度 空间复杂度 适用场景
方法一:前缀和 + HashMap 前缀和思想,O(1) 查找路径和 O(n) O(h) 面试首选
方法二:双重 DFS 以每个节点为起点的 DFS O(n^2) O(h) 容易想到,但不是最优

核心原理: 利用前缀和思想,将"路径和等于 targetSum"转化为"当前前缀和 - targetSum 是否在历史前缀和中"


方法一:前缀和 + HashMap(推荐)

思路

这是一道经典的前缀和题目。核心思想:

  1. 前缀和定义:从根节点到当前节点的路径上所有节点值之和

  2. 路径和转化:如果从节点 A 到节点 B 的路径和等于 targetSum,那么:

    • 前缀和[B] - 前缀和[A的父节点] = targetSum
    • 前缀和[B] - targetSum 应该在历史前缀和中存在
  3. HashMap 记录:用 HashMap 记录从根到当前节点路径上,每个前缀和出现的次数

  4. 初始化 :添加 cnt[0] = 1,表示空路径(当前缀和等于 targetSum 时,需要用到)

完整代码

cpp 复制代码
/**
 * 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:
    void dfs(TreeNode* node, long long curSum, int targetSum,
            unordered_map<long long, int>& cnt, int& ans) {
        if (!node) return;

        // 1. 计算当前节点的前缀和
        curSum += node->val;

        // 2. 如果存在一条从某个祖先到当前节点的路径和等于 targetSum
        //    即:curSum - targetSum 在历史前缀和中存在
        ans += cnt[curSum - targetSum];

        // 3. 将当前前缀和加入 HashMap
        cnt[curSum]++;

        // 4. 递归处理左右子树
        dfs(node->left, curSum, targetSum, cnt, ans);
        dfs(node->right, curSum, targetSum, cnt, ans);

        // 5. 回溯:移除当前前缀和(因为要返回上层继续处理)
        cnt[curSum]--;
    }

    int pathSum(TreeNode* root, int targetSum) {
        // 初始化:空路径的前缀和为 0,出现 1 次
        unordered_map<long long, int> cnt = {{0, 1}};
        int ans = 0;
        dfs(root, 0, targetSum, cnt, ans);
        return ans;
    }
};

算法流程图

以示例 1 为例,root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8

复制代码
树结构:
        10(10)
       /      \
    5(15)     -3(7)
    /   \       \
 3(18)  2(17)   11(18)
 /  \     \
3   -2    1

括号内为从根开始的前缀和

DFS 遍历过程:

Step 1: node=10, curSum=0+10=10
  cnt[10-8]=cnt[2]=0 → ans=0
  cnt[10]++ → cnt[10]=1
  递归左子树 5

Step 2: node=5, curSum=10+5=15
  cnt[15-8]=cnt[7]=0 → ans=0
  cnt[15]++ → cnt[15]=1
  递归到 3

Step 3: node=3, curSum=15+3=18
  cnt[18-8]=cnt[10]=1 → ans=1  (路径:10→5→3)
  cnt[18]++ → cnt[18]=1
  递归到 3 的左子节点

Step 4: node=3(叶子), curSum=18+3=21
  cnt[21-8]=cnt[13]=0 → ans=1
  cnt[21]++ → cnt[21]=1
  回溯,cnt[21]--, cnt[18]--(继续递归其他分支)

...继续遍历所有节点...

最终 ans = 3

这 3 条路径分别是:
1. 节点5 → 节点3(5+3=8)
2. 节点5 → 节点2 → 节点1(5+2+1=8)
3. 节点-3 → 节点11(-3+11=8)

逐行解析

cpp 复制代码
void dfs(TreeNode* node, long long curSum, int targetSum,
        unordered_map<long long, int>& cnt, int& ans) {
    if (!node) return;
  • DFS 递归函数。
  • curSum:从根到当前节点的前缀和。
  • cnt:HashMap,键是前缀和,值是该前缀和出现的次数。
  • ans:满足条件的路径数量(引用传递,全局共享)。
cpp 复制代码
    curSum += node->val;
  • 计算当前节点的前缀和 = 父节点前缀和 + 当前节点值。
cpp 复制代码
    ans += cnt[curSum - targetSum];
  • 核心逻辑 :如果存在一个历史前缀和 s,使得 curSum - s = targetSum(即 s = curSum - targetSum),那么从该历史节点到当前节点的路径和就等于 targetSum。
  • cnt[curSum - targetSum] 表示有多少个历史前缀和满足条件。
  • 这里利用了初始化时添加的 cnt[0] = 1,所以当 curSum == targetSum 时,cnt[0] = 1 表示"从根到当前节点的路径自己就等于 targetSum"。
cpp 复制代码
    cnt[curSum]++;
  • 将当前前缀和加入 HashMap,记录出现次数 +1。
  • 这样在递归到子节点时,子节点就能找到这个前缀和。
cpp 复制代码
    dfs(node->left, curSum, targetSum, cnt, ans);
    dfs(node->right, curSum, targetSum, cnt, ans);
  • 递归处理左右子树,传递当前前缀和 curSum
cpp 复制代码
    cnt[curSum]--;
  • 回溯:递归返回时,需要将当前前缀和从 HashMap 中移除。
  • 这是因为当前节点的路径只在其子树范围内有效,不能影响其他分支。
cpp 复制代码
    unordered_map<long long, int> cnt = {{0, 1}};
  • 初始化cnt[0] = 1 表示空路径的前缀和为 0,出现了 1 次。
  • 作用:当当前缀和等于 targetSum 时,cnt[curSum - targetSum] = cnt[0] = 1,确保能计数到从根开始的路径。

复杂度分析

复杂度 说明
时间 O(n) 每个节点访问一次
空间 O(h) HashMap 存储路径上的前缀和,最多 h 个(树高)

优点: 时间复杂度优秀,代码简洁
缺点: 需要理解前缀和 + 回溯的思想


方法二:双重 DFS

思路

以每个节点为起点,向下 DFS 累加路径和,统计等于 targetSum 的路径数量。

完整代码

cpp 复制代码
class Solution {
public:
    int ans = 0;

    int pathSum(TreeNode* root, int targetSum) {
        if (!root) return 0;

        // 以 root 为起点的路径
        dfs(root, targetSum);

        // 递归处理左右子树作为新的起点
        pathSum(root->left, targetSum);
        pathSum(root->right, targetSum);

        return ans;
    }

    void dfs(TreeNode* node, long long sum) {
        if (!node) return;

        sum -= node->val;
        if (sum == 0) ans++;  // 找到一条路径

        if (node->left) dfs(node->left, sum);
        if (node->right) dfs(node->right, sum);
    }
};

复杂度分析

复杂度 说明
时间 O(n^2) 每个节点作为起点需要一次 DFS
空间 O(h) 递归栈深度

优点: 思路直观,容易想到
缺点: 时间复杂度较高,不适合大规模数据


两种方法对比

维度 方法一 前缀和+HashMap 方法二 双重 DFS
代码复杂度 中等 简单
时间复杂度 O(n) O(n^2)
空间复杂度 O(h) O(h)
面试推荐度 首选 简化版/容易想到

面试追问 FAQ

问题 解答
Q1:为什么需要初始化 cnt[0] = 1 当当前缀和恰好等于 targetSum 时,说明从根到当前节点的路径本身就是一条满足条件的路径。此时 curSum - targetSum = 0cnt[0] = 1 使得 ans += 1 能正确计数。
Q2:为什么在递归返回时需要 cnt[curSum]-- 这是回溯操作。当前节点的路径只在其子树范围内有效。当递归返回到父节点后,父节点需要继续探索其他分支,此时当前节点的前缀和不应该再影响其他路径的计算。
Q3:如何理解前缀和思想? 从根到当前节点的路径可以看成一个数组,前缀和就是前 i 个元素的和。路径和等于 targetSum 意味着:存在 j < i,使得 前缀和[i] - 前缀和[j] = targetSum,即 前缀和[j] = 前缀和[i] - targetSum
Q4:为什么不用记录路径的起止位置? 因为我们只关心前缀和的差值是否等于 targetSum,不需要具体知道是哪两个节点。只要前缀和的差值存在,就说明存在一条路径和等于 targetSum。
Q5:为什么使用 long long 而不是 int 题目中 Node.val 的范围是 -10^9 到 10^9,路径可能很长,累加后可能超过 int 的范围(32 位有符号整数最大约 2.1 * 10^9)。为了避免溢出,使用 long long。
Q6:如果要找最短路径怎么办? 需要额外记录最短路径长度,在找到满足条件的路径时更新。这需要在递归中传递当前路径长度。

相关题目

题目 难度 关键点
437. 路径总和 III 中等 前缀和,HashMap
112. 路径总和 简单 从根到叶子的路径和
113. 路径总和 II 中等 返回所有路径
124. 二叉树中的最大路径和 困难 前缀和变形
560. 和为 K 的子数组 中等 一维版本的前缀和

总结

要点 说明
核心原理 前缀和思想:curSum - targetSum 在历史前缀和中存在,则存在满足条件的路径
关键技巧 初始化 cnt[0] = 1 计算空路径,cnt[curSum]++ 记录前缀和
回溯操作 递归返回时 cnt[curSum]--,移除当前节点的影响
时间复杂度 O(n),每个节点访问一次
空间复杂度 O(h),HashMap 存储路径上的前缀和

相关推荐
小侯不躺平.1 小时前
C++ Boost库【2】 --stringalgo字符串算法
linux·c++·算法
流年如夢1 小时前
二叉树详解
c语言·数据结构·算法
xiaoxiaoxiaolll2 小时前
Nature Communications:三维超原子库+原子层保护,突破全彩VR超透镜量产瓶颈
人工智能·算法
仍然.2 小时前
算法题目---栈
算法
feifeigo1232 小时前
基于布谷鸟算法的配电网分布式电源选址定容 MATLAB 实现
开发语言·算法·matlab
MicroTech20252 小时前
微算法科技(NASDAQ: MLGO)噪声图像的量子图像边缘提取算法:技术革新与产业赋能
科技·算法·量子计算
大模型最新论文速读2 小时前
EvoLM:8B 模型自写评分标准,RL 后超越 GPT-4
人工智能·深度学习·算法·机器学习·自然语言处理
木子墨5162 小时前
工程算法实战 | 从LRU到手写本地缓存:LinkedHashMap → 双向链表+哈希表 → Caffeine 原理
java·数据结构·算法·链表·缓存
数智工坊2 小时前
【Offline RL1】离线强化学习全景:从基础理论到前沿算法与工业落地
算法