Facebook Pixel

971. Flip Binary Tree To Match Preorder Traversal

Problem Description

You have a binary tree where each node has a unique value from 1 to n. You're also given an array voyage that represents the desired pre-order traversal sequence of the tree.

In a pre-order traversal, you visit nodes in this order:

  1. Visit the current node
  2. Traverse the left subtree
  3. Traverse the right subtree

You can perform a "flip" operation on any node, which swaps its left and right children. For example, if a node has left child A and right child B, after flipping, it will have left child B and right child A.

Your task is to find the minimum number of nodes to flip so that the pre-order traversal of the modified tree matches the given voyage array exactly.

Return a list containing the values of all nodes that need to be flipped. You can return these values in any order. If it's impossible to achieve the desired traversal through any combination of flips, return [-1].

For example, if you have a tree with root 1, left child 2, and right child 3, and the desired voyage is [1, 3, 2], you would need to flip node 1 to swap its children, making the pre-order traversal match the voyage.

The solution uses DFS to traverse the tree while tracking the current position in the voyage array. At each node, it checks if the current node's value matches the expected value in voyage[i]. If not, the matching is impossible. If it matches, the algorithm decides whether to flip based on whether the left child's value matches the next expected value in the voyage sequence.

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, which is a special type of graph with nodes (tree nodes) and edges (parent-child relationships).

Is it a tree?

  • Yes: The problem explicitly states we're working with a binary tree where each node has at most two children (left and right).

DFS

  • Yes: We arrive at DFS as the solution approach.

Conclusion: The flowchart suggests using DFS (Depth-First Search) for this problem.

This makes perfect sense because:

  1. We need to traverse the tree in pre-order (a form of DFS traversal)
  2. We need to visit each node and make decisions about flipping based on the expected voyage sequence
  3. DFS allows us to process nodes in the exact order of pre-order traversal (root → left → right), which aligns perfectly with comparing against the given voyage array
  4. At each node during DFS, we can check if the current traversal matches the expected voyage sequence and decide whether to flip the node's children

The DFS approach naturally handles the recursive nature of checking each subtree and making flip decisions based on whether the left child matches the next expected value in the voyage sequence.

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

Intuition

The key insight is that pre-order traversal visits nodes in a fixed order: root → left → right. When we traverse the tree, we must match the given voyage sequence exactly. If at any point the current node's value doesn't match what we expect in voyage, it's impossible to achieve the desired traversal.

Think about what happens when we're at a node during traversal. After visiting the current node (which must match voyage[i]), we need to decide which child to visit next. In normal pre-order, we'd go left then right. But what if the left child's value doesn't match the next expected value voyage[i+1]?

This is where flipping comes in. If the left child doesn't match but the right child does match voyage[i+1], we can flip the current node to swap its children. After flipping, the right child becomes the left child, allowing us to continue matching the voyage sequence.

The beauty of this approach is that we make greedy decisions during traversal. At each node, we check:

  • Does my value match the current position in voyage? If not, it's impossible.
  • Does my left child match the next position? If yes, proceed normally (left → right).
  • If not, but my right child matches the next position, flip and proceed (right → left).

We only need to track which nodes we flip and whether the entire traversal remains valid. If we can traverse the entire tree while matching every value in voyage, we return the list of flipped nodes. Otherwise, we return [-1].

This greedy strategy works because each node appears exactly once in the pre-order traversal, and we must match the voyage sequence in order. There's no benefit to delaying a flip decision - if we need to flip, we must do it immediately to match the expected sequence.

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

Solution Approach

We implement the solution using DFS traversal with a few key variables to track our progress:

  1. Index Tracking (i): We maintain a global index i that tracks our current position in the voyage array. As we visit each node in pre-order, we increment this index.

  2. Validity Flag (ok): A boolean flag that becomes false if we encounter an impossible situation where no amount of flipping can match the voyage.

  3. Answer List (ans): Collects the values of all nodes that need to be flipped.

The DFS function works as follows:

Base Cases:

  • If the current node is None or ok is already false, return immediately
  • If the current node's value doesn't match voyage[i], set ok = false and return (impossible to match)

Core Logic: After confirming the current node matches voyage[i], we increment i and need to decide the traversal order:

  • Check the left child:

    • If there's no left child (root.left is None), proceed with normal traversal
    • If the left child's value equals voyage[i] (the next expected value), proceed with normal traversal: left → right
  • Need to flip:

    • If the left child exists but doesn't match voyage[i], we need to flip this node
    • Add the current node's value to ans to record the flip
    • Traverse in reversed order: right → left

