给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/path-sum-iii
分析:这是一道典型的考前缀和知识的题。
以我的理解,所谓前缀和,就是求从根节点到当前节点所经过的所有节点的和。设根节点的为root,当前节点为node5,从root到node5依次进过了node2,node3,node4,则node5的前缀和为root+node2+node3+node4+node5。其他的节点以此类推。
当求是否存在路径总和为target时,以target为8举例子,假设node2的前缀和为2,node5的前缀和为10,则可以断定node3+node4+node5的路径总和为8.
因此,我们在每走到一个节点时,计算它的前缀和,对求路径总和很有意义。
又因为前缀和是从根节点到当前节点所经过的所有节点的和,即我们要遍历同一个路径的上的节点,因此应使用深度优先搜索来遍历节点。
又因为前缀和是从根节点到当前节点所经过的所有节点的和,要求每次要加上本节点的和,因此要先拿到本节点的值,再遍历本节点的子节点。这就是所谓前序遍历,又叫先序遍历,即根在前,然后左右子节点。
综上,我们在求某个节点的前缀和时,应使用前序遍历,将这个节点之前的所有节点的值加起来,成为这个节点的前缀和。
------------------------------------------
接下来,继续以当求是否存在路径总和为target时,以target为8举例子,假设node2的前缀和为2,node5的前缀和为10,则可以断定node3+node4+node5的路径总和为8。当我们求导node5的前缀和时,我们必须要知道它之前的节点有没有一个前缀和为2的节点。这就需要有一个备忘录,显然字典是备忘录的首选。因此,我们要定义一个字典,用来存储每个节点以及它之前的节点的前缀和。
那么,字典如何设计?它的键值分别是什么?让我们再回到题目,只求和为8的路径的个数,而不是求和为8的路径。因此,我们只关心有没有符合要求的前缀和就行了。即当node5的前缀和是10时,我们只需要知道在node5之前的节点里,有没有前缀和为2的节点就行了,至于这个前缀和为2的节点距离node5有多远,经过了哪几个节点,不关心。因此字典里,只需要呈现前缀和为2的节点,有还是没有。那么,我们是否要求知道前缀和为2的节点有几个呢?我理解是需要的。因为题目里并没有说节点的值都是正值,因此节点里可能有负值。假设node2的前缀和为2,node3的值(非前缀和)为3,node4的值(非前缀和)为-3,那么node4的前缀和也为2.此时,若node5的前缀和为10,则如果字典里显示前缀和为2的节点有2个,则我们可以知道截止到node5,存在2个路径之和为8.
因此,该字典的键为出现的前缀和,值为这个前缀和出现的次数。
----------------------------------------------
最后,要注意一个情况:
继续以以当求是否存在路径总和为target时,以target为8举例子,假设node2的前缀和为2,node5的前缀和为10,则可以断定node3+node4+node5的路径总和为8为例子。假设有一个node10的前缀和也是10,但是node10压根不和node2~4在一个路径上,那node10是不能用node2的前缀和的。那么,该如何避免这种情况呢?那就是当node2的左右两个子树均搜索完毕后,应在字典里,将前缀和为2的数量减去1。为什么呢?因为node2的前缀和只能为它的子树所用,其他的树节点不能用node2的前缀和。因此在搜索完node2的左右子树后,要在字典里把node2的前缀和对应的值减去1.
整体代码如下,下面代码里insert是添加节点的函数,find_sum_path是本文所讲的求路径之和的函数:
class Node:
def __init__(self,value):
self.value = value
self.left_child = None
self.right_child = None
def insert(self,value):
queue = [self]
while len(queue) != 0:
current = queue.pop(0)
if current.left_child is None:
current.left_child = Node(value)
return
else:
queue.append(current.left_child)
if current.right_child is None:
current.right_child = Node(value)
return
else:
queue.append(current.right_child)
def find_sum_path(self,target):
sum_dict = collections.defaultdict(int)
# 这里的persum[0] = 1是为了计算某些单个节点就直接为target的情况
sum_dict[0] = 1
def dfs(current,current_sum):
if current is None:
return 0
current_sum += current.val
result = sum_dict[current_sum-targetSum]
# 注意result一定要先算,然后才能将当前的前缀和放入字典,不能反了。
# 因为sum_dict指示的是当前节点之前的前缀和和有哪些,如果先把包含自己的前缀和放入字典,
# 就会出现自己算自己的情况。比如设tree=[1],target=0,如果先把sum_dict[1]=1放入字典,
# 然后再算result = sum_dict[1-0],就会得出result=1.
sum_dict[current_sum] += 1
if current.left:
result += dfs(current.left,current_sum)
if current.right:
result += dfs(current.right,current_sum)
sum_dict[current_sum] -= 1
return result
return dfs(root,0)
root = Node(5)
root.insert(2)
root.insert(4)
root.insert(1)
root.insert(6)
root.insert(3)
root.insert(7)
print(root.find_sum_path(7))