-
Notifications
You must be signed in to change notification settings - Fork 0
98. Validate Binary Search Tree #7
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
|
|
||
| ## step1: | ||
| ひとまずdfsで一番下の根からvalidateしていって再帰的にnode.right, node.leftに対してnodeがvalidかを判別していく。まずnode.right, node.leftがvalidである必要があるがそこはそれぞれをvalidateする際に早期にFalseをマークしておいて、すべてvalidateが終わった時にそのマークがFalseであるかで判断する。nodeがvalidであるかを判別するためにはnode.rightの最小値より小さく、node.leftの最大値より大きい必要がある。そこでdfs関数で判別したnodeのsmallest, biggestなnumberを返すようにする。葉に到達した際には必ずその根をvalidにする必要があるのでsmallestに無限大を、biggestにマイナス無限大を返すようにする。しかし、そうするとその根のnodeのleftのbiggestがマイナス無限大、rightのsmallestが無限大になってsmallest, biggestの更新が正しく行われずにvalidationが常にTrueになってしまう。そこでmin(node.val, smallest_l), max(node.val, biggest_r)を常に判断して返すようにしたが、これを葉付きのノード以外に毎回行うのは効率が悪い気がした。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ひとまずdfsで一番下のノード(葉)からvalidateしていって、、、早期にTrueをマークしておいて、、、、 根と葉とそれ以外のノードを混同しているように感じました。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 下からvalidateしていって、どこかで引っ掛かったら早期にFalseをis_validにマークしてますね。できればFalseであることがわかった瞬間に早期returnしたいのですがdfs関数で進めてしまった為全部探索するまで終わらない不自然な感じになってしまいました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 読み間違えてました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 木を"正しい向き"、根が下になるように書くと一番下の根というのは正しいですが、leetcodeとかの図は逆ですね。あと、根は常に端になりますね。意図は通じますが一応。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (atmaxstarさんの補足があったので一応)ここでは一番下のノード(葉ノード)を根とする部分木のことを指していますね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
(読み間違えていたらすみません。)葉以外のノードに対してもこのチェックは必要かと思います。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここのdfsでnode.right、node.leftのサブツリーはvalidであることが保証されてると想定して、 ここで
これが済んでいると思います。 こんな感じになると思います。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
これを、この処理は葉ノード以外は冗長である(なくてもいい)という主張だと思っていました。
のほうが効率がいいのではという主張ですね。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
個人的にこれが一番言いたかったことですかね。冗長さをなくした結果速度も上がるんじゃないかとついでに思ったのですが、Claudeに聞いてみると There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 用語が正確でないように見えています。
というコメントがあるのですが、葉=葉ノード(leaf = leaf node)とは、空でない子を持たないノードのことです。 したがって、例えば「leftに葉を持つ」場合とは、node.leftが子を持たないノードになるので、
という条件でいうとnode.leftがTruthyになり、(空でない)部分木を持つ場合と一緒になってしまいます。 また、これは文脈で判断はできますが、一般に単に部分木という場合は、空集合も部分木に含まれる場合があります(たとえば部分集合ということばには空集合も含まれることが多いです)。 問題を解けるかどうかは重要ですが、正しい言葉で会話ができるか?ということは、同等以上の強いシグナルになることが多いです。難しい問題では、厳密に解ききれるかどうかということ以上に、技術的に会話ができるかということが重要になるケースもあります。 また、それはそれとして、例えばbytecodeを生成させるなどして、差をある程度は定量的に把握することはできると思います。(普通の状況では概ね無視できるとは思いますが) |
||
| ### code | ||
| ```python | ||
| class Solution: | ||
| # let me think of solving this using dfs | ||
| # when in node A, if A.val is greater than biggest val of A.left tree and smaller than smallest val of A.right tree, A is binary tree | ||
| # ↑ in this case, A.right and A.left must be valid binary trees as well, so I'm gonna validate both in advance using dfs and return False early | ||
|
Comment on lines
+8
to
+9
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/binary tree/binary search tree/ でしょうか?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. コメントに脳死でbinary treeと書いてしまいました... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 必要条件とかの話はBSTとbinary treeについてではなく、英文の方についてですね。一文目で十分条件を提示しているように読めますが、二文目に追加で必要条件もありますと書いているように見えます。そして、両方チェックしますとありますね。論理構造としてかなり不自然で、BSTの定義を理解していないか、必要条件・十分条件・必要十分条件を区別していないと解釈されうる文章であるように思います。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 英語のコメントは実際に対面で説明するならどんな感じ説明しようかのシミュレーションで書いていて、1行目に思考法、2行目に実装方法を説明するみたいな感じで書きました。ただそうなると、
この説明を1行目に移したほうが自然ですね。1行目で特定のノードを根とする2分木が2分探索木であるための条件を書き、2行目でdfsを使った再帰的な方法で解きますといった感じで説明する様に。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 説明方法の問題というよりも、「ノードAの値が、左部分木の最大値より大きく、右部分木の最小値よりも小さい時、ノードAを根とする木はBSTである」というのは、単純に誤りですかね。BSTの必要条件の一つに過ぎないので。
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
この説明は仰る通り誤りですね。ただその説明にwhen A.right and A.left are valid binary treesと左と右の部分木がvalidな2分探索木であるという前提条件を加えれば十分条件になると思います。ChatGPTに簡単に英訳させたんですけどこんな感じにすれば説明からの実装方法のつながりが分かりやすくなると個人的には思います。 A tree rooted at node A is a valid Binary Search Tree if: Implementation approach: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 十分条件を確認すれば、大丈夫そうでしょうか?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 必要十分条件になっているかということですかね。
この2条件はBSTの必要十分条件になっていると思います。十分条件(2条件 → BST)は再帰的に各部分木のBST性質が保証されるので成立し、必要条件(BST → 2条件)もBSTの定義から直接導けるので成立します。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. そうですね。ほぼ定義を言い換えただけなので、証明は不要だとは思います。 |
||
|
|
||
| # when judged that node A tree is valid, I validate the upper node B. then, if A is B.left, the beggist val of A tree is needed, and if A is B.right, the smallest val of A is needed. So both the smallest and the biggest val have to be stored. | ||
| def isValidBST(self, root: Optional[TreeNode]) -> bool: | ||
| is_valid = True | ||
|
|
||
| def dfs(node): | ||
| nonlocal is_valid | ||
| if not node: | ||
| return float('inf'), -float('inf') | ||
| smallest_l, biggest_l = dfs(node.left) | ||
| smallest_r, biggest_r = dfs(node.right) | ||
|
Comment on lines
+19
to
+20
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. smallest, biggestという命名が理解しづらかったです。 今注目しているノードをnode、その左の子と右の子をleft, rightとすると、 ただ、leftをルートとする左側の部分木の中で、leftの値が一番大きいわけではないですよね。そうすると、biggest_lという命名の変数をnode.valの有効範囲の小さい方に使うのは疑問を抱きました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. あと、末端から有効かどうかを検証していれば、left,node,rightの3つのノード間の関係のみで検証ができそうだなと思いました。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
min_of_left_subtree, max_of_left_subtreeみたいなのはいかがでしょうか There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 末端から有効かどうかを検証する場合ですが だとどうでしょうか? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Shunii85 nodeを根とする部分木がBSTである必要十分条件は何でしょうか?
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
node.leftの部分木、node.rightの部分木が二分探索木であることが分かれば、あとはnode.valが有効範囲内にある事を確認すればいいのですがnode.leftの部分木での最大値をnode.valが下回ってしまうとあれ、なんでその最大値を持つノードは左に振り分けられてるの?右にいるべきじゃん!となってしまうのでnode.valはnode.leftの部分木の最大値を上回らなければなりません。node.rightの最小値がnode.valの上限となっているのも同じ理由です。 |
||
| if not (biggest_l < node.val < smallest_r): | ||
| is_valid = False | ||
| return min(node.val, smallest_l), max(node.val, biggest_r) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 結果的に正しく動くのですが、smallest/biggestという言葉をきちんと体現させるとしたら、smallest_rも含めるか、あるいは変数名を変えるか、厳密な体現は諦めてコメントするか、でしょうか。 または、全体を早期リターンしていれば、嘘がなくなるとは思います(ただ、その場合に結果的に正しく動くことを読み手に考えさせる事になるかもしれません)
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ここでmin(node.val, smallest_l), max(node.val, biggest_r)とやっているのはnode.leftやnode.rightが葉だった場合にそれらのsmallestやbiggestを無限大に飛ばしているのでそのエッジケースを解消するために書いてます。もしnode.left, node.rightが葉でなく部分木である場合は全く無駄な処理となります。 こう書いた方がより説明的でエッジケースに適切に対処したコードになりますね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sasanquaneufさんのコメントは、BSTではなかった場合に、smallest/biggestというのは嘘になるよねという話だと思います。
ここで、「葉」はどういった意味で使っていますか?コードを参照すると、空ノード(None)の意味で使っているように見えますが、葉は子供がいないノードのことです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 私の解釈が間違っておりました... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. もとのコメントが言葉足らずですみません、
が意図で、 などとしないと一般のBSTでない場合においては本当のsmallestではない、というのが言いたいことでした。 変数名は、読み手がメンタルモデルを作るときのヒントになるので、読み手はまず となっているので、左側はこのサブツリーのsmallestではなく、あれ?と思ってコードを精読します。 このとき、たとえば変数名やminを取る操作のところにコメントがあったりすると、 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
についての補足になると思いますが、各
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Claudeに提案させてみるとこのようになりました。 その地点でのノードでinvalidということが分かればFalse, 0, 0となってこれ以降の判定が全て無効になるということが分かりやすいですね。 |
||
|
|
||
| _, _ = dfs(root) | ||
| return is_valid | ||
| ``` | ||
| 他の人のコードも読んでみる。 | ||
| https://github.com/h-masder/Arai60/pull/31/changes#diff-348997c4e487fecb306108a0041036c236327dcee8c3a97a3c11fbd62a2c74f7R1:~:text=%2D-,%E5%86%8D%E5%B8%B0,-%E3%82%92%E4%BD%BF%E3%82%8F | ||
| この人はiterative dfsで解いているが、親が子に対してlower < child.val < upperである制約を課して、それを破れば早期Falseを返すようにしている。lowerかupperがNoneの場合はそれは制約がないということである。これはdfs関数で書いてないので早期リターンしやすい。 | ||
| ### code | ||
| ```python | ||
| class Solution: | ||
| def isValidBST(self, root: Optional[TreeNode]) -> bool: | ||
| if root is None: | ||
| return True | ||
|
|
||
| node_and_ranges = [(root, None, None)] | ||
| while node_and_ranges: | ||
| node, lower, upper = node_and_ranges.pop() | ||
| if node is None: | ||
| continue | ||
| if lower is not None and node.val <= lower: | ||
| return False | ||
| if upper is not None and upper <= node.val: | ||
| return False | ||
|
|
||
| node_and_ranges.append((node.left, lower, node.val)) | ||
| node_and_ranges.append((node.right, node.val, upper)) | ||
| return True | ||
| ``` | ||
|
|
||
| ## step2: | ||
| 個人的にNone判定よりmath.infでやった方がNone判定のif文がなくなり見やすいのでmath.infを使う。 | ||
| ### code | ||
| ```python | ||
| import math | ||
| class Solution: | ||
| def isValidBST(self, root: Optional[TreeNode]) -> bool: | ||
| node_and_range = [] | ||
| node_and_range.append([root, math.inf, -math.inf]) | ||
| while node_and_range: | ||
| node, upper, lower = node_and_range.pop() | ||
| if not node: | ||
| continue | ||
| if node.val <= lower: | ||
| return False | ||
| if node.val >= upper: | ||
| return False | ||
| node_and_range.append([node.right, upper, node.val]) | ||
| node_and_range.append([node.left, node.val, lower]) | ||
|
|
||
| return True | ||
| ``` | ||
|
|
||
| ## step3: | ||
| ### code | ||
| ```python | ||
| # Definition for a binary tree node. | ||
| # class TreeNode: | ||
| # def __init__(self, val=0, left=None, right=None): | ||
| # self.val = val | ||
| # self.left = left | ||
| # self.right = right | ||
| import math | ||
| # let me solve this with dfs | ||
| # the left must be less than parent.val and bigger than smallest val of ancestor | ||
| # the right must be bigger than parent.val and smaller than biggest val of ancestor | ||
|
Comment on lines
+87
to
+88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 総ノード数が2の木で考えてみれば分かりますが、この記述は誤りです。
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ancestorはparentより上の祖先の想定でしたが、
8 上記の場合で4をparentとしてみたときの3に対して誤りでした。正しく書くなら
みたいな感じですかね。 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
こちらも標準的な用語の使い方から外れますね。 |
||
| class Solution: | ||
| def isValidBST(self, root: Optional[TreeNode]) -> bool: | ||
| node_and_range = [[root, -math.inf, math.inf]] | ||
| while node_and_range: | ||
| node, lower, upper = node_and_range.pop() | ||
| if not node: | ||
| continue | ||
| if not lower < node.val < upper: | ||
| return False | ||
| node_and_range.append([node.left, lower, node.val]) | ||
| node_and_range.append([node.right, node.val, upper]) | ||
| return True | ||
| ``` | ||
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.
典型コメント集の「用語を雑に使わない」に関連していると思いますが、全体的にあまり通じないか不正確な文章になっていると思います。
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.
今見返してみるとstep1の説明でかなり目的語を省いているのでわかりづらい文章になってました...
ざっと翻訳してみると、あるnodeを根とする木が二分探索木であることを検証するにはnode.leftを根とする部分木とnode.rightを根とする部分木がそれぞれ二分探索木であり、かつ
node.leftが根となる部分木の最大値 < node.val < node.rightが根となる部分木の最小値である事を検証する必要がある。そこで、nodeに対しての左右の部分木が二分探索木であることの検証とそれぞれの木全体での最小値、最大値の返却を再帰的に行うことで元のrootが二分探索木である事を検証できる。
といった感じでしょうか。
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.
目的語の省略ではなく、単純に用語の使い方が不適切なことについてのコメントでした。省略されているのは、文脈で補えるかもしれませんが、標準的な用語の使い方から外れると、エスパーしないと意図が読み取れないかなと。例えばですが、
といった表現から、「根」を標準的な意味である「一番上のノード」の意味で使っていないと推測しました。
新しい方は正しいと思います。
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.
「一番下の根」ではなく「一番下のノード」や、そのノードを「根」とする部分木みたいにきちんと定義に則って使うべきですね。
読み手にとって理解しやすい言葉の取り扱いに気をつけるようにします。