题目描述

✅ 666. 路径总和 IV

题意分析

每个三位数的百位表示深度,十位表示该节点在这一层满二叉树中的位置,个位表示节点值;深度和位置都从 1 开始。要求先计算每条根到叶路径上的节点值之和,再把所有路径和相加。缺失节点不改变其他节点的位置编号。

解法:哈希表 + 深度优先搜索

核心思路

[!blue]

深度和层内位置已经足以定位父子关系,不必再创建树节点。用 depth * 10 + position 作键,记录对应节点值。深度为 d、位置为 p 的节点,其左、右孩子分别位于 (d + 1, 2p - 1) 和 (d + 1, 2p),因为满二叉树中每个父节点按从左到右的顺序占用两个孩子位置。

定义 dfs(depth, position, prefixSum):prefixSum 是从根到当前节点父亲的路径和,返回当前子树内所有根到叶路径的总贡献。位置不存在时返回 0;位置存在时,先加上当前节点值,得到 currentSum。

只有左右孩子都不存在,当前节点才是叶子,此时恰好完成一条路径,返回 currentSum。若至少存在一个孩子,就把同一个 currentSum 传给两侧,返回左右贡献之和;不存在的一侧贡献 0,不会把未结束的路径另算一次。

左右子树的叶子集合互不重叠,因此每条根到叶路径只在自己的叶子处结算一次。共享的祖先值会包含在每条经过它的路径中,符合题目对所有路径分别求和的要求。路径和作为参数传递,两次递归互不修改对方的累计值,无需回溯撤销。

解题步骤

  1. 对每个编码分别取百位、十位和个位,建立“位置编号 → 节点值”的哈希表。
  2. 从根位置 (1, 1) 开始递归,初始路径和为 0。
  3. 当前位置不存在时返回 0;存在时把节点值加到路径和中。
  4. 计算左右孩子位置。若都不存在,返回当前路径和。
  5. 否则分别递归左右孩子,把两侧返回值相加。

代码实现

// 对于深度为 d、位置为 p 的节点,左孩子位置是 2 * p - 1,右孩子位置是 2 * p。
class Solution {
    public int pathSum(int[] nums) {
        Map<Integer, Integer> values = new HashMap<>();

        for (int num : nums) {
            int depth = num / 100;
            int position = (num / 10) % 10;
            int value = num % 10;

            values.put(depth * 10 + position, value);
        }

        return dfs(values, 1, 1, 0);
    }

    private int dfs(Map<Integer, Integer> values, int depth, int position, int prefixSum) {
        int key = depth * 10 + position;

        // 不存在的位置贡献零,不能把未完成路径当作答案
        if (!values.containsKey(key)) {
            return 0;
        }

        int currentSum = prefixSum + values.get(key);
        int nextDepth = depth + 1;
        int leftPosition = position * 2 - 1;
        int rightPosition = position * 2;
        int leftKey = nextDepth * 10 + leftPosition;
        int rightKey = nextDepth * 10 + rightPosition;

        if (!values.containsKey(leftKey) && !values.containsKey(rightKey)) {
            // 只有自身两个孩子都不存在,才结算一条根到叶路径
            return currentSum;
        }

        return dfs(values, nextDepth, leftPosition, currentSum)
                + dfs(values, nextDepth, rightPosition, currentSum);
    }
}
// 对于深度为 d、位置为 p 的节点,左孩子位置是 2 * p - 1,右孩子位置是 2 * p。
func pathSum(nums []int) int {
    values := make(map[int]int, len(nums))
    for _, num := range nums {
        depth := num / 100
        position := (num / 10) % 10
        value := num % 10
        values[depth*10+position] = value
    }

    var dfs func(depth int, position int, prefixSum int) int
    dfs = func(depth int, position int, prefixSum int) int {
        key := depth*10 + position
        value, exists := values[key]
        // 不存在的位置贡献零,不能把未完成路径当作答案
        if !exists {
            return 0
        }

        currentSum := prefixSum + value
        nextDepth := depth + 1
        leftPosition := position*2 - 1
        rightPosition := position * 2
        leftKey := nextDepth*10 + leftPosition
        rightKey := nextDepth*10 + rightPosition

        if _, hasLeft := values[leftKey]; !hasLeft {
            if _, hasRight := values[rightKey]; !hasRight {
                // 只有自身两个孩子都不存在,才结算一条根到叶路径
                return currentSum
            }
        }

        return dfs(nextDepth, leftPosition, currentSum) + dfs(nextDepth, rightPosition, currentSum)
    }

    return dfs(1, 1, 0)
}

复杂度分析

  • 时间复杂度:期望 $O(n)$,其中 $n$ 是编码数量。建表处理每个编码一次,递归访问每个节点及其孩子位置常数次。
  • 空间复杂度:$O(n)$,哈希表保存全部节点。递归栈占 $O(h)$,本题树深度最多为 4。

关键点总结

[!green]

  • 层内位置沿用满二叉树编号,由此直接推导孩子坐标。
  • 递归参数携带路径和,返回值累加叶子贡献,两者含义不同。
  • 只有真正的叶子才结算路径,空孩子直接贡献 0。

易错点总结

[!yellow]

  • 不能按输入中实际出现的次序重新编号节点,否则缺失位置会使父子关系错乱。
  • 位置从 1 开始,孩子位置是 2p - 1、2p,不能套用从 0 开始的数组下标公式。
  • 节点值允许为 0,必须检查键是否存在,不能用取出的值是否为 0 判断节点存在。
  • 叶子由自身两个孩子是否存在决定,不能仅看它是否位于全树最深一层。

相似题目

题目 难度 关联与区别
112. 路径总和 简单 可先由层号和位置还原父子关系,再复用根到叶路径和递推;本题最终累加全部路径的和。
257. 二叉树的所有路径 简单 同样遍历所有根到叶路径,本题只保存累计和而非完整路径文本。
113. 路径总和 II 中等 路径总和系列。都在 DFS 中维护根到当前节点的累加和;II 还需保存路径并按目标和筛选。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/38425630
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!