Height of Binary Tree After Subtree Removal Queries

Updated on 02 June, 2025
Height of Binary Tree After Subtree Removal Queries header image

Problem Statement

In this problem, you are provided with the root of a binary tree comprising n unique nodes, each labeled with a unique integer from 1 to n. Besides the tree, you are presented with an array of queries consisting of m elements. Each element in this array represents a specific operation on the tree wherein you must remove an entire subtree originating from a node specified in the queries.

The operation specified requires you to:

  1. Identify the subtree rooted at the node indicated by each query value queries[i].
  2. Remove this subtree entirely from the main tree.

After each operation, the structure of the tree is restored to its original state before proceeding to the next operation. This characteristic of the operation ensures the independency of each query from the others.

You are tasked to determine the height of the tree after each subtree removal. The height of the tree is defined as the number of edges on the longest path from the root node to any leaf. The result is to be returned as an array representing the tree height after each respective operation.

Examples

Example 1

Input:

root = [1,3,4,2,null,6,5,null,null,null,null,null,7], queries = [4]

Output:

[2]

Explanation:

The diagram above shows the tree after removing the subtree rooted at node with value 4.
The height of the tree is 2 (The path 1 -> 3 -> 2).

Example 2

Input:

root = [5,8,9,2,1,3,7,4,6], queries = [3,2,4,8]

Output:

[3,2,3,2]

Explanation:

We have the following queries:
- Removing the subtree rooted at node with value 3. The height of the tree becomes 3 (The path 5 -> 8 -> 2 -> 4).
- Removing the subtree rooted at node with value 2. The height of the tree becomes 2 (The path 5 -> 8 -> 1).
- Removing the subtree rooted at node with value 4. The height of the tree becomes 3 (The path 5 -> 8 -> 2 -> 6).
- Removing the subtree rooted at node with value 8. The height of the tree becomes 2 (The path 5 -> 9 -> 3).

Constraints

  • The number of nodes in the tree is n.
  • 2 <= n <= 105
  • 1 <= Node.val <= n
  • All the values in the tree are unique.
  • m == queries.length
  • 1 <= m <= min(n, 104)
  • 1 <= queries[i] <= n
  • queries[i] != root.val

Approach and Intuition

To solve this problem, let’s break down the necessary steps based on the provided examples and the constraints:

  1. Construct the Binary Tree: First, you need to correctly construct the binary tree from the input data. Each node in the tree should have its value and pointers to its left and right children, according to the binary tree rules.

  2. Perform Query-based Deletion:

    • For each query value queries[i], identify the corresponding node in the binary tree.
    • Remove the subtree rooted at this specified node.
  3. Calculate the Height of the Modified Tree:

    • Post-deletion, traverse the tree to calculate its height by finding the longest path from the root node to any of its leaves.
    • Store the resulting height in an array.
  4. Restore the Tree: After each operation and height calculation, restore the tree to its initial state before proceeding with the next query. This step is crucial to ensure that each query operation is independent.

In terms of implementation, the process to locate a node and delete its subtree can utilize tree-based search algorithms like Depth-First Search (DFS). Calculating the height of a tree post-modification can also be efficiently done via a DFS approach, checking each node’s depth.

The challenge in this task is ensuring efficient modifications and restorations of the tree, especially considering the constraints suggest n could be as large as 105 and up to 104 queries. This might require efficient tree manipulation techniques and possibly optimizing tree building and traversal algorithms.

Solutions

  • C++
  • Java
  • Python
