路径总和 III 前缀法核心分析

LeetCode 437. 路径总和 III 的学习

题目链接:LeetCode 437. Path Sum III

🧩 题目描述

给定一个二叉树的根节点 root 和一个整数 targetSum,求 路径和等于 targetSum 的路径个数。路径不需要从根节点开始,也不需要在叶子节点结束,但路径必须向下(只能从父节点走到子节点)。


方法一:暴力 DFS(从每个节点出发)

✅ 思路

  1. 遍历整棵树,对每个节点都尝试作为起点去找路径。
  2. 用 DFS 深度遍历每条从当前节点出发的路径,判断路径和是否等于 targetSum
  3. 对所有节点都做一遍这个过程,统计结果。

🚫 缺点

  • 时间复杂度较高:每个节点都要作为起点做一遍 DFS
  • 容易内存超限或超时,尤其是节点值较大时

✅ Java代码

public class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        if (root == null) return 0;
        // 从当前节点出发找路径 + 左子树递归 + 右子树递归
        return dfs(root, targetSum) 
            + pathSum(root.left, targetSum) 
            + pathSum(root.right, targetSum);
    }

    private int dfs(TreeNode node, long target) {
        if (node == null) return 0;
        int count = 0;
        if (node.val == target) count++;
        count += dfs(node.left, target - node.val);
        count += dfs(node.right, target - node.val);
        return count;
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(N²),最差情况是每个节点都遍历一次其所有子节点
  • 空间复杂度:O(H),H 为树的高度(递归栈)

方法二:前缀和 + 哈希表优化

✅ 思路

  1. 维护一个 Map<Long, Integer> 表示前缀路径和的出现次数。
  2. 每次到达一个节点,计算当前从根到这个节点的和 currSum
  3. 判断是否存在某个前缀和 currSum - targetSum
    • 如果存在,说明从这个前缀节点到当前节点构成一条目标路径。
  4. 递归左/右子树,并回溯移除当前节点的路径和

✅ 关键点

  • 把树路径转换成“前缀和差值问题”

  • 类似数组中找“连续子数组和为 K”的问题

    ✅ 核心:map 中的 “前缀和” 到底是什么?为什么要存?


    🧠 简单类比:数组中的前缀和

    先看一维数组:

    int[] arr = {1, 2, 3, 4};
    

    我们要找连续子数组和等于 5 的个数,我们可以用前缀和:

    prefixSum(i) 表示从 arr[0] 到 arr[i] 的总和
    
    prefixSum(j) - prefixSum(i) = arr[i+1] + ... + arr[j]
    

    所以,如果你知道某个 prefixSum 值在前面出现过,那么就可以快速判断后面有没有子数组和为目标。


    🧩 类比到树上

    在一棵树中,我们可以把 “从根节点到某个节点的路径和” 看作前缀和。

    假设路径如下:

           10
           /
          5
         /
        3
    

    路径:

    • 10 -> prefixSum = 10
    • 10 + 5 -> prefixSum = 15
    • 10 + 5 + 3 -> prefixSum = 18

    现在假设我们 target = 8,我们希望找到从某个祖先节点出发,到当前节点,路径和是 8

    • 当前节点 prefixSum 是 18
    • 想找从某个祖先到当前路径为 8,就等价于:
      18 - x = 8 → x = 10

    这说明:如果之前有路径和为 10 的前缀出现过,那这条从该前缀之后到当前节点的路径,一定和为 8!


    ✅ 所以我们记录什么?

    我们用:

    Map<Long, Integer> map
    

    记录:

    • 键:从根到某个祖先节点的路径和
    • 值:这个路径和出现了多少次(因为多条路径可能产生相同前缀和)

    ✅ 用例演示(一步步)

    输入树:

          10
         /  \
        5   -3
       / \    \
      3   2    11
    

    目标 target = 8

    我们 DFS 遍历树,每走一个节点,就计算到该节点的 currSum,并:

    • 查找 map.get(currSum - target),看前面有没有出现过这个“前缀和”
    • 如果有,那就说明有路径和 = target

    ✅ 示例代码中的关键一行:

    int res = map.getOrDefault(currSum - targetSum, 0);
    

    它的含义是:

    当前节点能否通过某个祖先节点构成一条路径和为 target 的路径

    如果有,就说明路径数量可以加上。

✅ Java代码

public class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        Map<Long, Integer> map = new HashMap<>();
        map.put(0L, 1); // base case
        return dfs(root, 0L, targetSum, map);
    }

    private int dfs(TreeNode node, long currSum, int targetSum, Map<Long, Integer> map) {
        if (node == null) return 0;

        currSum += node.val;
        int count = map.getOrDefault(currSum - targetSum, 0);

        map.put(currSum, map.getOrDefault(currSum, 0) + 1);
        count += dfs(node.left, currSum, targetSum, map);
        count += dfs(node.right, currSum, targetSum, map);
        map.put(currSum, map.get(currSum) - 1); // 回溯

        return count;
    }
}

⏱️ 复杂度分析

  • 时间复杂度:O(N),每个节点只访问一次
  • 空间复杂度:O(N),哈希表和递归栈的最大空间

🧠 总结对比

方法 时间复杂度 空间复杂度 优点 缺点
暴力 DFS O(N²) O(H) 思路简单 性能差,易超时
前缀和优化法 O(N) O(N) 性能好,易扩展 思路抽象,需要理解
posted @ 2025-06-18 15:45  发光的反派  阅读(16)  评论(0)    收藏  举报