Skip to content

35.search insert position#39

Open
tom4649 wants to merge 2 commits intomainfrom
35.Search-Insert-Position
Open

35.search insert position#39
tom4649 wants to merge 2 commits intomainfrom
35.Search-Insert-Position

Conversation

@tom4649
Copy link
Copy Markdown
Owner

@tom4649 tom4649 commented Mar 30, 2026

@tom4649 tom4649 force-pushed the 35.Search-Insert-Position branch from a8810bc to d9dccdc Compare March 30, 2026 21:16
Comment thread 35/memo.md
# この書き方は正式な書き方ではなく、自分が考えるときのイメージです。
# [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。
# ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。
# よってループの不変条件は left < right とする。
Copy link
Copy Markdown

@arahi10 arahi10 Mar 31, 2026

Choose a reason for hiding this comment

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

ループ不変条件(ループ不変式)の意図するところと理解が異なるように思います。
というのも、ループ不変条件はループ前・ループ内の各反復における処理の前後・ループ後、のどのタイミングでも真となるような条件のことを指します。
また、ループ不変式はアルゴリズムの正当性を容易に理解・証明するのに用いられます。詳しくはアルゴリズムイントロダクションWikipediaなどをご覧になってみてはいかがでしょうか。

Copy link
Copy Markdown

@arahi10 arahi10 Mar 31, 2026

Choose a reason for hiding this comment

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

個人的にはwikipediaの説明はあっさりしててややありがたみが分かりにくいので、アルゴリズムイントロダクションをお勧めしたいです。お送りしたリンクは4版ですが、自分が読んだ3版なら最初の20ページほどを読めばループ不変条件(ループ不変式)の説明にあたれるはずで、wikiよりわかりやすかったように思います

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.

リンクの紹介までありがとうございます。
ループ不変式はループの後にも成り立つ条件なので、この表現は正式ではないなとは自分も感じました。
コードを書くときにループ不変式を意識できるようになりたいです。

英語の第三版に目を通してみました。Loop invarianは最初の20ページほどで現れますね。勉強になりました。
https://www.cs.mcgill.ca/~akroit/math/compsci/Cormen%20Introduction%20to%20Algorithms.pdf

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こちら自分がループ不変条件の意味を誤って理解していたようでした。失礼いたしました。

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.

いえ、考え方は参考になりました

