leetcode
leetcode 901 ~ 950
在二叉树中分配硬币

在二叉树中分配硬币

难度:

标签:

题目描述

给你一个有 n 个结点的二叉树的根结点 root ,其中树中每个结点 node 都对应有 node.val 枚硬币。整棵树上一共有 n 枚硬币。

在一次移动中,我们可以选择两个相邻的结点,然后将一枚硬币从其中一个结点移动到另一个结点。移动可以是从父结点到子结点,或者从子结点移动到父结点。

返回使每个结点上 只有 一枚硬币所需的 最少 移动次数。

 

示例 1:

输入:root = [3,0,0]
输出:2
解释:一枚硬币从根结点移动到左子结点,一枚硬币从根结点移动到右子结点。

示例 2:

输入:root = [0,3,0]
输出:3
解释:将两枚硬币从根结点的左子结点移动到根结点(两次移动)。然后,将一枚硬币从根结点移动到右子结点。

 

提示:

  • 树中节点的数目为 n
  • 1 <= n <= 100
  • 0 <= Node.val <= n
  • 所有 Node.val 的值之和是 n

代码结果

运行时间: 22 ms, 内存: 16.0 MB


/* 
 * 题目思路: 
 * 使用递归方法处理树的遍历,Java Stream不适合直接处理树的结构。 
 * 这个解法与Java中的递归解法基本相同。 
 * 我们还是使用深度优先搜索(DFS)来计算每个节点的余额。 
 */

class Solution {
    private int moves;

    public int distributeCoins(TreeNode root) {
        moves = 0;
        dfs(root);
        return moves;
    }

    private int dfs(TreeNode node) {
        if (node == null) return 0;
        int left = dfs(node.left);
        int right = dfs(node.right);
        moves += Math.abs(left) + Math.abs(right);
        return node.val + left + right - 1;
    }
}

解释

方法:

该题解采用了深度优先搜索(DFS)来解决问题。在遍历每个节点时,计算从该节点的左子树和右子树移动硬币所需的最小移动次数,并累加到全局变量 res 中。为每个节点定义一个返回值 (total_coins, total_nodes),其中 total_coins 表示以当前节点为根的子树中所有节点的硬币总数,total_nodes 表示子树中节点的总数。对于每个节点,其需要的硬币数应为其节点数 (即 total_nodes),因此其余硬币应向上或向下移动,移动的次数即为 abs(total_coins - total_nodes)。通过递归地计算每个节点的这两个值,并更新 res,最终得到全树的最小移动次数。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
在定义返回值(total_coins, total_nodes)时,total_coins和total_nodes具体代表什么含义?
在这个问题中,`total_coins`代表的是以当前节点为根的子树中所有节点的硬币总数。这包括当前节点及其所有子节点的硬币数。而`total_nodes`则代表的是以当前节点为根的子树中节点的总数,包括当前节点自身及所有子节点。这两个值的计算对于决定每个节点需要向上或向其他节点移动多少硬币至关重要,因为理想状态下每个节点应有一个硬币。
🦆
如何理解移动次数是 abs(total_coins - total_nodes),这里的计算逻辑是怎样的?
每个节点理想上应持有一个硬币,因此如果一个节点及其子树(统称为子树)拥有的硬币数`total_coins`与节点数`total_nodes`不相等,就会存在硬币过多或不足的情况。`abs(total_coins - total_nodes)`计算的是当前子树的硬币数与其节点总数的差的绝对值,这代表了为了使每个节点都有一个硬币,需要从这个节点移动到其他节点或从其他节点移动到这个节点的硬币数。因此,这个值直接反映了从当前节点及其子树中移入或移出的硬币的最小次数。
🦆
题解中提到的`累加左右子树的硬币移动次数`是如何影响最终结果的?能否具体解释这一过程?
在递归处理每个节点时,我们计算左子树和右子树分别需要移动的硬币次数,即`abs(l[0] - l[1])`和`abs(r[0] - r[1])`。这两个值分别表示从左子树和右子树中平衡硬币到每个节点所需要的移动次数。通过在每次递归调用时累加这些值到一个全局变量`res`中,我们实际上是在计算整棵树中所有不平衡的部分所需的总移动次数。因此,这种累加过程确保了我们能够得到整棵树从初始状态到每个节点都拥有一个硬币的状态所需要的最小移动次数。
🦆
在递归函数中,为什么要选择返回子树的总硬币数和总节点数,这样的设计有什么特别的考虑吗?
返回子树的总硬币数和总节点数是为了在递归的每一层能够有效地计算出当前节点及其子树的状态,从而决定需要如何移动硬币来达到平衡状态。通过持续返回这两个值,递归函数能够在上溯过程中逐层汇总信息,并计算出每一步所需的硬币移动次数。此外,这种设计模式简化了问题的处理,因为它将问题分解为可以通过递归方法解决的较小子问题,每个子问题只关注于计算和返回其子树的硬币和节点总数。这样的设计提高了算法的效率并降低了实现复杂性。

相关问题

树中距离之和

给定一个无向、连通的树。树中有 n 个标记为 0...n-1 的节点以及 n-1 条边 。

给定整数 n 和数组 edges , edges[i] = [ai, bi]表示树中的节点 ai 和 bi 之间有一条边。

返回长度为 n 的数组 answer ,其中 answer[i] 是树中第 i 个节点与所有其他节点之间的距离之和。

 

示例 1:

输入: n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]]
输出: [8,12,6,10,10,10]
解释: 树如图所示。
我们可以计算出 dist(0,1) + dist(0,2) + dist(0,3) + dist(0,4) + dist(0,5) 
也就是 1 + 1 + 2 + 2 + 2 = 8。 因此,answer[0] = 8,以此类推。

示例 2:

输入: n = 1, edges = []
输出: [0]

示例 3:

输入: n = 2, edges = [[1,0]]
输出: [1,1]

 

提示:

  • 1 <= n <= 3 * 104
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • ai != bi
  • 给定的输入保证为有效的树

监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

 

示例 1:

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。


提示:

  1. 给定树的节点数的范围是 [1, 1000]
  2. 每个节点的值都是 0。