-
Notifications
You must be signed in to change notification settings - Fork 0
35.search insert position #39
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,126 @@ | ||
| # 35. Search Insert Position | ||
|
|
||
| sol1.py: bisect ライブラリを使うと秒殺 | ||
|
|
||
| これでは何の練習にならないので自力で書く | ||
|
|
||
| sol2.py:インデックスの1のずれで混乱 | ||
|
|
||
| sol3.pyに改善。bisect_leftに関数を分けるのもやめる | ||
|
|
||
|
|
||
| left == rightのときこれらが同時になりたつ→答え | ||
| leftに+1があるので無限ループが発生しない | ||
|
|
||
| https://discord.com/channels/1084280443945353267/1196498607977799853/1269532028819476562 | ||
|
|
||
| > 二分探索を、 [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 false と true の境界の位置を求める問題、または一番左の true の位置を求める問題と捉えているか? | ||
| 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉えているか? | ||
| 範囲を考えるにあたり、閉区間・開区間・半開区間の違いを理解できているか? | ||
| 用いた区間の種類に対し、適切な初期値を、理由を理解したうえで、設定できるか? | ||
| 用いた区間の種類に対し、適切なループ不変条件を、理由を理解したうえで、設定できるか? | ||
| 用いた区間の種類に対し、範囲を狭めるためのロジックを、理由を理解したうえで、適切に記述できるか? | ||
|
|
||
|
|
||
| ```python | ||
| # 1. [false, false, false, ..., false, true, true, ture, ..., true] と並んだ配列があったとき、 | ||
| # false と true の境界の位置を求める問題と捉える。 | ||
| # ここでは、 target より小さい値は false、 target 以上の値は true とする。 | ||
| # 2. 位置を求めるにあたり、答えが含まれる範囲を狭めていく問題と捉える。 | ||
| # 3. 範囲を考えるにあたり、半開区間を用いる。 | ||
| # 4. 初期値は 0, len(numbers) とする。 | ||
| def lower_bound(numbers, left, right, target): | ||
| # 5. ループの終了状態で false, false, ..., false, [true), true, ..., true となって欲しい。 | ||
| # この書き方は正式な書き方ではなく、自分が考えるときのイメージです。 | ||
| # [ は区間の左端で、 true の左側にあります。 ) は区間の右端で、 true の右側にあります。 | ||
| # ただし、右側は開区間のため、インデックスの値は true の位置と同じで、 true を区間には含めない。 | ||
| # よってループの不変条件は left < 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. ループ不変条件(ループ不変式)の意図するところと理解が異なるように思います。 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. 個人的にはwikipediaの説明はあっさりしててややありがたみが分かりにくいので、アルゴリズムイントロダクションをお勧めしたいです。お送りしたリンクは4版ですが、自分が読んだ3版なら最初の20ページほどを読めばループ不変条件(ループ不変式)の説明にあたれるはずで、wikiよりわかりやすかったように思います
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. リンクの紹介までありがとうございます。 英語の第三版に目を通してみました。Loop invarianは最初の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. こちら自分がループ不変条件の意味を誤って理解していたようでした。失礼いたしました。
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. いえ、考え方は参考になりました |
||
| while left < right: | ||
| middle = (left + right) // 2 | ||
| # target より小さい値は false、 target 以上の値は true とする。 | ||
| if (numbers[middle] < target): | ||
|
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. () を付けないほうが多いと思います。 if numbers[middle] < target: |
||
| # 6. 右に狭める場合。 | ||
| # middle の位置の要素を、狭めたあとの区間に含めたくない。 | ||
| # 左側は閉区間である。 | ||
| # middle の位置の要素を区間に含めないようにするには、 | ||
| # left の位置を middle + 1 にすればよい。 | ||
| left = middle + 1 | ||
| else: | ||
| # 左に狭める場合。 | ||
| # middle の位置の要素を、狭めたあとの区間に含めたくない。 | ||
| # なぜならば、今回は境界の位置を求める問題と捉ええているためである。 | ||
| # 別の言い方をすると、境界の位置を 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)) | ||
| ``` | ||
|
|
||
|
|
||
| 保証している条件 | ||
|
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. それはそれとして、こちらの条件の記述は(言いたいことはわかるんですが)ちょっと細かいところが違いますね ループ不変式が初期化時、ループブロック内の更新前後、およびループ終了時の全てで成り立っていることがループ不変条件を用いたアルゴリズムの正当性の証明には肝要ですので、丁寧に記述してあげた方が良いかと思います
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. コメントを受けて追記しました。 先ほど紹介していただいたアルゴリズムイントロダクションでもループ不変条件が成立していることの説明が丁寧に行われていましたね。 |
||
| nums[left-1] < target | ||
| nums[right] >= target or rightが右端 | ||
|
|
||
| ループ不変条件: left < right | ||
|
|
||
| つまり flag_nums = [n>=target for n in nums] としたとき | ||
| i < left→ flag_nums[i] == False | ||
| i >= right → flag_nums[i] == True | ||
| 終了時 left = rightでは right: flag_nums[i] == Trueを満たす最小のi が保証される | ||
|
|
||
| --- | ||
| ## コメントをいただいて追記: | ||
|
|
||
| ### ループ不変式: | ||
|
|
||
| i < left -> nums[i] <= target (1) | ||
|
|
||
| && j >= right -> nums[j] > target (2) | ||
|
|
||
|
|
||
| ### 成り立つことの確認 | ||
|
|
||
| #### 初期化時: left=0, right=len(nums) | ||
| i < left, j >= rightとなるi, jは存在しない。 | ||
|
|
||
| #### 維持: ある反復で成り立つと仮定して次の反復で成り立つことを示す。場合分け。 | ||
| - nums[middle] <= target の場合: numsがソートされているため left=middle+1とした場合、(1) は依然として成立 | ||
| - nums[middle] > target の場合 numsがソートされているため right=middle とした場合(2) は依然として成立 | ||
|
|
||
| #### 終了: left < rightがFalseとなる。更新式よりleft==rightである | ||
| (1),(2)と合わせて、 | ||
|
|
||
| i<left -> nums[i] <= target && i >= left -> nums[i] > target | ||
|
|
||
| これはleftがnums[i] > targetを満たす最小のiであることを意味する。 | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| https://github.com/seal-azarashi/leetcode/pull/38 | ||
|
|
||
| しっかりと理解する必要があるな | ||
|
|
||
| > 二分探索で「書き方」を固定することはできるのですが、「読む方法」を固定することは普通はできないです。書いている人がどういう考えで書くか普通は強制できないからです。 | ||
| > ジャッジシステムを相手にしているならば別にいいのですが、人間に技術面接で出題された場合は、多くの場合、どうしてそのコードが動くのかを聞きます。 | ||
| > 技術面接で見ているのが「一緒に働いたときに成果がより出るか」で、そのために必要な「簡単なコードが読めて書けて管理ができる」かを知りたいからです。(余計な絶対値があれば、分かっているかより気になるでしょう。) | ||
| > というわけで、書き方を固定してもいいけれども、幅のある表現を読めるようにしてくださいね、ということです。 | ||
|
|
||
| https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import bisect | ||
|
|
||
|
|
||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| return bisect.bisect_left(nums, target) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| def bisect_left(nums, target): | ||
| left = 0 | ||
| right = len(nums) - 1 | ||
| while right - left > 1: | ||
| mid = left + (right - left) // 2 | ||
|
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. オーバーフロー回避のための記述ですが、pythonのintは多倍長整数ですので、直感的な記述
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. ここは個人的な好みですがこのままにしておきます。 |
||
| if nums[mid] == target: | ||
|
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. numsにtargetが複数含まれていた場合に、bisect_leftとして返したい「target以上の要素の最小のインデックス」が返らないですね。
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. その通りでこのコードには問題が色々とありますね(sol3.pyで改善しています。) 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.
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. 追記しました。言語化するのは大事ですね。 |
||
| return mid | ||
| elif nums[mid] < target: | ||
| left = mid | ||
| else: | ||
| right = mid | ||
| if target <= nums[left]: | ||
|
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. sol1.pyやsol3.pyと比べてreturn までに処理が増えたのは、bisect_leftとして返しうる値の範囲(0以上len(nums)以下)をleft, right の初期化時に覆わなかったのが原因ですかね。
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. なるほど、targetがすべての要素より大きい場合を考慮できていないということですね。答えは 0~NのN+1通りですから。 (二分探索をしばらく書いておらず、記憶を頼りに空で書いたらこのようなコードになったという次第です。改めて勉強しなおそうと思います。) |
||
| return left | ||
| if target <= nums[right]: | ||
| return right | ||
| return right + 1 | ||
|
|
||
|
|
||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| return bisect_left(nums, target) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
| left = 0 | ||
| right = len(nums) | ||
| while right > left: | ||
| mid = left + (right - left) // 2 | ||
| if nums[mid] < target: | ||
| left = mid + 1 | ||
| else: | ||
| right = mid | ||
| return left |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| class Solution: | ||
| def searchInsert(self, nums: List[int], target: int) -> int: | ||
|
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. こちらのsol4.py の計算量って見積もりましたか?
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. 二分探索でO(log N)だった計算量がO(N)になってしまっていますね。 同様に空間計算量もリストのスライス操作でデータのコピーが走るのでO(N)となります。 itertools.isliceを用いる場合も考えてみると空間計算量はO(log N)に改善しますが、時間計算量は変わりませんね。 |
||
| if not nums: | ||
| return 0 | ||
| right = len(nums) | ||
| mid = right // 2 | ||
| if nums[mid] < target: | ||
| return mid + 1 + self.searchInsert(nums[mid + 1 : right], target) | ||
|
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. スライスの終了インデックス [~:right] はなくても良いですね。 |
||
| return self.searchInsert(nums[:mid], target) | ||
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.
自分の書いた表記と説明です。読み直してみたのですが、自分でもあまり良く分からなかったです。
大幅に書き直してみたのですが、これだと分かりますでしょうか。
https://discord.com/channels/1084280443945353267/1196498607977799853/1269560324818731028
Uh oh!
There was an error while loading. Please reload this page.
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.
だと、配列の最大値より大きい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は少なくとも一つ存在することは分かっているので、閉区間を最後の一個になるまで狭めると良いですよね。
個人的には、leftが区間の一番左側の要素、rightが区間の一番右側の要素を指すインデックスと考える方が、インデックスとの対応が取れて、直感的に思いました。
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.
ありがとうございます。こちらで問題なくわかりました。
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.
自明かもしれませんが、
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が範囲外の時は、条件が常に成り立つと仮定)
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.
なるほど。「未探索領域をどう定義するか」という視点で整理すると、初期値や更新式が分かりやすくなりますね。