Leetcode(437)——路径总和 III
题目
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例 1:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
示例 2:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:3
提示:
- 二叉树的结点个数的范围是 [0,1000]
- − 1 0 9 -10^9 −109 <= Node.val <= 1 0 9 10^9 109
- − 1000 -1000 −1000 <= targetSum <= 1000 1000 1000
题解
方法一:深度优先搜索,DFS
思路
我们首先想到的解法是穷举所有的可能,我们访问每一个结点 node \textit{node} node,检测以 node \textit{node} node 为起始结点且向下延深的路径有多少种。我们递归遍历每一个结点的所有可能的路径,然后将这些路径数目加起来即为返回结果。
- 我们首先定义 rootSum ( p , val ) \textit{rootSum}(p,\textit{val}) rootSum(p,val) 表示以结点 p p p 为起点向下且满足路径总和为 v a l val val 的路径数目。我们对二叉树上每个结点 p p p 求出 rootSum ( p , targetSum ) \textit{rootSum}(p,\textit{targetSum}) rootSum(p,targetSum),然后对这些路径数目求和即为返回结果。
- 我们对结点 p p p 求 rootSum ( p , targetSum ) \textit{rootSum}(p,\textit{targetSum}) rootSum(p,targetSum) 时,以当前结点 p p p 为目标路径的起点递归向下进行搜索。假设当前的结点 p p p 的值为 val \textit{val} val,我们对左子树和右子树进行递归搜索,对结点 p p p 的左孩子结点 p l p_{l} pl 求出 rootSum ( p l , targetSum − val ) \textit{rootSum}(p_{l}, \textit{targetSum}-\textit{val}) rootSum(pl,targetSum−val),以及对右孩子结点 p r p_{r} pr 求出 rootSum ( p r , targetSum − val ) \textit{rootSum}(p_{r},\textit{targetSum}-\textit{val}) rootSum(pr,targetSum−val)。结点 p p p 的 rootSum ( p , targetSum ) \textit{rootSum}(p,\textit{targetSum}) rootSum(p,targetSum) 即等于 rootSum ( p l , targetSum − val ) \textit{rootSum}(p_{l},\textit{targetSum}-\textit{val}) rootSum(pl,targetSum−val) 与 rootSum ( p r , targetSum − val ) \textit{rootSum}(p_{r},\textit{targetSum}-\textit{val}) rootSum(pr,targetSum−val) 之和,同时我们还需要判断一下当前结点 p p p 的值是否刚好等于 targetSum \textit{targetSum} targetSum。
- 我们采用递归遍历二叉树的每个结点 p p p,对结点 p p p 求 rootSum ( p , val ) \textit{rootSum}(p,\textit{val}) rootSum(p,val),然后将每个结点所有求的值进行相加求和返回。
代码实现
我的
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
int psum = 0; // 二叉树的所有路径中值之和为 targetSum 的路径个数
long pval = 0; // 当前路径上的值之和
long target= 0; // targetSum
public:
int pathSum(TreeNode* root, int targetSum){
if(root == nullptr) return 0;
target = targetSum;
valueSum(root); // 以 root 为起点的值和为 targetSum 的路径个数
pathSum(root->left, targetSum);
pathSum(root->right, targetSum);
return psum;
}
void valueSum(TreeNode* root){
if(root == nullptr) return;
pval += root->val;
if(pval == target) psum++;
valueSum(root->left);
valueSum(root->right);
pval -= root->val;
}
};
Leetcode 官方题解
class Solution {
public:
int rootSum(TreeNode* root, int targetSum) {
if (!root) return 0;
int ret = 0;
if (root->val == targetSum) ret++;
ret += rootSum(root->left, targetSum - root->val);
ret += rootSum(root->right, targetSum - root->val);
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
if (!root) return 0;
int ret = rootSum(root, targetSum);
ret += pathSum(root->left, targetSum);
ret += pathSum(root->right, targetSum);
return ret;
}
};
复杂度分析
时间复杂度:
O
(
N
2
)
O(N^2)
O(N2),其中
N
N
N 为该二叉树结点的个数。对于每一个结点,求以该结点为起点的路径数目时,则需要遍历以该结点为根结点的子树的所有结点,因此求该路径所花费的最大时间为
O
(
N
)
O(N)
O(N),我们会对每个结点都求一次以该结点为起点的路径数目,因此时间复杂度为
O
(
N
2
)
O(N^{2})
O(N2)。
空间复杂度:
O
(
N
)
O(N)
O(N),考虑到递归需要在栈上开辟空间。
方法二:前缀和 + 递归 + 回溯(路径值的回溯)
思路
我们仔细思考一下,解法一(DFS)中应该存在许多重复计算——重复计算相同路径。此时我们就想起之前的一道题,即数组的前缀和。
定义结点的前缀和为:由根结点到当前结点的路径上所有结点的和。
所以可以用先序遍历二叉树,记录下根结点
root
\textit{root}
root 到当前结点
p
p
p 的路径上除当前结点以外所有结点的前缀和,在已保存的路径前缀和中查找是否存在前缀和刚好等于当前结点到根结点的前缀和
c
u
r
r
curr
curr 减去
targetSum
\textit{targetSum}
targetSum。
- 对于空路径我们也需要保存预先处理一下,此时因为空路径不经过任何结点,因此它的前缀和为 0 0 0。
- 假设根结点为 root \textit{root} root,我们当前刚好访问结点 node \textit{node} node,则此时从根结点 root \textit{root} root 到结点 node \textit{node} node 的路径(无重复结点)刚好为 root → p 1 → p 2 → … → p k → node \textit{root} \rightarrow p_1 \rightarrow p_2 \rightarrow \ldots \rightarrow p_k \rightarrow \textit{node} root→p1→p2→…→pk→node,此时我们可以已经保存了结点 p 1 , p 2 , p 3 , … , p k p_1, p_2, p_3, \ldots, p_k p1,p2,p3,…,pk 的前缀和,并且计算出了结点 node \textit{node} node 的前缀和。
- 假设当前从根结点 root \textit{root} root 到结点 node \textit{node} node 的前缀和为 curr \textit{curr} curr,则此时我们在已保存的前缀和查找是否存在前缀和刚好等于 curr − targetSum \textit{curr} - \textit{targetSum} curr−targetSum。假设从根结点 root \textit{root} root 到结点 node \textit{node} node 的路径中存在结点 p i p_i pi 到根结点 root \textit{root} root 的前缀和为 curr − targetSum \textit{curr} - \textit{targetSum} curr−targetSum,则结点 p i + 1 p_{i+1} pi+1 到 node \textit{node} node 的路径上所有结点的和一定为 targetSum \textit{targetSum} targetSum。
- 我们利用深度搜索遍历树,当我们退出当前结点时,我们需要及时更新已经保存的前缀和。
代码实现
我写的
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
int psum = 0;
long lastval = 0;
unordered_map<long, int> pathval; // <前缀和, 个数>
public:
int pathSum(TreeNode* root, int targetSum){
if(root == nullptr) return psum;
long nowval = 0;
if(pathval.size() == 0){
nowval = root->val;
lastval = nowval;
pathval.emplace(nowval, 1);
}else{
nowval = root->val + lastval;
lastval = nowval;
if(pathval.count(nowval) != 0) pathval[nowval]++;
else pathval.emplace(nowval, 1);
}
pathSum(root->left, targetSum);
lastval = nowval;
pathSum(root->right, targetSum);
if(pathval.size() != 1 || pathval.begin()->second != 1){ // 是不是根结点
if(nowval == targetSum) psum++;
if(pathval.count(nowval - targetSum) != 0){
if(targetSum == 0)// 为了防止 targetSum 为0
psum += pathval[nowval] - 1;
else psum += pathval[nowval - targetSum];
}
if(pathval[nowval] != 1) --pathval[nowval];
else pathval.erase(nowval);
}else if(pathval.count(targetSum) != 0) psum++;
return psum;
}
};
Leetcode 官方题解
class Solution {
public:
unordered_map<long long, int> prefix;
int dfs(TreeNode *root, long long curr, int targetSum) {
if (!root) {
return 0;
}
int ret = 0;
curr += root->val;
if (prefix.count(curr - targetSum)) {
ret = prefix[curr - targetSum];
}
prefix[curr]++;
ret += dfs(root->left, curr, targetSum);
ret += dfs(root->right, curr, targetSum);
prefix[curr]--;
return ret;
}
int pathSum(TreeNode* root, int targetSum) {
prefix[0] = 1;
return dfs(root, 0, targetSum);
}
};
网友简洁写法:
每个结点的过程是 : 求当前结点结束的路径数量,将以当前结点为结尾的前缀和入表,递归当前结点的左右子树,回溯(本结点前缀和出表)
class Solution {
public:
unordered_map<int64_t, int> m{{0,1}};
int pathSum(TreeNode* root, int targetSum, int64_t sum = 0) {
if(!root) return 0;
int v = (m.count(sum + root->val - targetSum) ? m[sum + root->val - targetSum] : 0);
++m[sum + root->val];
int ret = pathSum(root->left, targetSum, sum + root->val) + pathSum(root->right, targetSum, sum + root->val);
if(--m[sum + root->val] == 0) m.erase(sum + root->val);
return v + ret;
}
};
复杂度分析
时间复杂度:
O
(
N
)
O(N)
O(N),其中
N
N
N 为二叉树中节点的个数。利用前缀和只需遍历一次二叉树即可。
空间复杂度:
O
(
N
)
O(N)
O(N),主要是递归所使用的空间和哈希表。