Flip Equivalent Binary Trees
MedPrompt
For a binary tree T, we can define a flip operation as follows: choose any node, and swap the left and right child subtrees.
A binary tree X is flip equivalent to a binary tree Y if and only if we can make X equal to Y after some number of flip operations.
Given the roots of two binary trees root1 and root2, return true if the two trees are flip equivalent or false otherwise.
Example 1:
Input: root1 = [1,2,3,4,5,6,null,null,null,7,8], root2 = [1,3,2,null,6,4,5,null,null,null,null,8,7]
Output: true
Explanation: We flipped at nodes with values 1, 3, and 5.Example 2:
Input: root1 = [], root2 = []
Output: trueExample 3:
Input: root1 = [], root2 = [1]
Output: false
Constraints:
- The number of nodes in each tree is in the range
[0, 100]. - Each tree will have unique node values in the range
[0, 99].
Approaches
2 approaches with complexity analysis and trade-offs.
This approach involves converting each tree into a canonical string representation. Two trees are flip equivalent if and only if their canonical representations are identical. The canonical form is constructed recursively, ensuring that for any node, the representations of its children are ordered consistently (e.g., lexicographically). This makes the final representation independent of the initial left/right child arrangement.
Algorithm
- Implement a recursive function
getCanonical(TreeNode node)that returns a canonical string representation of the subtree. - Base Case: If the input
nodeisnull, return a special marker string like"#". - Recursive Step:
- Recursively call
getCanonicalon the left and right children to get their string representations,leftStrandrightStr. - To ensure canonical ordering, compare
leftStrandrightStrlexicographically. Swap them ifleftStris greater thanrightStr. - Construct and return the final string for the current node, for example:
node.val + "(" + leftStr + ")(" + rightStr + ")".
- Recursively call
- In the main
flipEquivfunction, generate the canonical strings for bothroot1androot2. - Return
trueif the two strings are equal,falseotherwise.
Walkthrough
The core idea is to define a unique representation for any tree that is invariant under flip operations. We can achieve this with a recursive function, say getCanonical(node), which returns a string.
For a given node, we first recursively get the canonical strings for its left and right children, let's call them leftStr and rightStr. If a child is null, we can represent it with a special marker like "#". To ensure the representation is canonical, we order the child strings by comparing leftStr and rightStr lexicographically. The smaller string comes first. The final string for the node is formed by concatenating the node's value, the smaller child string, and the larger child string.
After generating the canonical strings for both root1 and root2, we simply compare them for equality.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean flipEquiv(TreeNode root1, TreeNode root2) { String s1 = getCanonical(root1); String s2 = getCanonical(root2); return s1.equals(s2); } private String getCanonical(TreeNode node) { if (node == null) { return "#"; } String left = getCanonical(node.left); String right = getCanonical(node.right); // To ensure canonical representation, order the children's strings lexicographically if (left.compareTo(right) > 0) { String temp = left; left = right; right = temp; } return String.valueOf(node.val) + "(" + left + ")(" + right + ")"; }}Complexity
Time
O(N1^2 + N2^2), where N1 and N2 are the number of nodes in the two trees. For each node in a tree of size N, we construct a string. The string length can be up to O(N), and string operations (concatenation, comparison) take time proportional to the length. This leads to a roughly O(N^2) complexity for processing one tree.
Space
O(N1^2 + N2^2), where N1 and N2 are the node counts. The recursion depth is O(H) (height), and at each of the N calls, we create new strings. The total size of strings stored in the call stack can be large, reaching O(N^2) for a skewed tree.
Trade-offs
Pros
Conceptually interesting, as it solves the problem by finding a canonical form.
This approach can be generalized to other isomorphism problems.
Cons
Inefficient in both time and space due to expensive string manipulations (concatenation, comparison).
More complex to implement correctly compared to the direct recursive solution.
This is a direct and efficient approach that recursively checks for flip equivalence. The function compares two nodes and then explores two possibilities for their children: either they match directly (left with left, right with right) or they match after a flip (left with right, right with left).
Algorithm
- Define a recursive function, let's call it
flipEquiv(node1, node2). - Base Cases:
- If both
node1andnode2arenull, returntrue. - If either
node1ornode2isnull, or ifnode1.val != node2.val, returnfalse.
- If both
- Recursive Step:
- Recursively check the "no-flip" case:
flipEquiv(node1.left, node2.left)ANDflipEquiv(node1.right, node2.right). - Recursively check the "flipped" case:
flipEquiv(node1.left, node2.right)ANDflipEquiv(node1.right, node2.left).
- Recursively check the "no-flip" case:
- Return
trueif either the "no-flip" case OR the "flipped" case is true. Otherwise, returnfalse.
Walkthrough
This method is based on the definition of flip equivalence. We define a recursive function that takes two nodes, one from each tree, and returns true if the subtrees rooted at these nodes are flip equivalent.
The function first handles the base cases: if both nodes are null, they are equivalent (true); if only one is null or their values differ, they are not equivalent (false).
If the base cases don't apply, it means we have two non-null nodes with the same value. We then proceed to the recursive step. There are two ways their children can match for the subtrees to be flip equivalent:
- No Flip:
root1.leftmatchesroot2.leftANDroot1.rightmatchesroot2.right. - Flipped:
root1.leftmatchesroot2.rightANDroot1.rightmatchesroot2.left.
We recursively check both possibilities. If either one is true, the function returns true.
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */class Solution { public boolean flipEquiv(TreeNode root1, TreeNode root2) { // Base case: both are null, they are equivalent if (root1 == null && root2 == null) { return true; } // Base case: one is null, or values don't match, they are not equivalent if (root1 == null || root2 == null || root1.val != root2.val) { return false; } // Check for non-flipped equivalence boolean nonFlipped = flipEquiv(root1.left, root2.left) && flipEquiv(root1.right, root2.right); // Check for flipped equivalence boolean flipped = flipEquiv(root1.left, root2.right) && flipEquiv(root1.right, root2.left); // The trees are flip equivalent if either of the above conditions is true return nonFlipped || flipped; }}Complexity
Time
O(min(N1, N2)), where N1 and N2 are the number of nodes in the two trees. The function is called for each pair of nodes that could potentially match. In the worst case (when they are flip equivalent), we visit every node in the smaller tree once.
Space
O(min(H1, H2)), where H1 and H2 are the heights of the two trees. This space is used by the recursion stack. In the worst case of a skewed tree, this can be O(min(N1, N2)).
Trade-offs
Pros
Highly efficient in terms of both time and space.
The logic directly follows the problem definition, making it intuitive and easy to implement.
Optimal solution for this problem.
Cons
There are no significant cons to this approach as it is optimal for this problem.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode() {} * TreeNode(int val) { this.val = val; } * TreeNode(int val, TreeNode left, TreeNode right) { * this.val = val; * this.left = left; * this.right = right; * } * } */ class Solution { public boolean flipEquiv ( TreeNode root1 , TreeNode root2 ) { return dfs ( root1 , root2 ); } private boolean dfs ( TreeNode root1 , TreeNode root2 ) { if ( root1 == root2 || ( root1 == null && root2 == null )) { return true ; } if ( root1 == null || root2 == null || root1 . val != root2 . val ) { return false ; } return ( dfs ( root1 . left , root2 . left ) && dfs ( root1 . right , root2 . right )) || ( dfs ( root1 . left , root2 . right ) && dfs ( root1 . right , root2 . left )); } }function flipEquiv ( root1 , root2 ) { if ( root1 === root2 ) return true ; if ( ! root1 || ! root2 || root1 ?. val !== root2 ?. val ) return false ; const { left : l1 , right : r1 } = root1 ; const { left : l2 , right : r2 } = root2 ; return ( flipEquiv ( l1 , l2 ) && flipEquiv ( r1 , r2 )) || ( flipEquiv ( l1 , r2 ) && flipEquiv ( r1 , l2 )); }/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode() : val(0), left(nullptr), right(nullptr) {} * TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} * TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {} * }; */ class Solution { public: bool flipEquiv ( TreeNode * root1 , TreeNode * root2 ) { return dfs ( root1 , root2 ); } bool dfs ( TreeNode * root1 , TreeNode * root2 ) { if ( root1 == root2 || ( ! root1 && ! root2 )) return true ; if ( ! root1 || ! root2 || root1 -> val != root2 -> val ) return false ; return ( dfs ( root1 -> left , root2 -> left ) && dfs ( root1 -> right , root2 -> right )) || ( dfs ( root1 -> left , root2 -> right ) && dfs ( root1 -> right , root2 -> left )); } };# Definition for a binary tree node. # class TreeNode: # def __init__(self, val=0, left=None, right=None): # self.val = val # self.left = left # self.right = right class Solution : def flipEquiv ( self , root1 : Optional [ TreeNode ], root2 : Optional [ TreeNode ]) -> bool : def dfs ( root1 , root2 ): if root1 == root2 or ( root1 is None and root2 is None ): return True if root1 is None or root2 is None or root1 . val != root2 . val : return False return ( dfs ( root1 . left , root2 . left ) and dfs ( root1 . right , root2 . right )) or ( dfs ( root1 . left , root2 . right ) and dfs ( root1 . right , root2 . left ) ) return dfs ( root1 , root2 )Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.