Skip to content

Conversation

@Fuminiton
Copy link
Owner

- `(l, r)`は`mid`が`-1`になる可能性を秘めていそうで抵抗感がある
- 切り上げを使うのは、leftを返す話なのに、あえて更新が右寄りになるようにするところが変な感じ

- `bisect_left`の実装は、`[lo, hi)`で探索をし、`mid`には切り捨てを使っていた
Copy link

Choose a reason for hiding this comment

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

この方法は、同じ値があった場合に一番左が出てきていることに注意です。なので、考え方がちょっと違いますね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

@oda
レビューありがとうございます。

bisect_leftは、「探索対象と等しい値の位置、なければ探索対象を越す初めの位置」を返すコードではなく、「探索対象以上の値が最初に現れる位置」を返すコードなので、考え方がちょっと違うということで合っていますでしょうか?

Copy link

Choose a reason for hiding this comment

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

はい。

```

### step4
暗記で書いてないか確かめるために`(lo, hi)`で探索するパターンも書いてみる
Copy link

Choose a reason for hiding this comment

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

いろいろなパターンで練習されていて素晴らしいと思います!もし範囲に関して毎回頭を使いたくなかったら「めぐる式」のようにokの範囲とngの範囲で分けるのも手かもしれません。(変数名はvalid_indexなどにしたほうがいい気がしますが)
https://qiita.com/drken/items/97e37dd6143e33a64c8c

Copy link

Choose a reason for hiding this comment

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

二分探索で「書き方」を固定することはできるのですが、「読む方法」を固定することは普通はできないです。書いている人がどういう考えで書くか普通は強制できないからです。

ジャッジシステムを相手にしているならば別にいいのですが、人間に技術面接で出題された場合は、多くの場合、どうしてそのコードが動くのかを聞きます。

技術面接で見ているのが「一緒に働いたときに成果がより出るか」で、そのために必要な「簡単なコードが読めて書けて管理ができる」かを知りたいからです。(余計な絶対値があれば、分かっているかより気になるでしょう。)

というわけで、書き方を固定してもいいけれども、幅のある表現を読めるようにしてくださいね、ということです。

二分探索についてはマニュアルに色々書いています。
https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.c15qprmvxkc2

Copy link

@sota009 sota009 May 9, 2025

Choose a reason for hiding this comment

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

@oda 読める幅を増やすべきという意見は同意です。また共有したアイデアもその幅のひとつとして、意味がわかって説明できるならば運用してもいいかなとも思ってます(それしかできない、テンプレ通り使うだけでロジックがわかっていないはアウト)。個人的にはこんな感じなら更新している範囲の解釈がしやすくて、むしろleft, rightの以上に意味をもたせられるという立場なのですがどうですかね?

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        if nums[0] >= target:
            return 0

        def is_valid(index: int) -> bool:
            # valid range := {i | target <= nums[i]}
            return  target <= nums[index]

        valid_index = len(nums) # Intentional. This index is not directly accessed.
        invalid_index = 0

        # Find the left most valid index by binary search
        while valid_index - invalid_index > 1:
            mid_index = (valid_index + invalid_index) // 2
            if is_valid(mid_index):
                valid_index = mid_index
            else:
                invalid_index = mid_index

        return valid_index

Copy link

Choose a reason for hiding this comment

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

私は読めますが、私の読む力を試しても仕方がないので、色々なものを読んで理解ができるかを確認していきましょう。はじめの分岐は invalid_index = -1 として下に合わせてもいいでしょうか。

Copy link

Choose a reason for hiding this comment

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

@Fuminiton さんごめんなさい、勝手に別の書き方の流れを作ってしまい💦

@oda

はじめの分岐は invalid_index = -1 として下に合わせてもいいでしょうか。

結果としては、そうしても大丈夫です。それを許容すると最初の if nums[0] >= target: のチェックもいらなくなります。むしろ valid_indexで答えを捕まえにいくので (invalid_index, valid_index] (左端がopen, 右端がclosed) の意識のもと、is_valid()の定義を以下のように拡張して、初期化直後のvalid_indexinvalid_indexも判定関数に適応可能にしても良かったもしれませんね。ただ改めて考えてみて、初見の人にとってはむしろ読む負荷を上げているようにも感じてきました。

class Solution:
    def searchInsert(self, nums: List[int], target: int) -> int:
        """
        Approach:
        * Consider all indices in [0, 1, ..., len(nums)-1, len(nums)] as candidates.
        * Split these indices into two consecutive sections: `invalid` and `valid`.
        * Define `valid` so that the answer is the smallest valid index.
        * Use binary search to find this minimum valid index, which is the answer.
        * The search range is defined as (invalid_index, valid_index], starting from (-1, len(nums)].
        """
        def is_valid(index: int) -> bool:
            # Valid indices are those from 0 to len(nums), 
            # where either nums[index] >= target,
            # or index == len(nums) (just after the last element)
            return 0 <= index and (index == len(nums) or target <= nums[index])

        valid_index = len(nums)
        invalid_index = -1

        # Find the left most valid index by binary search
        while valid_index - invalid_index > 1:
            mid_index = (valid_index + invalid_index) // 2
            if is_valid(mid_index):
                valid_index = mid_index
            else:
                invalid_index = mid_index

        return valid_index

この話題を上げた意図は次のようなものでした。

  • 既存のleft, rightの書き方だと mid = left + 1 のような ±1 が発生して、コード内に解説コメントがないと、読み手はなぜそれをしているかすぐわからないから
  • 典型コメント集の忠告で感情を育ててほしいとあったので、上記理由から、「もっと読みやすい書き方を探求してやるぞ!」の気持ちが強くなったから

ただ私が書いた方法も、相手になんでその is_valid()の定義なの?そもそも急にvalidの概念が出てきたけど何?それを理解させるためにコメントを沢山書いたら元も子もないんじゃない?という疑問が出ると今更ながら感じ、ジレンマに陥っています😓

Copy link

Choose a reason for hiding this comment

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

is_valid の中で範囲チェックしていますが、(valid_index + invalid_index) // 2 の関係からはみでることはないのではないですか。

この場合は、valid_index は valid であると確認された最小のインデックスで、invalid_index は invalid であると確認された最大のインデックスですね。これはこれで一つの案でしょう。

もしも興味があるならば、色々な言語の標準ライブラリーを調べてみたらいかがですか。きちんとした人が作って色々なテストをしているでしょうから。

Copy link

Choose a reason for hiding this comment

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

is_valid の中で範囲チェックしていますが、(valid_index + invalid_index) // 2 の関係からはみでることはないのではないですか。

質問内容を正しく理解できているか不安ですが、「mid_index = (valid_index + invalid_index) // 2 で定義されるあらゆるmidis_valid()が想定す入力になっているか?」ということで合っていますでしょうか?もしそうであれば問題はないと思いっております。むしろ is_valid()では validたりえるindexを定義しているので、それ以外の入力は正しくFalseの判定が来ます。言い換えれば,

  1. [False, False, True, True, ...] または [True, True, False, False, ...] のようにある位置から真偽が一度だけ逆転する構造になっている(これはインデックス-1len(nums) など、現実の配列でアクセスできるインデックスより広くても is_valid()が対応していれば問題なし)
  2. is_valid()が1で想定されるされる範囲において、正しく真偽を返す。

が満たせていれば良いという認識です。今回は is_valid()-1にもlen(nums)にも対応しています。

この場合は、valid_index は valid であると確認された最小のインデックスで、invalid_index は invalid であると確認された最大のインデックスですね。これはこれで一つの案でしょう。

while valid_index - invalid_index > 1とした場合そうなり、それがこの方針の肝だと考えています。

もしも興味があるならば、色々な言語の標準ライブラリーを調べてみたらいかがですか。きちんとした人が作って色々なテストをしているでしょうから。

これは興味のあるところで、統一化されているのか、実装が異なるのかチェックしてみようと思います!

Copy link

Choose a reason for hiding this comment

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

あ、はい。is_valid(-1) という呼び出しは起きうるか。ない。なぜならば、invalid_index == -1 のときに、valid_index >= 1 だからだ、という話をしています。
このチェックは確かにあって悪いものではないが、しかし実際に実行時に必要とされることはないですね、ということです。

ライブラリーのコードはどういう環境で動くか分からないので、比較的速度が優先される傾向があるでしょう。

hi = mid

return lo + 1
``` No newline at end of file
Copy link

