【问题标题】:Given a node, how long will it take to burn the whole binary tree?给定一个节点,烧掉整棵二叉树需要多长时间?
【发布时间】:2019-03-18 04:10:03
【问题描述】:

我在一次模拟面试中遇到了一个问题,我必须找出一个给定节点已经着火后二叉树会完全烧毁多长时间。

"一棵二叉树从叶子节点开始燃烧。什么是 时间(1秒从一个节点燃烧到另一个节点)获取整棵树所需的时间 烧了?火势会从一个节点蔓延到所有路径。"

假设你有一棵像这样的树,其中 N 是着火的节点。这是 发生在第一秒,其中秒是 s,所以在第零个 s:

           1
       /       \
      1          1
    /  \          \
   1    1          1
      /   \         \
     1     N         1
                      \
                       1

一秒钟后,树将更新为更多已烧毁的节点。 下一秒 (s + 1) 的示例将是这样的:

           1
       /       \
      1          1
    /  \          \
   1    N          1
      /   \         \
     1     N         1
                      \
                       1

下一秒(s + 2)的例子会是这样的:

           1
       /       \
      N          1
    /  \          \
   1    N          1
      /   \         \
     N     N         1
                      \
                       1  

现在在第三秒 (s + 3) 将是这样的:

           N
       /       \
      N          1
    /  \          \
   N    N          1
      /   \         \
     N     N         1
                      \
                       1

同样的模式,树会在(s + 7)中被烧毁

           N
       /       \
      N          N
    /  \          \
   N    N          N
      /   \         \
     N     N         N
                      \
                       N

在了解了一点之后,我做了一个小的研究来弄清楚如何去做。我发现了这个很酷的article 并跟进并实施了背后的想法。

我的方法是找到树的直径和高度,以寻找最远的节点到节点。但是,当我实现我的功能时,我只得到 starting node 的结果到 given node 的末尾,而不检查以前的父节点。这是我在 Python 3 中的实现:

# Tree class
class Node:
    def __init__(self, key):
        self.left = None
        self.right = None
        self.value = key

# Maximum height of a tree
def maxHeight(root):
    if root is None:
        return 0
    else:
        return 1 + max(maxHeight(root.left), maxHeight(root.right))

# Diameter of the tree
def maxDiameter(root):
    how_long = 0
    if root is None:
        return 0
    else:
        root_diameter = maxHeight(root.left) + maxHeight(root.right)

        left_diameter = maxDiameter(root.left)
        right_diameter = maxDiameter(root.right)
        how_long = max(max(left_diameter, right_diameter), root_diameter)
        return how_long

# Sample code
root = Node(1)
root.left = Node(1)
root.right = Node(1)
root.left.left = Node(1)
root.left.right = Node(1)
root.left.right.left = Node(1)
root.left.right.right = Node(1)
root.right.right = Node(1)
root.right.right.right = Node(1)
root.right.right.right.right = Node(1)
print ("Starting from the given node, it will take %ds to burn the whole tree" % (maxDiameter(root.left.right)))

此示例的预期输出应为 6s(从给定节点的 0s 开始)。但同样,我没有得到树的全部范围。根据我自己的理解,它必须适用于所有情况。那么,什么搜索在这里会有所帮助,DFS 或 BFS?我认为考虑到这一点将引导我找到我的解决方案,但又一次。任何反馈表示赞赏:)

【问题讨论】:

  • 问题说火灾始于叶节点,但您的示例始于非叶节点。
  • 还有,最后一秒是怎么回事?您烧毁了 3 个未连接到最近源的节点
  • 我猜最长的应该是根的权重 + 对共同祖先权重最高的叶子的权重,以秒为单位的时间将是加在一起的时间。将在更短的时间内到达每个其他节点。
  • 你是对的@m69
  • 在我做了一些更改后修复了我的示例@Yuca。

标签: python-3.x algorithm class recursion binary-tree


