A clear explanation of matching a binary tree preorder traversal by greedily flipping nodes.
Problem Restatement
We are given a binary tree and an array voyage.
A flip operation at a node swaps its left and right children.
We need to flip the smallest number of nodes so that the preorder traversal of the tree becomes exactly equal to voyage.
Preorder traversal visits nodes in this order:
root
left subtree
right subtreeIf it is possible, return the values of the flipped nodes. The values may be returned in any order. If it is impossible, return:
[-1]The tree has n nodes, n == voyage.length, 1 <= n <= 100, and all values in the tree and in voyage are unique.
Input and Output
| Item | Meaning |
|---|---|
| Input | root, the root of a binary tree |
| Input | voyage, the target preorder traversal |
| Output | Values of nodes to flip, or [-1] |
| Flip operation | Swap a node’s left and right children |
Example function shape:
def flipMatchVoyage(root: Optional[TreeNode], voyage: list[int]) -> list[int]:
...Examples
Example 1:
root = [1, 2]
voyage = [2, 1]Output:
[-1]The preorder traversal must start with the root value 1, but voyage starts with 2. No flip can change the root value.
Example 2:
root = [1, 2, 3]
voyage = [1, 3, 2]Output:
[1]Flip node 1, so its children become 3 then 2. The preorder traversal becomes:
[1, 3, 2]Example 3:
root = [1, 2, 3]
voyage = [1, 2, 3]Output:
[]The preorder traversal already matches voyage.
First Thought: Try Flip Choices
At each node, we could try two possibilities:
- Do not flip the node.
- Flip the node.
This gives many possible trees.
But we do not need to try both blindly. Preorder traversal tells us exactly which node must come next.
Key Insight
During preorder DFS, when we visit a node, its value must match the current value in voyage.
After visiting the current node, the next value in voyage should be the root of the next subtree we visit.
Normally, we visit the left child first.
So if the left child exists but its value does not match the next value in voyage, then the only possible way to match the voyage is to flip the current node and visit the right child first.
This gives a greedy rule:
if node.left exists and node.left.val != voyage[index]:
flip this node
visit right first, then left
else:
visit left first, then rightIf even this does not work, the match is impossible.
Algorithm
Use DFS with an index into voyage.
- Start with
index = 0. - Visit the current node.
- If the node is
None, returnTrue. - If
node.val != voyage[index], returnFalse. - Move
indexforward. - If the left child exists and its value does not match the next expected value:
- Record
node.valas flipped. - Visit right child first.
- Then visit left child.
- Record
- Otherwise:
- Visit left child first.
- Then visit right child.
- If DFS succeeds, return the recorded flips.
- Otherwise, return
[-1].
Correctness
Preorder traversal always visits the current node before its children. Therefore, when DFS reaches a node, its value must equal the current value in voyage. If it does not, no sequence of child flips can fix this mismatch, because flips never change node values or move a child above its parent.
After matching the current node, preorder must next visit one of its children, if any child exists. Without a flip, the left child is visited first. With a flip, the right child is visited first.
If the left child exists and does not match the next expected voyage value, then visiting the left child first cannot work. Since values are unique, the only possible child that can match next is the right child. Therefore, flipping the current node is forced.
If the left child already matches the next expected value, then no flip is needed at this node. Flipping would move a different child first and would break the required preorder order.
Thus, at every node, the algorithm makes the only valid choice. If the traversal completes, the recorded flips produce exactly voyage. If the algorithm fails, no valid flip sequence exists.
Complexity
Let n be the number of nodes.
| Metric | Value | Why |
|---|---|---|
| Time | O(n) | Each node is visited once |
| Space | O(h) | The recursion stack depends on tree height |
The output list can contain up to n flipped node values.
Implementation
from typing import Optional
# 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 flipMatchVoyage(
self,
root: Optional[TreeNode],
voyage: list[int],
) -> list[int]:
flips = []
index = 0
def dfs(node: Optional[TreeNode]) -> bool:
nonlocal index
if node is None:
return True
if index >= len(voyage) or node.val != voyage[index]:
return False
index += 1
if (
node.left is not None
and index < len(voyage)
and node.left.val != voyage[index]
):
flips.append(node.val)
return dfs(node.right) and dfs(node.left)
return dfs(node.left) and dfs(node.right)
if dfs(root):
return flips
return [-1]Code Explanation
We store flipped node values here:
flips = []The variable index points to the next expected value in voyage:
index = 0At each node, we first check the preorder match:
if index >= len(voyage) or node.val != voyage[index]:
return FalseIf the current node matches, we consume that voyage value:
index += 1Then we decide whether to flip.
If the left child exists but is not the next expected node:
if (
node.left is not None
and index < len(voyage)
and node.left.val != voyage[index]
):we must flip the current node. We record it:
flips.append(node.val)and traverse right before left:
return dfs(node.right) and dfs(node.left)Otherwise, we keep normal preorder:
return dfs(node.left) and dfs(node.right)At the end:
if dfs(root):
return flips
return [-1]we return the flip list if successful, otherwise [-1].
Testing
from collections import deque
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def build_tree(values):
if not values:
return None
root = TreeNode(values[0])
queue = deque([root])
i = 1
while queue and i < len(values):
node = queue.popleft()
if i < len(values) and values[i] is not None:
node.left = TreeNode(values[i])
queue.append(node.left)
i += 1
if i < len(values) and values[i] is not None:
node.right = TreeNode(values[i])
queue.append(node.right)
i += 1
return root
def run_tests():
s = Solution()
root = build_tree([1, 2])
assert s.flipMatchVoyage(root, [2, 1]) == [-1]
root = build_tree([1, 2, 3])
assert s.flipMatchVoyage(root, [1, 3, 2]) == [1]
root = build_tree([1, 2, 3])
assert s.flipMatchVoyage(root, [1, 2, 3]) == []
root = build_tree([1, 2, 3, 4, 5])
assert s.flipMatchVoyage(root, [1, 2, 5, 4, 3]) == [2]
root = build_tree([1, 2, 3, None, None, 4])
assert s.flipMatchVoyage(root, [1, 3, 4, 2]) == [1]
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Root mismatch | Impossible immediately |
| Flip at root | Basic forced flip |
| Already matching preorder | No flips needed |
| Flip below root | Checks recursive flip decision |
| Right subtree before left subtree | Checks normal use of flipped order |