题目描述

✅ 113. 路径总和 II

image-20260928195140463

image-20260928195140464

题意分析

找出所有从根节点出发、沿父子连接一直走到叶子节点的路径,要求每条路径的节点值之和等于 targetSum,返回各条路径从根到叶子的完整节点值序列。叶子必须左右孩子都为空,途中某个前缀的和等于目标不能作为答案。

本题要求收集全部匹配路径,找到一条后还要继续搜索其他分支。不同分支即使得到相同的值序列,也分别对应一条路径,不应按数值去重。空树或没有匹配路径时返回空列表;节点值和目标都可能为负数。

解法:DFS 回溯记录根到叶路径

核心思路

[!blue]

从根向下搜索时,当前路径只会在末尾增加或移除节点,因此适合用 DFS 配合回溯。调用 dfs(node, remain) 之前,path 保存从根到当前节点父亲的值,remain 等于原目标减去这些祖先值之和,仍然包含当前节点需要贡献的部分。

进入非空节点后,把它的值加入 path。若它是叶子,当前路径已经完整;此时 remain == node.val 恰好表示整条路径和等于目标,可以保存答案。若还要搜索孩子,就把 remain - node.val 传下去,使子调用继续满足同一个状态定义。

左右分支共用同一份 path。一个子调用结束时,必须删掉它自己加入的节点,使路径恢复成进入该调用之前的状态。这样搜索完左子树后,路径重新停在当前节点,右子树不会带上左边分支的内容。即使当前节点刚好命中答案,返回之前也必须执行这一步撤销。

答案不能直接保存共享路径本身。Java 的列表对象会继续被修改,Go 的切片也可能共用底层数组;必须复制当前路径,才能让已经找到的答案独立于后续回溯。remain 则按值传给子调用,各层拥有自己的值,无需像 path 一样手动恢复。

每个节点沿唯一的根路径被访问一次,每片叶子对应一条完整候选路径,因此遍历所有分支就能找全答案。由于后面的节点可能是负数,即使当前累计和超过目标,也不能据此剪枝。

解题步骤

  1. 创建结果列表和共享路径,以根节点及原目标调用 DFS。
  2. 当前节点为空时直接返回;否则将当前值加入路径。
  3. 若左右孩子都为空且 remain == node.val,复制当前路径加入结果。
  4. 未命中上述条件时,分别对左右孩子递归,传入 remain - node.val;空孩子会立即返回。
  5. 在本层返回前删除路径末尾的当前节点,恢复父调用的路径状态。
  6. 所有分支搜索完后,返回收集到的全部路径。

代码实现

class Solution {
    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> path = new ArrayList<>();

        dfs(root, targetSum, path, res);

        return res;
    }

    private void dfs(TreeNode node, int remain, List<Integer> path, List<List<Integer>> res) {
        if (node == null) {
            return;
        }

        path.add(node.val);

        if (node.left == null && node.right == null && remain == node.val) {
            // 当前路径会继续回溯修改,加入答案时必须复制。
            res.add(new ArrayList<>(path));
        } else {
            dfs(node.left, remain - node.val, path, res);
            dfs(node.right, remain - node.val, path, res);
        }

        // 命中答案后也要撤销当前节点,避免污染后续分支。
        path.remove(path.size() - 1);
    }
}
func pathSum(root *TreeNode, targetSum int) [][]int {
    res := make([][]int, 0)
    path := make([]int, 0)

    var dfs func(*TreeNode, int)
    dfs = func(node *TreeNode, remain int) {
        if node == nil {
            return
        }

        path = append(path, node.Val)
        if node.Left == nil && node.Right == nil && remain == node.Val {
            // 复制当前路径,避免后续回溯覆盖结果。
            onePath := append([]int(nil), path...)
            res = append(res, onePath)
        } else {
            dfs(node.Left, remain-node.Val)
            dfs(node.Right, remain-node.Val)
        }
        // 命中答案后也要撤销当前节点,避免污染后续分支。
        path = path[:len(path)-1]
    }

    dfs(root, targetSum)
    return res
}

复杂度分析

  • 时间复杂度:$O(n + S)$,n 为节点数,S 为所有答案路径的长度总和。遍历与回溯处理每个节点一次,另需花费 S 的时间复制结果;最坏为 $O(nh)$,h 为树高。
  • 空间复杂度:$O(h)$,递归栈和当前路径都至多包含一条根到叶路径;不计返回结果所占的 $O(S)$ 空间。

关键点总结

[!green]

  • remain 扣除了祖先,尚未扣除当前节点,与叶子处的比较方式配套。
  • 加入当前节点和离开时撤销成对出现,让每个子调用恢复进入前的共享路径。
  • 保存路径副本,才能避免后续回溯修改已有答案。
  • 找到一条只表示收集到一个结果,仍要搜索其他分支。

易错点总结

[!yellow]

  • 直接保存共享 path,后续删除或覆盖会改变之前的答案;Java 列表和 Go 切片都需要复制。
  • 只判断路径和,不判断叶子,会收集尚未完整走到底的路径。
  • 用左右孩子为空的“或”条件判断叶子,会把只有一个孩子的节点误判为终点。
  • 命中后提前返回而没有撤销当前节点,会污染后续兄弟分支的路径。
  • 按剩余目标为负进行剪枝,会漏掉后面包含负数的合法路径。

相似题目

题目 难度 关联与区别
112. 路径总和 简单 匹配条件相同,原题只返回布尔值,本题需要维护路径并在命中时复制。
437. 路径总和 III 中等 原题统计任意起点的向下路径,本题只收集根到叶的完整路径。
257. 二叉树的所有路径 简单 回溯维护从根到当前节点的路径;本题复制并输出全部目标和叶路径,该题输出所有根到叶的字符串路径。
129. 求根节点到叶节点数字之和 中等 回溯维护从根到当前节点的路径;本题复制并输出全部目标和叶路径,该题逐层按十进制累加根到叶数字。
666. 路径总和 IV 中等 路径总和系列。II 记录满足目标和的具体路径;IV 对编码树累加所有根到叶路径和。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/58259664
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!