Builds on Bottom-Up DFS, which you haven’t finished yet.
Tree as a String (Serialize and Canonical Key)
The input is one binary tree, passed as its root. A subtree is a node together with all the nodes below it. Two subtrees are duplicates when they have the same shape and the same value at every position.
Some shapes appear more than once in the tree. Find every shape that appears at least twice. For each such shape, report the root of one of its copies, not the root of every copy.
[2, 4].Tree As String
tree ➔ one string per subtree, built from its children's strings ➔ equal subtrees give equal strings, so count, group or compare the strings| 1 | from typing import Callable, List, Optional, Tuple |
| 2 | |
| 3 | class TreeNode: |
| 4 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 5 | self.val = val |
| 6 | self.left = left |
| 7 | self.right = right |
| 8 | |
| 9 | |
| 10 | def tree_keys(root: Optional[TreeNode], make_key: Callable[[TreeNode, str, str], str], missing: str = "#") -> List[Tuple[TreeNode, str]]: |
| 11 | """Tree as a String: the key of every subtree, in postorder, so the last pair is the |
| 12 | root. |
| 13 | make_key(node, left_key, right_key) builds the key of node from its value and the |
| 14 | keys of its two children. missing is the key of a missing child. |
| 15 | Precondition: make_key keeps the position of every part, so two subtrees have the |
| 16 | same key exactly when they have the same shape and the same values. |
| 17 | N = number of nodes. K = the length of the longest key, which is at most N times the |
| 18 | length of one value. |
| 19 | Time: O(N * K) | Space: O(N * K) |
| 20 | """ |
| 21 | keys: List[Tuple[TreeNode, str]] = [] |
| 22 | |
| 23 | def solve(node: Optional[TreeNode]) -> str: |
| 24 | # 1. Base case: a missing child has the key missing |
| 25 | if not node: |
| 26 | return missing |
| 27 | |
| 28 | # 2. Divide: get the key of each child subtree |
| 29 | left_key = solve(node.left) |
| 30 | right_key = solve(node.right) |
| 31 | |
| 32 | # 3. Combine: this node's key comes from its value and its two child keys, then |
| 33 | # it is recorded |
| 34 | key = make_key(node, left_key, right_key) |
| 35 | keys.append((node, key)) |
| 36 | return key |
| 37 | |
| 38 | solve(root) |
| 39 | return keys |
key_of(node) returns a string that equals the string of another subtree exactly when the two subtrees have the same shape and the same values, because it holds the value, the key of the left child and the key of the right child in fixed positions, with # for a missing child.Write each subtree as one string, and two subtrees are equal exactly when their strings are equal. key_of(node) gets the key of node.left and the key of node.right first, then joins them with node.val into key. Every key has the same parts in the same order, the value, the left key and the right key, with commas between them and # for a missing child. A position in the string then always means one position in the tree, so two different shapes cannot give the same string. The dictionary seen counts each key, and a shape is reported when its count reaches 2, so one pass finds every repeated shape. The trap is leaving a missing child out of the key. If the key skips left_key or right_key when that child is missing, a node 1 with only a left child 2 and a node 1 with only a right child 2 both give 1,2, and Find Duplicate Subtrees (LC 652) on [0,1,1,2,null,null,2] reports the root value 1 as repeated.
See it run
Tree as a String, step by step
key now -count of this key -answer not yet
Each circle is a tree node. A node is tagged when its key is built. The keys and their counts are listed under the picture.
nowthe call that has just startedwaitinga call that waits for the keys of its childrenkeyeda node whose key has been seen oncerepeata node whose key has been seen two or more times
Call key_of on the root 1. Each call asks its two children for their keys first, then builds its own key.
Gotcha 1 of 3
- The trap
Gotcha 1: Leaving a missing child out of the key
Mistakeparts = [str(node.val)] if node.left: parts.append(left_key) if node.right: parts.append(right_key) key = ",".join(parts)Fixkey = f"{node.val},{left_key},{right_key}"What happens: A key that skips a missing child loses the position of the child that is present. A node
1with only a left child2gets the key1,2. A node1with only a right child2also gets the key1,2. The two shapes are different, but they get the same key, soseencounts them as one repeated shape. On[0,1,1,2,null,null,2], the left1and the right1get the same key, so the root value1is reported although the two subtrees are different.Example: Find Duplicate Subtrees (LC 652), root = [0,1,1,2,null,null,2]Mistake: returns [1, 2]Fix: returns [2]
- Each subtree needs only a yes or a number, as in Count Univalue Subtrees (LC 250): use Bottom-Up DFS and return that value from each call. Build a string only when the subtrees of one tree must be compared with each other. See Bottom-Up DFS
- Two trees are given and the question is whether they match, as in Same Tree (LC 100): use Two Tree Walk, which compares the two trees node by node and builds no string. Build a string key when the subtrees of one tree must be compared with each other. See Two Tree Walk
Deep Dive
Why equal strings mean equal subtrees
key_of(node) is defined by one sentence: it returns a string that describes the subtree at node. The code only has to keep that sentence true for one call, and the sentence then holds for every call.
A missing child returns #. For a real node, left_key and right_key are the true descriptions of its two subtrees, by the same sentence. The key of the node is its value, then left_key, then right_key, with commas between them. So the key lists the nodes in preorder, and it lists a # for every missing child.
Two different subtrees cannot get the same key. Read a key from the left. The first part is the value of the root. The next parts describe the left subtree, and they end exactly where that subtree ends, because every real node is followed by two keys and every # ends a branch. What remains describes the right subtree. So a key can be read in only one way, and it describes one tree.
Take root = [1, 2, 3, 4, null, 2, 4, null, null, 4]. The three leaves 4 each get the key 4,#,#. The two nodes 2 each have one left child 4, so both get the key 2,4,#,#,#. The count of 4,#,# reaches 2 at the second leaf, and the count of 2,4,#,#,# reaches 2 at the second node 2. So repeated becomes [4, 2], and sorted(repeated) is [2, 4].
The walk makes 13 calls: 6 calls on real nodes and 7 calls on missing children.
The trap: a key that leaves out the missing child
Suppose the key skips a child that is missing. A node 1 with only a left child 2 gets the key 1,2. A node 1 with only a right child 2 also gets the key 1,2. These are different shapes with the same key.
On root = [0, 1, 1, 2, null, null, 2], the left 1 and the right 1 get the same key, so 1 is reported as repeated. The correct answer is [2], because only the leaf 2 appears twice. The code that skips the missing child returns [1, 2].
The # for a missing child fixes this. It keeps the position of every part: the left 1 gets 1,2,#,#,# and the right 1 gets 1,#,2,#,#.
The separators matter too. Without commas, a node 1 with a left child 11 gets 111###, and a node 11 with a left child 1 gets the same 111###. On root = [0, 1, 11, 11, null, 1] the code without commas reports 11, and the correct answer is [].
A test for your own key: can you write down two different trees that give the same string? If you can, add a marker or a separator until you cannot.
Other problems on this card: the string is the answer, or the string is the lookup
In Find Duplicate Subtrees (LC 652) the strings are keys. They are counted and never printed. In the other problems on this card, the same string is the answer, or it is used for a lookup.
In Serialize and Deserialize Binary Tree (LC 297), the key of the root is the answer. For [1, 2, 3, null, null, 4, 5] it is 1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A # is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree. The tree built from this string gives the same string again.
In Construct String from Binary Tree (LC 606), the marker is a pair of parentheses, and the key is value(left)(right). A missing right child can be dropped. A missing left child must stay as () when a right child exists. For [1, 2, 3, null, 4] the answer is 1(2()(4))(3). The () that stays is the same marker as #: without it, a left child and a right child would look the same.
In Delete Duplicate Folders in System (LC 1948), the key of a folder is built from its subfolders only. It lists each subfolder's name followed by that subfolder's key, with the subfolders sorted by name. The name of the folder itself is not in its key, because two folders with different names can be identical. A non-empty key that appears twice or more marks all of those folders for deletion.
In Subtree of Another Tree (LC 572), build the key of subRoot, and check whether it is the key of any subtree of root.
The cost, and when a string is the wrong tool
A key is as long as its subtree. Joining it and reading it for the dictionary costs time in proportion to its length. The lengths of all keys add up to the sum of the subtree sizes.
On a complete tree of 1023 nodes with every value 0, the keys add up to 37,891 characters. On a chain of 1000 nodes with every value 0, they add up to 2,003,000 characters. In the worst case the sum is N * (N + 1) / 2 subtree nodes, so the time and the space are O(N^2).
When N is large, give each different key a small number instead of a string. The key of a node is then the triple (node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key has a fixed size, so the time and the space are O(N). Two subtrees get the same number exactly when they have the same shape and the same values.
A chain of 5000 nodes is 5000 calls deep, which is past Python's default limit of about 1000 nested calls. Raise the limit with sys.setrecursionlimit, or walk the tree with an explicit stack.
60-Second Pitch
"The question compares subtrees of one tree with each other, so every subtree needs a name that is the same for equal subtrees. I write key_of(node). It asks the left child and the right child for their keys first, then joins the node's value with those two keys into one string, with commas between the parts. A missing child returns #, so every key has a place for both children. It is correct because the string can be read back in only one way, so two subtrees have the same key exactly when they are equal. I count each key in a dictionary, and when a count reaches 2 I record the root value. The time and space are O(N^2) on a chain, and I can replace each string with a number to get O(N). The trap I avoid is leaving out the # for a missing child."
Common Questions
- How is Tree as a String different from Bottom-Up DFS?
Bottom-Up DFS returns a number or a yes from each call, and the answer comes from the root. Tree as a String returns a string that describes the whole subtree, so a call can be compared with every other call. The strings are kept in a dictionary, and the answer comes from the counts.
- Why does a missing child need a marker?
The marker keeps the position of every part of the key. Without it, a node with only a left child
2and a node with only a right child2both give1,2, and two different shapes get the same key.- How do you turn a tree into a string and back?
The key of the root is the string, for example
1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A#is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree.- Why report a shape when the count is exactly 2?
LeetCode needs one root for each repeated shape. A shape that appears three times is reached three times, and only the second sighting has the count 2. With
>= 2, the third sighting adds the same shape again.- How do you make the walk faster than O(N^2)?
Give each different key a number. The key of a node becomes the triple
(node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key then has a fixed size, so the walk costsO(N)time and space.
SWE Reasoning
Spot it
- The statement asks which subtrees repeat, or whether two parts of a tree are identical.
- The statement asks for a text form of a tree, or gives one and asks you to rebuild the tree.
- Two subtrees are equal when they have the same shape and the same values, and you must compare many pairs of them.
- You can say in one sentence what
key_of(node)returns for the subtree atnode. - Each subtree needs only a yes or a number, with no comparison between subtrees: use Bottom-Up DFS.
The invariant, step by step
- Define in one sentence what
key_of(node)returns: a string that describes the subtree atnode. - The base case gives a missing child the key
#, so a missing child still takes a place in the string. - For a real node, the two child calls run first. By the sentence in step 1, each returns a string that describes its subtree.
- The key of the node joins its value, the left key and the right key in this fixed order, with separators.
- So two subtrees have the same key exactly when they have the same shape and the same values. Counting keys counts shapes.
Traps
- Leaving out the missing child. A key that skips a missing child gives
1,2to a node with only a left child2and to a node with only a right child2. On[0,1,1,2,null,null,2]the answer is[1, 2]instead of[2]. - No separators.
111###stands for both1over11and11over1. On[0,1,11,11,null,1]the answer is[11]instead of[]. - Reporting a shape at every sighting. With
>= 2, a shape that appears four times is reported three times. Record it when the count is exactly 2. - Long keys on a deep tree. The keys add up to O(N^2) characters on a chain. Use numbers for keys when N is large.
Practice
Builds on Bottom-Up DFS, which you haven’t finished yet.
Tree as a String (Serialize and Canonical Key)
The input is one binary tree, passed as its root. A subtree is a node together with all the nodes below it. Two subtrees are duplicates when they have the same shape and the same value at every position.
Some shapes appear more than once in the tree. Find every shape that appears at least twice. For each such shape, report the root of one of its copies, not the root of every copy.
[2, 4].Tree As String
tree ➔ one string per subtree, built from its children's strings ➔ equal subtrees give equal strings, so count, group or compare the strings| 1 | from typing import Callable, List, Optional, Tuple |
| 2 | |
| 3 | class TreeNode: |
| 4 | def __init__(self, val: int = 0, left: Optional["TreeNode"] = None, right: Optional["TreeNode"] = None): |
| 5 | self.val = val |
| 6 | self.left = left |
| 7 | self.right = right |
| 8 | |
| 9 | |
| 10 | def tree_keys(root: Optional[TreeNode], make_key: Callable[[TreeNode, str, str], str], missing: str = "#") -> List[Tuple[TreeNode, str]]: |
| 11 | """Tree as a String: the key of every subtree, in postorder, so the last pair is the |
| 12 | root. |
| 13 | make_key(node, left_key, right_key) builds the key of node from its value and the |
| 14 | keys of its two children. missing is the key of a missing child. |
| 15 | Precondition: make_key keeps the position of every part, so two subtrees have the |
| 16 | same key exactly when they have the same shape and the same values. |
| 17 | N = number of nodes. K = the length of the longest key, which is at most N times the |
| 18 | length of one value. |
| 19 | Time: O(N * K) | Space: O(N * K) |
| 20 | """ |
| 21 | keys: List[Tuple[TreeNode, str]] = [] |
| 22 | |
| 23 | def solve(node: Optional[TreeNode]) -> str: |
| 24 | # 1. Base case: a missing child has the key missing |
| 25 | if not node: |
| 26 | return missing |
| 27 | |
| 28 | # 2. Divide: get the key of each child subtree |
| 29 | left_key = solve(node.left) |
| 30 | right_key = solve(node.right) |
| 31 | |
| 32 | # 3. Combine: this node's key comes from its value and its two child keys, then |
| 33 | # it is recorded |
| 34 | key = make_key(node, left_key, right_key) |
| 35 | keys.append((node, key)) |
| 36 | return key |
| 37 | |
| 38 | solve(root) |
| 39 | return keys |
key_of(node) returns a string that equals the string of another subtree exactly when the two subtrees have the same shape and the same values, because it holds the value, the key of the left child and the key of the right child in fixed positions, with # for a missing child.Write each subtree as one string, and two subtrees are equal exactly when their strings are equal. key_of(node) gets the key of node.left and the key of node.right first, then joins them with node.val into key. Every key has the same parts in the same order, the value, the left key and the right key, with commas between them and # for a missing child. A position in the string then always means one position in the tree, so two different shapes cannot give the same string. The dictionary seen counts each key, and a shape is reported when its count reaches 2, so one pass finds every repeated shape. The trap is leaving a missing child out of the key. If the key skips left_key or right_key when that child is missing, a node 1 with only a left child 2 and a node 1 with only a right child 2 both give 1,2, and Find Duplicate Subtrees (LC 652) on [0,1,1,2,null,null,2] reports the root value 1 as repeated.
See it run
Tree as a String, step by step
key now -count of this key -answer not yet
Each circle is a tree node. A node is tagged when its key is built. The keys and their counts are listed under the picture.
nowthe call that has just startedwaitinga call that waits for the keys of its childrenkeyeda node whose key has been seen oncerepeata node whose key has been seen two or more times
Call key_of on the root 1. Each call asks its two children for their keys first, then builds its own key.
Gotcha 1 of 3
- The trap
Gotcha 1: Leaving a missing child out of the key
Mistakeparts = [str(node.val)] if node.left: parts.append(left_key) if node.right: parts.append(right_key) key = ",".join(parts)Fixkey = f"{node.val},{left_key},{right_key}"What happens: A key that skips a missing child loses the position of the child that is present. A node
1with only a left child2gets the key1,2. A node1with only a right child2also gets the key1,2. The two shapes are different, but they get the same key, soseencounts them as one repeated shape. On[0,1,1,2,null,null,2], the left1and the right1get the same key, so the root value1is reported although the two subtrees are different.Example: Find Duplicate Subtrees (LC 652), root = [0,1,1,2,null,null,2]Mistake: returns [1, 2]Fix: returns [2]
- Each subtree needs only a yes or a number, as in Count Univalue Subtrees (LC 250): use Bottom-Up DFS and return that value from each call. Build a string only when the subtrees of one tree must be compared with each other. See Bottom-Up DFS
- Two trees are given and the question is whether they match, as in Same Tree (LC 100): use Two Tree Walk, which compares the two trees node by node and builds no string. Build a string key when the subtrees of one tree must be compared with each other. See Two Tree Walk
Deep Dive
Why equal strings mean equal subtrees
key_of(node) is defined by one sentence: it returns a string that describes the subtree at node. The code only has to keep that sentence true for one call, and the sentence then holds for every call.
A missing child returns #. For a real node, left_key and right_key are the true descriptions of its two subtrees, by the same sentence. The key of the node is its value, then left_key, then right_key, with commas between them. So the key lists the nodes in preorder, and it lists a # for every missing child.
Two different subtrees cannot get the same key. Read a key from the left. The first part is the value of the root. The next parts describe the left subtree, and they end exactly where that subtree ends, because every real node is followed by two keys and every # ends a branch. What remains describes the right subtree. So a key can be read in only one way, and it describes one tree.
Take root = [1, 2, 3, 4, null, 2, 4, null, null, 4]. The three leaves 4 each get the key 4,#,#. The two nodes 2 each have one left child 4, so both get the key 2,4,#,#,#. The count of 4,#,# reaches 2 at the second leaf, and the count of 2,4,#,#,# reaches 2 at the second node 2. So repeated becomes [4, 2], and sorted(repeated) is [2, 4].
The walk makes 13 calls: 6 calls on real nodes and 7 calls on missing children.
The trap: a key that leaves out the missing child
Suppose the key skips a child that is missing. A node 1 with only a left child 2 gets the key 1,2. A node 1 with only a right child 2 also gets the key 1,2. These are different shapes with the same key.
On root = [0, 1, 1, 2, null, null, 2], the left 1 and the right 1 get the same key, so 1 is reported as repeated. The correct answer is [2], because only the leaf 2 appears twice. The code that skips the missing child returns [1, 2].
The # for a missing child fixes this. It keeps the position of every part: the left 1 gets 1,2,#,#,# and the right 1 gets 1,#,2,#,#.
The separators matter too. Without commas, a node 1 with a left child 11 gets 111###, and a node 11 with a left child 1 gets the same 111###. On root = [0, 1, 11, 11, null, 1] the code without commas reports 11, and the correct answer is [].
A test for your own key: can you write down two different trees that give the same string? If you can, add a marker or a separator until you cannot.
Other problems on this card: the string is the answer, or the string is the lookup
In Find Duplicate Subtrees (LC 652) the strings are keys. They are counted and never printed. In the other problems on this card, the same string is the answer, or it is used for a lookup.
In Serialize and Deserialize Binary Tree (LC 297), the key of the root is the answer. For [1, 2, 3, null, null, 4, 5] it is 1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A # is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree. The tree built from this string gives the same string again.
In Construct String from Binary Tree (LC 606), the marker is a pair of parentheses, and the key is value(left)(right). A missing right child can be dropped. A missing left child must stay as () when a right child exists. For [1, 2, 3, null, 4] the answer is 1(2()(4))(3). The () that stays is the same marker as #: without it, a left child and a right child would look the same.
In Delete Duplicate Folders in System (LC 1948), the key of a folder is built from its subfolders only. It lists each subfolder's name followed by that subfolder's key, with the subfolders sorted by name. The name of the folder itself is not in its key, because two folders with different names can be identical. A non-empty key that appears twice or more marks all of those folders for deletion.
In Subtree of Another Tree (LC 572), build the key of subRoot, and check whether it is the key of any subtree of root.
The cost, and when a string is the wrong tool
A key is as long as its subtree. Joining it and reading it for the dictionary costs time in proportion to its length. The lengths of all keys add up to the sum of the subtree sizes.
On a complete tree of 1023 nodes with every value 0, the keys add up to 37,891 characters. On a chain of 1000 nodes with every value 0, they add up to 2,003,000 characters. In the worst case the sum is N * (N + 1) / 2 subtree nodes, so the time and the space are O(N^2).
When N is large, give each different key a small number instead of a string. The key of a node is then the triple (node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key has a fixed size, so the time and the space are O(N). Two subtrees get the same number exactly when they have the same shape and the same values.
A chain of 5000 nodes is 5000 calls deep, which is past Python's default limit of about 1000 nested calls. Raise the limit with sys.setrecursionlimit, or walk the tree with an explicit stack.
60-Second Pitch
"The question compares subtrees of one tree with each other, so every subtree needs a name that is the same for equal subtrees. I write key_of(node). It asks the left child and the right child for their keys first, then joins the node's value with those two keys into one string, with commas between the parts. A missing child returns #, so every key has a place for both children. It is correct because the string can be read back in only one way, so two subtrees have the same key exactly when they are equal. I count each key in a dictionary, and when a count reaches 2 I record the root value. The time and space are O(N^2) on a chain, and I can replace each string with a number to get O(N). The trap I avoid is leaving out the # for a missing child."
Common Questions
- How is Tree as a String different from Bottom-Up DFS?
Bottom-Up DFS returns a number or a yes from each call, and the answer comes from the root. Tree as a String returns a string that describes the whole subtree, so a call can be compared with every other call. The strings are kept in a dictionary, and the answer comes from the counts.
- Why does a missing child need a marker?
The marker keeps the position of every part of the key. Without it, a node with only a left child
2and a node with only a right child2both give1,2, and two different shapes get the same key.- How do you turn a tree into a string and back?
The key of the root is the string, for example
1,2,#,#,3,4,#,#,5,#,#. To read it back, take one token at a time. A#is a missing node. Any other token is a node, and the next calls read its left subtree and then its right subtree.- Why report a shape when the count is exactly 2?
LeetCode needs one root for each repeated shape. A shape that appears three times is reached three times, and only the second sighting has the count 2. With
>= 2, the third sighting adds the same shape again.- How do you make the walk faster than O(N^2)?
Give each different key a number. The key of a node becomes the triple
(node.val, left_id, right_id), and a dictionary turns each new triple into the next number. Every key then has a fixed size, so the walk costsO(N)time and space.
SWE Reasoning
Spot it
- The statement asks which subtrees repeat, or whether two parts of a tree are identical.
- The statement asks for a text form of a tree, or gives one and asks you to rebuild the tree.
- Two subtrees are equal when they have the same shape and the same values, and you must compare many pairs of them.
- You can say in one sentence what
key_of(node)returns for the subtree atnode. - Each subtree needs only a yes or a number, with no comparison between subtrees: use Bottom-Up DFS.
The invariant, step by step
- Define in one sentence what
key_of(node)returns: a string that describes the subtree atnode. - The base case gives a missing child the key
#, so a missing child still takes a place in the string. - For a real node, the two child calls run first. By the sentence in step 1, each returns a string that describes its subtree.
- The key of the node joins its value, the left key and the right key in this fixed order, with separators.
- So two subtrees have the same key exactly when they have the same shape and the same values. Counting keys counts shapes.
Traps
- Leaving out the missing child. A key that skips a missing child gives
1,2to a node with only a left child2and to a node with only a right child2. On[0,1,1,2,null,null,2]the answer is[1, 2]instead of[2]. - No separators.
111###stands for both1over11and11over1. On[0,1,11,11,null,1]the answer is[11]instead of[]. - Reporting a shape at every sighting. With
>= 2, a shape that appears four times is reported three times. Record it when the count is exactly 2. - Long keys on a deep tree. The keys add up to O(N^2) characters on a chain. Use numbers for keys when N is large.