LeetCode437 路径总和 III

69 阅读5分钟

leetcode.cn/problems/pa…

前言

先看看这题的基础版

image.png

看示例 1,我们可以从 targetSum=22 开始,不断地减去路径上的节点值,如果走到叶子节点发现 targetSum=0,就说明我们找到了一条符合题目要求的路径。具体来说:

  1. 递归前,targetSum=22。
  2. 从根节点 5 开始递归,把 targetSum 减少 5,现在 targetSum=17。
  3. 向下递归到 4,把 targetSum 减少 4,现在 targetSum=13。
  4. 向下递归到 11,把 targetSum 减少 11,现在 targetSum=2。
  5. 向下递归到 2,把 targetSum 减少 2,现在 targetSum=0,找到答案。

很显然,上述是一种分解子问题的思路,可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案

解法:基于分解子问题思维模式的递归

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil{
        return false
    }
    targetSum -= root.Val
    if root.Left == nil && root.Right == nil{
        return targetSum == 0
    }
    leftHas := hasPathSum(root.Left, targetSum)
    rightHas := hasPathSum(root.Right, targetSum)
    return leftHas || rightHas
}

另一种写法

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func hasPathSum(root *TreeNode, targetSum int) bool {
    if root == nil{
        return false
    }
    // 遍历到叶子节点且找到目标值
    if root.Left == nil && root.Right == nil && targetSum == root.Val{
        return true
    }
    // 递归遍历左右子树,target更新为target-当前节点值
    leftHas := hasPathSum(root.Left, targetSum-root.Val)
    rightHas := hasPathSum(root.Right, targetSum-root.Val)
    // 只要有一边能找到路径即可
    return leftHas || rightHas
}

再看看进阶版 image.png

这个需要找到匹配路径和的同时记录路径,显然是需要遍历过程不断做一些记录操作的,找到匹配路径时就更新答案,由于可能有多条路径,需要回溯。

解法:基于遍历思维模式的递归

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func pathSum(root *TreeNode, targetSum int) [][]int {
    res := [][]int{}
    path := []int{}
    traverse(root, targetSum, path, &res)
    return res
}

func traverse(root *TreeNode, targetSum int, path []int, res *[][]int) {
    if root == nil{
        return
    }
    // 每次将遍历到的当前节点加入路径
    path = append(path, root.Val)
    targetSum -= root.Val
    if root.Left == nil && root.Right == nil{ // 找到叶子节点
        if targetSum == 0{ // 找到答案,复制当前路径,避免引用问题
            tmp := make([]int, len(path))
            copy(tmp, path)
            *res = append(*res, tmp)
        }
    }
    traverse(root.Left, targetSum, path, res)
    traverse(root.Right, targetSum, path, res)
    // 撤销选择,回溯前移除当前节点
    path = path[:len(path)-1]
}

回到本题解析

image.png

解法一:基于分解子问题思维模式的递归

暴力解法:穷举,每个节点都可能成为路径起点,因为我们需要多层递归

定义辅助函数rootSum(p,val) 表示以节点 p 为起点向下且满足路径总和为 val 的路径数目

访问每一个节点 node,检测以 node 为起始节点且向下延伸的路径有多少种。我们递归遍历每一个节点的所有可能的路径,然后将这些路径数目加起来即为返回结果。

对节点 p 求 rootSum(p,targetSum) 时,以当前节点 p 为目标路径的起点递归向下进行搜索。假设当前的节点 p 的值为 val,我们对左子树和右子树进行递归搜索,对节点 p 的左孩子节点 pl求出 rootSum(pl,targetSum−val),以及对右孩子节点 pr求出 rootSum(pr,targetSum−val)。节点 p 的 rootSum(p,targetSum) 即等于 rootSum(pl,targetSum−val) 与 rootSum(pr,targetSum−val) 之和,同时我们还需要判断一下当前节点 p 的值是否刚好等于 targetSum。

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func pathSum(root *TreeNode, targetSum int) int {
    if root == nil{
        return 0
    }
    // 先计算从根节点开始的路径数目
    curPathSum := rootSum(root, targetSum)
    // 每个节点都可能作为路径起点,递归计算左右子树的路径数目
    leftPathSum := pathSum(root.Left, targetSum)
    rightPathSum := pathSum(root.Right, targetSum)
    return curPathSum + leftPathSum + rightPathSum
}

// rootSum(p,val) 表示以节点 p 为起点向下且满足路径总和为 val 的路径数目
func rootSum(root *TreeNode, targetSum int) int {
    if root == nil{
        return 0
    }
    curRes := 0
    if root.Val == targetSum{ // 一个节点匹配也可以是一条路径
        curRes++
    }
    leftRes := rootSum(root.Left, targetSum-root.Val)
    rightRes := rootSum(root.Right, targetSum-root.Val)
    return curRes + leftRes + rightRes
}

解法二:优化时间复杂度,前缀和技巧

解法一中应该存在许多重复计算。我们定义节点的前缀和为:由根结点到当前结点的路径上所有节点的和。

我们利用先序遍历二叉树,记录下根节点 root 到当前节点 p 的路径上除当前节点 p 以外所有节点的前缀和,在已保存的路径前缀和中查找是否存在前缀和刚好等于当前节点到根节点的前缀和 curr 减去 targetSum。

前缀和的思路和这道题的解法基本一致

其实对于二叉树的写法,通用是基于遍历思维模式的递归,其实就是一种深度优先搜索DFS

/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func pathSum(root *TreeNode, targetSum int) int {
    if root == nil{
        return 0
    }
    preSumCntMap := make(map[int]int) // 统计前缀和的出现次数
    // 为了把任意路径和都表示成两个前缀和的差,需要添加一个前缀和为0也就是一个节点都没有的路径,方便处理根节点
    preSumCntMap[0] = 1
    var res int
    curSum := 0 // 初始为0,从根节点开始遍历
    traverse(root, preSumCntMap, targetSum, curSum, &res)
    return res
}

func traverse(root *TreeNode, preSumCntMap map[int]int, targetSum int, curSum int, res *int) {
    if root == nil{
        return
    }
    curSum += root.Val // curSum表示从root到当前节点的路径和
    if cnt, ok := preSumCntMap[curSum - targetSum]; ok{ // 找路径和为curSum - targetSum的数目,更新答案
        *res += cnt
    }
    // 加入当前节点
    preSumCntMap[curSum]++
    traverse(root.Left, preSumCntMap, targetSum, curSum, res)
    traverse(root.Right, preSumCntMap, targetSum, curSum, res)
    // 撤销选择
    preSumCntMap[curSum]-- 
}

注意:回溯前需要撤销选择,如果不恢复现场,当我们递归完左子树,要递归右子树时,preSumCntMap 中还保存着左子树的数据。但递归到右子树,要计算的路径并不涉及到左子树的任何节点,如果不恢复现场,preSumCntMap 中统计的前缀和个数会更多,我们算出来的答案可能比正确答案更大。