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:
- Visit the current node
- Traverse the left subtree
- 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:
- We need to traverse the tree in pre-order (a form of DFS traversal)
- We need to visit each node and make decisions about flipping based on the expected voyage sequence
- 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
- 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.
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:
-
Index Tracking (
i
): We maintain a global indexi
that tracks our current position in thevoyage
array. As we visit each node in pre-order, we increment this index. -
Validity Flag (
ok
): A boolean flag that becomesfalse
if we encounter an impossible situation where no amount of flipping can match the voyage. -
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
orok
is alreadyfalse
, return immediately - If the current node's value doesn't match
voyage[i]
, setok = 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
- If there's no left child (
-
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
- If the left child exists but doesn't match
The algorithm processes each node exactly once, making decisions greedily. After the complete traversal:
- If
ok
remainstrue
, return the list of flipped nodes (ans
) - If
ok
isfalse
, 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 EvaluatorExample 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
withvoyage[i]
takesO(1)
- Incrementing the index
i
takesO(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:
-
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 usesO(1)
space on the call stack, giving usO(n)
space for the recursion stack. -
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 mostn-1
internal nodes, the answer list could contain up toO(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 incrementingvoyage_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.
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
Binary Tree Min Depth Prereq BFS on Tree problems bfs_intro Given a binary tree find the depth of the shallowest leaf node https assets algo monster binary_tree_min_depth png Explanation We can solve this problem with either DFS or BFS With DFS we traverse the whole tree looking for leaf nodes and record and update the minimum depth as we go With BFS though since we search level by level we are guaranteed to find the shallowest leaf node
Want a Structured Path to Master System Design Too? Don’t Miss This!