Facebook Pixel

951. Flip Equivalent Binary Trees

Problem Description

You are given two binary trees and need to determine if they are "flip equivalent" to each other.

A flip operation on a binary tree means you can select any node and swap its left and right child subtrees. You can perform this operation on any node, any number of times.

Two binary trees are considered flip equivalent if you can transform one tree into the other by applying zero or more flip operations.

Given the root nodes of two binary trees root1 and root2, you need to return true if the trees are flip equivalent, or false if they are not.

The solution uses a recursive approach to check flip equivalence:

  1. Base cases:

    • If both nodes are None, they are equivalent (return true)
    • If only one is None, they cannot be equivalent (return false)
    • If the node values are different, they cannot be equivalent (return false)
  2. Recursive check: For nodes with the same value, check if:

    • The left subtree of root1 matches the left subtree of root2 AND the right subtree of root1 matches the right subtree of root2 (no flip needed)
    • OR
    • The left subtree of root1 matches the right subtree of root2 AND the right subtree of root1 matches the left subtree of root2 (flip occurred)

The algorithm explores both possibilities at each node - either the children match directly or they match after a flip operation. This ensures all possible combinations of flips are considered to determine if the trees can be made equivalent.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: A binary tree is a special type of graph where each node has at most two children and there are no cycles.

Is it a tree?

  • Yes: The problem explicitly deals with binary trees. We have two binary tree structures that we need to compare while considering possible flip operations at any node.

DFS

  • Yes: Based on the flowchart, when we have a tree structure, DFS (Depth-First Search) is the recommended approach.

Conclusion: The flowchart suggests using DFS for this problem. This makes perfect sense because:

  1. We need to traverse both trees simultaneously to compare their structure
  2. At each node, we need to recursively check if the subtrees are equivalent (either directly or after a flip)
  3. DFS allows us to explore each path completely, checking all possible flip combinations
  4. The recursive nature of DFS naturally matches the recursive structure of trees

The DFS approach in the solution recursively compares nodes from both trees, checking at each level whether the children match directly or match after a flip operation. This exhaustive comparison through DFS ensures we consider all possible flip configurations to determine equivalence.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When comparing two binary trees for flip equivalence, we need to think about what makes two trees equivalent after flipping operations. The key insight is that flipping a node simply swaps its left and right children - it doesn't change the node's value or its relationship with its parent.

Starting from the roots of both trees, we can observe that:

  • If both roots are None, the trees are trivially equivalent
  • If the root values differ, no amount of flipping can make them equivalent
  • If the root values are the same, we need to check if their subtrees can be made equivalent

The crucial realization is that for any pair of nodes with matching values, their children can match in one of two ways:

  1. Direct match: left child of tree1 matches left child of tree2, AND right child of tree1 matches right child of tree2
  2. Flipped match: left child of tree1 matches right child of tree2, AND right child of tree1 matches left child of tree2

This naturally leads to a recursive solution where at each node, we check both possibilities. If either possibility results in equivalent subtrees, then the trees are flip equivalent at that level.

The beauty of this approach is that we don't need to actually perform any flips or keep track of which nodes were flipped. We simply explore both configurations at each node and see if any valid path exists that makes the trees equivalent. The recursion handles the complexity of trying all possible combinations of flips throughout the tree.

This is why DFS is perfect here - it allows us to deeply explore each possibility before backtracking to try alternatives, ensuring we don't miss any valid flip configuration that could make the trees equivalent.

Learn more about Tree, Depth-First Search and Binary Tree patterns.

Solution Approach

The implementation uses a recursive DFS helper function to traverse both trees simultaneously and check for flip equivalence at each level.

Implementation Details:

  1. Helper Function Definition: We define a nested function dfs(root1, root2) that takes two nodes as parameters and returns a boolean indicating whether the subtrees rooted at these nodes are flip equivalent.

  2. Base Cases Handling:

    • if root1 == root2 or (root1 is None and root2 is None): If both nodes are None, they're equivalent. This covers the case where we've reached leaf nodes' children.
    • if root1 is None or root2 is None or root1.val != root2.val: If only one node is None or the values don't match, the trees cannot be equivalent at this point.
  3. Recursive Check for Two Configurations: The core logic checks both possible child configurations using logical OR:

    return (dfs(root1.left, root2.left) and dfs(root1.right, root2.right)) or 
           (dfs(root1.left, root2.right) and dfs(root1.right, root2.left))
    • First configuration (dfs(root1.left, root2.left) and dfs(root1.right, root2.right)): Checks if the trees match without any flip at the current node
    • Second configuration (dfs(root1.left, root2.right) and dfs(root1.right, root2.left)): Checks if the trees match after flipping the current node
  4. Short-circuit Evaluation: The use of or between the two configurations means that if the first configuration returns True, the second won't be evaluated (Python's short-circuit evaluation), making the solution efficient.

  5. Return Statement: The main function simply calls dfs(root1, root2) with the original roots and returns its result.

