-
Notifications
You must be signed in to change notification settings - Fork 0
Lowest Common Ancestor Of A Binary Tree #92
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Yuto729
wants to merge
1
commit into
main
Choose a base branch
from
lowest-common-ancestor-of-a-binary-tree
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| # lowest-common-ancestor-of-a-binary-tree | ||
|
|
||
| Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. | ||
|
|
||
| According to the definition of LCA on Wikipedia: “The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself).” | ||
|
|
||
| Input: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 | ||
| Output: 3 | ||
| Explanation: The LCA of nodes 5 and 1 is 3. | ||
|
|
||
| ## Step1 | ||
|
|
||
| 条件の確認 | ||
|
|
||
| - 数字が重複しないか | ||
| - p, qは必ず二分木内に存在するノードか | ||
| - p != qで良いか | ||
|
|
||
| BSTの場合は簡単だったが、普通の二分木なので少しめんどくさい。再帰で解く | ||
|
|
||
| left, rightの部分木を対象にp, qを探してもらう。部下に返してもらうのは、(pがあるか, qがあるか, lca) | ||
|
|
||
| すでにlcaがある場合はそれを返す。ない場合でnodeがp, q自身の場合はqがどこにあるかをチェックし適切にreturnする | ||
| nodeがp, qではない場合でさらにp, qの場所が左部分木と右部分木でちょうど別れている場合、node自身がlcaとなる。上記の全てに当てはまらない場合は、p, qの存在を部分木について集約して上司に返す。 | ||
|
|
||
| 30分くらいかかってしまった | ||
|
|
||
| Time: O(N) N: ノード数 | ||
| Space: O(H) H: 深さ | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': | ||
| def helper(node): | ||
| """ | ||
| return p_exists, q_exists, lca | ||
| """ | ||
| if node is None: | ||
| return False, False, None | ||
|
|
||
| p_exists_left, q_exists_left, lca_left = helper(node.left) | ||
| p_exists_right, q_exists_right, lca_right = helper(node.right) | ||
| if lca_left is not None: | ||
| return True, True, lca_left | ||
|
|
||
| if lca_right is not None: | ||
| return True, True, lca_right | ||
|
|
||
| if node is p: | ||
| if q_exists_left or q_exists_right: | ||
| return True, True, p | ||
|
|
||
| return True, False, None | ||
|
|
||
| if node is q: | ||
| if p_exists_left or p_exists_right: | ||
| return True, True, q | ||
|
|
||
| return False, True, None | ||
|
|
||
| if (p_exists_left and q_exists_right) or (q_exists_left and p_exists_right): | ||
| return True, True, node | ||
|
|
||
| return (p_exists_left or p_exists_right), (q_exists_left or q_exists_right), None | ||
|
|
||
| _, _, lca = helper(root) | ||
| return lca | ||
| ``` | ||
|
|
||
| ### AI Review | ||
|
|
||
| 1. 状態管理が複雑すぎる | ||
| (p_exists, q_exists, lca) という3値タプルを引き回す必要は実はありません。以下のように「見つかったノード(またはNone)」だけを返す設計にすると、同じロジックが半分以下の行数で書けます。 | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def lowestCommonAncestor(self, root, p, q): | ||
| if root is None or root is p or root is q: | ||
| return root | ||
|
|
||
| left = self.lowestCommonAncestor(root.left, p, q) | ||
| right = self.lowestCommonAncestor(root.right, p, q) | ||
| if left and right: | ||
| return root | ||
|
|
||
| return left or right | ||
| ``` | ||
|
|
||
| ポイントは「rootがpまたはqと一致したら、それ以上左右を探さずその場で返してよい」という点です。これは「pの子孫にqがいるかもしれないが、いたとしてもLCAはp自身になる」という性質("a node is a descendant of itself"という問題文の定義)を使っています。あなたのコードはnode is pのケースでq_exists_left or q_exists_rightをわざわざ確認していますが、実は確認しなくてもpを返してしまってよく、その後上位の再帰で自然に正しいLCAに収束します。ここが設計を複雑にしていた主因だと思います。 | ||
|
|
||
| 2. ヘルパー関数名・docstring | ||
| helperは中身が予測できないので find や dfs の方が意図が伝わります。docstringも「戻り値の意味」だけでなく、pythonなら型ヒント (-> tuple[bool, bool, Optional[TreeNode]]) で表現する手もあります。 | ||
|
|
||
| ### Follow Up | ||
|
|
||
| 1. p, qが木の中に必ず存在するとは限らない場合、シンプル版のコード(および今回のあなたのコード)はどう振る舞いますか?正しくNoneや不正な結果を返せますか?対処するには設計をどう変えますか? | ||
| 2. 各ノードに 親へのポインタ (parent) が生えている場合、この問題はどう解き方が変わりますか?(LeetCode 1650 相当) | ||
| 3. p, qの2つではなく、N個のノードのLCAを求めたい場合、どうアプローチしますか? | ||
| 4. 再帰ではなく反復(iterative)で書くとしたら、どういうデータ構造が必要になりますか? | ||
| 5. 非常に偏った木(実質リスト状)の場合、Space O(H) は最悪どうなりますか?大きな入力でこれが問題になるケースは? | ||
|
|
||
| 1について 自分のコードはうまくいく。qが存在しないとすると、`q_exists`と`node is q`の分岐が実行されずNoneしか返らない。Noneを返した時にエラーでも良い。 | ||
| しかし、シンプル版のコードは、pかqかのどちらかにマッチした時点でそのノードを返す設計になっているのでうまく行かない。確認していない方が本当に存在するのか確認する必要がある。2回走査するか自分の解法のように情報を明示的に運ぶ設計にする | ||
|
|
||
| 4については以下のように考えることができる | ||
|
|
||
| 1. p, qそれぞれに至るまでの親ポインタを作成する | ||
| 2. 親ポインターのリストをpからrootまで辿った記録をsetとして作成する | ||
| 3. qから親を辿って初めて上記のsetの中にある要素がLCAとなる | ||
|
|
||
| Time: O(N) 最悪 | ||
| Space: O(N) 最悪 | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': | ||
| stack = [root] | ||
| parent = {root: None} | ||
| node = root | ||
| while p not in parent or q not in parent: | ||
| node = stack.pop() | ||
| if node.left is not None: | ||
| parent[node.left] = node | ||
| stack.append(node.left) | ||
| if node.right is not None: | ||
| parent[node.right] = node | ||
| stack.append(node.right) | ||
|
|
||
| ancestors = set() | ||
| node = p | ||
| while node is not None: | ||
| ancestors.add(node) | ||
| node = parent[node] | ||
|
|
||
| node = q | ||
| while node not in ancestors: | ||
| node = parent[node] | ||
|
|
||
| return node | ||
| ``` | ||
|
|
||
| ## Step3 | ||
|
|
||
| ```py | ||
| class Solution: | ||
| def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode': | ||
| if root is None or root is p or root is q: | ||
| return root | ||
|
|
||
| left = self.lowestCommonAncestor(root.left, p, q) | ||
| right = self.lowestCommonAncestor(root.right, p, q) | ||
| if left and right: | ||
| return root | ||
|
|
||
| return left if left else right | ||
| ``` | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
こちらのコメントをご参照ください。
ksaito0629/leetcode_arai60#1 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
おっしゃる通りだと思います
この場合は
is not Noneを用いて判定するのが良いですね