cpp
class Solution {
public:
    vector<int> evaluateTree(TreeNode* root, vector<int>& requests) {
        int limit = 100000;
        vector<int> depths(limit + 1, 0), heights(limit + 1, 0);
        vector<int> topHeight(limit + 1, 0), nextHeight(limit + 1, 0);

        dfsHelper(root, 0, depths, heights, topHeight, nextHeight);

        vector<int> output;
        output.reserve(requests.size());

        for (int request : requests) {
            int level = depths[request];
            if (heights[request] == topHeight[level]) {
                output.push_back(level + nextHeight[level] - 1);
            } else {
                output.push_back(level + topHeight[level] - 1);
            }
        }

        return output;
    }

private:
    int dfsHelper(TreeNode* node, int level, vector<int>& depths,
                  vector<int>& heights, vector<int>& topHeight,
                  vector<int>& nextHeight) {
        if (!node) return 0;

        depths[node->val] = level;
        int leftSubtreeHeight = dfsHelper(node->left, level + 1, depths, heights, topHeight, nextHeight);
        int rightSubtreeHeight = dfsHelper(node->right, level + 1, depths, heights, topHeight, nextHeight);
        int currentSubtreeHeight = 1 + max(leftSubtreeHeight, rightSubtreeHeight);

        heights[node->val] = currentSubtreeHeight;
        if (currentSubtreeHeight > topHeight[level]) {
            nextHeight[level] = topHeight[level];
            topHeight[level] = currentSubtreeHeight;
        } else if (currentSubtreeHeight > nextHeight[level]) {
            nextHeight[level] = currentSubtreeHeight;
        }

        return currentSubtreeHeight;
    }
};

This solution involves determining the height of a binary tree after certain subtrees, identified by their node values (requests), are "virtually" removed. It's written in C++ and can handle a variety of tree sizes effectively, aiming to solve the problem efficiently.

The Solution class includes the evaluateTree method, which processes a vector of requests. Each request signifies a node that is considered removed, and the function calculates the height of the tree considering this removal.

  • Initialize vectors up to a pre-defined limit to store tree depths, actual heights, top heights per level, and next best heights if the tallest sub-tree is removed.
  • Use the dfsHelper function recursively to traverse the tree and fill details about each node's depth and its subtree's heights. In this traversal, compute:
    • The height of the node's subtrees.
    • Assign levels and heights for navigation and comparison.
    • Determine the maximum and second maximum heights at each level of depth, crucial for post-removal height calculation.

After pre-processing, the solution iterates through each request:

  • Determine the direct impact on the tree's height by comparing the requested node's height to the pre-computed maximum heights at that level.
  • If the node being virtually removed had the greatest height, the tree height is recalculated using the next highest value.

The output is a vector where for each request, you receive the recalculated tree height if that specific node were removed.

This approach ensures that the tree is only traversed once initially, and subsequent operations (processing each request) are efficient, resulting in a complexity that's well-suited for handling large data inputs effectively.

java
class Solution {

    public int[] processTree(TreeNode rootNode, int[] queryValues) {
        Map<Integer, Integer> nodeDepth = new HashMap<>();
        Map<Integer, Integer> subtreeHeight = new HashMap<>();
        Map<Integer, Integer> maxHeightAtLevel = new HashMap<>();
        Map<Integer, Integer> secondMaxHeightAtLevel = new HashMap<>();

        computeTreeProperties(rootNode, 0, nodeDepth, subtreeHeight, maxHeightAtLevel, secondMaxHeightAtLevel);

        int[] outcomes = new int[queryValues.length];

        for (int i = 0; i < queryValues.length; i++) {
            int currentNodeValue = queryValues[i];
            int currentDepth = nodeDepth.get(currentNodeValue);

            if (subtreeHeight.get(currentNodeValue).equals(maxHeightAtLevel.get(currentDepth))) {
                outcomes[i] = currentDepth + secondMaxHeightAtLevel.getOrDefault(currentDepth, 0) - 1;
            } else {
                outcomes[i] = currentDepth + maxHeightAtLevel.getOrDefault(currentDepth, 0) - 1;
            }
        }

        return outcomes;
    }

    private int computeTreeProperties(
        TreeNode current,
        int depth,
        Map<Integer, Integer> depthMap,
        Map<Integer, Integer> heights,
        Map<Integer, Integer> firstMax,
        Map<Integer, Integer> secondMax
    ) {
        if (current == null) return 0;

        depthMap.put(current.val, depth);

        int leftSubtreeHeight = computeTreeProperties(current.left, depth + 1, depthMap, heights, firstMax, secondMax);
        int rightSubtreeHeight = computeTreeProperties(current.right, depth + 1, depthMap, heights, firstMax, secondMax);
        int treeHeight = 1 + Math.max(leftSubtreeHeight, rightSubtreeHeight);

        heights.put(current.val, treeHeight);

        int currentLevelFirstMax = firstMax.getOrDefault(depth, 0);
        if (treeHeight > currentLevelFirstMax) {
            secondMax.put(depth, currentLevelFirstMax);
            firstMax.put(depth, treeHeight);
        } else if (treeHeight > secondMax.getOrDefault(depth, 0)) {
            secondMax.put(depth, treeHeight);
        }

        return treeHeight;
    }
}

