Facebook Pixel

1457. Pseudo-Palindromic Paths in a Binary Tree

Problem Description

You are given a binary tree where each node contains a digit from 1 to 9. A path from the root to a leaf node is called pseudo-palindromic if you can rearrange the digits in that path to form a palindrome.

For example, if a path contains the digits [2, 3, 3], this is pseudo-palindromic because you can rearrange them to form [3, 2, 3], which is a palindrome.

Your task is to count how many pseudo-palindromic paths exist from the root node to all leaf nodes in the tree.

Key points to understand:

  • A path must go from the root to a leaf (a node with no children)
  • The path is pseudo-palindromic if ANY permutation of the node values in that path forms a palindrome
  • A palindrome reads the same forwards and backwards
  • For a sequence of digits to form a palindrome after rearrangement, at most one digit can appear an odd number of times (this would be the middle digit in an odd-length palindrome)

The solution uses bit manipulation to track which digits appear an odd number of times. The variable mask uses bits to represent the parity (odd/even count) of each digit 1-9. When we XOR with 1 << root.val, we flip the bit corresponding to that digit. The expression (mask & (mask - 1)) == 0 checks if at most one bit is set in mask, meaning at most one digit appears an odd number of times, which is the requirement for forming a palindrome.

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 states we're working with a binary tree structure.

DFS

  • Yes: We arrive at DFS (Depth-First Search) as our algorithm choice.

The flowchart leads us directly to DFS because:

  1. We're dealing with a tree structure (binary tree)
  2. We need to explore all paths from root to leaf nodes
  3. Each path requires us to traverse deeply from root to leaf before backtracking to explore other paths

Why DFS is the perfect fit for this problem:

  • We need to track the complete path from root to each leaf to determine if it's pseudo-palindromic
  • DFS naturally maintains the current path state as we traverse down the tree
  • When we reach a leaf, we can check if the current path is pseudo-palindromic
  • As we backtrack, the path state is naturally restored for exploring other branches
  • The recursive nature of DFS makes it easy to accumulate the count of valid paths

Conclusion: The flowchart correctly identifies DFS as the optimal approach for traversing all root-to-leaf paths in the binary tree and counting pseudo-palindromic paths.

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

Intuition

The key insight comes from understanding what makes a sequence of numbers rearrangeable into a palindrome. Let's think about palindromes:

  • In [1, 2, 1], we can form 121 - a palindrome
  • In [1, 2, 2, 1], we can form 1221 - a palindrome
  • In [1, 2, 3], we cannot form any palindrome

What's the pattern? For any sequence to be rearrangeable into a palindrome:

  • If the length is even, every digit must appear an even number of times
  • If the length is odd, exactly one digit can appear an odd number of times (this becomes the middle element)

This means at most one digit can have an odd count for a valid pseudo-palindromic path.

Now, how do we efficiently track odd/even counts as we traverse the tree? We could use a frequency array, but there's a clever trick with bit manipulation:

  • Use a bitmask where each bit represents whether a digit (1-9) appears an odd number of times
  • When we encounter a digit, we flip its corresponding bit using XOR (mask ^= 1 << digit)
  • If a digit appears even times, its bit will be 0; if odd times, its bit will be 1

The beauty of XOR is that it naturally toggles between odd and even states:

  • Start with bit 0 (even count)
  • See the digit once: bit becomes 1 (odd count)
  • See it again: bit becomes 0 (even count)
  • And so on...

Finally, to check if at most one bit is set in our mask (meaning at most one digit has odd count), we use the clever bit trick (mask & (mask - 1)) == 0. This expression is true only when:

  • mask = 0 (no bits set - all digits have even count)
  • mask is a power of 2 (exactly one bit set - exactly one digit has odd count)

By combining DFS traversal with this bit manipulation technique, we can efficiently count all pseudo-palindromic paths without explicitly storing or counting digit frequencies.

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

Solution Approach

The solution implements a DFS traversal with bit manipulation to track digit occurrences. Let's walk through the implementation step by step:

1. Main Function Setup: The main function pseudoPalindromicPaths initiates the DFS traversal by calling dfs(root, 0), where 0 is the initial mask representing that no digits have been encountered yet.

