Taro Logo

Equal Tree Partition

Medium
Asked by:
Profile picture
Profile picture
68 views
Topics:
TreesRecursionDynamic Programming

Given the root of a binary tree, check whether there is at least one root to leaf path for which the sum of all the node values along the path equals the given targetSum. Besides, each node only could appear once in the path.

Return true if there exists a path whose sum equals the targetSum, and false otherwise.

Example 1:

Input: root = [5,10,null,3,null,1,11,null,null,null,2], targetSum = 22
Output: true
Explanation: There exist a path whose sum equals the given targetSum.

Example 2:

Input: root = [1,2,3,4,5,6,7,8,9,10], targetSum = 15
Output: false
Explanation: There exist no path whose sum equals the given targetSum.

Constraints:

  • The number of the nodes in the tree is in the range [1, 104].
  • -105 <= Node.val <= 105
  • -108 <= targetSum <= 108

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 range of values for the nodes in the tree? Can they be negative, zero, or floating-point numbers?
  2. Can the tree be empty or consist of only one node? If so, what should I return in those cases?
  3. If multiple partitions result in equal subtree sums, should I return true as soon as I find one, or are there specific criteria for choosing a particular partition?
  4. Is the tree guaranteed to be a valid binary tree, or do I need to handle cases with invalid tree structures?
  5. Could you clarify what constitutes a 'subtree' in this context? Does it necessarily have to include all descendants of the node where the edge is removed?

Brute Force Solution

Approach

The brute force approach to the Equal Tree Partition problem involves checking every possible way to split the tree into two parts by removing a single edge. We try each edge one by one, calculating the sum of the nodes on each side of the removed edge and checking if the sums are equal.

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

  1. First, calculate the total sum of all the node values in the tree.
  2. Then, consider removing each edge in the tree, one at a time.
  3. For each edge that you consider removing, calculate the sum of the node values in the two resulting subtrees.
  4. Check if the sum of one subtree is equal to half the total sum of all nodes in the original tree. If it is, this means the other subtree also has the same sum, and therefore you've found an equal partition.
  5. If after checking all the edges, you haven't found a split where the sum of one subtree is half the total sum, then there is no equal tree partition.

Code Implementation

