Skip to content

235. Lowest Common Ancestor of a Binary Search Tree - #66

Open
naoto-iwase wants to merge 4 commits into
mainfrom
0235-lowest-common-ancestor-of-a-binary-search-tree
Open

235. Lowest Common Ancestor of a Binary Search Tree#66
naoto-iwase wants to merge 4 commits into
mainfrom
0235-lowest-common-ancestor-of-a-binary-search-tree

Conversation

@naoto-iwase

Copy link
Copy Markdown
Owner

235. Lowest Common Ancestor of a Binary Search Tree


Next: 232. Implement Queue using Stacks

node = root
lower, upper = sorted([p.val, q.val])
while node is not None:
if node.val > upper:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if lower <= node.val <= upper:
    return node

を一番初めに持ってきたほうが明確ではないですかね。

Comment on lines +37 to +38
if any(node is None for node in [root, p, q]):
raise ValueError("input nodes must not be None")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

どこまで引数の型を信頼するかによると思いますが、TreeNode と型が明示されている root, p, q に対して None かどうかの判定まではしなくてよいのではと思いました。

raise ValueError("input nodes must not be None")

node = root
lower, upper = sorted([p.val, q.val])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この書き方いいですね、今まで以下のような書き方しかしてなかったのでこんな書き方もあるんだと勉強になりました。

lower = min(p.val, q.val)
upper = max(p.val, q.val)

### 実装2

- 問題設定の検討
- 単にBinary Treeの場合を考える。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

単に 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 right

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。

大変興味深いです。これは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
    """
    ...

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

また p または q が存在しないケースでは None が最終的に返ってきます)

たしかにこの部分の認識間違ってましたね、訂正ありがとうございます。このアルゴリズムで問題なく機能するのは、「必ず p および q が二分木の中に存在する」という前提があるケースですね。

上記前提がない場合にこのアルゴリズムを使おうと思うと、p または q が返ってきた際にはそれが正しいものか判定するためにもう一方(ex. p が返ってきた場合には q)が部分木中に存在するかの確認までする必要がありそうですね。

時間計算量を考えてみると、LCA 候補の発見に O(N)、LCA 候補が p または q の際にはもう一方の存在確認をするのに追加で O(N) かかるので、全体としても O(N) になりそうですね。

@naoto-iwase naoto-iwase Nov 29, 2025

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。

上記前提がない場合にこのアルゴリズムを使おうと思うと、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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

個人的には、ここで return found_paths[0][i - 1] し、下の return 文の部分は unreachable であることを示すような書き方にすると思います。
unreachable であることを示すには、 raise Exception("unreachable") などと書く方法があります。詳しくは過去のレビューコメントを参考にすることをおすすめします。

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ありがとうございます。

実際には、p, qのうち一方のパス上に他方が存在する(つまり一方が他方の祖先である)場合、ここのif条件は一度も成立せず、初期化時のlca_depth = min_length - 1を使って返り値が決まるので、unreachableではないと思うのですが、どうでしょうか。自分もここのまとまりの悪さには苦慮していました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

失礼いたしました。おっしゃる通りだと思います。

どうすればまとまるか、自分も考えてみたのですが、あまりまとまりませんでした。

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

@naoto-iwase

Copy link
Copy Markdown
Owner Author

Step 4(単に Binary Tree な場合の別解)を追加。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants