啥是前缀和
对于一个给定的数列 A, 它的前缀和数列S 是通过递推能求出来得:
S
[
i
]
=
∑
j
=
1
i
A
[
j
]
S[i]=\sum_{j=1}^{i}A[j]
S[i]=j=1∑iA[j]
部分和是指数列A中某个下标区间的数之和,可理解为前缀和的相减:
s
u
m
[
l
,
r
]
=
∑
i
=
l
r
A
[
i
]
=
S
[
r
]
−
S
[
l
−
1
]
sum[l, r]=\sum_{i=l}^{r}A[i]=S[r]-S[l-1]
sum[l,r]=i=l∑rA[i]=S[r]−S[l−1]
在路径类问题中,前缀和可定义为到当前节点路径上的元素之和。如果两个节点的前缀和相等,则说明节点间的距离为0;如果前缀和相差为target,说明节点间存在距离target的路径。
例题1
这里以LeetCode437为例,传统二重DFS的解法时间复杂度是O(n^2)
。如果使用前缀和建模,本题中的路径是一棵树,从根往任一节点的路径上(不走回头路),有且仅有一条路径。
简单情形
先分析个简单例子:如下图,所有节点没有左子树,因此变成单一路径。我们初始化一个<K, V>
结构来记录前缀和的出现与否,此时前缀和的存在等同于节点的存在。已知某个节点的前缀和,若要看与它相距sum
的节点存在与否,直接preSum_Cur + sum
或者preSum_Cur - sum
并检查其结果是否存在。
注意在初始化时,前缀和0也要算进去。为了节省遍历次数,只判断每个节点减去距离后的节点是否存在,即寻找已走过的节点是否存在。如下图,前缀和3的节点减去距离3后变为0,而0已经存在;前缀和6的节点减去距离3后变为3,3也已经存在,所以总共2条路经。
复杂情形
再看看更加普适的情形,下图中的二叉树结构,仔细思考就可发现:在遍历的过程中,在递归的每个阶段仍然可看作是单条路径。
因此,在这个阶段可以用简单情形的判断方式对结果计数,但要注意每层递归结束时,要将<K,V>
中的出现状态置为否,这样在进入另一条分支(比如,刚从左子树递归回来,正要进入右子树),这样才不会产生干扰,从而获得正确结果。如上图,11、1、3节点减去距离后的前缀和均是10(根节点),因此存在3条路径。
注意事项
因为我们map的初始值是<0,1>
,这就造成特殊情况下的统计出错——根节点的值恰好是0的情况,正常状态下,递归进入根节点(假设它非零)后,map中应该是<0, 1>;<roo.val, 1>
,但当根节点为零时造成了两条记录的重合。为了解决这一特殊情况,可对出现次数加一处理,具体代码参照一下版本:
public void helper(TreeNode root, Map<Integer, Integer> map, int cur, int sum){
if (root == null)
return;
cur += root.val; // 前缀和
if( map.getOrDefault(cur-sum, 0)>0 )
res+=map.getOrDefault(cur-sum, 0);
map.put(cur, map.getOrDefault(cur, 0)+1); // 出现,则次数加一
helper(root.left, map, cur, sum);
helper(root.right, map, cur, sum);
map.put(cur, map.getOrDefault(cur, 0)-1);
}