Time Complexity: O(min(n1, n2)) where n1 and n2 are the number of nodes in the two trees. In the worst case, we visit each node once in the smaller tree.

Space Complexity: O(min(h1, h2)) where h1 and h2 are the heights of the two trees, due to the recursion call stack.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate how the flip equivalence algorithm works.

Consider these two binary trees:

    Tree 1:        Tree 2:
       1              1
      / \            / \
     2   3          3   2
    / \                / \
   4   5              5   4

Step 1: Compare roots

  • Both roots have value 1 ✓
  • Now check if their subtrees can be made equivalent

Step 2: Check both configurations for root's children

  • Configuration 1 (no flip): Does left(2) match left(3)? No, values differ (2 ≠ 3)
  • Configuration 2 (with flip): Does left(2) match right(2) AND right(3) match left(3)?
    • Compare node 2 with node 2 ✓ (values match)
    • Compare node 3 with node 3 ✓ (values match)
    • Continue recursively...

Step 3: Check subtrees of node 2 in Tree1 and node 2 in Tree2

  • Node 2 in Tree1 has children [4, 5]
  • Node 2 in Tree2 has children [5, 4]
  • Configuration 1: Does left(4) match left(5)? No (4 ≠ 5)
  • Configuration 2: Does left(4) match right(4) AND right(5) match left(5)? Yes! ✓

Step 4: Check subtrees of node 3 in Tree1 and node 3 in Tree2

  • Both are leaf nodes with no children
  • Both None == None ✓

Result: The trees are flip equivalent!

  • We need to flip the root node (swap children 2 and 3)
  • We need to flip node 2 (swap children 4 and 5)

The algorithm discovers this without actually performing flips - it simply explores both possibilities at each node and finds that a valid configuration exists where all subtrees match.

Solution Implementation

