Leetcode437 路径总和 III 双递归与前缀和

  最简单的思路为双递归,内部递归函数用于计算以 node 节点为头元素的路径和, 外部递归函数用于遍历所有节点。

  即遍历以每个节点为头元素的所有符合条件的路径:

    /**
     * @Author Niuxy
     * @Date 2020/7/15 11:04 下午
     * @Description 双递归
     */
    public int pathSum(TreeNode root, int sum) {
        if (root == null) {
            return 0;
        }
        return pathSum0(root, sum, 0) + pathSum(root.right, sum) + pathSum(root.left, sum);
    }

    public int pathSum0(TreeNode node, int sum, int preSum) {
        if (node == null) {
            return 0;
        }
        int currentSum = preSum + node.val;
        int re = pathSum0(node.left, sum, currentSum) + pathSum0(node.right, sum, currentSum);
        if (sum == currentSum) {
            re++;
        }
        return re;
    }

  计算过程存在大量重复求和计算。比如 [1,2,3,4,5] ,1-5 的路径和为 2-5 的路径和加 1 ,而在计算过程中,2-5 的路径和被计算了两次。

  内部函数建立缓存只能由 node,currentSum 两个坐标为依据,但这两个坐标命中的结果,整个计算过程只被使用过一次。也就是说内部函数的定义无法帮我们找出上述的重复计算情况。

  参考数组求区间和时,可以使用双指针复用局部的区间和。

  也可以在遍历一条路径时,通过区间和的差来复用该路径上的区间和。

  从头元素到节点 i 的区间和为 I ,到节点 j 的区间和为 J 。则节点 i 到 j 的区间和为 J-I 。其中 I 与 J 即为节点 I 与 节点 J 的前缀和。

  在遍历一条路径时,记录下每一个节点的前缀和,通过前缀和的差值寻找区间和为某值的路径是否存在。

  因为要寻找路径的总和是确定的 sum ,某节点的前缀和 currentSum 是确定的。寻找的区间为 currentSum-x = sum , 也就是遍历每个节点时,寻找其前面前缀和为 x = currentSum-sum

的节点的数量。因为 x 是确定的,在一条路径上嗅探时,可以用哈希表存储前面节点的前缀和,以 O(1) 时间复杂度寻找前缀和为 x 的节点。

  语义为遍历每个节点时,寻找该路径上,所有以该节点为尾结点的符合题意的区间。

  目标区间的尾结点必为该路径上某一节点,该方式可完整遍历解空间,并帮助我们避免重复的求和计算。

/**
     * @Author Niuxy
     * @Date 2020/7/15 11:52 下午
     * @Description 前缀和解法
     */
    public final int pathSum1(TreeNode root, int sum) {
        return pathSum1(root, sum, 0, new HashMap<Integer, Integer>());
    }

    public final int pathSum1(TreeNode root, int sum, int preSum, Map<Integer, Integer> cache) {
        if (root == null) {
            return 0;
        }
        int currentRe = 0;
        int currentSum = preSum + root.val;
        if (currentSum == sum) {
            currentRe++;
        }
        //以该节点为末尾节点,向上寻找满足条件的路径数: currentRe - x = sum -> x = currentSum - sum
        currentRe += cache.getOrDefault(currentSum - sum, 0);
        //本节点前缀和
        cache.put(currentSum, cache.getOrDefault(currentSum, 0) + 1);
        currentRe += pathSum1(root.left, sum, currentSum, cache) + pathSum1(root.right, sum, currentSum, cache);
        cache.put(currentSum, cache.get(currentSum) - 1);
        return currentRe;
    }

  与数组双指针遍历区间和的思路类似,前缀和也是避免重复计算的一种思路。

  同时,在“以某个节点为头节点”进行遍历时无法避免的重复计算,转变为“以某个元素为尾结点”进行遍历便解决了。

  因为“以某个节点为头节点”时,已计算的结果不足以支撑计算出当前结果。“以某个元素为尾结点”则是在根节点与尾结点间寻找子路径,以计算出的前缀和结果足以支撑计算出当前结果,以此可以避免重复计算。

  在很多情况下,逆向思维是破局的不二法门。但局内逻辑繁多,确定可以在哪个逻辑上尝试逆向,需要大量的练习。

 

posted @ 2020-07-16 00:35  牛有肉  阅读(366)  评论(0编辑  收藏  举报