|
| 1 | +""" |
| 2 | +236. Lowest Common Ancestor of a Binary Tree |
| 3 | +
|
| 4 | +https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-tree |
| 5 | +
|
| 6 | +NOTES |
| 7 | +
|
| 8 | +Wikipedia has an excellent article on lowest common ancestor: |
| 9 | +
|
| 10 | + The lowest common ancestor (LCA) (also called least common ancestor) of two |
| 11 | + nodes v and w in a tree or directed acyclic graph (DAG) T is the lowest |
| 12 | + (i.e., deepest) node that has both v and w as descendants, where we define |
| 13 | + each node to be a descendant of itself (so if v has a direct connection from |
| 14 | + w, w is the lowest common ancestor). |
| 15 | +
|
| 16 | + The LCA of v and w in T is the shared ancestor of v and w that is located |
| 17 | + farthest from the root. Computation of lowest common ancestors may be useful, |
| 18 | + for instance, as part of a procedure for determining the distance between |
| 19 | + pairs of nodes in a tree: the distance from v to w can be computed as the |
| 20 | + distance from the root to v, plus the distance from the root to w, minus |
| 21 | + twice the distance from the root to their lowest common ancestor. |
| 22 | +
|
| 23 | + In a tree data structure where each node points to its parent, the lowest |
| 24 | + common ancestor can be easily determined by finding the first intersection of |
| 25 | + the paths from v and w to the root. In general, the computational time |
| 26 | + required for this algorithm is O(h) where h is the height of the tree (length |
| 27 | + of longest path from a leaf to the root). However, there exist several |
| 28 | + algorithms for processing trees so that lowest common ancestors may be found |
| 29 | + more quickly. |
| 30 | +
|
| 31 | +Unlike with 'Lowest Common Ancestor of a Binary Search Tree' we cannot leverage |
| 32 | +the fact that the tree is a binary search tree. This makes the problem slightly |
| 33 | +more challenging and increases the time complexity from O(log n) to O(n). This |
| 34 | +problem can be solved recursively and iteratively. |
| 35 | +
|
| 36 | +Recursive Approach |
| 37 | +------------------ |
| 38 | +In order to visualize the recursive solution, imagine moving down the tree in |
| 39 | +a depth-first manner. Each recursive call postpones the processing of the |
| 40 | +current node until calls higher in the stack (or lower in the tree) have |
| 41 | +returned. The first recursive call is the last to fully execute and returns the |
| 42 | +solution, which is "passed up" from previous calls. Now, let's recall that the |
| 43 | +lowest common ancestor of two nodes is the deepest node of the subtree that |
| 44 | +contains both nodes. Therefore, we need to develop a base condition and |
| 45 | +recursive logic such that the final recursive call returns the lowest common |
| 46 | +ancestor, which may have been determined somewhere within the recursion. |
| 47 | +
|
| 48 | +Base Case: |
| 49 | +
|
| 50 | + >If the node is a leaf (root == None) or root is p or q, return root. |
| 51 | +
|
| 52 | +Basically, this passes information up the recursive call stack (or up the |
| 53 | +tree). Either, no, the current path terminated without finding p or q, or, yes, |
| 54 | +p or q was found. Within this conditional is an implicit boolean, which is used |
| 55 | +later in the recursive logic. |
| 56 | +
|
| 57 | +Recursive Logic: |
| 58 | +
|
| 59 | + >Recurse into the left and right subtrees. If both subtrees find the target, |
| 60 | + return root. Otherwise, return the result of either the left or right |
| 61 | + subtree. |
| 62 | +
|
| 63 | +This is where the solution is "passed up". It is also where the both macro- and |
| 64 | +microstructure of the recursive solution is codified. If either of the subtrees |
| 65 | +returns a non-null, this value is returned up the call stack until both |
| 66 | +subtrees return a non-null. The final return call returns the solution. |
| 67 | +
|
| 68 | +It is probably helpful here to consider two cases: |
| 69 | + 1. The LCA is contained in the left or right subtree of root. |
| 70 | + 2. The LCA is the root itself. |
| 71 | +
|
| 72 | +As you can see, the recursive invariant holds for all subtrees. |
| 73 | +
|
| 74 | + ``` |
| 75 | + # The LCA is the root itself. |
| 76 | + if left and right: |
| 77 | + return root |
| 78 | +
|
| 79 | + # The LCA is contained in the left or right subtree of root. |
| 80 | + return left if left else right |
| 81 | + ``` |
| 82 | +
|
| 83 | +Iterative Approach |
| 84 | +------------------ |
| 85 | +The iterative approach is more intuitive, since it uses the fact that the node |
| 86 | +at which the path from the root to p diverges (or converges if viewed from |
| 87 | +bottom-up) from the path from the root to q is the lowest common ancestor. |
| 88 | +Since nodes do not inherently contain a pointer to their parent (trees are |
| 89 | +typically directed from parent to child), a hash map is used to maintain a |
| 90 | +mapping of child->parent for all nodes traversed while searching for p and q. |
| 91 | +We can then trace back from p to root using the hash map, producing a set of |
| 92 | +nodes (ancestors) for p. Tracing back from q to root, the first node contained |
| 93 | +in ancestors is the lowest common ancestor for p and q. |
| 94 | +""" |
| 95 | + |
| 96 | +from collections import deque |
| 97 | + |
| 98 | +from src.classes import TreeNode |
| 99 | + |
| 100 | + |
| 101 | +class Solution: |
| 102 | + """ |
| 103 | + Returns the LCA of `p` and `q` for the tree rooted at `root` recursively. |
| 104 | + """ |
| 105 | + |
| 106 | + def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: |
| 107 | + # Base Case: If root is a leaf (None) or if root is p or q, return |
| 108 | + # root. |
| 109 | + if not root or root in (p, q): |
| 110 | + return root |
| 111 | + |
| 112 | + # Recurse into the left and right subtrees of root. |
| 113 | + # |
| 114 | + # left and right may be one of the following: |
| 115 | + # 1. root.left and root.right if root is the LCA. |
| 116 | + # 2. The LCA of the left subtree and None, if the LCA was found in |
| 117 | + # the left subtree. |
| 118 | + # 3. None and the LCA of the right subtree, if the LCA was found in |
| 119 | + # the right subtree. |
| 120 | + # |
| 121 | + # This invariant holds for all subtrees. |
| 122 | + left = self.lowestCommonAncestor(root.left, p, q) |
| 123 | + right = self.lowestCommonAncestor(root.right, p, q) |
| 124 | + |
| 125 | + # root is the LCA |
| 126 | + if left and right: |
| 127 | + return root |
| 128 | + |
| 129 | + # left or right is the LCA (or p or q was found) |
| 130 | + return left if left else right |
| 131 | + |
| 132 | + |
| 133 | +class IterativeSolution: |
| 134 | + """ |
| 135 | + Returns the LCA of `p` and `q` for the tree rooted at `root` iteratively. |
| 136 | + """ |
| 137 | + |
| 138 | + def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode: |
| 139 | + stack: deque[TreeNode] = deque([root]) |
| 140 | + parents: dict[TreeNode, TreeNode | None] = {root: None} |
| 141 | + curr: TreeNode = root |
| 142 | + |
| 143 | + # Traverse the tree until both p and q are found. This populates the |
| 144 | + # `parents` hash map and requires O(n) time complexity in the |
| 145 | + # worst-case. |
| 146 | + while stack: |
| 147 | + curr = stack.pop() |
| 148 | + # NOTE: For pre-order traversal, the right child is pushed onto the |
| 149 | + # stack first. |
| 150 | + if curr.right: |
| 151 | + stack.append(curr.right) |
| 152 | + parents[curr.right] = curr |
| 153 | + if curr.left: |
| 154 | + stack.append(curr.left) |
| 155 | + parents[curr.left] = curr |
| 156 | + |
| 157 | + # Using the populated `parents` hash map, create a set of ancestors by |
| 158 | + # tracing the path from `p` to `root`. |
| 159 | + ancestors: set[TreeNode] = set() |
| 160 | + |
| 161 | + # NOTE: Remember to add p! This handles the case where the node itself, |
| 162 | + # (p or q) is the LCA: |
| 163 | + # |
| 164 | + # >...we define each node to be a descendant of itself (so if v has a |
| 165 | + # direct connection from w, w is the lowest common ancestor). |
| 166 | + while p: |
| 167 | + ancestors.add(p) |
| 168 | + p = parents[p] |
| 169 | + |
| 170 | + # Now, trace the path from `q` to `root`. The first ancestor in the |
| 171 | + # path that is shared with `p` (is part of the set of ancestors) is the |
| 172 | + # LCA of `p` and `q`. |
| 173 | + while q: |
| 174 | + if q in ancestors: |
| 175 | + return q |
| 176 | + q = parents[q] |
| 177 | + |
| 178 | + return root |
0 commit comments