Taro Logo

Diameter of N-Ary Tree

Medium
Asked by:
Profile picture
Profile picture
33 views
Topics:
TreesRecursion

Given a root of an N-ary tree, you need to compute the length of the longest path between any two nodes in the tree. This path may or may not pass through the root.

(An N-ary tree is a tree in which each node has no more than N children.)

The length of path between two nodes is represented by the number of edges between them.

Example 1:


Input: root = [1,null,3,2,4,null,5,6]
Output: 3
Explanation: Longest path is 5 - 3 - 1 - 2, or 6 - 3 - 1 - 2, or 5 - 3 - 1 - 4, or 6 - 3 - 1 - 4.

Example 2:


Input: root = [1,null,2,null,3,null,4,null,5,null,6]
Output: 5

Constraints:

  • The depth of the n-ary tree is less than or equal to 1000.
  • The total number of nodes is between [0, 10^4].

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What is the maximum number of children a node in the N-ary tree can have?
  2. Can the tree be empty, or contain only a single node?
  3. Are the node values relevant to calculating the diameter, or is it purely based on the structure of the tree?
  4. In the case of multiple paths resulting in the same maximum diameter, is any valid path acceptable?
  5. What data type are the node values, and is there a possibility of integer overflow when calculating path lengths?

Brute Force Solution

Approach

Finding the diameter of a tree involves identifying the longest path between any two points in the tree. The brute force method explores every possible path to find the longest one. It is an exhaustive approach, ensuring no path is missed.

Here's how the algorithm would work step-by-step:

  1. Consider every single node in the tree as a potential starting point.
  2. From that starting point, explore every possible path to every other node in the tree.
  3. Calculate the length of each of these paths.
  4. Compare the lengths of all the paths you've calculated.
  5. The longest path you've found among all possibilities is the diameter of the tree.

Code Implementation

class Node:
    def __init__(self, value):
        self.value = value
        self.children = []

def diameter_of_n_ary_tree_brute_force(root):
    if not root:
        return 0

    max_diameter = 0

    def height(node):
        if not node:
            return 0
        if not node.children:
            return 1

        maximum_height = 0
        for child in node.children:
            maximum_height = max(maximum_height, height(child))

        return maximum_height + 1

    def calculate_distance(start_node, end_node):
        if not start_node or not end_node:
            return 0

        if start_node == end_node:
            return 0

        # This list keeps track of visited nodes
        visited_nodes = set()
        queue = [(start_node, 0)]
        visited_nodes.add(start_node)

        while queue:
            current_node, current_distance = queue.pop(0)

            if current_node == end_node:
                return current_distance

            for child in current_node.children:
                if child not in visited_nodes:
                    queue.append((child, current_distance + 1))
                    visited_nodes.add(child)

        return float('inf')

    # Consider every node as a potential start
    all_nodes = []

    def traverse(node):
        if node:
            all_nodes.append(node)
            for child in node.children:
                traverse(child)

    traverse(root)

    for start_node in all_nodes:
        # Explore every possible path to every other node
        for end_node in all_nodes:
            path_length = calculate_distance(start_node, end_node)
            # Find the longest path
            max_diameter = max(max_diameter, path_length)

    return max_diameter

Big(O) Analysis

Time Complexity
O(n³)The algorithm considers each node in the n-ary tree as a starting point. For each starting node, it explores every possible path to every other node, which could involve visiting all n nodes. Determining the length of each of these paths from a given start node requires traversing the path, taking O(n) time in the worst case. Since we repeat this process for all n nodes, the total time complexity approximates to n * n * n, simplifying to O(n³).
Space Complexity
O(N)The brute force approach, as described, explores every possible path from each node to every other node. This implicitly requires storing information about the visited nodes during path exploration, most likely using a data structure like a set or a list to avoid cycles. In the worst-case scenario, where the tree is highly unbalanced or skewed, the maximum length of any such path could be proportional to the number of nodes in the tree, N. This results in storing up to N visited nodes in the auxiliary data structure, giving a space complexity of O(N).