2. DFS Function Logic: The recursive dfs(root, mask) function handles the traversal:

  • Base Case: If root is None, return 0 since there's no path to explore.

  • Update Mask: For the current node, update the mask using XOR: mask ^= 1 << root.val. This flips the bit at position root.val:

    • If the bit was 0 (even occurrences), it becomes 1 (odd occurrences)
    • If the bit was 1 (odd occurrences), it becomes 0 (even occurrences)
  • Leaf Node Check: If we reach a leaf node (both root.left and root.right are None):

    • Check if the path is pseudo-palindromic using (mask & (mask - 1)) == 0
    • This expression evaluates to True when mask has at most one bit set
    • Return 1 if it's pseudo-palindromic, 0 otherwise
  • Internal Node: If it's not a leaf node, recursively explore both subtrees:

    • Return dfs(root.left, mask) + dfs(root.right, mask)
    • The same mask value is passed to both recursive calls, maintaining the current path state

3. Bit Manipulation Trick Explained: The expression (mask & (mask - 1)) == 0 deserves special attention:

  • When mask = 0: Result is 0 & -1 = 0, condition is True
  • When mask is a power of 2 (like 4 = 100 in binary): 4 & 3 = 100 & 011 = 0, condition is True
  • When mask has multiple bits set (like 5 = 101 in binary): 5 & 4 = 101 & 100 = 100 ≠ 0, condition is False

4. Space and Time Complexity:

  • Time Complexity: O(n) where n is the number of nodes, as we visit each node exactly once
  • Space Complexity: O(h) where h is the height of the tree, due to the recursion stack

The elegance of this solution lies in using a single integer mask to track the parity of all digits, avoiding the need for frequency arrays or hashmaps while efficiently determining pseudo-palindromic paths.

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 binary tree example to illustrate the solution approach:

       2
      / \
     3   1
    / \
   3   1

We'll trace through the DFS traversal and show how the mask changes at each step.

Step 1: Start at root (value = 2)

  • Initial mask: 0000000000 (binary)
  • Update mask: mask ^= 1 << 20000000100 (bit 2 is set)
  • Not a leaf, so continue to children

Step 2: Go left to node (value = 3)

  • Current mask: 0000000100
  • Update mask: mask ^= 1 << 30000001100 (bits 2 and 3 are set)
  • Not a leaf, so continue to children

Step 3: Go left to leaf (value = 3)

  • Current mask: 0000001100
  • Update mask: mask ^= 1 << 30000000100 (bit 3 flipped back to 0)
  • This is a leaf! Path is [2, 3, 3]
  • Check: (mask & (mask - 1)) == 0?
    • mask = 4 (only bit 2 is set)
    • 4 & 3 = 0100 & 0011 = 0
  • Path is pseudo-palindromic! (Can arrange as "323")
  • Return 1