【解决方案1】:
//C++ implementation
#include <bits/stdc++.h>
using namespace std;
//Constructing tree
struct Node {
    int data;
    struct Node *left,*right;
    Node(int el){
        data=el;
        left=NULL;right=NULL;
    }
};
typedef struct Node Node;
Node *root=NULL;

//Constructing tree
void getparent(Node *n,int el,Node **temp){
    if(n==NULL)return;
    if(n->data==el){
        *temp=n;
    }
    getparent(n->left,el,temp);
    getparent(n->right,el,temp);
}

//Constructing tree
void create(){
    int el;
    cin>>el;
    Node *p = new Node(el);
    if(root==NULL){
        root=p;
    }else{
        Node *temp;
        int ch;
        cin>>ch;
        getparent(root,ch,&temp);
        if(temp->left==NULL){
            temp->left=p;
        }
        else{
            temp->right=p;
        }
    }
}
//Inorder traversal of tree
void print(Node *n){
    if(n!=NULL){
        print(n->left);
        cout<<n->data<<" ";
        print(n->right);
    }
}
//Height of tree from nth node
int height(Node *n){
    if(n==NULL)return 0;
    return max( height(n->left),height(n->right) )+1;
}

//Code For calculating max time in seconds when burnt at node with value k
int diameter(Node *n,int el,int *maxx){
    if(n!=NULL ){
        if(n->data==el)return  1;
        else {
            if(diameter(n->left,el,maxx)>0){
                if(*maxx<1+diameter(n->left,el,maxx)+height(n->right) )
                                *maxx=1+diameter(n->left,el,maxx)+height(n->right);
                return 1+diameter(n->left,el,maxx);
            }else if(diameter(n->right,el,maxx)>0) {
                if(*maxx<1+diameter(n->right,el,maxx)+height(n->left) )
                                *maxx=1+diameter(n->right,el,maxx)+height(n->left);
                return 1+diameter(n->right,el,maxx);
            }
            return 0;
        }
    }
    return 0;
}

int main() {
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        create();
    }
    print(root);
    cout<<"\n";
    int k;
    cin>>k;
    int maxx=0;
    diameter(root,k,&maxx);
    cout<<"Time taken will be : "<<maxx<<"\n";
}


//It is working fine . I made the tree to make it understandable.

【讨论】:

  • 该问题被标记为 Python,并且已经有许多有效的解决方案。您为什么发布 C++ 答案?恕我直言,只有代码而没有解释的答案并不是真正的答案。
【解决方案2】:

以下是在给定源节点(可以是离开节点或非离开节点)的情况下找到烧树所需时间的解决方案之一

解决方法如下:

1) 在树中找到源节点并找到节点的高度(这里我们将它存储在变量“sourceDepth”中)

2) 对于给定源节点的所有祖先

 ->Take distance from the source node and present node 

 ->Find the height of the opposite subtree in which the source is not present

 ->Add both of the above + 1 (for the edge between ancestor and sub tree).Lets call this d

3) 取第 2 步中所有 d 的最大值和第 1 步中的 sourceDepth,这是所需的答案。

对于下面的例子,让 source 为 3:

     7
    / \
   8   4
  / \   \
 10   9   3
    / \   \
   0   11   2
             \
              1

源深度(即 3)为:sourceDepth = 2

source 的所有祖先都是 [7 , 4 ]

对于祖先 4:

与源的距离为 1,并且源的相反方向没有子树(即源在右子树中,没有左子树)。所以这里的 d 是 1。

为了祖先 7

与源的距离为 2 ,与源相反方向的子树的高度为 2 。所以这里的 d 是 2+2+1=5。 (1 代表 7 和 8 之间的边)

高度为 2 的节点 7 右子树

   8   
  / \  
 10   9  
    / \  
   0   11 

在这种情况下,解决方案将是 Max of (2,1,5),即 5 。所以答案是 5

上述解决方案的Java实现是:

static int max = Integer.MIN_VALUE;

