程序员面试金典 4.12 求和路径

题目描述

给定一棵二叉树,其中每个节点都含有一个整数数值(该值或正或负)。设计一个算法,打印节点数值总和等于某个给定值的所有路径的数量。注意,路径不一定非得从二叉树的根节点或叶节点开始或结束,但是其方向必须向下(只能从父节点指向子节点方向)。
示例:
给定如下二叉树,以及目标和 sum = 22,


              5
             / \
            4   8
           /   / \
          11  13  4
         /  \    / \
        7    2  5   1

返回:

3
解释:和为 22 的路径有:[5,4,11,2], [5,8,4,5], [4,11,7]
提示:

节点总数 <= 10000

分析

典型的树形DP问题。

思路一:DFS

二叉树类问题中往往需要分析两个规模减半的子问题的解要如何合并为原问题的解。也就是说,假设我们知道root左右子树的解,如何推出以root为根的树的解。本题要求和为特定值的向下的路径有多少条。那么根据这条路径包不包括根节点可以将其划分为两种状态:向下的路径包括根节点、向下的路径不包括根节点。

可以用f[root][0]表示不包括根节点向下的路径中和为sum的路径条数,用f[root][1]表示包括根节点向下的路径中和为sum的路径条数。则f[root][0] = f[left][0] + f[left][1] + f[right][0] + f[right][1],也就是说如果这条路径不包括root节点,那么这条路径可以在root的左右子树上寻找,可以包括也可以不包括root的左右节点,所以一共是四种状态的集合。f[root][1] = f[left][1] + f[right][1],这里要注意的是,因为路径是连续的,所以路径包括根节点,要么路径到根节点为止,要想继续往下延伸,则其左右孩子必须也包含在路径中,一旦其孩子节点不在路径中,就代表着路径终止。

由于本题是采用结构体来存储树的,直接用状态数组表示状态可能不太方便,所以可以直接用dfs来进行状态转移。在遍历到以root为根的子树时,我们首先需要判断,是否root的val等于剩下的路径和sum,如果是,则可以增加合法路径的条数。

然后就是判断当前遍历到的root节点能否加入到路径里来了,如果root的父节点在路径里,那么root一定要加入路径,因为路径末尾元素是root的父节点的情况在遍历root的父节点时候已经考虑过了,如果路径的末元素不是root的父元素,那么root一定要加入路径,否则路径不一定连续,比如选择了root的父节点,不选root节点,后续遍历又选择了root的孩子节点,路径就不连续了。

如果root的父节点不在路径里,那么root节点加入或者不加入路径就都是可以的,就算不加入路径,也不会造成路径不连续的情况。

通过上面的分析,可以得出dfs的参数应该有当前遍历到的节点root,路径中还需要加入多少才能达到要求的路径和的剩余路径和sum,以及root的父节点是否已经加入路径这三个参数。初始情况由于root是没有父节点的,所以可以视为其父节点没有加入路径开始遍历。

dfs的代码如下:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int ans = 0;
    void dfs(TreeNode* root, int sum, int st) {
        if(!root)   return;
        if(sum == root->val) ans++;
        dfs(root->left,sum - root->val,1);
        dfs(root->right,sum - root->val,1);
        if(!st) {
            dfs(root->left,sum,0);
            dfs(root->right,sum,0);
        }
    }
    int pathSum(TreeNode* root, int sum) {
        dfs(root,sum,0);
        return ans;
    }
};

上面dfs的代码比较简洁,相对于官方题解那种递归函数里调用另一个递归函数可读性更强。这个算法的时间复杂度是很值得我们分析的。因为如果st是0,那么可以沿着四条规模减半的子问题继续延伸;如果st是1,就只能沿着两条规模减半的子问题延伸了。

设原问题的规模为2n,T(2n)为原问题的时间复杂度,T0(n)为规模为n的父节点没有加入路径的子问题的时间复杂度,T1(n)为规模为n的父节点加入了路径的子问题的时间复杂度。显然T(2n) = 2T0(n) + 2T1(n)。

T0(n) = 2T0(n/2) + 2T1(n/2)
T1(n) = 2T1(n/2)

先从T1(n)入手,T1(n) = 2T1(n / 2) = 22T1(n / 4) =…= 2lognT1(1) = O(n)。
则T0(n) = 2T0(n/2) + 2T1(n/2) = 2T0(n/2) + n,可以推出T0(n) + n = 2(T0(n/2) + n) =…= 2logn(T0(1) + n) = n(T(1) + n) = O(n2),
既然原问题时间复杂度等于一个 线性复杂度的子问题和一个平方级复杂度的子问题之和,所以原问题的时间复杂度就是O(n2)。

思路二:前缀和+hash

hash的思路有时会被我们忽略,比如在数组里求两数之和等于k,如果数组是有序的,使用hash固然可以在线性的复杂度内解决,却需要耗费线性的空间复杂度,而使用双指针就可以在线性时间复杂度和常数的空间复杂度内解决。这使得一些问题我们第一反应是双指针而忽略了hash。还是两数之和等于k的问题,如果数组是无序的,尽管双指针的时间复杂度是线性的,但是排序算法的复杂度却是O(nlogn)的,总的时间复杂度还是O(nlogn);如果使用hash尽管需要线性的空间复杂度,但是时间复杂度依旧是线性的。这说明hash的思路在很多时候是相当实用的。

hash和前缀和用在树的问题中比较不常见,本题就是这样一种情况。本题是要求在树中连续路径和等于特定值有多少种方案。如果简化到线性序列里我们应该怎么求解呢?首先一个无序数组,里面的数有正有负,是无法使用双指针的,暴力做法就是二重循环枚举序列的起点和终点,再用一重循环来求和,复杂度是立方级别的。使用前缀和可以优化掉求和这一重循环,继续优化就需要使用DP的思想了。设f[i]表示以i为末尾的连续子序列和等于sum的方案数,那么状态转移方程为f[i] = sum(f[[j]),其中j < i并且j到i的和是sum,即s[i] - s[j-1] = sum,s[j-1] = s[i] - sum,也就是说,如果我们用hash表统计下前i - 1个前缀和中值为s[i] - sum的数量,我们就不需要去枚举j了,这样一来复杂度就降低到了线性。

既然前缀和+hash的思路在线性序列里可行,那么在树的问题中是否可行呢?dfs的时间复杂度之所以是平方级别,在于每个节点我们都需要考虑选与不选。如果我们只去求根节点到当前节点路径上路径的前缀和以及用hash表来维护前缀和中每个值出现的次数,就可以在线性的复杂度中求解出本题了。

求出根节点到当前节点的前缀和只需要在树的dfs里面加上值的累加的操作即可,需要注意的是,hash数组里存储着的只能是根节点到当前节点所有前缀和出现的次数,所以当当前节点的左右子树遍历完成后,就需要将从根节点到以该节点为根的子树中所有节点的前缀和从hash表里删除。

官方题解中的讲解我感觉只是在解释使用前缀和+hash的解法是正确的,并没有从开始一步步的推导出这种解法是怎么来的。前缀和+hash解法的代码如下:

class Solution {
public:
    unordered_map<int,int> prefix;
    int s;
    int dfs(TreeNode* root, int sum) {
        if(!root)   return 0;
        sum += root->val;
        int res = prefix[sum - s];
        prefix[sum]++;
        res += dfs(root->left,sum);
        res += dfs(root->right,sum);
        prefix[sum]--;//遍历完成删除前缀和
        return res;
    }
    int pathSum(TreeNode* root, int sum) {
        prefix[0] = 1;//路径为空前缀和是0的方案数
        s = sum;
        return dfs(root,0);
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值