1# Definition for a binary tree node.
2# class TreeNode:
3#     def __init__(self, val=0, left=None, right=None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8class Solution:
9    def flipEquiv(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool:
10        """
11        Determine if two binary trees are flip equivalent.
12        Two trees are flip equivalent if they are equal after some number of flip operations.
13        A flip operation swaps the left and right children of a node.
14      
15        Args:
16            root1: Root of the first binary tree
17            root2: Root of the second binary tree
18          
19        Returns:
20            True if the trees are flip equivalent, False otherwise
21        """
22      
23        def dfs(node1: Optional[TreeNode], node2: Optional[TreeNode]) -> bool:
24            """
25            Recursively check if two subtrees are flip equivalent.
26          
27            Args:
28                node1: Current node in the first tree
29                node2: Current node in the second tree
30              
31            Returns:
32                True if subtrees rooted at node1 and node2 are flip equivalent
33            """
34          
35            # Base case: both nodes are None (empty subtrees are equivalent)
36            if node1 is None and node2 is None:
37                return True
38          
39            # If only one node is None, or values don't match, trees aren't equivalent
40            if node1 is None or node2 is None or node1.val != node2.val:
41                return False
42          
43            # Check both possibilities:
44            # 1. No flip: left matches left, right matches right
45            # 2. With flip: left matches right, right matches left
46            no_flip = (dfs(node1.left, node2.left) and 
47                      dfs(node1.right, node2.right))
48          
49            with_flip = (dfs(node1.left, node2.right) and 
50                        dfs(node1.right, node2.left))
51          
52            return no_flip or with_flip
53      
54        return dfs(root1, root2)
55
1/**
2 * Definition for a binary tree node.
3 * public class TreeNode {
4 *     int val;
5 *     TreeNode left;
6 *     TreeNode right;
7 *     TreeNode() {}
8 *     TreeNode(int val) { this.val = val; }
9 *     TreeNode(int val, TreeNode left, TreeNode right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    /**
18     * Determines if two binary trees are flip equivalent.
19     * Two trees are flip equivalent if they are equal after some number of flip operations.
20     * A flip operation swaps the left and right children of a node.
21     * 
22     * @param root1 The root of the first binary tree
23     * @param root2 The root of the second binary tree
24     * @return true if the trees are flip equivalent, false otherwise
25     */
26    public boolean flipEquiv(TreeNode root1, TreeNode root2) {
27        return checkFlipEquivalence(root1, root2);
28    }
29
30    /**
31     * Recursively checks if two subtrees are flip equivalent.
32     * 
33     * @param node1 The root of the first subtree
34     * @param node2 The root of the second subtree
35     * @return true if the subtrees are flip equivalent, false otherwise
36     */
37    private boolean checkFlipEquivalence(TreeNode node1, TreeNode node2) {
38        // Base case: both nodes are null (empty subtrees are equivalent)
39        if (node1 == null && node2 == null) {
40            return true;
41        }
42      
43        // If only one node is null or values don't match, trees are not equivalent
44        if (node1 == null || node2 == null || node1.val != node2.val) {
45            return false;
46        }
47      
48        // Check two possibilities:
49        // 1. Children match without flipping (left-to-left, right-to-right)
50        // 2. Children match with flipping (left-to-right, right-to-left)
51        boolean withoutFlip = checkFlipEquivalence(node1.left, node2.left) && 
52                             checkFlipEquivalence(node1.right, node2.right);
53      
54        boolean withFlip = checkFlipEquivalence(node1.left, node2.right) && 
55                          checkFlipEquivalence(node1.right, node2.left);
56      
57        return withoutFlip || withFlip;
58    }
59}
60
1/**
2 * Definition for a binary tree node.
3 * struct TreeNode {
4 *     int val;
5 *     TreeNode *left;
6 *     TreeNode *right;
7 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
8 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
9 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    /**
15     * Determines if two binary trees are flip equivalent.
16     * Two trees are flip equivalent if they are equal when one can swap
17     * the left and right children of any number of nodes in one tree.
18     * 
19     * @param root1 Root of the first binary tree
20     * @param root2 Root of the second binary tree
21     * @return true if the trees are flip equivalent, false otherwise
22     */
23    bool flipEquiv(TreeNode* root1, TreeNode* root2) {
24        return isFlipEquivalent(root1, root2);
25    }
26
27private:
28    /**
29     * Recursively checks if two subtrees are flip equivalent.
30     * 
31     * @param node1 Current node in the first tree
32     * @param node2 Current node in the second tree
33     * @return true if subtrees rooted at node1 and node2 are flip equivalent
34     */
35    bool isFlipEquivalent(TreeNode* node1, TreeNode* node2) {
36        // Base case: both nodes are null (empty subtrees are equivalent)
37        if (node1 == nullptr && node2 == nullptr) {
38            return true;
39        }
40      
41        // If only one node is null or values don't match, trees aren't equivalent
42        if (node1 == nullptr || node2 == nullptr || node1->val != node2->val) {
43            return false;
44        }
45      
46        // Check both possibilities:
47        // 1. Children match without flipping (left-to-left, right-to-right)
48        // 2. Children match with flipping (left-to-right, right-to-left)
49        bool noFlip = isFlipEquivalent(node1->left, node2->left) && 
50                      isFlipEquivalent(node1->right, node2->right);
51      
52        bool withFlip = isFlipEquivalent(node1->left, node2->right) && 
53                        isFlipEquivalent(node1->right, node2->left);
54      
55        return noFlip || withFlip;
56    }
57};
58
1/**
2 * Definition for a binary tree node
3 */
4interface TreeNode {
5    val: number;
6    left: TreeNode | null;
7    right: TreeNode | null;
8}
9
10/**
11 * Determines if two binary trees are flip equivalent.
12 * Two binary trees are flip equivalent if we can make them equal by swapping
13 * the left and right children of some nodes (possibly none).
14 * 
15 * @param root1 - The root of the first binary tree
16 * @param root2 - The root of the second binary tree
17 * @returns true if the trees are flip equivalent, false otherwise
18 */
19function flipEquiv(root1: TreeNode | null, root2: TreeNode | null): boolean {
20    // Base case: both nodes are the same reference (including both null)
21    if (root1 === root2) {
22        return true;
23    }
24  
25    // If one is null or values don't match, trees aren't equivalent
26    if (!root1 || !root2 || root1.val !== root2.val) {
27        return false;
28    }
29  
30    // Extract children from both nodes
31    const leftChild1 = root1.left;
32    const rightChild1 = root1.right;
33    const leftChild2 = root2.left;
34    const rightChild2 = root2.right;
35  
36    // Check both possibilities:
37    // 1. No flip: left1 matches left2, right1 matches right2
38    // 2. With flip: left1 matches right2, right1 matches left2
39    const noFlipMatch = flipEquiv(leftChild1, leftChild2) && flipEquiv(rightChild1, rightChild2);
40    const withFlipMatch = flipEquiv(leftChild1, rightChild2) && flipEquiv(rightChild1, leftChild2);
41  
42    return noFlipMatch || withFlipMatch;
43}
44

