题目描述

✅ LCR 050. 路径总和 III

image-20260929005738708

image-20260929005738712

题意分析

统计节点值之和等于 targetSum 的非空路径数。路径只能从父节点向子节点延伸,起点不必是根,终点也不必是叶子。节点值可以为负数,路径和不具有单调性,不能因当前和超过目标而停止搜索。

解法:前缀和与回溯计数

核心思路

[!blue]

固定当前节点作为终点,所有合法路径都是根到当前节点这条链的一段后缀。设根到当前节点的累计和为 s,某个祖先处的累计和为 p,那么从该祖先的下一个节点走到当前节点,路径和就是 s-p。要等于目标,只需找出祖先链上有多少个 p=s-targetSum。

用 cnt 记录当前祖先链中每种前缀和出现的次数。同一前缀和值可能出现在多个祖先处,每个位置都对应不同的路径起点,因此需要频次而非仅记录是否存在。预置 cnt[0]=1,表示根之前的空前缀,使从根开始的路径也能用相减公式统计。

到达当前节点后,先累加节点值得到 s,再查询 cnt[s-targetSum]。此时表中只应有当前节点之前的祖先前缀,查到的都是非空路径。随后将 s 登记到表中,供左右子树作为祖先前缀使用。

两侧递归结束后,把 cnt[s] 减一,恢复进入当前节点前的状态。这样一条分支中的节点不会被另一分支误当成祖先。每条合法路径在自己的终点被统计一次,对当前节点与左右子树的计数相加,就得到总数。

解题步骤

  • 初始化空前缀计数,从累计和 0 开始递归;空节点返回 0。
  • 加上当前节点值,用 cnt[s-targetSum] 得到以当前节点为终点的路径数。
  • 执行 cnt[s]++,递归左右孩子,并累加它们的返回值。
  • 执行 cnt[s]-- 撤销当前前缀,再返回当前子树内统计到的路径数。

查询必须早于登记:当目标为 0 时,若先登记当前前缀,就会把当前前缀与自身相减,当成一条实际上不含任何节点的空路径。单个零节点的合法路径应由它之前的祖先前缀来计数。累计和可能超过 32 位,Java 用 long,Go 显式使用 int64。

代码实现

class Solution {
    private Map<Long, Integer> cnt = new HashMap<>();
    private int targetSum;

    public int pathSum(TreeNode root, int targetSum) {
        cnt.put(0L, 1);
        this.targetSum = targetSum;

        return dfs(root, 0);
    }

    private int dfs(TreeNode node, long s) {
        if (node == null) {
            return 0;
        }

        s += node.val;
        int answer = cnt.getOrDefault(s - targetSum, 0);

        cnt.merge(s, 1, Integer::sum);
        answer += dfs(node.left, s);
        answer += dfs(node.right, s);
        cnt.merge(s, -1, Integer::sum);

        return answer;
    }
}
func pathSum(root *TreeNode, targetSum int) int {
    cnt := map[int64]int{0: 1}
    var dfs func(*TreeNode, int64) int
    dfs = func(node *TreeNode, s int64) int {
        if node == nil {
            return 0
        }
        s += int64(node.Val)
        answer := cnt[s-int64(targetSum)]
        cnt[s]++
        answer += dfs(node.Left, s) + dfs(node.Right, s)
        cnt[s]--
        return answer
    }
    return dfs(root, 0)
}

复杂度分析

  • 时间复杂度:期望 $O(n)$,每个节点只查询和更新哈希计数常数次。
  • 空间复杂度:$O(n)$。当前实现不删除计数归零的键,一次遍历最多留下 $n$ 个不同节点前缀;递归栈为 $O(h)$。非零计数只对应当前链,不意味着映射实际只占 $O(h)$ 空间。

关键点总结

[!green]

  • 查询的是当前祖先链上的前缀频次,不能使用整棵树已经访问过的所有前缀。
  • 空前缀让从根开始的路径参与计数;先查询再登记排除空路径。
  • 进入时登记、离开时撤销,左右子树才能安全共用一张表。

易错点总结

[!yellow]

  • 预置空前缀 0 一次,先查询当前和减 target,再登记当前前缀。
  • 两侧递归完成后撤销当前前缀,让非零计数只描述当前祖先路径。
  • 累计和使用 64 位,节点值可有正负,不能因当前和超过目标就剪枝。
  • 路径只能向下,不允许从左子树经过父亲转到右子树。

相似题目

题目 难度 关联与区别
560. 和为 K 的子数组 中等 把数组的前缀和频次计数推广到树上,离开分支必须撤销当前前缀。
113. 路径总和 II 中等 原题只枚举根到叶路径,本题允许任意祖先作为起点且只统计数量。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/35331271
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!