Step 4: Backtrack and go right to leaf (value = 1)

  • Current mask: 0000001100 (back to parent's state)
  • Update mask: mask ^= 1 << 10000001110 (bits 1, 2, and 3 are set)
  • This is a leaf! Path is [2, 3, 1]
  • Check: (mask & (mask - 1)) == 0?
    • mask = 14 (three bits are set)
    • 14 & 13 = 1110 & 1101 = 1100 ≠ 0
  • Path is NOT pseudo-palindromic (can't form palindrome from "231")
  • Return 0

Step 5: Backtrack to root and go right to leaf (value = 1)

  • Current mask: 0000000100 (back to root's state)
  • Update mask: mask ^= 1 << 10000000110 (bits 1 and 2 are set)
  • This is a leaf! Path is [2, 1]
  • Check: (mask & (mask - 1)) == 0?
    • mask = 6 (two bits are set)
    • 6 & 5 = 0110 & 0101 = 0100 ≠ 0
  • Path is NOT pseudo-palindromic (can't form palindrome from "21")
  • Return 0

Final Result: 1 + 0 + 0 = 1 pseudo-palindromic path

The key insight demonstrated here is how XOR naturally toggles bits to track odd/even occurrences. When we saw digit 3 twice in the first path, its bit flipped from 0→1→0, leaving only digit 2 with odd count, making it a valid pseudo-palindrome.

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
8from typing import Optional
9
10class Solution:
11    def pseudoPalindromicPaths(self, root: Optional[TreeNode]) -> int:
12        """
13        Count the number of pseudo-palindromic paths from root to leaf nodes.
14        A path is pseudo-palindromic if its node values can be rearranged to form a palindrome.
15      
16        Args:
17            root: The root of the binary tree
18          
19        Returns:
20            The count of pseudo-palindromic paths
21        """
22      
23        def dfs(node: Optional[TreeNode], frequency_mask: int) -> int:
24            """
25            Depth-first search to traverse the tree and count pseudo-palindromic paths.
26            Uses bit manipulation to track odd/even frequency of digits (1-9).
27          
28            Args:
29                node: Current tree node being processed
30                frequency_mask: Bitmask representing odd/even frequency of each digit
31                               (bit i is 1 if digit i appears odd times, 0 if even times)
32              
33            Returns:
34                Number of pseudo-palindromic paths from this node to leaf nodes
35            """
36            # Base case: if node is None, no paths exist
37            if node is None:
38                return 0
39          
40            # Toggle the bit corresponding to current node's value
41            # XOR flips the bit: even->odd or odd->even
42            frequency_mask ^= 1 << node.val
43          
44            # Check if we've reached a leaf node
45            if node.left is None and node.right is None:
46                # A path forms a palindrome if at most one digit has odd frequency
47                # Check if frequency_mask has at most one bit set
48                # (frequency_mask & (frequency_mask - 1)) == 0 means 0 or 1 bit is set
49                return int((frequency_mask & (frequency_mask - 1)) == 0)
50          
51            # Recursively count paths in left and right subtrees
52            left_count = dfs(node.left, frequency_mask)
53            right_count = dfs(node.right, frequency_mask)
54          
55            return left_count + right_count
56      
57        # Start DFS from root with initial mask of 0 (all frequencies are even)
58        return dfs(root, 0)
59
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     * Counts the number of pseudo-palindromic paths from root to leaf nodes.
19     * A path is pseudo-palindromic if its node values can be rearranged to form a palindrome.
20     * 
21     * @param root The root of the binary tree
22     * @return The count of pseudo-palindromic paths
23     */
24    public int pseudoPalindromicPaths(TreeNode root) {
25        // Start DFS traversal with initial bitmask of 0
26        return dfs(root, 0);
27    }
28
29    /**
30     * Performs depth-first search to count pseudo-palindromic paths.
31     * Uses bit manipulation to track the frequency parity of each digit (1-9).
32     * 
33     * @param node The current node in the traversal
34     * @param frequencyMask Bitmask representing odd/even frequency of digits seen so far
35     * @return The count of pseudo-palindromic paths from this node to leaves
36     */
37    private int dfs(TreeNode node, int frequencyMask) {
38        // Base case: null node contributes 0 paths
39        if (node == null) {
40            return 0;
41        }
42      
43        // Toggle the bit corresponding to current node's value
44        // XOR flips the bit: even frequency becomes odd, odd becomes even
45        frequencyMask ^= (1 << node.val);
46      
47        // Check if current node is a leaf
48        if (node.left == null && node.right == null) {
49            // A palindrome can have at most one digit with odd frequency
50            // Check if frequencyMask has at most one bit set
51            // (frequencyMask & (frequencyMask - 1)) == 0 when frequencyMask is 0 or power of 2
52            return ((frequencyMask & (frequencyMask - 1)) == 0) ? 1 : 0;
53        }
54      
55        // Recursively count paths in left and right subtrees
56        int leftPaths = dfs(node.left, frequencyMask);
57        int rightPaths = dfs(node.right, frequencyMask);
58      
59        return leftPaths + rightPaths;
60    }
61}
62
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     * Counts the number of pseudo-palindromic paths from root to leaf nodes.
16     * A path is pseudo-palindromic if its node values can be rearranged to form a palindrome.
17     * 
18     * @param root The root of the binary tree
19     * @return The count of pseudo-palindromic paths
20     */
21    int pseudoPalindromicPaths(TreeNode* root) {
22        // Start DFS traversal with initial bitmask of 0
23        return dfsHelper(root, 0);
24    }
25  
26private:
27    /**
28     * Performs depth-first search to count pseudo-palindromic paths.
29     * Uses a bitmask to track the parity (odd/even count) of each digit (1-9).
30     * 
31     * @param node Current node being processed
32     * @param bitmask Tracks odd/even frequency of digits seen so far in the path
33     * @return Number of pseudo-palindromic paths from current node to leaves
34     */
35    int dfsHelper(TreeNode* node, int bitmask) {
36        // Base case: null node contributes 0 paths
37        if (!node) {
38            return 0;
39        }
40      
41        // Toggle the bit corresponding to current node's value
42        // XOR flips the bit: 0->1 (even->odd) or 1->0 (odd->even)
43        bitmask ^= (1 << node->val);
44      
45        // Check if we've reached a leaf node
46        if (!node->left && !node->right) {
47            // A path forms a palindrome if at most one digit has odd frequency
48            // This means the bitmask should have at most one bit set
49            // (bitmask & (bitmask - 1)) == 0 checks if bitmask is 0 or a power of 2
50            return ((bitmask & (bitmask - 1)) == 0) ? 1 : 0;
51        }
52      
53        // Recursively count paths in left and right subtrees
54        int leftPaths = dfsHelper(node->left, bitmask);
55        int rightPaths = dfsHelper(node->right, bitmask);
56      
57        return leftPaths + rightPaths;
58    }
59};
60
1/**
2 * Definition for a binary tree node.
3 * class TreeNode {
4 *     val: number
5 *     left: TreeNode | null
6 *     right: TreeNode | null
7 *     constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) {
8 *         this.val = (val===undefined ? 0 : val)
9 *         this.left = (left===undefined ? null : left)
10 *         this.right = (right===undefined ? null : right)
11 *     }
12 * }
13 */
14
15/**
16 * Counts the number of pseudo-palindromic paths from root to leaf nodes.
17 * A path is pseudo-palindromic if its node values can be rearranged to form a palindrome.
18 * 
19 * @param root - The root node of the binary tree
20 * @returns The count of pseudo-palindromic paths
21 */
22function pseudoPalindromicPaths(root: TreeNode | null): number {
23    /**
24     * Depth-first search helper function to traverse the tree and count pseudo-palindromic paths.
25     * Uses bit manipulation to track the frequency parity of each digit (0-9).
26     * 
27     * @param node - Current node being processed
28     * @param frequencyMask - Bitmask representing odd/even frequency of digits (0-9)
29     *                        Each bit position represents a digit, bit value 1 means odd frequency
30     * @returns Number of pseudo-palindromic paths from current node to leaves
31     */
32    const depthFirstSearch = (node: TreeNode | null, frequencyMask: number): number => {
33        // Base case: empty node contributes no paths
34        if (!node) {
35            return 0;
36        }
37      
38        // Toggle the bit corresponding to current node's value
39        // XOR flips the bit: even frequency becomes odd, odd becomes even
40        frequencyMask ^= 1 << node.val;
41      
42        // Check if current node is a leaf
43        if (!node.left && !node.right) {
44            // A palindrome can have at most one digit with odd frequency
45            // Check if frequencyMask has at most one bit set to 1
46            // (frequencyMask & (frequencyMask - 1)) === 0 when frequencyMask is 0 or a power of 2
47            return (frequencyMask & (frequencyMask - 1)) === 0 ? 1 : 0;
48        }
49      
50        // Recursively count paths in left and right subtrees
51        return depthFirstSearch(node.left, frequencyMask) + depthFirstSearch(node.right, frequencyMask);
52    };
53  
54    // Start DFS from root with initial mask of 0 (all digits have even frequency)
55    return depthFirstSearch(root, 0);
56}
57

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs a depth-first search (DFS) traversal of the binary tree. Each node is visited exactly once during the traversal. At each node, the operations performed are:

  • XOR operation with the mask: mask ^= 1 << root.val - O(1)
  • Checking if it's a leaf node - O(1)
  • For leaf nodes, checking if the path forms a pseudo-palindrome: (mask & (mask - 1)) == 0 - O(1)

Since we visit all n nodes exactly once and perform constant time operations at each node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity comes from the recursive call stack used during DFS traversal. In the worst case, when the tree is completely unbalanced (essentially a linked list), the recursion depth can be n, requiring O(n) space on the call stack. In the best case of a perfectly balanced tree, the recursion depth would be O(log n), but we consider the worst-case scenario for space complexity analysis.

Additionally, the algorithm uses only a constant amount of extra space for the mask variable at each recursive call, which doesn't change the overall space complexity.

Therefore, the space complexity is O(n).

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

Common Pitfalls

Pitfall 1: Incorrectly Passing the Mask to Child Nodes

The Problem: A common mistake is trying to "reset" or modify the mask after exploring each subtree, thinking that the mask needs to be restored to its original state:

def dfs(node, mask):
    if node is None:
        return 0
  
    mask ^= 1 << node.val
  
    if node.left is None and node.right is None:
        return int((mask & (mask - 1)) == 0)
  
    # WRONG: Trying to manually reset mask after left subtree
    left_count = dfs(node.left, mask)
    mask ^= 1 << node.val  # Attempting to "undo" the change
    right_count = dfs(node.right, mask)
  
    return left_count + right_count

Why This is Wrong: The mask parameter is passed by value in Python (for integers), so each recursive call gets its own copy. The XOR operation only affects the local copy, not the caller's version. Attempting to "undo" changes is unnecessary and can lead to incorrect results.

The Correct Approach: Simply pass the same mask value to both recursive calls. Each call maintains its own copy:

def dfs(node, mask):
    if node is None:
        return 0
  
    mask ^= 1 << node.val  # Local modification
  
    if node.left is None and node.right is None:
        return int((mask & (mask - 1)) == 0)
  
    # Both calls receive the same mask value
    return dfs(node.left, mask) + dfs(node.right, mask)

Pitfall 2: Misunderstanding the Bit Position for Node Values

The Problem: Using 1 << (node.val - 1) instead of 1 << node.val, thinking that bit positions should be 0-indexed:

# WRONG: Adjusting for 0-indexing
mask ^= 1 << (node.val - 1)

Why This is Wrong: While it's true that bit positions are typically 0-indexed, the problem states that node values range from 1 to 9. Using 1 << node.val directly means:

  • Digit 1 uses bit position 1
  • Digit 2 uses bit position 2
  • And so on...

This leaves bit position 0 unused, but that's perfectly fine since we have only 9 possible digits and plenty of bits in an integer.

The Correct Approach: Use the node value directly as the bit position:

mask ^= 1 << node.val  # Digit i uses bit position i

Pitfall 3: Checking Palindrome Condition at Non-Leaf Nodes

The Problem: Checking the palindrome condition at every node instead of only at leaf nodes:

def dfs(node, mask):
    if node is None:
        return 0
  
    mask ^= 1 << node.val
  
    # WRONG: Checking at every node
    count = 0
    if (mask & (mask - 1)) == 0:
        count = 1
  
    count += dfs(node.left, mask) + dfs(node.right, mask)
    return count

Why This is Wrong: The problem specifically asks for paths from root to leaf nodes. A path is only complete when we reach a leaf. Intermediate nodes don't represent complete paths and shouldn't be counted.

The Correct Approach: Only check the palindrome condition when both children are None:

def dfs(node, mask):
    if node is None:
        return 0
  
    mask ^= 1 << node.val
  
    # Check only at leaf nodes
    if node.left is None and node.right is None:
        return int((mask & (mask - 1)) == 0)
  
    return dfs(node.left, mask) + dfs(node.right, mask)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Given a sorted array of integers and an integer called target, find the element that equals to the target and return its index. Select the correct code that fills the ___ in the given code snippet.

1def binary_search(arr, target):
2    left, right = 0, len(arr) - 1
3    while left ___ right:
4        mid = (left + right) // 2
5        if arr[mid] == target:
6            return mid
7        if arr[mid] < target:
8            ___ = mid + 1
9        else:
10            ___ = mid - 1
11    return -1
12
1public static int binarySearch(int[] arr, int target) {
2    int left = 0;
3    int right = arr.length - 1;
4
5    while (left ___ right) {
6        int mid = left + (right - left) / 2;
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16
1function binarySearch(arr, target) {
2    let left = 0;
3    let right = arr.length - 1;
4
5    while (left ___ right) {
6        let mid = left + Math.trunc((right - left) / 2);
7        if (arr[mid] == target) return mid;
8        if (arr[mid] < target) {
9            ___ = mid + 1;
10        } else {
11            ___ = mid - 1;
12        }
13    }
14    return -1;
15}
16

Recommended Readings

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

Load More