Path Sum III

題目描述

給定一棵二元樹和一個整數 targetSum。回傳路徑和等於 targetSum 的路徑數量。路徑方向必須向下(從父到子),但不需要從根開始或在葉結束。

輸入: root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
輸出: 3
解釋: 路徑 5→3, 5→2→1, -3→11 的和都等於 8

解題思路

1

初始化前綴和 Map

設 map[0] = 1,代表「前綴和為 0」出現 1 次,處理從根開始的路徑

2

DFS 遍歷

每到一個節點執行 currSum += node.val,累加從根到此的前綴和

3

查找配對

查 map[currSum - targetSum] 的值,即有幾個祖先的前綴和使得中間路徑和恰好等於 target

4

更新 Map

執行 map[currSum]++ 記錄當前前綴和出現次數,供子節點查找用

5

回溯

遞迴左右子樹後執行 map[currSum]--,移除當前節點的前綴和避免汙染其他分支

程式碼

C#

csharp
public class Solution {
    public int PathSum(TreeNode root, int targetSum) {
        var prefixMap = new Dictionary<long, int> { { 0, 1 } };
        return Dfs(root, 0, targetSum, prefixMap);
    }
 
    private int Dfs(TreeNode node, long currSum, int target, Dictionary<long, int> map) {
        if (node == null) return 0;
 
        currSum += node.val;
        // 有多少前綴和等於 currSum - target
        int count = map.GetValueOrDefault(currSum - target, 0);
 
        // 記錄當前前綴和
        map[currSum] = map.GetValueOrDefault(currSum, 0) + 1;
 
        count += Dfs(node.left, currSum, target, map);
        count += Dfs(node.right, currSum, target, map);
 
        // 回溯:離開此節點,移除此前綴和
        map[currSum]--;
        return count;
    }
}

TypeScript

typescript
function pathSum(root: TreeNode | null, targetSum: number): number {
    const prefixMap = new Map<number, number>([[0, 1]]);
 
    function dfs(node: TreeNode | null, currSum: number): number {
        if (!node) return 0;
 
        currSum += node.val;
        // 有多少前綴和等於 currSum - target
        let count = prefixMap.get(currSum - targetSum) ?? 0;
 
        // 記錄當前前綴和
        prefixMap.set(currSum, (prefixMap.get(currSum) ?? 0) + 1);
 
        count += dfs(node.left, currSum);
        count += dfs(node.right, currSum);
 
        // 回溯:離開此節點,移除此前綴和
        prefixMap.set(currSum, prefixMap.get(currSum)! - 1);
        return count;
    }
 
    return dfs(root, 0);
}

Python

python
from collections import defaultdict
 
def pathSum(root: Optional[TreeNode], targetSum: int) -> int:
    prefix_map = defaultdict(int)
    prefix_map[0] = 1
 
    def dfs(node, curr_sum):
        if not node:
            return 0
 
        curr_sum += node.val
        # 有多少前綴和等於 curr_sum - target
        count = prefix_map[curr_sum - targetSum]
 
        # 記錄當前前綴和
        prefix_map[curr_sum] += 1
 
        count += dfs(node.left, curr_sum)
        count += dfs(node.right, curr_sum)
 
        # 回溯:離開此節點,移除此前綴和
        prefix_map[curr_sum] -= 1
        return count
 
    return dfs(root, 0)

複雜度分析

| | Time | Space | |--|------|-------| | 前綴和解法 | O(n) | O(h) | | 暴力解法 | O(n²) | O(h) |

前綴和方法把每個節點的查找從 O(n) 降到 O(1)。Time O(n) 是因為每個節點訪問一次、HashMap 查找和更新都是 O(1)。暴力法 O(n^2) 是因為對每個節點都要往下遍歷整條路徑。Space O(h) 是因為 HashMap 最多同時存 h 個前綴和(一條根到葉的路徑),加上遞迴 call stack 深度 O(h)。

邊界情況

圖解

10 (sum=10)
5 (sum=15)
3 (sum=18)
2 (sum=17)
1 (sum=18)
-3 (sum=7)
11 (sum=18)

圖表展示了前綴和的計算過程。以路徑 10→5→3 為例:前綴和 18,查找 18-8=10 在 map 中出現 1 次(根節點 10 的前綴和),代表從節點 5 開始到節點 3 的路徑和為 8。三條命中路徑分別是 5→3、5→2→1、-3→11。

進階觀點

💡面試追問

Q: 為什麼初始化 map[0] = 1 A: 如果從根到某節點的前綴和恰好等於 targetSum,需要計入這條路徑。此時 currSum - target = 0,map 中必須有 0 對應的計數 1 才能正確命中。

Q: 為什麼要回溯(離開節點時 map[currSum]--)? A: 前綴和只在「根到當前節點的路徑」上有效。如果不回溯,處理完左子樹後 map 中殘留左子樹的前綴和,右子樹會錯誤地匹配到不在同一路徑上的祖先前綴和。一維陣列版(LC 560)不需要回溯,因為不存在分支。

Q: 為什麼用 long 而不是 int? A: 節點值可能很大,路徑上累加的前綴和可能溢出 int 範圍。C# 解法中 currSum 使用 long 型別來避免溢位。

變體題:LC 560 Subarray Sum Equals K(一維陣列版,不需回溯)、LC 113 Path Sum II(收集路徑而非計數)。

理解測驗

🤔 為什麼 DFS 離開節點時要把 prefixMap[currSum] 減 1?

🤔 相比暴力解法,前綴和方法的優勢是什麼?

你可能也想看

1448. Count Good Nodes in Binary Tree1372. Longest ZigZag Path in a Binary Tree

按 ← → 鍵切換課程