Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 233 additions & 0 deletions 競技プロ就活部PR用/35. Search Insert Position.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
```
範囲を絞っていく、というお題なわけですよね。
left = 0
right = len(nums) - 1
と書いたら、left と right の両端を含む、この範囲にある、ということを考えていますね。
言い換えると、「target はあるとすると、left 以上、right 以下に必ずある」ということです。

「target はあるとすると、10以上10以下にあるんだよねー。」といわれたら、10確認しろよ、ってなりますよね。
だから、両端を含む場合は、同じ値でも確認しないといけません。

また、middle として、100を選んで、そこになかった場合。そこよりも左か、そこよりも右にあるかが、値次第で分かるわけです。

left, right は両端を含むわけですから、99以下にあることか101以上にあることかが分かるわけですね。だから1を足し引きします。

middle の選び方は、left <= middle <= right であれば、この議論だとどこでも大丈夫なはずです。区間は、最低1減っていきますから。

これが頭にあれば、それほど抵抗なく書けませんか?

ちなみに、現実的には、たぶん、個数が50個くらい以下ならば、ループで頭から探したほうが速いでしょう。(少なくとも C++ では。)
分岐予測との兼ね合いです

left = 0
right = len(nums)

とすることもできて、そうすると、左は含むが右は含まないつもりで書いているわけですね。left 以上 right 未満。
「target はあるとすると、10以上10未満にあるんだよねー。」といったらそんな数はありません。

また、middle として、100を選んで、そこになかった場合。そこよりも左か、そこよりも右にあるかが、値次第で分かるわけです。

要は、100未満にあることか101以上にあることかが分かるわけですね。

middle の選び方は、left <= middle < right に変わります。

というわけで、注目ポイント次第ですね。開区間、半開区間、閉区間とかいったりします。
個数が50個くらい以下ならば、ループで頭から探したほうが速いでしょう。
```

```
コードはいいと思います。

これ、どう考えるといいかなあと思っています。

n 個要素があると、植木算で n + 1 個の切れ目がありますね。

そのうち、どこで切ると、
右はすべて、target <= nums[i] で、
左はすべて、nums[i] < target となるか、ということですね。
これを answer とでもしましょう。

だから、left と right は実は閉区間で、一致するまで回さないといけませんね。

mid = (left + right) // 2

とすると、切り捨てられるので、
left <= mid < right
になります。

nums[mid] < target
が判明すると、
mid < answer が分かります。
一方、
target <= nums[mid]
が判明すると、
answer <= mid
が分かりますね。

この辺、どう考えていますか?
```

1. 閉区間 ↔︎ a ≤ x ≤ b ↔︎ left = 0, right = len(nums) - 1
以上、以下の範囲で探索を行うため右端を調査対象に含むべき ↔︎ while left <= right
2. 半開区間 ↔︎ a ≤ x < b ↔︎ left = 0, right = len(nums)
以上、より小さいの範囲で探索を行うため右端を調査対象に含まない ↔︎ while left < right

* かなり悩んでしまった。bisect_leftの値に重複がないパターンを実装すべき問題ということには気がついた。
* 手で解いてみて、調整方法を検討して一応通せた。
* 二分探索を理解できていないことに気がついたので、Discord内の議論を参考に復習した。
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

さっき出てたのでご覧になったかわかりませんが、これわかりやすかったです
Yoshiki-Iwasa/Arai60#35 (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.

有難うございます!読みました、分かりやすかったです!

* 半開区間のleftは、left = rightかつ、重複がなければ挿入されるべき位置に収束する。
* まだ、"境界"の感覚がわかっていないので、この後の問題を通して理解したい。


```
[0, 1, 3, 5, 6], target = 2
nums = [0, 1, 3, 5, 6] (left, right) = (0, 4)
nums = [0, 1] (left, right) = (0, 2)
nums = [1, 3] (left, right) = (1, 2)
```


### 全探索による解法 (1m52s)
* 時間計算量: O(N)
* 空間計算量: O(1)

```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
for i in range(len(nums)):
if nums[i] >= target:
return i

return len(nums)
```


### 半開区間による解法
### 1回目 (14m13s)
* 時間計算量: O(logN)
* 空間計算量: O(1)

```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:

left, right = 0, len(nums) - 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.

2 行に分けて書いたほうが読みやすいように感じます。

ただし、今回のコードでは関係ナインドエスが、値を swap する場合は 1 行で書いたほうが読みやすいと思います。


while left <= right:
mid = (left + right) // 2
value = nums[mid]

if value == target:
return mid

if value > target:
right = mid - 1

if value < target:
left = mid + 1

return left
```


### 2回目
* ifの分岐が対称性があるため、if-elif-elseに修正
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.

nodechipさん、ありがとうございます。
自分で気づけたのでよかったです。

* valueという変数名がわかりづらいこともあるが、nums[mid]のままの方が読みやすいと感じたため修正

```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)

while left < right:
mid = (left + right) // 2

if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid

return left
```


### 3回目
* 2回目から変化なし
* midが略記の認識がある場合を考慮し、一応middleに。
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums)

while left < right:
middle = (left + right) // 2

if nums[middle] == target:
return middle
elif nums[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 にしますが、趣味の範囲でしょう。ところで、少し空行が多い気がします。

left = middle + 1
else:
right = middle

return left
```



## bisect_leftによる解法
* ドキュメント: https://docs.python.org/3/library/bisect.html
* 実装: https://github.com/python/cpython/blob/cfbdce72083fca791947cbb18114115c90738d99/Lib/bisect.py#L74

### 半開区間
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
def _is_sorted(arr: List[int]) -> bool:
for i in range(1, len(arr)):
if arr[i - 1] > arr[i]:
return False
return True

def bisect_left(arr: List[int], value: int) -> int:
if _is_sorted(arr) == False:
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 not _is_sored(arr): でもいいかなと思いました

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 notにしてますね

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.

一応ですが、ここで配列を舐めるので線形時間かかりますね。
ご指摘のとおりですね。。。後から加えたのですが、意味なくなっちゃいますね。

あと、if not _is_sored(arr): でもいいかなと思いました 下ではif notにしてますね
有難うございます。ちょっと悩んだので、どっちのパターンも書いてみました。
選択の根拠は、みやすさ以外特に現状ないです。

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.

Odaさん
ありがとうございます。bisect_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.

ここで配列内の全要素見てるので、下で二分探索する意味が無い気がします。

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.

ご指摘ありがとうございます。そのとおりですね。元々のbisectになかったので、後から良かれと思って追加しましたが、意味無くなっちゃいますね。以後気をつけます。

raise ValueError("Input array is not sorted!")

left, right = 0, len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] >= value: # 等号を外した場合(else側に等号を持っていった場合)、bisect_rightになる。
right = mid
else:
left = mid + 1
return left

return bisect_left(nums, target)
```

### 閉区間
```python
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
def _is_sorted(arr: List[int]) -> bool:
for i in range(1, len(arr)):
if arr[i - 1] > arr[i]:
return False
return True

def bisect_left(arr: List[int], value: int) -> int:
if not _is_sorted(arr):
raise ValueError("Input array is not sorted!")

left, right = 0, len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] >= target:
right = mid - 1
else:
left = mid + 1
return left

return bisect_left(nums, target)
```