Comment thread 35/memo.md
```


保証している条件
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ここで説明されている条件がループ不変条件の意図するところですね。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

それはそれとして、こちらの条件の記述は(言いたいことはわかるんですが)ちょっと細かいところが違いますね
5ky7/arai60#41 (comment)

ループ不変式が初期化時、ループブロック内の更新前後、およびループ終了時の全てで成り立っていることがループ不変条件を用いたアルゴリズムの正当性の証明には肝要ですので、丁寧に記述してあげた方が良いかと思います

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.

コメントを受けて追記しました。

先ほど紹介していただいたアルゴリズムイントロダクションでもループ不変条件が成立していることの説明が丁寧に行われていましたね。

Comment thread 35/sol2.py
left = 0
right = len(nums) - 1
while right - left > 1:
mid = left + (right - left) // 2
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

オーバーフロー回避のための記述ですが、pythonのintは多倍長整数ですので、直感的な記述
mid = (left + right) // 2
で良いかと思います。
(numpyとかは裏でCが走っていて64bit integerを扱うのでこのオーバーフロー回避が必要になりうるんですが。)

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.

ここは個人的な好みですがこのままにしておきます。
numpyではオーバーフローが発生すること、知らなかったです。関連しそうな記事があったので読んでみました。

https://qiita.com/morgen-code/items/daebbfe6d55add1012a7

Comment thread 35/sol2.py
right = len(nums) - 1
while right - left > 1:
mid = left + (right - left) // 2
if nums[mid] == target:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

numsにtargetが複数含まれていた場合に、bisect_leftとして返したい「target以上の要素の最小のインデックス」が返らないですね。

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.

その通りでこのコードには問題が色々とありますね(sol3.pyで改善しています。)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

何を問題視してどう変えたかとかはメモに書いておくと,時間を経てご自身で読み返したとき・他の人が参照するとき・コメントをもらうとき,などで色々学びがあると思います.
ご自身の言葉とある程度の粒度で思考を整理すれば記憶の定着にもよいでしょうし.
ま〜忙しいときは割ける時間との兼ね合いなんですが.

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.

  • 問題点1: right = len(nums) - 1と初期化しているため探索範囲が(煩雑なロジックを経ないと)len(nums)をカバーできていない
  • 問題点2: while right - left > 1がループ条件のためleft==rightではない場合があり得る。どちらを返すか固定されていない。
  • 問題点3: nums[mid] == target で即座にreturnしてしまっているため、target が重複している場合にもっとも左を返す保証がない

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.

追記しました。言語化するのは大事ですね。

Comment thread 35/sol4.py
@@ -0,0 +1,9 @@
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こちらのsol4.py の計算量って見積もりましたか?
sol1 から3と比較するとどうでしょうか

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.

二分探索でO(log N)だった計算量がO(N)になってしまっていますね。
配列のスライス操作で合計 N/2 + N/4 + ... 1=O(N)の時間計算量となってしまいます。

同様に空間計算量もリストのスライス操作でデータのコピーが走るのでO(N)となります。

itertools.isliceを用いる場合も考えてみると空間計算量はO(log N)に改善しますが、時間計算量は変わりませんね。

Comment thread 35/sol4.py
right = len(nums)
mid = right // 2
if nums[mid] < target:
return mid + 1 + self.searchInsert(nums[mid + 1 : right], target)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

スライスの終了インデックス [~:right] はなくても良いですね。
ない方がpythonicでしょう

Comment thread 35/sol2.py
left = mid
else:
right = mid
if target <= nums[left]:
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

sol1.pyやsol3.pyと比べてreturn までに処理が増えたのは、bisect_leftとして返しうる値の範囲(0以上len(nums)以下)をleft, 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.

なるほど、targetがすべての要素より大きい場合を考慮できていないということですね。答えは 0~NのN+1通りですから。

(二分探索をしばらく書いておらず、記憶を頼りに空で書いたらこのようなコードになったという次第です。改めて勉強しなおそうと思います。)

Comment thread 35/memo.md
# この書き方は正式な書き方ではなく、自分が考えるときのイメージです。
# [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。
# ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。
# よってループの不変条件は left < right とする。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

こちら自分がループ不変条件の意味を誤って理解していたようでした。失礼いたしました。

Comment thread 35/memo.md
while left < right:
middle = (left + right) // 2
# target より小さい値は false、 target 以上の値は true とする。
if (numbers[middle] < target):
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 numbers[middle] < target:

Comment thread 35/memo.md
Comment on lines +33 to +36
# 5. ループの終了状態で false, false, ..., false, [true), true, ..., true となって欲しい。
# この書き方は正式な書き方ではなく、自分が考えるときのイメージです。
# [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。
# ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

この表記と説明はあまり分からなかったです。

Copy link
Copy Markdown

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, true, ..., true と並んだ配列があったとき、
#    一番左側の true の位置を求める問題だと捉える。
#    ここでは、 target より小さい値は false、 target 以上の値は true とする。
# 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉える。
# 3. 範囲を考えるにあたり、区間を閉区間で捉える。
#    (区間の左端の要素と右端の要素は区間に含まれると捉える。)
# 4. 初期値は 0, len(numbers) - 1 とする。
def lower_bound(numbers, left, right, target):
    # 5. ループの終了時に false, false, ..., false, [true], true, ..., true となって欲しい。
    # (この書き方は正式な書き方ではなく、自分が考えるときのイメージです。)
    # [ は区間の左端で、 true のすぐ左側にあるイメージ。
    # ] は区間の右端で、 true のすぐ右側にあるイメージ。
    # left == right となったときに、区間の中に含まれる要素が 1 つだけになり、
    # 一番左側の true の位置が求まる。
    # よって while 文の条件式は left < right とする。
    while left < right:
        middle = (left + right) // 2
        # target より小さい値は false、 target 以上の値は true とする。
        if numbers[middle] < target:
            # 6. 右に狭める場合。
            # numbers[middle] < target のため、
            # middle の位置の要素を、狭めたあとの区間に含めたくない。
            # middle の位置の要素を区間に含めないようにするには、
            # left の位置を middle + 1 にすればよい。
            left = middle + 1
        else:
            # 左に狭める場合。
            # numbers[middle] == target かもしれないため、
            # middle の位置の要素を、狭めたあとの区間に含めたい。
            # middle の位置の要素を区間に含めるには、
            # right の位置を middle にすればよい。
            right = middle
    return left

https://discord.com/channels/1084280443945353267/1196498607977799853/1269560324818731028

Copy link
Copy Markdown

@liquo-rice liquo-rice Mar 31, 2026

Choose a reason for hiding this comment

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

初期値は 0, len(numbers) - 1 とする。

だと、配列の最大値より大きいtargetの時、std::lower_boundと振る舞いが異なりませんか?

長さnの配列numsの挿入位置は[0, 1, .., n - 1, n]のn + 1個(挿入位置iは、要素iの左側を指す)あり、これをnums[i] >= target (nums[n] = INFとする)で投影すると、[false, false, ..., true, true]というbool型のソートされた仮想配列になるので、この上で二分探索するイメージで考えていました。この仮想配列上で一番左のtrueを探す問題で、必ずtrueは少なくとも一つ存在することは分かっているので、閉区間を最後の一個になるまで狭めると良いですよね。

[ は区間の左端で、 true のすぐ左側にあるイメージ。

個人的には、leftが区間の一番左側の要素、rightが区間の一番右側の要素を指すインデックスと考える方が、インデックスとの対応が取れて、直感的に思いました。

Copy link
Copy Markdown

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, true, ..., true と並んだ配列があったとき、
#    一番左側の true の位置を求める問題だと捉える。
#    ここでは、 target より小さい値は false、 target 以上の値は true とする。
# 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉える。
# 3. 範囲を考えるにあたり、 left を区間の左端の要素の位置、 right を区間の右端の要素の位置とする。
#    (閉区間)
# 4. 初期値は left = 0, right = len(numbers) とする。
#    ただし、 numbers 右端の要素の右側に、番兵 INF が置かれているものとみなす。
def lower_bound(numbers, left, right, target):
    # 5. ループの終了時に区間の中に含まれる要素が 1 つだけになるようにし、それを答えとしたい。
    # 区間の中に含まれる要素が 1 つだけになるのは left == right のときである。
    # よって while 文の条件式は left < right とする。
    while left < right:
        middle = (left + right) // 2
        # target より小さい値は false、 target 以上の値は true とする。
        if numbers[middle] < target:
            # 6. 右に狭める場合。
            # numbers[middle] < target のため、
            # middle の位置の要素を、狭めたあとの区間に含めたくない。
            # middle の位置の要素を区間に含めないようにするには、
            # left の位置を middle + 1 にすればよい。
            left = middle + 1
        else:
            # 左に狭める場合。
            # numbers[middle] == target かもしれないため、
            # middle の位置の要素を、狭めたあとの区間に含めたい。
            # middle の位置の要素を区間に含めるには、
            # right の位置を middle にすればよい。
            right = middle
    return left


def upper_bound(numbers, left, right, target):
    while left < right:
        middle = (left + right) // 2
        if (numbers[middle] <= target):
            left = middle + 1
        else:
            right = middle
    return left


if __name__ == '__main__':
    print(lower_bound([0, 1, 2, 3, 4, 5], 0, 6, 3))
    print(upper_bound([0, 1, 2, 3, 4, 5], 0, 6, 3))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

ありがとうございます。こちらで問題なくわかりました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自明かもしれませんが、left = 0, right = nの示す範囲を、閉区間だと考えると、最終的に知りたい答えのある領域を指していて、半閉区間だと考えると、未探索の領域ですね。世間一般で、二分探索の初期値・whileの条件式・更新式を議論するとき、後者の方を指しているのだと、arahi10さんのコメントを見てようやく気づきました。

ソート状態を維持したまま挿入できる位置の左端を考える問題の場合だと、探索済みの領域について以下のことが言えますね。こちらが、loop invariantとなり、未探索の領域を空にすることで、すべての範囲についての情報を得ることができます。
閉区間: nums[i] < target (i < left). nums[i] >= target (i > right)
半閉区間: nums[i] < target (i < left). nums[i] >= target (i >= right)
開区間: nums[i] < target (i <= left). nums[i] >= target (i >= right)
(iが範囲外の時は、条件が常に成り立つと仮定)

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.

なるほど。「未探索領域をどう定義するか」という視点で整理すると、初期値や更新式が分かりやすくなりますね。

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