235. Lowest Common Ancestor of a Binary Search Tree - #66
Conversation
| node = root | ||
| lower, upper = sorted([p.val, q.val]) | ||
| while node is not None: | ||
| if node.val > upper: |
There was a problem hiding this comment.
if lower <= node.val <= upper:
return nodeを一番初めに持ってきたほうが明確ではないですかね。
| if any(node is None for node in [root, p, q]): | ||
| raise ValueError("input nodes must not be None") |
There was a problem hiding this comment.
どこまで引数の型を信頼するかによると思いますが、TreeNode と型が明示されている root, p, q に対して None かどうかの判定まではしなくてよいのではと思いました。
| raise ValueError("input nodes must not be None") | ||
|
|
||
| node = root | ||
| lower, upper = sorted([p.val, q.val]) |
There was a problem hiding this comment.
この書き方いいですね、今まで以下のような書き方しかしてなかったのでこんな書き方もあるんだと勉強になりました。
lower = min(p.val, q.val)
upper = max(p.val, q.val)| ### 実装2 | ||
|
|
||
| - 問題設定の検討 | ||
| - 単にBinary Treeの場合を考える。 |
There was a problem hiding this comment.
単に Binary Tree の場合だと以下のような実装も可能そうです。(動作確認はしていないです)
p または q が見つかった場合にはそれを返し、見つからなかった場合には None を返すようなコードで再帰的に処理するイメージです。(None を返すので返り値の型を Optional[TreeNode] に変えているので、LeetCode 側で与えられたものとは微妙にシグネチャが変わっています。また p または q が存在しないケースでは None が最終的に返ってきます)
class Solution:
def lowestCommonAncestor(
self,
root: TreeNode,
p: TreeNode,
q: TreeNode
) -> Optional[TreeNode]:
if root == p or root == q:
return root
left = None
if root.left:
left = self.lowestCommonAncestor(root.left, p, q)
right = None
if root.right:
right = self.lowestCommonAncestor(root.right, p, q)
# 左右両方で見つかれば root が LCA
if left and right:
return root
# 片方だけ見つかった場合、見つかった方を返す
return left if left else rightThere was a problem hiding this comment.
ありがとうございます。
大変興味深いです。これはpathを追う必要がなく、引き継ぎがLCAノードだけなので空間計算量的に優しそうですね。
自分の実装2は実装1同様にtop-down(preorder)で考えていましたが、bottom-up(postorder)の方がシンプルになったわけですね。rootがpかqのとき、部分木について考えないというのも一部のケースで有用だなと思いました。
また p または q が存在しないケースでは None が最終的に返ってきます)
これについては、pもqも存在しない場合はNoneが返りますが、一方だけが存在しない場合は、存在する方が返りますね。下に場合分けを表でまとめました。
この再帰関数のアルゴリズムの意味は「p, qのうち、木内にあるものについてのLCAを返す」ということですね。
| 状況 | 返り値 |
|---|---|
| p, q 両方存在 | 正しい LCA |
| p のみ存在 | p が返る |
| q のみ存在 | q が返る |
| 両方存在しない | None が返る |
ユースケース次第では受け入れられそうなある意味自然な挙動ですが、メソッド名を調整したり、docstringにこの場合分けを書いておいたりした方が良いかもですね。
def get_lca_or_existing_node(
self,
root: TreeNode,
p: TreeNode,
q: TreeNode
) -> Optional[TreeNode]:
"""
Returns:
- LCA if both p and q exist
- p if only p exists
- q if only q exists
- None if neither exists
"""
...There was a problem hiding this comment.
また p または q が存在しないケースでは None が最終的に返ってきます)
たしかにこの部分の認識間違ってましたね、訂正ありがとうございます。このアルゴリズムで問題なく機能するのは、「必ず p および q が二分木の中に存在する」という前提があるケースですね。
上記前提がない場合にこのアルゴリズムを使おうと思うと、p または q が返ってきた際にはそれが正しいものか判定するためにもう一方(ex. p が返ってきた場合には q)が部分木中に存在するかの確認までする必要がありそうですね。
時間計算量を考えてみると、LCA 候補の発見に O(N)、LCA 候補が p または q の際にはもう一方の存在確認をするのに追加で O(N) かかるので、全体としても O(N) になりそうですね。
There was a problem hiding this comment.
ありがとうございます。
上記前提がない場合にこのアルゴリズムを使おうと思うと、p または q が返ってきた際にはそれが正しいものか判定するためにもう一方(ex. p が返ってきた場合には q)が部分木中に存在するかの確認までする必要がありそうですね。
同意です。実装的には以下の2行:
if root == p or root == q:
return rootを、再帰関数を2回呼び出す部分の直下に移動させ、p, qが見つかったかどうかをフラグで管理する感じですね。
それでも時間計算量的には小さな定数倍増加する程度で、空間計算量の改善のメリットの方が大きいと感じます。
| lca_depth = min_length - 1 | ||
| for i in range(1, min_length): | ||
| if found_paths[0][i] is not found_paths[1][i]: | ||
| lca_depth = i - 1 |
There was a problem hiding this comment.
個人的には、ここで return found_paths[0][i - 1] し、下の return 文の部分は unreachable であることを示すような書き方にすると思います。
unreachable であることを示すには、 raise Exception("unreachable") などと書く方法があります。詳しくは過去のレビューコメントを参考にすることをおすすめします。
There was a problem hiding this comment.
ありがとうございます。
実際には、p, qのうち一方のパス上に他方が存在する(つまり一方が他方の祖先である)場合、ここのif条件は一度も成立せず、初期化時のlca_depth = min_length - 1を使って返り値が決まるので、unreachableではないと思うのですが、どうでしょうか。自分もここのまとまりの悪さには苦慮していました。
There was a problem hiding this comment.
失礼いたしました。おっしゃる通りだと思います。
どうすればまとまるか、自分も考えてみたのですが、あまりまとまりませんでした。
lca_depth = 0
while lca_depth < len(found_paths[0][i]) and lca_depth < len(found_paths[1][i]):
if found_paths[0][i] is not found_paths[1][i]:
lca_depth -= 1
break
lca_depth += 1
return lca_depth|
Step 4(単に Binary Tree な場合の別解)を追加。 |
235. Lowest Common Ancestor of a Binary Search Tree
Next: 232. Implement Queue using Stacks