ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【树】力扣437:路径总和 III(真的不是简单题吧)

2022-07-15 16:04:57  阅读:156  来源: 互联网

标签:结点 root sum 路径 dfs 力扣 437 III self


给定一个二叉树的根节点 root ,和一个整数 sum ,求该二叉树里节点值之和等于 sum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例:

image

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条:[[5,3],[5,2,1],[-3,11]],如图所示。

递归每个节点时,需要分情况考虑:(1)如果选取该节点加入路径,则之后必须继续加入连续节点,或停止加入节点(2)如果不选取该节点加入路径,则对其左右节点进行重新进行考虑。因此一个方便的方法是创建一个辅函数,专门用来计算连续加入节点的路径。

深度优先搜索

首先想到的解法是穷举所有的可能,访问每一个结点 node,检测以 node 为起始结点且向下延深的路径有多少种。递归遍历每一个结点的所有可能的路径,然后将这些路径数目加起来即为返回结果。

因此写出双层递归。核心在于:

  • 每个 node 都要计算以它作为起点往下是否有 path --> 这是一层递归

  • 在考虑当前点为起点往下有没有 path 的时候,它的 path 可以往左也可以往右,于是要综合考虑 --> 这是另一层递归

第一层递归,把每个子结点都看作以它为根结点一颗独立的树来判断:

    def pathSum(self, root: Optional[TreeNode], sum: int) -> int:
        if not root: # 如果没有根结点,整个返回值应该为0,没有路径
            return 0
        '''
        self.dfs(root, sum):这个方法是判断以当前点为起点往下是否有路径,也就是路径的数量,返回值应该是 0 或 1
        self.pathSum(root.left, sum):对于左结点,依然要考虑以它为起点往下判断
        self.pathSum(root.right, sum):同上,于是,此时的sum是不变化的,仍然为初始值
        '''
        return self.dfs(root, sum) + self.pathSum(root.left, sum) + self.pathSum(root.right, sum)

作者:sammy-4
链接:https://leetcode.cn/problems/path-sum-iii/solution/hot-100-437lu-jing-zong-he-iii-python3-li-jie-di-g/

那么对于第二层递归——计算当前点往下是否有路径,就需要每一步都更新路径的和,题目是希望找到是否有路径总和为 sum,那么为了简便,可以每一次都减去当前结点的值 node.val,当 root.val == sum,就说明找到了一条路径,返回 1,否则返回 0。若此时的根节点 root == None,说明一直找不到路径,返回 0。

需要注意的是,辅函数self.dfs只能递归当前的根结点。

    def dfs(self, root, sum):
        if not root:
            return 0
        count = 1 if root.val == sum else 0 # root.val == sum 时说明找到了路径,count加一
        sum -= root.val # 不论 root.val 是否与 sum 相等,每一次都要减去当前结点的节点值
        count += + self.dfs(root.left, sum) + self.dfs(root.right, sum) # 此时路径更新过,这是因为当前点既可以往左走,也可以往右走,是 or 的关系。只要有一边找到了路径,最终结果都会为 1
        return count

作者:sammy-4
链接:https://leetcode.cn/problems/path-sum-iii/solution/hot-100-437lu-jing-zong-he-iii-python3-li-jie-di-g/

说明:

这里的 self.dfs(root, sum) 可以理解为从当前这个结点往下找到以这个节点开始的 path ,但是对于这道题,没有限制必须从根节点开始,那就说明,除了根结点要继续找到它的 path ,它的每一个子结点也都要以自身节点开始去找到对应的 path 。那么对于主函数来说,就是从根结点去用子方法,即用 self.dfs 方法去找到一个解,对于其他结点,依然要递归地去找到对应的解,也就是 self.pathSum 方法。

如果写成 return self.dfs(root, sum) + self.dfs(root.left, sum) + self.dfs(root.right, sum) ,那么只有第一二层的三个节点会去找path,其他的子节点就无法继续递归了。

而写成 return self.dfs(root, sum) + self.pathSum(root.left, sum) + self.pathSum(root.right, sum) 就会持续递归,直到遍历完全部的结点,实现了对于每一个结点,都去找对应是否存在 path 的求解。