The provided Java solution processes a binary tree to answer specific queries concerning the height of the tree after removing certain subtrees. The solution employs a Solution class with two methods: processTree and computeTreeProperties.

  • processTree takes a binary tree's root node and an array of values representing queries. It initializes four HashMaps to store the depth of each node, the height of each subtree, and the maximum and second maximum heights at each level in the tree. It then calls computeTreeProperties to populate these maps and, finally, computes the resulting tree height for each query value based on whether the height of the subtree being considered is the maximum at its level.

  • computeTreeProperties is a recursive method that traverses the tree and computes the properties used in the processTree method. It calculates the depth of each node and the height of its subtrees. It appropriately updates the maximum and second maximum heights found at each level of the tree during the traversal. These values are used to determine how the tree's height changes when a node is removed during the queries.

By using the HashMaps, the queries are processed efficiently, allowing quick access to the necessary tree information. The solution handles each query independently, assessing the impact of the removal of a single subtree and ensuring accurate height computation.

Each query results in an outcome based on how significant the removed node's subtree is relative to other subtrees at the same depth. The response to each query is stored in an output array of integers and returned by processTree.

This approach efficiently addresses the problem by combining depth-first tree traversal with dynamic updates to a series of maps that track crucial properties at each level of the tree.

python
class Solution:
    def queryTree(
        self, root_node: Optional[TreeNode], inputs: List[int]
    ) -> List[int]:
        depths_of_nodes = {}
        heights_of_subtrees = {}

        highest_at_depth = {}
        second_highest_at_depth = {}

        def depth_search(node, depth):
            if not node:
                return 0

            depths_of_nodes[node.val] = depth

            left_tree_height = depth_search(node.left, depth + 1)
            right_tree_height = depth_search(node.right, depth + 1)
            subtree_height = 1 + max(left_tree_height, right_tree_height)

            heights_of_subtrees[node.val] = subtree_height

            if subtree_height > highest_at_depth.get(depth, 0):
                second_highest_at_depth[depth] = highest_at_depth.get(
                    depth, 0
                )
                highest_at_depth[depth] = subtree_height
            elif subtree_height > second_highest_at_depth.get(depth, 0):
                second_highest_at_depth[depth] = subtree_height

            return subtree_height

        depth_search(root_node, 0)

        return [
            depths_of_nodes[input_value]
            + (
                second_highest_at_depth.get(depths_of_nodes[input_value], 0)
                if heights_of_subtrees[input_value] == highest_at_depth[depths_of_nodes[input_value]]
                else highest_at_depth.get(depths_of_nodes[input_value], 0)
            )
            - 1
            for input_value in inputs
        ]

This Python solution addresses the problem of determining the height of a binary tree after a sequence of subtree removals based on provided queries. The process involves the following steps:

  1. Define nested dictionaries to store the depths of each node, the heights of subtrees, the highest and second-highest subtree heights at each level of depth.
  2. Implement a depth-first search (DFS) function depth_search that:
    • Traverses each node of the tree.
    • Calculates and stores the depth of the current node.
    • Recursively finds the height of left and right subtrees.
    • Updates the current subtree height.
    • Maintains records of the highest and second highest subtree heights at each depth.
  3. For each input query, the solution computes:
    • The depth of the node related to the query.
    • Checks if the height of the subtree at the queried node matches the highest height at that depth.
    • Utilizes the second highest height at that depth if the condition is satisfied, otherwise uses the highest height.
  4. Adjusts the final height of the tree after the specified subtrees (as per query nodes) have been removed by considering the above measurements. This adjustment is critical to account for the effective tree height post-removal of certain subtrees.

This approach ensures efficient management of tree traversal and dynamic updating of heights with each query, resulting in an optimized solution for varying inputs.

Comments

No comments yet.