题目描述

:::fold-green 相关原题

LeetCode 原题: ✅ 437. 路径总和 III

:::

给定二叉树 root 和整数 target,返回所有节点值之和等于 target 的非空路径。

路径只能从父节点走向子节点,可从任意节点开始,也可在任意节点结束。

结果顺序不限,不同节点组成的路径分别输出。

示例 1:

输入: root = [10,5,-3,3,2,null,11,3,-2,null,1], target = 8
输出: [[5,3],[5,2,1],[-3,11]]
解释: 树按层序表示,null 为空节点。三条路径都向下,且不要求从根 10 开始。

示例 2:

输入: root = [1,1,1], target = 1
输出: [[1],[1],[1]]
解释: 三个不同节点各自构成一条路径,不能按值序列去重。

提示:

  • 采用 n≤1000 的二叉树版本,节点值可为负数。
  • 相同值序列若对应不同节点路径,仍分别保留。
  • 路径和用 64 位整数累计。

题意分析

每条向下路径都是某条根到当前节点路径的一个连续后缀。当前前缀和减去某个祖先之前的前缀和,便得到该路径和;要枚举全部答案,哈希表必须保存前缀出现位置,不能只保存次数。

解法:前缀和记录起点并复制路径

核心思路

[!blue]

DFS 中 path 保存当前根到节点的值,sum 保存其 64 位和;starts[value] 保存当前祖先链上该前缀和对应的全部路径长度。预置和为 0、长度为 0,覆盖从根开始的路径。

进入节点后,所有前缀和为 sum-target 的旧位置都是合法起点,复制 path[start:] 加入结果。必须先查询再登记当前前缀,避免 target == 0 时把当前位置之后的空路径计入。相同和值有多个位置时全部保留,对应不同节点路径。

递归处理左右子树后,撤销当前前缀的最后一次登记并移除路径末项。这样哈希表只描述当前祖先链,不会把兄弟分支拼成不合法的向下路径;结果副本不会被回溯修改。

解题步骤

  1. DFS 保存当前路径和、节点路径,以及每个祖先前缀对应的路径长度。
  2. 查询 sum-target 的全部起点,复制对应后缀加入结果,再登记当前前缀。
  3. 处理孩子后撤销当前前缀与路径末项。

代码实现

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;

    TreeNode(int val) {
        this.val = val;
    }
}

class Solution {
    public List<List<Integer>> paths(TreeNode root, long target) {
        List<List<Integer>> out = new ArrayList<>();
        Map<Long, List<Integer>> starts = new HashMap<>();

        starts.put(0L, new ArrayList<>(List.of(0)));
        dfs(root, 0, target, new ArrayList<>(), starts, out);

        return out;
    }

    private void dfs(
            TreeNode node,
            long sum,
            long target,
            List<Integer> path,
            Map<Long, List<Integer>> starts,
            List<List<Integer>> out) {
        if (node == null) {
            return;
        }

        sum += node.val;
        path.add(node.val);

        for (int start : starts.getOrDefault(sum - target, List.of())) {
            out.add(new ArrayList<>(path.subList(start, path.size())));
        }

        starts.computeIfAbsent(sum, k -> new ArrayList<>()).add(path.size());
        dfs(node.left, sum, target, path, starts, out);
        dfs(node.right, sum, target, path, starts, out);
        List<Integer> positions = starts.get(sum);

        positions.remove(positions.size() - 1);

        if (positions.isEmpty()) {
            starts.remove(sum);
        }

        path.remove(path.size() - 1);
    }
}
type TreeNode struct {
    Val         int
    Left, Right *TreeNode
}

func paths(root *TreeNode, target int64) [][]int {
    out := [][]int{}
    path := []int{}
    starts := map[int64][]int{
        0: {
            0,
        },
    }
    var dfs func(*TreeNode, int64)
    dfs = func(node *TreeNode, sum int64) {
        if node == nil {
            return
        }
        sum += int64(node.Val)
        path = append(path, node.Val)
        for _, start := range starts[sum-target] {
            out = append(out, append([]int(nil), path[start:]...))
        }
        starts[sum] = append(starts[sum], len(path))
        dfs(node.Left, sum)
        dfs(node.Right, sum)
        starts[sum] = starts[sum][:len(starts[sum])-1]
        if len(starts[sum]) == 0 {
            delete(starts, sum)
        }
        path = path[:len(path)-1]
    }
    dfs(root, 0)
    return out
}

复杂度分析

  • 时间复杂度:期望 $O(n+R)$。
  • 空间复杂度:辅助空间 $O(h)$,结果空间 $O(R)$;R 为所有输出路径的总节点数,h 为树高。

关键点总结

[!green]

一个前缀和值可能有多个起点,全部需要保留;计数题只存次数,枚举题必须保留位置。

易错点总结

[!yellow]

先查询再登记当前前缀,避免空路径;离开节点必须回退,不能混入兄弟分支。

相似题目

题目 难度 关联与区别
437. 路径总和 III 中等 原题前缀和表只存次数,本题要输出路径,因此同一前缀和必须保存所有起点。
113. 路径总和 II 中等 同样复制节点路径作为输出,原题仅允许根到叶,本题允许任意祖先到后代的路径。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/1977249133
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!