两段整合即为完整代码:

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def pathSum(self, root: Optional[TreeNode], sum: int) -> int:
        if not root: # 如果没有根结点,整个返回值应该为0,没有路径
            return 0
        '''
        self.dfs(root, sum):这个方法是判断以当前点为起点往下是否有路径,也就是路径的数量,返回值应该是 0 或 1
        self.pathSum(root.left, sum):对于左结点,依然要考虑以它为起点往下判断
        self.pathSum(root.right, sum):同上,于是,此时的sum是不变化的,仍然为初始值
        '''
        return self.dfs(root, sum) + self.pathSum(root.left, sum) + self.pathSum(root.right, sum)
    def dfs(self, root, sum):
        if not root:
            return 0
        count = 1 if root.val == sum else 0 # root.val == sum 时说明找到了路径,count加一
        sum -= root.val # 不论 root.val 是否与 sum 相等,每一次都要减去当前结点的节点值
        count += + self.dfs(root.left, sum) + self.dfs(root.right, sum) # 此时路径更新过,这是因为当前点既可以往左走,也可以往右走,是 or 的关系。只要有一边找到了路径,最终结果都会为 1
        return count

时间复杂度:O(N^2),其中 N 为该二叉树结点的个数。对于每一个结点,求以该结点为起点的路径数目时,则需要遍历以该结点为根节点的子树的所有结点,因此求该路径所花费的最大时间为 O(N),会对每个结点都求一次以该结点为起点的路径数目,因此时间复杂度为 O(N^2)。

空间复杂度:O(N),考虑到递归需要在栈上开辟空间。

简短的总体代码:

class Solution:
    def pathSum(self, root: Optional[TreeNode], sum: int) -> int:
        return self.dfs(root, sum) + self.pathSum(root.left, sum) + self.pathSum(root.right, sum) if root else 0

    def dfs(self, root, sum):
        if not root:
            return 0
        return (1 if root.val == sum else 0) + self.dfs(root.left, sum - root.val) + self.dfs(root.right, sum - root.val)

方法一因为递归层数过深效率很低,需要优化:

  1. 遇到 subArray 就考虑 prefixSum 和 prefixSumArray,用 hashtable
  2. 数组、链表考虑迭代遍历
  3. 树可左可右的就用递归代替迭代,并且递归结束之后要删掉当前层的 tmp value
    递归理解调用栈
    并且:由于是递归而不是迭代,需要新的一个函数,且需要将 prefixSum 和 prefixSumArray 当作参数传入进去

作者:sammy-4
链接:https://leetcode.cn/problems/path-sum-iii/solution/hot-100-437lu-jing-zong-he-iii-python3-li-jie-di-g/

前缀和

类似 力扣560. 和为k的子数组 Subarray Sum Equals K,可以每次求出累加和preSum,然后找到 preSum 与 sum 的差值,也就是距离为 sum 的路径,判断差值是否在用来保存每一个 preSum 的 hashtable 里,也类似 2sum 问题。

定义结点的前缀和为:由根结点到当前结点的路径上所有结点的和。先序遍历二叉树,记录下根结点 root 到当前结点 p 的路径上除当前结点以外所有结点的前缀和 curr。

  • 对于空路径,需要保存预先处理,此时因为空路径不经过任何节点,因此它的前缀和为 0。

  • 假设根结点为 root,当前刚好访问结点 node,则此时从根结点 root 到结点 node 的路径(无重复节点)刚好为 \(root→p_1→p_2→…→p_k→node\),此时已经保存了节点 \(p_1, p_2, p_3, ..., p_k\) 的前缀和,并且计算出了结点 node 的前缀和。

  • 假设当前从根结点 root 到结点 node 的前缀和为 curr,则在已保存的前缀和查找是否存在前缀和刚好等于 curr − sum。假设这条路径中存在结点 \(p_i\) 到根结点 root 的前缀和为 curr - sum,则节点 \(p_{i+1}\) 到 node 的路径上所有结点的和一定为 sum。

  • 利用深度搜索遍历树,当退出当前结点时,需要及时更新已经保存的前缀和

  • 在每层深度搜索结束时要把当前层的 presum 的个数减一:遍历二叉树选择右子树的时候是从根节点往右(而不是从左子树继续),如果不重置就会带上左子树的信息和计算结果,重置是为了回到上一层,回到根节点,然后再才从右子树继续

from collections import defaultdict
class Solution:
    def pathSum(self, root: Optional[TreeNode], sum: int) -> int:
        presum = collections.defaultdict(int)
        presum[0] = 1

        def dfs(root, curr):
            if not root:
                return 0
            ret = 0
            curr += root.val
            ret += presum[curr - sum]
            presum[curr] += 1
            ret += dfs(root.left, curr) + dfs(root.right, curr)
            presum[curr] -= 1 # 一定要注意在递归回到上一层的时候要把当前层的 presum 的个数减一,类似回溯,要把条件重置。
            return ret

        return dfs(root, 0)

时间复杂度:O(N),其中 N 为二叉树中节点的个数。利用前缀和只需遍历一次二叉树即可。

空间复杂度:O(N)。

两种方法的时间对比:
image

标签:结点,root,sum,路径,dfs,力扣,437,III,self
来源: https://www.cnblogs.com/Jojo-L/p/16481680.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有