@fuga-98 fuga-98 May 9, 2025

Choose a reason for hiding this comment

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

私も二分探索の勉強中なのですが、解釈合っていますでしょうか。
この問題のとり得る範囲は0~len(nums)なので、148,149行目から
loは境界を含まない、hi は境界を含みそうと思いました。
loは答えにならない最小のindex
hiは答えとなる最大のindexという意味だと推測しました。
つまり、nums[lo] < target <= nums[hi]という関係です。
意味的にはreturn hiでも良さそうですかね。

(メモ)
whileの停止がどんな時に起きるか分からなかったので、[3] target=5のケースで考えてみます。
hi - lo = 1 + 1= 2なので、whileはTrue
mid=0で、153行目はTrue
lo=0に。
hi - lo = 1< 2なので、ループ終了、答えは1
終了条件は実質
hi - lo = 1なので
hi = lo + 1になりそうにみえます。

Copy link

@fuga-98 fuga-98 May 9, 2025

Choose a reason for hiding this comment

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

(メモ)
nums[mid] == targetとなるケースを考えると、
条件はfalseとなり、hi = midとなります。
ここでhiは答えになり得るので、hiの答えとなる最大のindexという意味はあってそうだなと感じました。
もし条件を nums[mid] <= targetとすると、
lo = midとなりloの意味とは合わなくなりそうですね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビュー、質問ありがとうございます。大変助かります。。