The algorithm processes each node exactly once, making decisions greedily. After the complete traversal:

  • If ok remains true, return the list of flipped nodes (ans)
  • If ok is false, return [-1] indicating it's impossible to match the voyage

This approach has O(n) time complexity where n is the number of nodes, as we visit each node once. The space complexity is O(h) for the recursion stack, where h is the height of the tree.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Consider this binary tree and voyage array:

Tree:           voyage = [1, 3, 4, 2]
      1
     / \
    2   3
       /
      4

We want to check if we can flip some nodes to make the pre-order traversal match [1, 3, 4, 2].

Step 1: Start at root (node 1)

  • Current position in voyage: i = 0
  • Does node 1 match voyage[0] (which is 1)? ✓ Yes
  • Increment i to 1
  • Check left child: node 2's value is 2, but voyage[1] is 3 (mismatch!)
  • Check right child: node 3's value is 3, which matches voyage[1]
  • Decision: Flip node 1 to swap its children
  • Add 1 to our answer list: ans = [1]
  • After flip, traverse right child first (which was originally right, now logically left)

Step 2: Visit node 3 (originally right child)

  • Current position: i = 1
  • Does node 3 match voyage[1] (which is 3)? ✓ Yes
  • Increment i to 2
  • Node 3 has only a left child (node 4)
  • Check left child: node 4's value is 4, and voyage[2] is 4 ✓ Match!
  • Proceed with normal traversal (left → right)

Step 3: Visit node 4

  • Current position: i = 2
  • Does node 4 match voyage[2] (which is 4)? ✓ Yes
  • Increment i to 3
  • Node 4 has no children, continue

Step 4: Visit node 2 (originally left child, now visited last due to flip)

  • Current position: i = 3
  • Does node 2 match voyage[3] (which is 2)? ✓ Yes
  • Increment i to 4
  • Node 2 has no children, traversal complete

Result:

  • All nodes matched successfully (ok = true)
  • We flipped node 1
  • Return [1]

The final tree after flipping node 1 has its children swapped:

After flip:
      1
     / \
    3   2
   /
  4