Optimal Solution

Approach

To find the longest path in this tree, we need to find the longest route between any two points. Instead of checking every possible path, we'll use a clever trick by focusing on each connection point and identifying the two longest paths that go through it.

Here's how the algorithm would work step-by-step:

  1. Think about each connection point in the tree, like a city in a road network.
  2. For each connection point, find the longest route going down from it to one of its farthest 'leaf' nodes.
  3. Also, find the second-longest route going down from the same connection point to another of its farthest 'leaf' nodes.
  4. Add the lengths of these two longest routes together. This gives you the longest path that passes through that specific connection point.
  5. Do this for every connection point in the tree.
  6. The largest number you found among all the connection points is the length of the longest path in the entire tree.

Code Implementation

class Node:
    def __init__(self):
        self.children = []

def diameter_of_n_ary_tree(root):
    longest_path = 0

    def depth(node):
        nonlocal longest_path
        if not node:
            return 0

        # Store the depths of all child nodes
        child_depths = []
        for child in node.children:
            child_depths.append(depth(child))

        # Find the two largest depths
        child_depths.sort(reverse=True)

        first_longest_path = child_depths[0] if len(child_depths) > 0 else 0
        second_longest_path = child_depths[1] if len(child_depths) > 1 else 0

        # Update the longest path if needed
        longest_path = max(longest_path,
                           first_longest_path + second_longest_path)

        # Return the depth of the current node
        return first_longest_path + 1

    depth(root)
    return longest_path

def create_tree():
    root = Node()

    child1 = Node()
    child2 = Node()
    child3 = Node()
    root.children = [child1, child2, child3]

    grandchild1 = Node()
    grandchild2 = Node()
    child1.children = [grandchild1, grandchild2]

    grandchild3 = Node()
    child2.children = [grandchild3]

    return root

if __name__ == '__main__':
    root_node = create_tree()

    #Calculate tree diameter
    tree_diameter = diameter_of_n_ary_tree(root_node)

    print(f"Diameter of N-ary Tree: {tree_diameter}")

Big(O) Analysis

Time Complexity
O(n)The algorithm traverses each node of the n-ary tree once to compute the longest and second-longest paths from that node to its descendants. For each node, finding the longest and second longest paths amongst its children takes time proportional to the number of children, but the sum of the number of children across all nodes is n-1 (the number of edges). Therefore, the overall time complexity is dominated by the single traversal of all nodes in the tree and is O(n).
Space Complexity
O(N)The algorithm implicitly uses a recursion stack. In the worst-case scenario, where the tree is highly skewed (resembling a linked list), the depth of the recursion can be proportional to the number of nodes, N. Each recursive call adds a new frame to the stack to store local variables and the return address. Therefore, the auxiliary space used by the recursion stack in the worst-case can grow linearly with the number of nodes, N. This results in a space complexity of O(N).

Edge Cases

Null or Empty Root Node
How to Handle:
Return 0 immediately as a null tree has a diameter of 0.
Single Node Tree
How to Handle:
Return 0 because a single node tree has a diameter of 0.
Tree with only root and leaves
How to Handle:
Calculates height and diameter based on leaf nodes connected to the root.
Deeply Skewed Tree (e.g., linked list)
How to Handle:
Ensure the height calculation and diameter update traverse the entire 'list' efficiently.
Large Tree with Many Nodes
How to Handle:
Ensure the solution has acceptable time and space complexity for potentially millions of nodes, avoiding stack overflow in recursive solutions.
Tree with nodes having different numbers of children.
How to Handle:
The algorithm should work regardless of each node's degree.
Tree where some subtrees have larger diameters than paths including the root.
How to Handle:
The algorithm must correctly track the global maximum diameter, not just the diameter at the root.
Integer Overflow in Height or Diameter Calculation
How to Handle:
Use appropriate data types (e.g., long) for height and diameter to prevent potential overflow in large trees.