つまり、nums[lo] < target <= nums[hi]という関係です。
意味的にはreturn hiでも良さそうですかね。

はい。解釈合っています。
自分としては、

  • loの位置の値は常にtargetより小さい
  • hiの位置の値は常にtarget以上

になるように意識していて、
この時の終了条件はlohiが隣り合って探索の対象が空、つまり lo + 1 = hi の時なのでreturn hiで問題ないです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

もし条件を nums[mid] <= targetとすると、
lo = midとなりloの意味とは合わなくなりそうですね。

この場合、

  • loの位置の値は常にtarget以下
  • hiの位置の値は常にtargetより大きい

と変わり、targetと同じ値がnumsに複数含まれる場合、初めてtargetを超える位置の断定はできなくなりますね。
また、returnする lo + 1の意味するところが、targetを超える最初の位置になります。

left = 0
right = len(nums) - 1

while left <= right:
Copy link

Choose a reason for hiding this comment

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

あくまで個人の好みの問題なのですが、 left > right になると、区間の定義と矛盾するように感じられて、違和感を感じます。ただ、こう感じるのは多数派ではないと思いますので、問題はないと思います。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。
left<=mid<rightの範囲に得たい値があるように範囲を狭めていき、
left == rightで止まるようにする実装の方が自然ということでしょうか?

Copy link

Choose a reason for hiding this comment

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

問題の設定によると思います。あくまで自分の場合ですが、二分探索の問題を設定する際は

  1. [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題とみなす。
  2. [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、一番左の true の位置を求める問題とみなす。

のいずれかで考えることが多いです。

  1. で考え、かつ区間を閉区間で考えた場合、最後の状態では left > right となると思います。そして left が指し示す境界のすぐ右側の値の位置が、求める位置になります。

  2. で考え、かつ区間を閉区間で考えた場合、最後の状態では left == right となると思います。このとき、値が丁度一つだけ区間に含まれる状態になり、その値の位置が求める答えとなります。ただし、左端が求める答えとなる場合、この考え方だとそれを出力することができません。

以下も参考にすることをお勧めいたします。
https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562

Copy link
Owner Author

Choose a reason for hiding this comment

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

@nodchip
丁寧なご説明、リンクの共有もありがとうございます。おかげさまで理解できた気がします。
理解の確認のため、以下の指摘の解釈と、二分探索の考え方の流れに違和感がないかをご確認していただけないでしょうか?

あくまで個人の好みの問題なのですが、 left > right になると、区間の定義と矛盾するように感じられて、違和感を感じます。ただ、こう感じるのは多数派ではないと思いますので、問題はないと思います。

FalseTrueの境界の位置を求めようとした時、
nums[left:right + 1]の要素(rightを含む)の中に境界があるとして考えると、
ループで評価する要素が空になるのはleft >= right + 1 left > rightとなり、
区間の定義と矛盾するように感じられたということであっていますでしょうか?

続いて、二分探索の考え方の流れですが、例えば、

  1. FalseTrueの境界の位置を求めたい
  2. 全てFalseの時はlen(nums)を返したい
  3. nums[left:right]の要素(rightを含まない)の中に境界があり、nums[:left]の要素は全てFalsenums[right:]の要素は全てTrueが満たされるように評価する範囲を狭めていくことにする
  4. これまでの設定を満たすように、「早期リターン、初期値、終了条件は、更新方法、切り捨て/切り上げ」を決める

となれば、二分探索を考える流れとして違和感なさそうでしょうか?

Copy link

Choose a reason for hiding this comment

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

あくまで個人的なイメージなのですが、閉区間で考えた場合、頭の中で以下のように考えています。
image
上記は、区間の中に 2~5 まで含まれている様子を表します。

left > right となったとき、以下のようになります。
image
このとき、左カッコが右カッコより右に来てしまっているのが、なんとなく違和感を感じます。

繰り返しになりますが、このように感じるのは多数派ではないと思います。

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.

6 participants