private static int find(TreeNode<Integer> root, int source, int sourceDepth) {

    if (root == null) {
        return -1;
    }

    if (root.getData() == source) {
        sourceDepth = getDepth(root);
        return 0;
    }

    int left = find(root.getLeft(), source, sourceDepth);
    if (left != -1) {
        int rightDepth = getDepth(root.getRight()) + 1;
        max = Math.max(rightDepth + left + 1, sourceDepth);
        return left + 1;
    }

    int right = find(root.getRight(), source, sourceDepth);
    if (right != -1) {
        int leftDepth = getDepth(root.getRight()) + 1;
        max = Math.max(leftDepth + right + 1, sourceDepth);
        return right + 1;
    }

    return -1;
}

private static int getDepth(TreeNode<Integer> root) {

    if (root == null) {
        return -1;
    }

    return Math.max(getDepth(root.getLeft()), getDepth(root.getRight())) + 1;
}

这里的源可以是任何离开节点,它会给出这里要求的答案。

【讨论】:

    【解决方案3】:

    对于那些想知道这篇文章发生了什么的人,使用的解决方案是:

    LeafSide = []
    
    class Node:
        """Tree class."""
    
        def __init__(self, key):
            """Declare values of a node."""
            self.left = None
            self.right = None
            self.value = key
    
    
    def leafHeight(root, leaf):
        """Height of the leaf."""
        if root is None:
            return 0
        else:
            if root.left is leaf:
                aux = 1 + leafHeight(root.right, leaf)
                LeafSide.append(aux)
                return 1
            if root.right is leaf:
                aux = 1 + leafHeight(root.left, leaf)
                LeafSide.append(aux)
                return 1
            return 1 + max(leafHeight(root.left, leaf), leafHeight(root.right, leaf))
    
    
    def timeBurn(root, leaf):
        """How long will it take to burn the the node to furthest node."""
        hl = leafHeight(root.left, leaf)
        hr = leafHeight(root.right, leaf)
        opposite_LeafSide = 1 + hl + hr
        return max(opposite_LeafSide, LeafSide[0])
    
    
    if __name__ == '__main__':
        root = Node(1)
        root.left = Node(1)
        root.right = Node(1)
        root.left.left = Node(1)
        root.left.right = Node(1)
        root.left.right.left = Node(1)
        root.left.right.right = Node(1)
        root.right.right = Node(1)
        root.right.right.right = Node(1)
        root.right.right.right.right = Node(1)
        print ("Starting from the given node, it will take %ds to burn the whole tree" % (timeBurn(root, root.left.right)))
    

    时间:O(n)

    空间:O(n)

    如果您注意到,每个节点的值都是 1。节点的值与此问题无关。它只是代表其中的一些价值。我有一个的原因是考虑第二个(1 秒节点)。感谢所有帮助过我的人。我喜欢阅读你们谈论的所有 cmets 和方法 :)。如果您对如何改进代码有更好的想法,请随时在下方发表评论!

    【讨论】:

      【解决方案4】:

      这可以使用递归函数来解决,该函数返回从当前节点到起始节点的路径长度(或者如果起始节点不在其下方,则返回到任何叶子的最长路径)。

      如果找到的话,我们还可以让它返回距离起始节点最近的最长路径,它只是对左右子节点调用的函数的总和(加一,对于当前节点)。

      这类似于m69描述的解决方案。

      这是在 O(n) 时间内运行的,因为函数在恒定时间内运行(如果排除递归调用),并且每个节点最多调用该函数三次(对于节点本身及其左右子节点,在叶节点的情况下)。

      这将使用 O(height) 空间,因为除了函数调用及其变量之外,我们不存储任何东西,并且在任何给定时间我们可以在内存中拥有的最大数量等于递归深度 (即树的高度)。

      class Node:
          def __init__(self, key):
              self.left = None
              self.right = None
              self.value = key
      
      # returns a tuple (max = the longest path so far, dist = current path)
      def _recurse(node, start):
          if node is None:
              return (None, 0)
          else:
              max_left, dist_left = _recurse(node.left, start)
              max_right, dist_right = _recurse(node.right, start)
              # this node is the starting node
              if node == start:
                  return (0, 0)
              # the starting node is in left or right
              elif max_right is not None or max_left is not None:
                  return (dist_right + dist_left + 1,
                          (dist_left if max_right is None else dist_right) + 1)
              # we haven't seen the starting node
              else:
                  return (None, max(dist_left, dist_right) + 1)
      
      def time_to_burn(root, start):
          return _recurse(root, start)[0]
      

      测试:

      root = Node(1)
      root.left = Node(1)
      root.right = Node(1)
      root.left.left = Node(1)
      root.left.right = Node(1)
      root.left.right.left = Node(1)
      root.left.right.right = Node(1)
      root.right.right = Node(1)
      root.right.right.right = Node(1)
      root.right.right.right.right = Node(1)
      
      >>> time_to_burn(root, root.left.right.right)
      7
      

      适用于非叶子起始节点的解决方案

      基本思想是每个节点有3个返回值:

      • max,这是到目前为止从起始节点开始的最长路径(或者 None,如果我们还没有看到起始节点)。
      • above,即起始节点上方的节点数(如果我们还没有看到起始节点,则为 None)。
      • below,这是起始节点下方的最长路径(或者如果我们还没有看到起始节点,则只是从当前节点开始的最长路径)。

      从子子树计算 abovebelow 非常简单 - 请参阅代码了解详细信息。

      我们可以将当前节点的最长路径max定义为:

      • 从起始节点向下的最长路径(即below
      • 以及包含当前节点的最长路径,即当前节点到起始节点的距离加上没有起始节点的子树中的最长路径(加一)。

      代码:(替换上面的_recurse函数)

      # returns a tuple (max, above, below)
      def _recurse(node, start):
          if node is None:
              return (None, None, 0)
          else:
              max_left, above_left, below_left = _recurse(node.left, start)
              max_right, above_right, below_right = _recurse(node.right, start)
              # this node is the starting node
              if node == start:
                  below = max(below_left, below_right)
                  return (below, 0, below)
              # the starting node is in left or right
              elif above_right is not None or above_left is not None:
                  return (max((0 if above_right is None else above_right) + below_left,
                              (0 if above_left is None else above_left) + below_right) + 1,
                          (above_right if above_left is None else above_left) + 1,
                          below_right if above_left is None else below_left)
              # we haven't seen the starting node
              else:
                  return (None, None, max(below_left, below_right) + 1)
      
      >>> time_to_burn(root, root.left.right)
      6
      

      【讨论】:

      • 就我阅读您的代码而言,它看起来运行在 O(n) 或 O(logn) 中?节点位于哪里?你如何解释你的时间和空间复杂性?因为您正在逐一检查递归调用是否正确?
      • 如果面试官会问,优化这个的最佳方法是什么?我正在考虑一个包含所有节点的哈希表并存储所有可能的组合,例如这个例子:techieme.in/tree-diameter(第二张图片)@Dukeling
      • @ZeidTisnes 没有办法(显着)改进这一点 - 它已经渐近最优 - 你不能比访问每个节点一次更好。除非您想在同一棵树上运行许多查询,以不同的节点为起点,但这完全是一个不同的问题。
      【解决方案5】:

      这是我的方法。根据左侧或右侧有叶子的节点,您有两种可能性:

      • 向下探索树
      • 探索树到另一边

      这两种可能性定义了两条路径。最长路径是问题的答案(所选叶子和任何其他叶子之间的最长路径)。在给定的burn(红色)节点和具有叶子引用的节点(蓝色)上,最好在此图中理解

      我们以编程方式探索树,直到找到引用叶子的节点。在这种情况下,我们计算探索树其余部分的路径(在具有叶子的原始树的一侧)并返回 1(以递归返回创建到另一侧的路径)。

      【讨论】:

      • @m69 我用其他情况重新检查了解决方案,结果发现代码没有按预期工作。解决方案仍然是使用递归创建最长路径,但我删除了代码部分以避免混淆。
      【解决方案6】:

      以下面的例子为例;首先,从根遍历到叶着火(F):

           N
          / \
         N   N
        / \   \
       N   N   N
          / \   \
         N   F   N
        / \       \
       N   N       N
            \
             N
      

      然后,向上移动到它的父节点,取燃烧叶的距离(1)和左子树的高度(3)之和,即为4:

           N
          / \
         N   N
        / \   \
       N   4   N
          / \   \
         3   1   N
        / \       \
       N   2       N
            \
             1
      

      所以 4 是当前最大值。现在,向上移动到父节点,取燃烧叶的距离(2)和左子树的深度(1)之和,即3:

           N
          / \
         3   N
        / \   \
       1   2   N
          / \   \
         N   1   N
        / \       \
       N   N       N
            \
             N
      

      所以当前最大值仍然是4。现在向上移动到父节点,取燃烧叶的距离(3)和右子树的深度(4)之和,即7:

           7
          / \
         3   4
        / \   \
       N   2   3
          / \   \
         N   1   2
        / \       \
       N   N       1
            \
             N
      

      新的最大值是 7,我们已经到达根节点,所以 7 是答案,因为您可以通过查看 x 秒后哪些节点着火来检查:

           3
          / \
         2   4
        / \   \
       3   1   5
          / \   \
         2   0   6
        / \       \
       3   3       7
            \
             4
      

      这是一个根不属于最长路径的示例:

               N            N            3                  2
              / \          / \          / \                / \
             N   N        4   N        2   1              1   3
            / \          / \          / \                / \
           N   F        3   1        N   1              2   0
          /            /            /                  /
         N            2            N                  3
        /            /            /                  /
       N            1            N                  4
      

      遇到的最大值是 4,在着火的叶子的父级中。


      这是一个简单的 JavaScript 代码 sn-p(我不会说 Python,但这应该可以作为伪代码工作)。它在我回答的第一个示例中使用了树的硬编码版本。如您所见,它对树进行了一次深度优先遍历。

      function burn(root) {
          var maximum = 0;
          traverse(root);
          return maximum;
      
          function traverse(node) {
              if (node.onfire) {
                  return {steps: 1, onfire: true};
              }
              var l = node.left ? traverse(node.left) : {steps: 0};
              var r = node.right ? traverse(node.right) : {steps: 0};
              if (l.onfire || r.onfire) {
                  maximum = Math.max(maximum, l.steps + r.steps);
                  return {steps: (l.onfire ? l.steps : r.steps) + 1, onfire: true};
              }
              return {steps: Math.max(l.steps, r.steps) + 1};
          }
      }
      
      var tree = {left: {left: {left: null, right: null}, right: {left: {left: {left: null, right: null}, right: {left: null, right: {left: null, right: null}}}, right: {left: null, right: null, onfire:true}}}, right: {left: null, right: {left: null, right: {left: null, right: {left: null, right: null}}}}}
      document.write(burn(tree));

      【讨论】:

      • 右子树可能不相关。想象一下左侧有一长串节点。
      • @fafl 或者它可能不会。这两种情况都用这种方法处理。
      • 可能值得注意的是,一个节点处的值不一定与到燃烧节点的距离相同——考虑添加root.left.left.left,这将使root.left处的值等于4 ,但你仍然应该使用 3 来给你 7。
      • @Dukeling 我已经更改了示例,并且还在图中指出了到燃烧的叶子的距离,并稍微扩展了解释。我认为现在更清楚了。
      【解决方案7】:

      我突然想到您需要以下内容:

      1. 起始节点是在根的左边还是右边。
      2. 起始节点的深度(称为dStart)。
      3. 起始节点分支上离根最远的节点的深度(即根的左侧或右侧)。我们称之为dSameSide
      4. 起始节点和#3 中标识的节点的最低共同祖先的深度。 (叫它dCommonAncestor
      5. 树对面最低节点的深度,dOppositeSide

      您可以从树的单个中序遍历中获取所有这些信息。

      从起始节点到树那一侧的最深节点所需的步数是(dSameSide - dCommonAncestor) + (dStart - dCommonAncestor)

      从起始节点到对面最深节点的步数为(dStart + dOppositeSide)

      而烧掉整棵树所需的步数是这两个中的最大值。

      我将把实现留给你。您可能会发现 How to find the lowest common ancestor of two nodes in any binary tree? 很有帮助。

      【讨论】:

      • 如你所见,Dukeling 的 Python 代码和我的 JS 代码 sn-p 做了一次完整的遍历(递归 BFS)。
      • @m69 看起来你和 Dukeling 正在做深度优先而不是广度优先的遍历。无论如何,我的回答从高层次上解释了我认为最简单的方法。以这种方式思考问题通常会使编写代码几乎成为事后的想法。
      • 抱歉,输入的是 DFS。
      • 是的,在和我的一些同事讨论之后,显然他们指的是共同祖先,我正在研究它。我试图了解它如何读取先前的父/祖先值。您给我的链接非常有用。谢谢@JimMischel :)
      【解决方案8】:

      这出乎我的意料,但平均而言,答案是ln(n),因为这与搜索排序二叉树的算法完全相同。

      编辑:我错了。我在想“从 X 到 Y 的最快路径”,即 ln(n),但这实际上是“从 X 到任何东西的最长路径”。这不是二分查找。

      【讨论】:

      • 对于给定的树,答案取决于燃烧叶子的位置。所以答案不仅仅与叶子的数量或树的深度有关。
      • @m69 不过,这只是一个系数,并且会降低复杂性。即 O(2*ln) == O(ln)
      • 这只有在树是平衡的时候才是正确的
      • 您对最坏情况和 O() 表示法考虑太多了;问题是在特定情况下找到确切的距离。
      • OP的问题是,给定一个特定树和一个特定节点,假设烧掉整棵树需要多少时间单位单个节点在时间 X 中燃烧,并按照描述的方式传播。计算复杂度无关紧要,只是说最好的情况是 X 个时间单位,最坏的情况是 n*X 个时间单位。平均值无关紧要。
      【解决方案9】:

      使用 BFS 可以快速完成:

      class Node:
          def __init__(self, value):
              self.left = None
              self.right = None
              self.parent = None
              self.value = value
      
          def set_left(self, other):
              self.left = other
              other.parent = self
      
          def set_right(self, other):
              self.right = other
              other.parent = self
      
      def get_distance_to_furthest(node):
          visited = set()
          queue = [(node, 0)]
          max_d = 0
          while queue:
              node, d = queue.pop(0)
      
              if node in visited:
                  continue
              visited.add(node)
      
              max_d = max(d, max_d)
      
              if node.left:
                  queue.append((node.left, d + 1))
              if node.right:
                  queue.append((node.right, d + 1))
              if node.parent:
                  queue.append((node.parent, d + 1))
      
          return max_d
      
      
      # Sample code
      root = Node(1)
      root.set_left(Node(1))
      root.set_right(Node(1))
      root.left.set_left(Node(1))
      root.left.set_right(Node(1))
      root.left.right.set_left(Node(1))
      root.left.right.set_right(Node(1))
      root.right.set_right(Node(1))
      root.right.right.set_right(Node(1))
      root.right.right.right.set_right(Node(1))
      print(
          "Starting from the given node, it will take %ds to burn the whole tree"
          % (get_distance_to_furthest(root.left.right))
      )
      

      二叉树只是一种特殊的图,因此您可以遍历所有节点并跟踪每个节点到火灾开始节点的距离。结果是您看到的最远距离。

      【讨论】:

      • 值得注意的是,访问父节点对于树来说是相当不标准的。
      • 当你试图获取到root.right.set_right的距离时,它返回一个错误,因为没有剩余。没有左或右的基本情况在哪里?
      • @ZeidTisnes root.right.set_right是函数,试试root.right.right
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-09-24
      • 1970-01-01
      • 2018-12-17
      • 2015-12-20
      • 1970-01-01
      • 2017-09-30
      • 1970-01-01
      相关资源
      最近更新 更多