Pre-order traversal of flipped tree: 1 → 3 → 4 → 2, which matches our voyage exactly!

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, List
9
10class Solution:
11    def flipMatchVoyage(self, root: Optional[TreeNode], voyage: List[int]) -> List[int]:
12        """
13        Determine which nodes need to be flipped to match the pre-order traversal with voyage.
14        Returns list of node values to flip, or [-1] if impossible.
15      
16        Args:
17            root: Root of the binary tree
18            voyage: Target pre-order traversal sequence
19          
20        Returns:
21            List of node values that need their children flipped, or [-1] if impossible
22        """
23      
24        def dfs(node: Optional[TreeNode]) -> None:
25            """
26            Perform DFS traversal and determine nodes that need flipping.
27          
28            Args:
29                node: Current node being processed
30            """
31            nonlocal voyage_index, is_valid
32          
33            # Base case: null node or already found invalid
34            if node is None or not is_valid:
35                return
36          
37            # Check if current node matches expected value in voyage
38            if node.val != voyage[voyage_index]:
39                is_valid = False
40                return
41          
42            # Move to next position in voyage
43            voyage_index += 1
44          
45            # Check if we need to flip children
46            # If left child is None or matches next expected value, traverse normally
47            if node.left is None or node.left.val == voyage[voyage_index]:
48                # Normal traversal: left then right
49                dfs(node.left)
50                dfs(node.right)
51            else:
52                # Need to flip: traverse right then left
53                flipped_nodes.append(node.val)
54                dfs(node.right)
55                dfs(node.left)
56      
57        # Initialize variables
58        flipped_nodes = []  # List to store nodes that need flipping
59        voyage_index = 0    # Current index in voyage array
60        is_valid = True     # Flag to track if matching is possible
61      
62        # Start DFS traversal
63        dfs(root)
64      
65        # Return result based on validity
66        return flipped_nodes if is_valid else [-1]
67
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    // Current index in the voyage array during traversal
18    private int currentIndex;
19  
20    // Flag to track if matching is possible
21    private boolean isMatchingPossible;
22  
23    // Target voyage array to match
24    private int[] targetVoyage;
25  
26    // List to store node values where flips are performed
27    private List<Integer> flippedNodes;
28
29    /**
30     * Determines if we can match the given voyage by flipping subtrees.
31     * Returns list of node values where flips are needed, or [-1] if impossible.
32     * 
33     * @param root The root of the binary tree
34     * @param voyage The target pre-order traversal sequence
35     * @return List of node values to flip, or [-1] if matching is impossible
36     */
37    public List<Integer> flipMatchVoyage(TreeNode root, int[] voyage) {
38        // Initialize instance variables
39        this.targetVoyage = voyage;
40        this.currentIndex = 0;
41        this.isMatchingPossible = true;
42        this.flippedNodes = new ArrayList<>();
43      
44        // Perform DFS traversal to check if matching is possible
45        performDFS(root);
46      
47        // Return result based on whether matching was successful
48        return isMatchingPossible ? flippedNodes : List.of(-1);
49    }
50
51    /**
52     * Performs depth-first search to match tree traversal with voyage array.
53     * Flips children when necessary and records flipped nodes.
54     * 
55     * @param node Current node being processed
56     */
57    private void performDFS(TreeNode node) {
58        // Base case: null node or matching already failed
59        if (node == null || !isMatchingPossible) {
60            return;
61        }
62      
63        // Check if current node matches expected value in voyage
64        if (node.val != targetVoyage[currentIndex]) {
65            isMatchingPossible = false;
66            return;
67        }
68      
69        // Move to next position in voyage array
70        currentIndex++;
71      
72        // Determine traversal order based on next expected value
73        if (node.left == null || node.left.val == targetVoyage[currentIndex]) {
74            // Normal order: left then right
75            performDFS(node.left);
76            performDFS(node.right);
77        } else {
78            // Flip required: traverse right then left
79            flippedNodes.add(node.val);
80            performDFS(node.right);
81            performDFS(node.left);
82        }
83    }
84}
85
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    vector<int> flipMatchVoyage(TreeNode* root, vector<int>& voyage) {
15        // Track if the voyage matching is possible
16        bool is_possible = true;
17      
18        // Current index in the voyage array
19        int voyage_index = 0;
20      
21        // Store nodes where flips are performed
22        vector<int> flipped_nodes;
23      
24        // Define DFS function to traverse and match with voyage
25        function<void(TreeNode*)> dfs = [&](TreeNode* current_node) {
26            // Base case: null node or matching already failed
27            if (!current_node || !is_possible) {
28                return;
29            }
30          
31            // Check if current node matches expected value in voyage
32            if (current_node->val != voyage[voyage_index]) {
33                is_possible = false;
34                return;
35            }
36          
37            // Move to next position in voyage
38            ++voyage_index;
39          
40            // Determine traversal order based on next expected value
41            if (!current_node->left || current_node->left->val == voyage[voyage_index]) {
42                // Normal order: left then right
43                dfs(current_node->left);
44                dfs(current_node->right);
45            } else {
46                // Flip required: traverse right then left
47                flipped_nodes.push_back(current_node->val);
48                dfs(current_node->right);
49                dfs(current_node->left);
50            }
51        };
52      
53        // Start DFS traversal from root
54        dfs(root);
55      
56        // Return result: flipped nodes if possible, otherwise [-1]
57        return is_possible ? flipped_nodes : vector<int>{-1};
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 * Determines if we can traverse the binary tree in the order specified by voyage,
17 * possibly by flipping some nodes' children.
18 * 
19 * @param root - The root of the binary tree
20 * @param voyage - The target traversal order array
21 * @returns Array of node values where we flip children, or [-1] if impossible
22 */
23function flipMatchVoyage(root: TreeNode | null, voyage: number[]): number[] {
24    // Flag to track if the voyage is still valid
25    let isValidVoyage: boolean = true;
26  
27    // Current index in the voyage array
28    let voyageIndex: number = 0;
29  
30    // Array to store the values of nodes where we flip children
31    const flippedNodes: number[] = [];
32  
33    /**
34     * Performs depth-first search traversal while checking voyage match
35     * @param currentNode - The current node being processed
36     */
37    const performDFS = (currentNode: TreeNode | null): void => {
38        // Base case: null node or invalid voyage detected
39        if (!currentNode || !isValidVoyage) {
40            return;
41        }
42      
43        // Check if current node matches the expected value in voyage
44        if (currentNode.val !== voyage[voyageIndex++]) {
45            isValidVoyage = false;
46            return;
47        }
48      
49        // Determine traversal order based on next expected value
50        if (!currentNode.left || currentNode.left.val === voyage[voyageIndex]) {
51            // Normal order: traverse left then right
52            performDFS(currentNode.left);
53            performDFS(currentNode.right);
54        } else {
55            // Flipped order: record the flip and traverse right then left
56            flippedNodes.push(currentNode.val);
57            performDFS(currentNode.right);
58            performDFS(currentNode.left);
59        }
60    };
61  
62    // Start DFS traversal from root
63    performDFS(root);
64  
65    // Return result based on whether voyage was valid
66    return isValidVoyage ? flippedNodes : [-1];
67}
68

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 algorithm performs constant time operations:

  • Comparing root.val with voyage[i] takes O(1)
  • Incrementing the index i takes O(1)
  • Checking conditions and potentially appending to the answer list takes O(1)
  • Making recursive calls (the calls themselves, not the work done in them)

Since we visit each of the n nodes exactly once and perform O(1) work at each node, the total time complexity is O(n).

Space Complexity: O(n)

The space complexity consists of two components:

  1. Recursion call stack: In the worst case, the tree could be completely unbalanced (like a linked list), resulting in a recursion depth of O(n). Each recursive call uses O(1) space on the call stack, giving us O(n) space for the recursion stack.

  2. Answer list: In the worst case, we might need to flip at every internal node. Since a binary tree with n nodes can have at most n-1 internal nodes, the answer list could contain up to O(n) elements.

Therefore, the total space complexity is O(n) + O(n) = O(n).

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

Common Pitfalls

1. Index Out of Bounds Error

One of the most common pitfalls is accessing voyage[voyage_index] without checking if voyage_index is within bounds. This can happen in two scenarios:

Problem Case:

  • When checking node.left.val == voyage[voyage_index] after incrementing voyage_index, if we've already processed all elements in voyage
  • The tree might have more nodes than the voyage array length

Solution: Add boundary checks before accessing the voyage array:

def dfs(node: Optional[TreeNode]) -> None:
    nonlocal voyage_index, is_valid
  
    if node is None or not is_valid:
        return
  
    # Check bounds before accessing voyage
    if voyage_index >= len(voyage):
        is_valid = False
        return
  
    if node.val != voyage[voyage_index]:
        is_valid = False
        return
  
    voyage_index += 1
  
    # Check bounds again before comparing with left child
    if node.left and voyage_index < len(voyage):
        if node.left.val == voyage[voyage_index]:
            dfs(node.left)
            dfs(node.right)
        else:
            flipped_nodes.append(node.val)
            dfs(node.right)
            dfs(node.left)
    else:
        # Handle edge cases
        dfs(node.left)
        dfs(node.right)

2. Incorrect Flip Decision Logic

Another pitfall is the flip decision when only one child exists. The original code assumes that if node.left is None, we should proceed with normal traversal. However, this doesn't account for the case where only the right child exists and its value should be checked against the voyage.

Problem Case:

  • Node has only a right child, but the code doesn't verify if the right child's value matches voyage[voyage_index]

Solution: Enhance the flip decision logic to handle all child configurations:

# After incrementing voyage_index
if voyage_index < len(voyage):
    # Both children exist
    if node.left and node.right:
        if node.left.val == voyage[voyage_index]:
            dfs(node.left)
            dfs(node.right)
        else:
            flipped_nodes.append(node.val)
            dfs(node.right)
            dfs(node.left)
    # Only left child exists
    elif node.left:
        if node.left.val != voyage[voyage_index]:
            is_valid = False
            return
        dfs(node.left)
    # Only right child exists
    elif node.right:
        if node.right.val != voyage[voyage_index]:
            is_valid = False
            return
        dfs(node.right)
    # No children (leaf node) - nothing to do

3. Mismatch Between Tree Size and Voyage Length

The code doesn't explicitly verify that the number of nodes in the tree matches the length of the voyage array.

Problem Case:

  • Tree has fewer nodes than voyage length
  • Tree has more nodes than voyage length

Solution: Add a final validation check:

def flipMatchVoyage(self, root: Optional[TreeNode], voyage: List[int]) -> List[int]:
    # ... existing code ...
  
    dfs(root)
  
    # Verify we've consumed the entire voyage
    if is_valid and voyage_index != len(voyage):
        is_valid = False
  
    return flipped_nodes if is_valid else [-1]

These pitfalls can cause runtime errors or incorrect results, particularly with edge cases involving unbalanced trees or mismatched input sizes.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More