Time and Space Complexity

Time Complexity: O(min(n1, n2)) where n1 and n2 are the number of nodes in root1 and root2 respectively.

The algorithm performs a depth-first search traversal comparing nodes from both trees. In the best case, we visit each node at most once when the trees are flip equivalent. The recursion explores four possible combinations at each node: matching children directly or with a flip. However, due to short-circuit evaluation with the or operator, once a valid matching is found, the other branch is not explored. Therefore, each node is visited at most once during a successful traversal, leading to O(min(n1, n2)) time complexity.

Space Complexity: O(min(h1, h2)) where h1 and h2 are the heights of root1 and root2 respectively.

The space complexity is determined by the recursive call stack. The maximum depth of recursion equals the height of the smaller tree (since recursion stops when one tree ends). In the worst case of a skewed tree, this could be O(min(n1, n2)), while in the best case of a balanced tree, it would be O(log(min(n1, n2))).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Inefficient Redundant Traversals

The Pitfall: The current implementation potentially explores both configurations (flipped and non-flipped) even when unnecessary. This can lead to exponential time complexity in pathological cases where many subtrees are identical, causing the algorithm to repeatedly check the same subtree combinations.

Example Scenario: Consider two identical balanced trees with many duplicate values. The algorithm will explore both flip and no-flip paths at every node, even though only one path is needed.

Solution: Implement memoization to cache results of subtree comparisons, or add early termination by checking if subtrees are identical before exploring both configurations:

def flipEquiv(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool:
    def dfs(node1, node2):
        if node1 is None and node2 is None:
            return True
        if node1 is None or node2 is None or node1.val != node2.val:
            return False
      
        # Early optimization: if children are identical, no need to check flip
        if (node1.left == node2.left and node1.right == node2.right):
            return dfs(node1.left, node2.left) and dfs(node1.right, node2.right)
      
        return ((dfs(node1.left, node2.left) and dfs(node1.right, node2.right)) or
                (dfs(node1.left, node2.right) and dfs(node1.right, node2.left)))
  
    return dfs(root1, root2)

2. Incorrect Base Case Handling

The Pitfall: Writing base cases in the wrong order or with incorrect logic. For example:

  • Checking node1.val != node2.val before checking if nodes are None (causes AttributeError)
  • Using node1 == node2 for reference equality instead of checking None explicitly

Problematic Code:

# Wrong - will crash if either node is None
if node1.val != node2.val:
    return False
if node1 is None and node2 is None:
    return True

Solution: Always check for None values before accessing node properties:

# Correct order
if node1 is None and node2 is None:
    return True
if node1 is None or node2 is None:
    return False
if node1.val != node2.val:
    return False

3. Stack Overflow on Deep Trees

The Pitfall: For extremely deep or skewed trees (e.g., a linked-list-like tree with thousands of nodes), the recursive solution can cause stack overflow due to excessive recursion depth.

Solution: Implement an iterative solution using explicit stacks:

def flipEquiv(self, root1: Optional[TreeNode], root2: Optional[TreeNode]) -> bool:
    stack = [(root1, root2)]
  
    while stack:
        node1, node2 = stack.pop()
      
        if node1 is None and node2 is None:
            continue
        if node1 is None or node2 is None or node1.val != node2.val:
            return False
      
        # Check if children match in either configuration
        if ((node1.left and node1.left.val if node1.left else None) == 
            (node2.left and node2.left.val if node2.left else None) and
            (node1.right and node1.right.val if node1.right else None) == 
            (node2.right and node2.right.val if node2.right else None)):
            # No flip needed
            stack.append((node1.left, node2.left))
            stack.append((node1.right, node2.right))
        elif ((node1.left and node1.left.val if node1.left else None) == 
              (node2.right and node2.right.val if node2.right else None) and
              (node1.right and node1.right.val if node1.right else None) == 
              (node2.left and node2.left.val if node2.left else None)):
            # Flip needed
            stack.append((node1.left, node2.right))
            stack.append((node1.right, node2.left))
        else:
            return False
  
    return True
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement recursion?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More