Facebook Pixel

1612. Check If Two Expression Trees are Equivalent

Problem Description

You are given two binary expression trees where each tree represents an arithmetic expression using only the addition (+) operator. In these trees:

  • Internal nodes (nodes with two children) contain the + operator
  • Leaf nodes (nodes with no children) contain variable names (operands)

Your task is to determine if the two trees are equivalent, meaning they would evaluate to the same value regardless of what values are assigned to the variables.

For example, if one tree represents a + b and another represents b + a, they are equivalent because addition is commutative. Similarly, (a + b) + c and a + (b + c) are equivalent due to the associative property of addition.

The key insight is that two expression trees with only addition are equivalent if and only if they contain the same variables with the same frequencies (since addition is both commutative and associative).

The solution counts the frequency of each variable in both trees. It increments the count for variables in the first tree and decrements for variables in the second tree. If all counts end up as zero, the trees have identical variable compositions and are therefore 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: The problem involves a binary tree structure, which 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 states we're working with binary expression trees. Trees are connected acyclic graphs with a hierarchical structure.

DFS

  • Yes: Since we're dealing with a tree structure, DFS (Depth-First Search) is the natural choice for traversal.

Conclusion: The flowchart suggests using DFS for this problem.

This makes perfect sense because:

  1. We need to traverse both binary expression trees completely to count all the variables (leaf nodes)
  2. DFS allows us to recursively visit each node and its children
  3. When we encounter a leaf node (where root.val != '+'), we can record the variable
  4. The recursive nature of DFS perfectly matches the recursive structure of trees

The solution implements DFS through the dfs function that:

  • Recursively traverses each tree
  • Counts occurrences of variables in the first tree (with +1)
  • Decrements counts for variables in the second tree (with -1)
  • If the trees are equivalent, all variable counts will cancel out to zero
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Since we're dealing with expression trees that only contain the addition operator, we can leverage an important mathematical property: addition is both commutative (a + b = b + a) and associative ((a + b) + c = a + (b + c)). This means that no matter how we rearrange or regroup the additions, as long as we're adding the same variables the same number of times, we'll get the same result.

Think of it like having two bags of colored marbles. If both bags contain the exact same marbles (same colors, same quantities), then no matter how you arrange or group them when pouring them out, you'll end up with the same collection. Similarly, two expression trees with only addition are equivalent if they contain the same variables with the same frequencies.

The key insight is that we don't need to evaluate the expressions or compare the tree structures directly. Instead, we can simply count how many times each variable appears in each tree. If the counts match, the trees must be equivalent.

The clever trick in the solution is to use a single counter with positive and negative values. When traversing the first tree, we add +1 for each variable occurrence. When traversing the second tree, we add -1 for each variable occurrence. If the trees are equivalent (same variables, same frequencies), all the positive counts from tree 1 will be perfectly canceled out by the negative counts from tree 2, leaving us with all zeros.

This approach transforms a potentially complex structural comparison problem into a simple counting problem, making it both elegant and efficient.

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

Solution Approach

The solution uses a DFS traversal combined with a frequency counter to determine if two expression trees are equivalent.

Implementation Details:

  1. Counter Data Structure: We use a Counter (hash map) to track the frequency of each variable across both trees. The key insight is using a single counter with signed values:

    • Variables from root1 contribute +1 to the count
    • Variables from root2 contribute -1 to the count
  2. DFS Traversal Function: The dfs function takes two parameters:

    • root: The current node being visited
    • v: The value to add to the counter (+1 for tree1, -1 for tree2)
  3. Traversal Logic:

    def dfs(root, v):
        if root is None:
            return
        if root.val != '+':
            cnt[root.val] += v
        dfs(root.left, v)
        dfs(root.right, v)
    • Base case: If the node is None, return immediately
    • If the node's value is not '+', it's a leaf node (variable), so we update its count
    • Recursively traverse left and right subtrees
  4. Main Algorithm:

    • Initialize an empty counter
    • Traverse the first tree with dfs(root1, 1) to add all variables
    • Traverse the second tree with dfs(root2, -1) to subtract all variables
    • Check if all values in the counter are zero using all(x == 0 for x in cnt.values())

Why This Works: If both trees contain the same variables with the same frequencies, each variable's count will be incremented exactly as many times as it's decremented, resulting in a final count of zero. Any non-zero value indicates a mismatch in variable frequencies, meaning the trees are not equivalent.

Time Complexity: O(n + m) where n and m are the number of nodes in the two trees, as we visit each node exactly once.