class TreeNode:
    def __init__(self, value=0, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

def equal_tree_partition_brute_force(root):
    node_values = []

    def traverse_tree(node):
        if not node:
            return 0

        left_sum = traverse_tree(node.left)
        right_sum = traverse_tree(node.right)
        node_values.append(node.value + left_sum + right_sum)
        return node.value + left_sum + right_sum

    total_sum = traverse_tree(root)

    # Check if the total sum is even, as odd sums cannot be partitioned equally
    if total_sum % 2 != 0:
        return False

    target_sum = total_sum / 2

    # Check for the existence of a subtree with sum equal to half of the total sum
    for subtree_sum in node_values[:-1]:
        if subtree_sum == target_sum:
            return True

    return False

Big(O) Analysis

Time Complexity
O(n²)The algorithm first calculates the total sum of the tree nodes, which takes O(n) time where n is the number of nodes. Then, for each possible edge removal, the algorithm calculates the sum of one of the resulting subtrees. In the worst case, calculating the subtree sum may require traversing all the nodes in that subtree, potentially taking O(n) time. Since there can be up to n-1 edges in a tree, and we potentially perform an O(n) operation for each edge, the overall time complexity becomes O(n * n). This simplifies to O(n²).
Space Complexity
O(N)The brute force approach calculates the total sum of the tree and then recursively calculates subtree sums. The recursion depth can go as deep as the number of nodes in the tree, N, in the worst-case scenario (e.g., a skewed tree). Each recursive call adds a new frame to the call stack, consuming memory. Therefore, the auxiliary space used by the recursion stack is proportional to the number of nodes, N.

Optimal Solution

Approach

The goal is to figure out if a tree can be split into two parts with equal sums. We calculate the total sum of all values in the tree and then check if there is any subtree that has a sum equal to half of the total sum.

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

  1. First, find the total sum of all the values in the tree.
  2. If the total sum is odd, then it's impossible to split the tree into two equal parts, so we can stop and say it cannot be done.
  3. If the total sum is even, then we need to find a subtree whose sum is exactly half of the total sum. We can do this by exploring the tree.
  4. Start from the root of the tree. For each node, calculate the sum of all the values in the subtree rooted at that node. This includes the node's own value and the values in all its children's subtrees.
  5. While calculating the subtree sums, check if any of them is equal to half of the total sum.
  6. If you find a subtree with a sum equal to half the total sum, then the tree can be split into two equal parts. We return that it can be done.
  7. If you explore the entire tree and don't find any subtree with a sum equal to half the total sum, then the tree cannot be split. We return that it cannot be done.

Code Implementation

class TreeNode:
    def __init__(self, value=0, left=None, right=None):
        self.value = value
        self.left = left
        self.right = right

class Solution:
    def canPartition(self, root: TreeNode) -> bool:
        total_tree_sum = self.calculate_tree_sum(root)

        # If the total sum is odd, we can't partition.
        if total_tree_sum % 2 != 0:
            return False

        target_subtree_sum = total_tree_sum // 2
        subtree_sums = set()

        def calculate_subtree_sum(node: TreeNode) -> int:
            if not node:
                return 0

            left_subtree_sum = calculate_subtree_sum(node.left)
            right_subtree_sum = calculate_subtree_sum(node.right)
            current_subtree_sum = node.value + left_subtree_sum + right_subtree_sum
            subtree_sums.add(current_subtree_sum)
            return current_subtree_sum

        calculate_subtree_sum(root)

        # Check if any subtree sum equals half the total sum.
        if target_subtree_sum in subtree_sums:
            return True

        return False

    def calculate_tree_sum(self, root: TreeNode) -> int:
        if not root:
            return 0

        return root.value + self.calculate_tree_sum(root.left) + self.calculate_tree_sum(root.right)

Big(O) Analysis

Time Complexity
O(n)The algorithm performs a depth-first traversal of the tree to calculate the total sum and then, in a subsequent traversal, computes subtree sums. Both traversals visit each of the n nodes in the tree exactly once. During the subtree sum calculation, each node's value is added into its parent's subtree sum. The work done at each node is constant. Therefore, the overall time complexity is proportional to the number of nodes, resulting in O(n).
Space Complexity
O(N)The algorithm uses recursion to traverse the tree. In the worst-case scenario (e.g., a skewed tree), the recursion depth can reach N, where N is the number of nodes in the tree. Each recursive call adds a new frame to the call stack to store function variables and the return address. Therefore, the auxiliary space complexity is proportional to the maximum depth of the recursion, which is O(N).

Edge Cases

Null root node (empty tree)
How to Handle:
Return false immediately as an empty tree cannot be partitioned.
Single node tree
How to Handle:
Return false because a single node cannot be partitioned into two subtrees.
Tree with all zero values
How to Handle:
Handle this case by checking for a total sum of zero and if any non-root node also has a subtree sum of zero; return true if there is a subtree summing to zero.
Tree with only negative values
How to Handle:
The algorithm should correctly calculate the sum of negative values and check if any subtree sums to half of the total sum.
Integer overflow for large node values
How to Handle:
Use a larger data type (e.g., long) to store the sum of the subtree values to prevent integer overflow.
Total sum is odd
How to Handle:
Return false immediately because if the total sum is odd, it cannot be divided into two equal integer subtrees.
Deeply skewed tree (e.g., linked list)
How to Handle:
Recursive solution may exceed maximum call stack size; consider iterative solution or tail-call optimization (if language supports it).
Tree with a very large number of nodes
How to Handle:
Ensure the chosen data structure (e.g., hash map for subtree sums) can handle the scale and optimize for memory usage.