Builds on Bottom-Up DFS, which you haven’t finished yet.
Two-Tree DFS (Walking a Pair of Nodes Together)
Same Tree (LC 100)(opens leetcode.com)
Two binary trees are given by their root nodes, p and q. Either tree may be empty. The trees are the same when they have the same shape and, at every position, the two nodes hold the same value.
Same shape means that wherever one tree has a left child, the other tree has a left child too, and the same for right children. Return True when the trees are the same and False otherwise.
Two Tree Walk
two roots ➔ pair the children by position ➔ combine the pair with its two child pairs' answers| 1 | from typing import Callable, Optional, TypeVar |
| 2 | |
| 3 | R = TypeVar("R") |
| 4 | |
| 5 | class TreeNode: |
| 6 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 7 | self.val = val |
| 8 | self.left = left |
| 9 | self.right = right |
| 10 | |
| 11 | |
| 12 | def walk_pair(first: Optional[TreeNode], second: Optional[TreeNode], end: Callable[[Optional[TreeNode], Optional[TreeNode]], R], combine: Callable[[TreeNode, TreeNode, R, R], R], mirror: bool = False) -> R: |
| 13 | """Two Tree Walk: the answer for the pair of trees rooted at first and second. |
| 14 | end(a, b) is the answer for a pair where a or b is missing. combine(a, b, left, |
| 15 | right) builds the answer for two present nodes from the answers of their two child |
| 16 | pairs. |
| 17 | Precondition: a pair's answer depends only on its two nodes and on the answers of |
| 18 | its two child pairs. |
| 19 | mirror=False pairs left with left and right with right. mirror=True crosses them: |
| 20 | a.left with b.right, a.right with b.left. |
| 21 | N = number of pairs of present nodes the walk reaches. H = depth of the walk. T = |
| 22 | the cost of one end or combine call. |
| 23 | Time: O(N * T) | Space: O(H) recursion stack |
| 24 | """ |
| 25 | def solve(a: Optional[TreeNode], b: Optional[TreeNode]) -> R: |
| 26 | # 1. Base case: a pair with a missing node ends the walk |
| 27 | if not a or not b: |
| 28 | return end(a, b) |
| 29 | |
| 30 | # 2. Divide: pair the children (crossed for a mirror walk), then solve each pair |
| 31 | # on its own |
| 32 | partner_of_left, partner_of_right = (b.right, b.left) if mirror else (b.left, b.right) |
| 33 | left = solve(a.left, partner_of_left) |
| 34 | right = solve(a.right, partner_of_right) |
| 35 | |
| 36 | # 3. Combine: this pair's answer comes only from its two nodes and its two child |
| 37 | # pairs' answers |
| 38 | return combine(a, b, left, right) |
| 39 | |
| 40 | return solve(first, second) |
isSameTree(p, q) returns True exactly when the subtrees at p and q have the same shape and the same values, built only from p.val == q.val and the answers for the two child pairs.Two trees match only if they match position by position, so walk them together: isSameTree(p, q) takes one node from each tree and pairs the children by position, p.left with q.left and p.right with q.right. Each call answers for the pair of subtrees below p and q, and that answer depends only on p.val == q.val and the two child answers, so every position is visited once and neither tree is read alone. Which children to pair is the whole design: Symmetric Tree (LC 101) crosses them, p.left with q.right, and Merge Two Binary Trees (LC 617) keeps them in place. The trap is a pair with one missing node. It must end the walk with False, so the base case is if not p or not q: return p is None and q is None. Returning True there treats a missing node as a match, and [1,2] and [1,null,2] pass as the same tree.
See it run
Two Tree Walk, step by step
pair now (1, 1)returns -answer not yet
The tree p is on the left and the tree q is on the right. Each call looks at one node of p and the node at the same position of q.
nowthe pair the running call is looking atwaitinga pair whose call waits for its child pairssamea pair whose call returned Truediffersa pair whose call returned False
Call isSameTree on the pair (1, 1). The call waits for the answers of its two child pairs.
Gotcha 1 of 4
- The trap
Gotcha 1: Returning
Truewhen only one of the two nodes is missingMistakereturn TrueFixreturn p is None and q is NoneWhat happens: A pair with a missing node ends the walk. When both nodes are missing, the two trees end at the same place, so
Trueis correct. When only one node is missing, one tree has a node where the other has none, so the shapes differ and the answer must beFalse.return Truereports a match in both cases. Onp = [1,2]andq = [1,null,2], the pair(p.left, q.left)is(2, None)and the pair(p.right, q.right)is(None, 2). Both pairs returnTrue, so two different trees are reported as the same.Example: Same Tree (LC 100), Example 2: p = [1,2], q = [1,null,2]Mistake: returns TrueFix: returns False
- Both
pandqare nodes of one tree, as in Lowest Common Ancestor of a Binary Tree (LC 236): there is only one tree to walk, so use Bottom-Up DFS, where each call returns what its subtree found. See Bottom-Up DFS
Deep Dive
Why a call can trust the answers of its child pairs
isSameTree(p, q) is defined by one sentence: it returns True exactly when the subtree at p and the subtree at q have the same shape and the same values. The code only has to keep that sentence true for one call, and the sentence then holds for every call.
Two missing nodes are two empty subtrees, and two empty subtrees are the same. So return p is None and q is None gives True there. One missing node and one real node cannot be the same, because one subtree has a node where the other has none. The same line gives False. For two real nodes, the subtrees are the same exactly when the two values are equal, the left subtrees are the same and the right subtrees are the same. The two child calls answer the last two parts, so the call only compares the two values.
Take p = [1, 2, 3, 4] and q = [1, 2, 3, null, 4]. In both trees the node 4 hangs under the node 2. It is the left child in p and the right child in q. The call on the pair (2, 2) makes two calls: the pair (4, None) and the pair (None, 4). Each pair has one missing node and returns False. So the pair (2, 2) returns False, even though its two values are equal. The pair (3, 3) returns True. The call on the roots receives False and True, and returns False.
The walk makes 7 calls in all: 5 calls on pairs where at least one node exists, and 2 calls below the pair (3, 3), where both nodes are missing. The two trees have the same preorder list of values, [1, 2, 4, 3]. A comparison of the two lists would answer True. The walk answers False because it compares positions.
Which children to pair: the same side, crossed, or both ways
The card's walk pairs p.left with q.left and p.right with q.right. That is one choice. The rest of the walk is the same for every problem on this card: a base case for a pair with a missing node, two calls on child pairs, and a combine step. Only the pairing and the combine change.
Symmetric Tree (LC 101) crosses the pairs. A mirror pairs the outer nodes and the inner nodes, so the walk starts on the root's two children and calls is_mirror(a.left, b.right) and is_mirror(a.right, b.left). In the card's template this is mirror=True. For root = [1, 2, 2, null, 3, null, 3] the answer is False: the left 2 has only a right child, and the right 2 has only a right child, so the right child of the left 2 faces a missing node. Pairing the same side instead answers True for this tree.
Merge Two Binary Trees (LC 617) builds a tree. When one node of a pair is missing, the base case returns the node that exists, a or b, and that whole branch joins the result. The combine builds TreeNode(a.val + b.val, left, right). For root1 = [1, 3, 2, 5] and root2 = [2, 1, 3, null, 4, null, 7] the result is [3, 4, 5, 5, 4, null, 7]. If the base case returned None for a pair with one missing node, as Same Tree does, the result would shrink to [3, 4, 5], because every branch that exists in only one tree would be lost.
Flip Equivalent Binary Trees (LC 951) accepts either pairing at every node. The combine tries the same side first and the crossed pairs second: the same-side pairs both match, or the crossed pairs both match. The node values are unique in this problem. For that reason only one of the two pairings can match the child values, and the other fails after one comparison. The cost stays at O(min(N, M)).
Subtree of Another Tree: the walk as a check at every node
Subtree of Another Tree (LC 572) asks whether subRoot appears inside root. Run the card's walk on the pair (node, subRoot) for every node of root, and stop at the first True. The walk is the check. The outer loop is an ordinary tree walk.
The check needs the full base case. Suppose the walk returned True as soon as subRoot was used up. Then root = [3, 4, 5, 1, 2, null, null, null, null, 0] would match subRoot = [4, 1, 2], because the subtree of the node 4 starts with 4, 1, 2. But the node 2 in root has an extra child 0, so the answer is False. With the card's base case, the pair (0, None) returns False, and the extra node is seen.
The cost is O(N * M) in the worst case. The walk runs at each of the N nodes of root, and it never goes past the smaller tree, so one walk costs at most O(M), where M is the number of nodes in subRoot. Giving every distinct subtree an id lets one pass answer the question, but it needs a different idea and is not part of this card.
The cost, and where the walk stops
A call on a pair where both nodes exist makes two more calls. A call on a pair with a missing node makes none. So the number of calls is at most 2 * min(N, M) + 1. For two identical trees of 7 nodes it is exactly 15. Each call does constant work, so the time is O(min(N, M)).
The recursion goes down only while both nodes exist. The stack holds at most one frame per level of the shallower tree, so the space is O(H), where H is the height of the shallower tree.
The blueprint calls both children before it compares the values. A difference at the root therefore does not stop the walk. A version that returns False as soon as p.val != q.val stops earlier on trees that differ near the root. The worst case is the same, because two identical trees must be walked to the end.
A chain of 1000 nodes goes past Python's default limit of about 1000 nested calls. The explicit-stack version keeps the pairs on its own list. It starts with stack = [(p, q)], pops a pair, returns False on a mismatch, and pushes (a.left, b.left) and (a.right, b.right).
60-Second Pitch
"The question is about two trees, so I use a two-tree walk: one recursive call holds a node from each tree. isSameTree(p, q) pairs the children by position, p.left with q.left and p.right with q.right. If a node is missing on either side, the trees match here only when both are missing, so the base case is return p is None and q is None. Otherwise the pair matches when the two values are equal and both child pairs match. It is correct because each call can trust the two answers it receives: they are the true answers for smaller pairs of subtrees. Each position is visited once, so the time is O(min(N, M)) and the stack is as deep as the shallower tree. For Symmetric Tree, I cross the pairs. For very deep trees, I use a stack of pairs."
Common Questions
- How is Two Tree Walk different from Bottom-Up DFS?
Bottom-Up DFS holds one node per call and builds the answer from the answers of its two subtrees. Two Tree Walk holds a pair of nodes, one from each tree, and builds the answer from the pair and from the answers of two child pairs. The base case also has more outcomes: two missing nodes, one missing node, and two real nodes.
- Why must a pair with one missing node return
Falsein Same Tree? A node that exists in one tree and not in the other means the shapes differ at that position. Returning
Truewould hide that difference. On[1, 2]and[1, null, 2]the walk would then report two different trees as the same.- How do you check that a tree is symmetric?
Start the walk on the root's left child and right child, and cross the pairs: the outer pair is
(a.left, b.right)and the inner pair is(a.right, b.left). The base case and the combine step are the same as in Same Tree. Symmetric Tree (LC 101) is this walk withmirror=True.- What is the cost of Subtree of Another Tree (LC 572) with this walk?
O(N * M) in the worst case. The walk runs on
(node, subRoot)for each of the N nodes ofroot, and one walk costs at most O(M), because it never goes past the smaller tree. The base case must returnFalsefor a pair with one missing node, so that extra nodes below a match are seen.- How do you walk two trees without recursion?
Keep a stack of pairs. Start with
[(p, q)]. Pop a pair. If one node is missing and the other exists, returnFalse. If both exist and the values differ, returnFalse. Otherwise push(a.left, b.left)and(a.right, b.right). If the stack empties, returnTrue.
SWE Reasoning
Spot it
- The statement gives you two trees,
pandq, or one tree that you compare with itself. - The question is whether they are the same, mirror images, or equal after flips, or the answer is a new tree built from both.
- The answer for a pair of nodes depends only on the two nodes and on the answers for the pairs of their children.
- You can say in one sentence what
solve(p, q)returns for the subtrees atpandq. - Only one tree is walked, or the answer depends on what lies above a node: use Bottom-Up DFS or Top-Down DFS instead.
The invariant, step by step
- Define in one sentence what
solve(p, q)returns: the answer for the subtree atpand the subtree atq. - The base case handles a pair with a missing node. Two missing nodes are two empty subtrees. One missing node means the shapes differ.
- For two real nodes, the two child calls run first. Each pairs a child of
pwith its partner inq, and by the sentence in step 1 each returns a finished answer. - The combine step builds this pair's answer from the two nodes and the two child answers. It does not look any deeper.
- So the sentence is true for this call and for every call. The call on the two roots returns the answer for the whole pair of trees.
Traps
- Pairing the wrong children. Same Tree pairs the same side. Symmetric Tree (LC 101) crosses the pairs. Pairing the same side for a mirror question accepts
[1, 2, 2, null, 3, null, 3], which is not symmetric. - Losing a branch that exists in one tree only. In Merge Two Binary Trees (LC 617) a pair with one missing node must return the node that exists. Returning
Noneshrinks the result to[3, 4, 5]on the LeetCode example. - Stopping when the smaller tree is used up. In Subtree of Another Tree (LC 572), a base case that returns
TruewhensubRootends accepts a tree that has extra nodes below the match. - A deep chain. Two chains of 1000 nodes pass Python's default limit of about 1000 nested calls. Use an explicit stack of pairs.
Practice
Builds on Bottom-Up DFS, which you haven’t finished yet.
Two-Tree DFS (Walking a Pair of Nodes Together)
Same Tree (LC 100)(opens leetcode.com)
Two binary trees are given by their root nodes, p and q. Either tree may be empty. The trees are the same when they have the same shape and, at every position, the two nodes hold the same value.
Same shape means that wherever one tree has a left child, the other tree has a left child too, and the same for right children. Return True when the trees are the same and False otherwise.
Two Tree Walk
two roots ➔ pair the children by position ➔ combine the pair with its two child pairs' answers| 1 | from typing import Callable, Optional, TypeVar |
| 2 | |
| 3 | R = TypeVar("R") |
| 4 | |
| 5 | class TreeNode: |
| 6 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 7 | self.val = val |
| 8 | self.left = left |
| 9 | self.right = right |
| 10 | |
| 11 | |
| 12 | def walk_pair(first: Optional[TreeNode], second: Optional[TreeNode], end: Callable[[Optional[TreeNode], Optional[TreeNode]], R], combine: Callable[[TreeNode, TreeNode, R, R], R], mirror: bool = False) -> R: |
| 13 | """Two Tree Walk: the answer for the pair of trees rooted at first and second. |
| 14 | end(a, b) is the answer for a pair where a or b is missing. combine(a, b, left, |
| 15 | right) builds the answer for two present nodes from the answers of their two child |
| 16 | pairs. |
| 17 | Precondition: a pair's answer depends only on its two nodes and on the answers of |
| 18 | its two child pairs. |
| 19 | mirror=False pairs left with left and right with right. mirror=True crosses them: |
| 20 | a.left with b.right, a.right with b.left. |
| 21 | N = number of pairs of present nodes the walk reaches. H = depth of the walk. T = |
| 22 | the cost of one end or combine call. |
| 23 | Time: O(N * T) | Space: O(H) recursion stack |
| 24 | """ |
| 25 | def solve(a: Optional[TreeNode], b: Optional[TreeNode]) -> R: |
| 26 | # 1. Base case: a pair with a missing node ends the walk |
| 27 | if not a or not b: |
| 28 | return end(a, b) |
| 29 | |
| 30 | # 2. Divide: pair the children (crossed for a mirror walk), then solve each pair |
| 31 | # on its own |
| 32 | partner_of_left, partner_of_right = (b.right, b.left) if mirror else (b.left, b.right) |
| 33 | left = solve(a.left, partner_of_left) |
| 34 | right = solve(a.right, partner_of_right) |
| 35 | |
| 36 | # 3. Combine: this pair's answer comes only from its two nodes and its two child |
| 37 | # pairs' answers |
| 38 | return combine(a, b, left, right) |
| 39 | |
| 40 | return solve(first, second) |
isSameTree(p, q) returns True exactly when the subtrees at p and q have the same shape and the same values, built only from p.val == q.val and the answers for the two child pairs.Two trees match only if they match position by position, so walk them together: isSameTree(p, q) takes one node from each tree and pairs the children by position, p.left with q.left and p.right with q.right. Each call answers for the pair of subtrees below p and q, and that answer depends only on p.val == q.val and the two child answers, so every position is visited once and neither tree is read alone. Which children to pair is the whole design: Symmetric Tree (LC 101) crosses them, p.left with q.right, and Merge Two Binary Trees (LC 617) keeps them in place. The trap is a pair with one missing node. It must end the walk with False, so the base case is if not p or not q: return p is None and q is None. Returning True there treats a missing node as a match, and [1,2] and [1,null,2] pass as the same tree.
See it run
Two Tree Walk, step by step
pair now (1, 1)returns -answer not yet
The tree p is on the left and the tree q is on the right. Each call looks at one node of p and the node at the same position of q.
nowthe pair the running call is looking atwaitinga pair whose call waits for its child pairssamea pair whose call returned Truediffersa pair whose call returned False
Call isSameTree on the pair (1, 1). The call waits for the answers of its two child pairs.
Gotcha 1 of 4
- The trap
Gotcha 1: Returning
Truewhen only one of the two nodes is missingMistakereturn TrueFixreturn p is None and q is NoneWhat happens: A pair with a missing node ends the walk. When both nodes are missing, the two trees end at the same place, so
Trueis correct. When only one node is missing, one tree has a node where the other has none, so the shapes differ and the answer must beFalse.return Truereports a match in both cases. Onp = [1,2]andq = [1,null,2], the pair(p.left, q.left)is(2, None)and the pair(p.right, q.right)is(None, 2). Both pairs returnTrue, so two different trees are reported as the same.Example: Same Tree (LC 100), Example 2: p = [1,2], q = [1,null,2]Mistake: returns TrueFix: returns False
- Both
pandqare nodes of one tree, as in Lowest Common Ancestor of a Binary Tree (LC 236): there is only one tree to walk, so use Bottom-Up DFS, where each call returns what its subtree found. See Bottom-Up DFS
Deep Dive
Why a call can trust the answers of its child pairs
isSameTree(p, q) is defined by one sentence: it returns True exactly when the subtree at p and the subtree at q have the same shape and the same values. The code only has to keep that sentence true for one call, and the sentence then holds for every call.
Two missing nodes are two empty subtrees, and two empty subtrees are the same. So return p is None and q is None gives True there. One missing node and one real node cannot be the same, because one subtree has a node where the other has none. The same line gives False. For two real nodes, the subtrees are the same exactly when the two values are equal, the left subtrees are the same and the right subtrees are the same. The two child calls answer the last two parts, so the call only compares the two values.
Take p = [1, 2, 3, 4] and q = [1, 2, 3, null, 4]. In both trees the node 4 hangs under the node 2. It is the left child in p and the right child in q. The call on the pair (2, 2) makes two calls: the pair (4, None) and the pair (None, 4). Each pair has one missing node and returns False. So the pair (2, 2) returns False, even though its two values are equal. The pair (3, 3) returns True. The call on the roots receives False and True, and returns False.
The walk makes 7 calls in all: 5 calls on pairs where at least one node exists, and 2 calls below the pair (3, 3), where both nodes are missing. The two trees have the same preorder list of values, [1, 2, 4, 3]. A comparison of the two lists would answer True. The walk answers False because it compares positions.
Which children to pair: the same side, crossed, or both ways
The card's walk pairs p.left with q.left and p.right with q.right. That is one choice. The rest of the walk is the same for every problem on this card: a base case for a pair with a missing node, two calls on child pairs, and a combine step. Only the pairing and the combine change.
Symmetric Tree (LC 101) crosses the pairs. A mirror pairs the outer nodes and the inner nodes, so the walk starts on the root's two children and calls is_mirror(a.left, b.right) and is_mirror(a.right, b.left). In the card's template this is mirror=True. For root = [1, 2, 2, null, 3, null, 3] the answer is False: the left 2 has only a right child, and the right 2 has only a right child, so the right child of the left 2 faces a missing node. Pairing the same side instead answers True for this tree.
Merge Two Binary Trees (LC 617) builds a tree. When one node of a pair is missing, the base case returns the node that exists, a or b, and that whole branch joins the result. The combine builds TreeNode(a.val + b.val, left, right). For root1 = [1, 3, 2, 5] and root2 = [2, 1, 3, null, 4, null, 7] the result is [3, 4, 5, 5, 4, null, 7]. If the base case returned None for a pair with one missing node, as Same Tree does, the result would shrink to [3, 4, 5], because every branch that exists in only one tree would be lost.
Flip Equivalent Binary Trees (LC 951) accepts either pairing at every node. The combine tries the same side first and the crossed pairs second: the same-side pairs both match, or the crossed pairs both match. The node values are unique in this problem. For that reason only one of the two pairings can match the child values, and the other fails after one comparison. The cost stays at O(min(N, M)).
Subtree of Another Tree: the walk as a check at every node
Subtree of Another Tree (LC 572) asks whether subRoot appears inside root. Run the card's walk on the pair (node, subRoot) for every node of root, and stop at the first True. The walk is the check. The outer loop is an ordinary tree walk.
The check needs the full base case. Suppose the walk returned True as soon as subRoot was used up. Then root = [3, 4, 5, 1, 2, null, null, null, null, 0] would match subRoot = [4, 1, 2], because the subtree of the node 4 starts with 4, 1, 2. But the node 2 in root has an extra child 0, so the answer is False. With the card's base case, the pair (0, None) returns False, and the extra node is seen.
The cost is O(N * M) in the worst case. The walk runs at each of the N nodes of root, and it never goes past the smaller tree, so one walk costs at most O(M), where M is the number of nodes in subRoot. Giving every distinct subtree an id lets one pass answer the question, but it needs a different idea and is not part of this card.
The cost, and where the walk stops
A call on a pair where both nodes exist makes two more calls. A call on a pair with a missing node makes none. So the number of calls is at most 2 * min(N, M) + 1. For two identical trees of 7 nodes it is exactly 15. Each call does constant work, so the time is O(min(N, M)).
The recursion goes down only while both nodes exist. The stack holds at most one frame per level of the shallower tree, so the space is O(H), where H is the height of the shallower tree.
The blueprint calls both children before it compares the values. A difference at the root therefore does not stop the walk. A version that returns False as soon as p.val != q.val stops earlier on trees that differ near the root. The worst case is the same, because two identical trees must be walked to the end.
A chain of 1000 nodes goes past Python's default limit of about 1000 nested calls. The explicit-stack version keeps the pairs on its own list. It starts with stack = [(p, q)], pops a pair, returns False on a mismatch, and pushes (a.left, b.left) and (a.right, b.right).
60-Second Pitch
"The question is about two trees, so I use a two-tree walk: one recursive call holds a node from each tree. isSameTree(p, q) pairs the children by position, p.left with q.left and p.right with q.right. If a node is missing on either side, the trees match here only when both are missing, so the base case is return p is None and q is None. Otherwise the pair matches when the two values are equal and both child pairs match. It is correct because each call can trust the two answers it receives: they are the true answers for smaller pairs of subtrees. Each position is visited once, so the time is O(min(N, M)) and the stack is as deep as the shallower tree. For Symmetric Tree, I cross the pairs. For very deep trees, I use a stack of pairs."
Common Questions
- How is Two Tree Walk different from Bottom-Up DFS?
Bottom-Up DFS holds one node per call and builds the answer from the answers of its two subtrees. Two Tree Walk holds a pair of nodes, one from each tree, and builds the answer from the pair and from the answers of two child pairs. The base case also has more outcomes: two missing nodes, one missing node, and two real nodes.
- Why must a pair with one missing node return
Falsein Same Tree? A node that exists in one tree and not in the other means the shapes differ at that position. Returning
Truewould hide that difference. On[1, 2]and[1, null, 2]the walk would then report two different trees as the same.- How do you check that a tree is symmetric?
Start the walk on the root's left child and right child, and cross the pairs: the outer pair is
(a.left, b.right)and the inner pair is(a.right, b.left). The base case and the combine step are the same as in Same Tree. Symmetric Tree (LC 101) is this walk withmirror=True.- What is the cost of Subtree of Another Tree (LC 572) with this walk?
O(N * M) in the worst case. The walk runs on
(node, subRoot)for each of the N nodes ofroot, and one walk costs at most O(M), because it never goes past the smaller tree. The base case must returnFalsefor a pair with one missing node, so that extra nodes below a match are seen.- How do you walk two trees without recursion?
Keep a stack of pairs. Start with
[(p, q)]. Pop a pair. If one node is missing and the other exists, returnFalse. If both exist and the values differ, returnFalse. Otherwise push(a.left, b.left)and(a.right, b.right). If the stack empties, returnTrue.
SWE Reasoning
Spot it
- The statement gives you two trees,
pandq, or one tree that you compare with itself. - The question is whether they are the same, mirror images, or equal after flips, or the answer is a new tree built from both.
- The answer for a pair of nodes depends only on the two nodes and on the answers for the pairs of their children.
- You can say in one sentence what
solve(p, q)returns for the subtrees atpandq. - Only one tree is walked, or the answer depends on what lies above a node: use Bottom-Up DFS or Top-Down DFS instead.
The invariant, step by step
- Define in one sentence what
solve(p, q)returns: the answer for the subtree atpand the subtree atq. - The base case handles a pair with a missing node. Two missing nodes are two empty subtrees. One missing node means the shapes differ.
- For two real nodes, the two child calls run first. Each pairs a child of
pwith its partner inq, and by the sentence in step 1 each returns a finished answer. - The combine step builds this pair's answer from the two nodes and the two child answers. It does not look any deeper.
- So the sentence is true for this call and for every call. The call on the two roots returns the answer for the whole pair of trees.
Traps
- Pairing the wrong children. Same Tree pairs the same side. Symmetric Tree (LC 101) crosses the pairs. Pairing the same side for a mirror question accepts
[1, 2, 2, null, 3, null, 3], which is not symmetric. - Losing a branch that exists in one tree only. In Merge Two Binary Trees (LC 617) a pair with one missing node must return the node that exists. Returning
Noneshrinks the result to[3, 4, 5]on the LeetCode example. - Stopping when the smaller tree is used up. In Subtree of Another Tree (LC 572), a base case that returns
TruewhensubRootends accepts a tree that has extra nodes below the match. - A deep chain. Two chains of 1000 nodes pass Python's default limit of about 1000 nested calls. Use an explicit stack of pairs.