|
1 | 1 | # [Problem 1530: Number of Good Leaf Nodes Pairs](https://leetcode.com/problems/number-of-good-leaf-nodes-pairs/description/?envType=daily-question)
|
2 | 2 |
|
3 | 3 | ## Initial thoughts (stream-of-consciousness)
|
| 4 | +- One thing I'm sure of is that there has to be a shortcut here, aside from brute-force computing every shortest path |
| 5 | +- I'm not seeing any obvious solution off the top of my head |
| 6 | +- I'm wondering if it'd be useful to traverse the tree in some way and keep track of the depth of each node. Then we could...what? |
| 7 | + - If the depth is .... ah, I misread the problem. We only have to worry about *leaf* nodes. So actually, this is much easier than I had initially expected (I thought we had to look for paths between *any* pair of nodes) |
| 8 | +- Ok, so with this new-found knowledge, let's see what we can do: |
| 9 | + - First, we need to find leaf nodes. We could do this by traversing the tree and finding nodes with no children (those are the leaves) |
| 10 | + - The shortest path between the nodes involves finding the closest common ancestor. If the ancestor is $d_1$ nodes above node 1 and $d_2$ nodes above node 2, then the shortest path is $d_1 + d_2$ steps long. Some observations: |
| 11 | + - If either $d_1$ or $d_2$ is greater than `distance`, we can ignore that pair |
| 12 | +- It's not super efficient (but maybe it'll be OK, because the max number of notes is 1024?), but what if we do something like this: |
| 13 | + - Find the leaves (DFS or BFS) and store them in a hash table. We can add pointers to the parents as we go so that we can easily find the common ancester. Let's also give each node a unique ID so that we can distinguish it from other nodes with the same value. |
| 14 | + - For each pair of leaves (need to do this in a nested loop): |
| 15 | + - Find the common ancestor (note: need to write this!) |
| 16 | + - Keep track of how many steps it takes to get to the common ancester from each leaf |
| 17 | + - If the sum of those distances is less than or equal to `distance`, increment a counter by 1 for that pair |
| 18 | +- Finding the common ancestor could go like this: |
| 19 | + - For the first leaf node, climb the tree back to the root, keeping track of the unique IDs of each node in a hash table (keys: unique ID; values: steps from first leaf node) |
| 20 | + - Now for the second leaf node, keep climbing the tree (keep track of the number of steps) until we find a node with a unique ID in the hash table |
| 21 | + - Then we just return the sum of the distances to the common ancestor (i.e., the length of the shortest path between the nodes) |
4 | 22 |
|
5 | 23 | ## Refining the problem, round 2 thoughts
|
| 24 | +- Lets define a new class for the nodes (that seems to have been working conveniently for these problems). In addition to value and left/right children, let's add attributes for the node's parent and a unique ID |
| 25 | +- Any tricky edge cases to think about? |
| 26 | + - If there's only 1 node, there are no pairs so we just return 0. But this is fine, since the body of the nested loop that goes through pairs of nodes just won't ever run. |
| 27 | + - If the depths of all leaves are less than half of `distance`, we can just return the total number of pairs (i.e., for `n` leaves this is `(n^2 - n) / 2`). But I'm not sure it's worth coding in this special case. |
| 28 | +- Ok, let's code it! |
6 | 29 |
|
7 | 30 | ## Attempted solution(s)
|
8 | 31 | ```python
|
9 |
| -class Solution: # paste your code here! |
10 |
| - ... |
| 32 | +class TreeNodeParID(TreeNode): |
| 33 | + current_ID = 0 |
| 34 | + |
| 35 | + def __init__(self, val=0, left=None, right=None, parent=None): |
| 36 | + self.val = val |
| 37 | + self.left = left |
| 38 | + self.right = right |
| 39 | + self.parent = parent |
| 40 | + self.ID = TreeNodeParID.current_ID |
| 41 | + |
| 42 | + TreeNodeParID.current_ID += 1 |
| 43 | + |
| 44 | +class Solution: |
| 45 | + def countPairs(self, root: TreeNode, distance: int) -> int: |
| 46 | + |
| 47 | + # find the leaves and add parent attributes. i'll use DFS since i've been using BFS for the past few problems. gotta keep things interesting! |
| 48 | + stack = [TreeNodeParID(val=root.val, left=root.left, right=root.right)] |
| 49 | + leaves = [] |
| 50 | + |
| 51 | + while len(stack) > 0: |
| 52 | + node = stack.pop() |
| 53 | + |
| 54 | + if node.left is not None: |
| 55 | + node.left = TreeNodeParID(val=node.left.val, left=node.left.left, right=node.left.right, parent=node) |
| 56 | + stack.append(node.left) |
| 57 | + |
| 58 | + if node.right is not None: |
| 59 | + node.right = TreeNodeParID(val=node.right.val, left=node.right.left, right=node.right.right, parent=node) |
| 60 | + stack.append(node.right) |
| 61 | + |
| 62 | + # is this a leaf? |
| 63 | + if not node.left and not node.right: |
| 64 | + leaves.append(node) |
| 65 | + |
| 66 | + # now loop through every pair of leaves and find the common ancestors |
| 67 | + good_node_count = 0 |
| 68 | + for i, a in enumerate(leaves): |
| 69 | + # go from node a to the root, tracking depth |
| 70 | + path_to_root = {a.ID: 0} |
| 71 | + |
| 72 | + x = a |
| 73 | + d1 = 1 |
| 74 | + while x.parent is not None: |
| 75 | + x = x.parent |
| 76 | + path_to_root[x.ID] = d1 |
| 77 | + d1 += 1 |
| 78 | + |
| 79 | + for b in leaves[(i + 1):]: |
| 80 | + # go from node b to anything in path_to_root |
| 81 | + if b.ID in path_to_root: |
| 82 | + if path_to_root[b.ID] <= distance: |
| 83 | + good_node_count += 1 |
| 84 | + continue |
| 85 | + |
| 86 | + x = b |
| 87 | + d2 = 1 |
| 88 | + while (x.parent is not None) and (d2 <= distance): #stop early if path from b to common ancestor is greater than distance |
| 89 | + x = x.parent |
| 90 | + if x.ID in path_to_root: |
| 91 | + if path_to_root[x.ID] + d2 <= distance: |
| 92 | + good_node_count += 1 |
| 93 | + break |
| 94 | + d2 += 1 |
| 95 | + |
| 96 | + return good_node_count |
11 | 97 | ```
|
| 98 | +- Got stuck for a bit there-- I had an extra indent in the `return` statement, which meant the function was returning after the first iteration of the outer loop 😵! |
| 99 | +- Ok, now that I've unindented that line, all given test cases pass |
| 100 | +- Other tests: |
| 101 | + - `root = [1,2,3,4,5,6,7,8, 9, null, 10, null, 11, 12, null], distance = 3`: pass |
| 102 | + - `root = [1,2,3,null,5,null,7,8, 9, null, 10, null, 11, 12, null], distance = 5`: pass |
| 103 | +- Seems to work; submitting... |
| 104 | + |
| 105 | + |
| 106 | +- Ouch, that's slow! |
| 107 | +- But...solved 🥳! |
| 108 | + |
| 109 | + |
0 commit comments