Space Complexity: O(h + v) where h is the maximum height of the trees (for recursion stack) and v is the number of unique variables.

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 concrete example to see how the solution works.

Example Trees:

  • Tree 1 represents: (a + b) + c
  • Tree 2 represents: a + (c + b)
Tree 1:           Tree 2:
    +                 +
   / \               / \
  +   c             a   +
 / \                   / \
a   b                 c   b

Step-by-step execution:

  1. Initialize counter: cnt = {}

  2. Traverse Tree 1 with dfs(root1, 1):

    • Visit root +: Skip (it's an operator)
    • Go left to +: Skip (it's an operator)
      • Go left to a: cnt['a'] += 1cnt = {'a': 1}
      • Go right to b: cnt['b'] += 1cnt = {'a': 1, 'b': 1}
    • Go right to c: cnt['c'] += 1cnt = {'a': 1, 'b': 1, 'c': 1}
  3. Traverse Tree 2 with dfs(root2, -1):

    • Visit root +: Skip (it's an operator)
    • Go left to a: cnt['a'] += -1cnt = {'a': 0, 'b': 1, 'c': 1}
    • Go right to +: Skip (it's an operator)
      • Go left to c: cnt['c'] += -1cnt = {'a': 0, 'b': 1, 'c': 0}
      • Go right to b: cnt['b'] += -1cnt = {'a': 0, 'b': 0, 'c': 0}
  4. Check if all values are zero:

    • cnt = {'a': 0, 'b': 0, 'c': 0}
    • All values are 0, so return True - the trees are equivalent!

Counter-example (non-equivalent trees): If Tree 2 had been a + (b + b) instead:

  • After traversing Tree 1: cnt = {'a': 1, 'b': 1, 'c': 1}
  • After traversing Tree 2: cnt = {'a': 0, 'b': -1, 'c': 1}
  • Since we have non-zero values, return False - the trees are not equivalent

The beauty of this approach is that it automatically handles the commutative and associative properties of addition by simply counting occurrences, without needing to worry about tree structure or parenthesization.

Solution Implementation

1# Definition for a binary tree node.
2# class Node:
3#     def __init__(self, val: str = " ", left: 'Node' = None, right: 'Node' = None):
4#         self.val = val
5#         self.left = left
6#         self.right = right
7
8from collections import Counter
9from typing import Optional
10
11class Solution:
12    def checkEquivalence(self, root1: Optional['Node'], root2: Optional['Node']) -> bool:
13        """
14        Check if two expression trees are equivalent.
15
16        Two expression trees are equivalent if they evaluate to the same result
17        for any variable assignment. This is true when both trees contain the
18        same count of each variable.
19
20        Args:
21            root1: Root of the first expression tree
22            root2: Root of the second expression tree
23
24        Returns:
25            True if the trees are equivalent, False otherwise
26        """
27
28        def count_variables(node: Optional['Node'], increment: int) -> None:
29            """
30            Traverse the tree and count occurrences of variables (leaf nodes).
31
32            Args:
33                node: Current node in the tree
34                increment: Value to add to the counter (+1 for first tree, -1 for second tree)
35            """
36            # Base case: empty node
37            if node is None:
38                return
39
40            # If the node is a variable (not an operator '+'), update its count
41            if node.val != '+':
42                variable_counter[node.val] += increment
43
44            # Recursively process left and right subtrees
45            count_variables(node.left, increment)
46            count_variables(node.right, increment)
47
48        # Counter to track the difference in variable counts between the two trees
49        variable_counter = Counter()
50
51        # Count variables in first tree (add to counter)
52        count_variables(root1, 1)
53
54        # Count variables in second tree (subtract from counter)
55        count_variables(root2, -1)
56
57        # Trees are equivalent if all variable counts are equal (differences are 0)
58        return all(count == 0 for count in variable_counter.values())
59
1/**
2 * Definition for a binary tree node.
3 * class Node {
4 *     char val;
5 *     Node left;
6 *     Node right;
7 *     Node() {this.val = ' ';}
8 *     Node(char val) { this.val = val; }
9 *     Node(char val, Node left, Node right) {
10 *         this.val = val;
11 *         this.left = left;
12 *         this.right = right;
13 *     }
14 * }
15 */
16class Solution {
17    // Array to count frequency of each character (a-z)
18    private int[] characterCount = new int[26];
19
20    /**
21     * Checks if two expression trees are equivalent
22     * @param root1 Root of the first expression tree
23     * @param root2 Root of the second expression tree
24     * @return true if both trees represent equivalent expressions, false otherwise
25     */
26    public boolean checkEquivalence(Node root1, Node root2) {
27        // Count characters in first tree (add to counter)
28        traverseAndCount(root1, 1);
29
30        // Count characters in second tree (subtract from counter)
31        traverseAndCount(root2, -1);
32
33        // Check if all character counts are zero (meaning both trees have same characters)
34        for (int count : characterCount) {
35            if (count != 0) {
36                return false;
37            }
38        }
39
40        return true;
41    }
42
43    /**
44     * Performs DFS traversal to count leaf node characters
45     * @param root Current node in the tree
46     * @param increment Value to add to character count (1 for addition, -1 for subtraction)
47     */
48    private void traverseAndCount(Node root, int increment) {
49        // Base case: null node
50        if (root == null) {
51            return;
52        }
53
54        // If node is a leaf (contains a variable, not an operator)
55        if (root.val != '+') {
56            // Update count for this character
57            characterCount[root.val - 'a'] += increment;
58        }
59
60        // Recursively traverse left and right subtrees
61        traverseAndCount(root.left, increment);
62        traverseAndCount(root.right, increment);
63    }
64}
65
1/**
2 * Definition for a binary tree node.
3 * struct Node {
4 *     char val;
5 *     Node *left;
6 *     Node *right;
7 *     Node() : val(' '), left(nullptr), right(nullptr) {}
8 *     Node(char x) : val(x), left(nullptr), right(nullptr) {}
9 *     Node(char x, Node *left, Node *right) : val(x), left(left), right(right) {}
10 * };
11 */
12class Solution {
13public:
14    /**
15     * Check if two expression trees are equivalent.
16     * Two expression trees are equivalent if they evaluate to the same expression
17     * (having the same variables with the same frequencies).
18     *
19     * @param root1 Root of the first expression tree
20     * @param root2 Root of the second expression tree
21     * @return true if the trees are equivalent, false otherwise
22     */
23    bool checkEquivalence(Node* root1, Node* root2) {
24        // Array to store the frequency difference of each variable (a-z)
25        int variableFrequency[26] = {0};
26
27        // Lambda function to traverse the tree and count variables
28        // increment: 1 for adding frequencies, -1 for subtracting frequencies
29        std::function<void(Node*, int)> traverseAndCount = [&](Node* currentNode, int increment) {
30            // Base case: if node is null, return
31            if (!currentNode) {
32                return;
33            }
34
35            // If the node is a variable (not the '+' operator), update its frequency
36            if (currentNode->val != '+') {
37                variableFrequency[currentNode->val - 'a'] += increment;
38            }
39
40            // Recursively traverse left and right subtrees
41            traverseAndCount(currentNode->left, increment);
42            traverseAndCount(currentNode->right, increment);
43        };
44
45        // Count variables in the first tree (add to frequency)
46        traverseAndCount(root1, 1);
47
48        // Count variables in the second tree (subtract from frequency)
49        traverseAndCount(root2, -1);
50
51        // Check if all variable frequencies are zero (meaning both trees have same variables)
52        for (int& frequency : variableFrequency) {
53            if (frequency != 0) {
54                return false;
55            }
56        }
57
58        return true;
59    }
60};
61
1/**
2 * Definition for a binary tree node.
3 */
4interface Node {
5    val: string;
6    left: Node | null;
7    right: Node | null;
8}
9
10/**
11 * Checks if two expression trees are equivalent by comparing the frequency of variables
12 * @param root1 - Root of the first expression tree
13 * @param root2 - Root of the second expression tree
14 * @returns true if both trees represent equivalent expressions, false otherwise
15 */
16const checkEquivalence = function(root1: Node | null, root2: Node | null): boolean {
17    // Array to count occurrences of each variable (a-z)
18    const variableCount: number[] = new Array(26).fill(0);
19
20    /**
21     * Depth-first search to traverse the tree and count variables
22     * @param root - Current node being processed
23     * @param countValue - Value to add to the count (1 for root1, -1 for root2)
24     */
25    const dfs = (root: Node | null, countValue: number): void => {
26        // Base case: if node is null, return
27        if (!root) {
28            return;
29        }
30
31        // If node is a variable (not '+' operator), update its count
32        if (root.val !== '+') {
33            const charIndex = root.val.charCodeAt(0) - 'a'.charCodeAt(0);
34            variableCount[charIndex] += countValue;
35        }
36
37        // Recursively process left and right subtrees
38        dfs(root.left, countValue);
39        dfs(root.right, countValue);
40    };
41
42    // Count variables in first tree (add to count)
43    dfs(root1, 1);
44
45    // Count variables in second tree (subtract from count)
46    dfs(root2, -1);
47
48    // Check if all variable counts are zero (meaning both trees have same variables)
49    for (const count of variableCount) {
50        if (count !== 0) {
51            return false;
52        }
53    }
54
55    return true;
56};
57

Time and Space Complexity

Time Complexity: O(n + m) where n is the number of nodes in root1 and m is the number of nodes in root2.

The algorithm performs a depth-first search (DFS) traversal on both trees:

  • DFS on root1 visits all n nodes exactly once
  • DFS on root2 visits all m nodes exactly once
  • The final check all(x == 0 for x in cnt.values()) iterates through at most 26 values (assuming lowercase letters), which is O(1)

Therefore, the total time complexity is O(n + m).

Space Complexity: O(h1 + h2 + k) where h1 is the height of root1, h2 is the height of root2, and k is the number of unique leaf values (variables).

The space is used for:

  • Recursive call stack for DFS on root1: O(h1) in the worst case
  • Recursive call stack for DFS on root2: O(h2) in the worst case
  • Counter dictionary cnt: O(k) where k is the number of unique leaf values (at most 26 for lowercase letters)

In the worst case of a skewed tree, the height could be O(n) and O(m) respectively, making the space complexity O(n + m) in the worst case. In the best case of a balanced tree, the heights would be O(log n) and O(log m), making the space complexity O(log n + log m + k).

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

Common Pitfalls

1. Incorrectly Identifying Leaf Nodes

A common mistake is assuming that leaf nodes are identified by checking node.left is None and node.right is None. However, the problem guarantees that internal nodes always have exactly two children (binary expression tree property), so checking node.val != '+' is sufficient and more direct.

Incorrect approach:

def dfs(root, v):
    if root is None:
        return
    # Wrong: overly complex check
    if root.left is None and root.right is None:
        cnt[root.val] += v
    dfs(root.left, v)
    dfs(root.right, v)

Correct approach:

def dfs(root, v):
    if root is None:
        return
    # Correct: simpler and clearer
    if root.val != '+':
        cnt[root.val] += v
    dfs(root.left, v)
    dfs(root.right, v)

2. Using Two Separate Counters

Some developers might use two separate counters for each tree and then compare them, which adds unnecessary complexity and potential for errors when comparing dictionaries with different keys.

Inefficient approach:

def checkEquivalence(self, root1, root2):
    cnt1 = Counter()
    cnt2 = Counter()

    def dfs(root, counter):
        if root is None:
            return
        if root.val != '+':
            counter[root.val] += 1
        dfs(root.left, counter)
        dfs(root.right, counter)

    dfs(root1, cnt1)
    dfs(root2, cnt2)

    # Pitfall: comparing counters directly might miss keys present in only one
    return cnt1 == cnt2  # This works but is less elegant

3. Forgetting to Handle None Nodes

If either root1 or root2 could be None (empty tree), the solution should handle this edge case properly. The current solution handles it correctly through the base case in the DFS function.

Missing edge case handling:

def checkEquivalence(self, root1, root2):
    # Pitfall: not explicitly handling None inputs
    cnt = Counter()
    dfs(root1, 1)   # Could fail if root1 is None and dfs doesn't handle it
    dfs(root2, -1)  # Could fail if root2 is None and dfs doesn't handle it

4. Using the Wrong Sign Convention

Mixing up the increment/decrement values (+1 and -1) would lead to incorrect results. Always ensure the first tree adds to the counter and the second tree subtracts.

Wrong sign convention:

# Incorrect: using the same sign for both trees
dfs(root1, 1)
dfs(root2, 1)  # Should be -1

# Or incorrect: reversed signs
dfs(root1, -1)  # Should be 1
dfs(root2, 1)   # Should be -1

5. Inefficient Final Check

Using len(cnt) == 0 or checking if the counter is empty would be incorrect since the counter will contain entries even when all values are zero.

Incorrect final check:

# Wrong: Counter with all zero values is not empty
return len(cnt) == 0

# Wrong: Counter with zero values still has keys
return not cnt

# Correct approach:
return all(x == 0 for x in cnt.values())
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does merge sort divide the problem into subproblems?


Recommended Readings

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

Load More