Skip to content
Open
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
191 changes: 191 additions & 0 deletions medium/153/answer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Step1

かかった時間:21min

計算量:nums.length=Nとして
Copy link

Choose a reason for hiding this comment

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

細かい点ですが、 N = num.length の順番のほうが違和感が少なく感じました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
Nを定義しているわけですから確かに左に来て欲しいですね。


時間計算量:O(logN)

空間計算量:O(1)

2分探索
```python
class Solution:
def findMin(self, nums: List[int]) -> int:
def is_index_between_min_val_and_last_val(i: int) -> bool:
return nums[i] <= nums[-1]

left = 0
right = len(nums)
while left < right:
middle = (left + right) // 2
if is_index_between_min_val_and_last_val(middle):
right = middle
else:
left = middle + 1

return nums[left]
```
思考ログ:
- 35.Search Insert Positionと同じ感じで、半開区間で書いてみた
Copy link

Choose a reason for hiding this comment

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

この「半開区間」もなかなか微妙で、何を探しているのかに対して意味が違うし、さらに左右の差もあるんですよ。

右が開いているのだとしても、
[0, 1, 2, 3, 4] に対して、2 を探しているのか、初めて2以上になる境界を探しているのかで、
前者ならば right は 3 で終了っぽいが、後者ならば、境界をその右側で表現して 2 で終了としたりします。

なので、とりあえず、半開と書いて通じた気になっているのよく分からないのですね。

Copy link

Choose a reason for hiding this comment

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

たとえば、理情の先輩方(同期同士)でも同じ半開区間と呼んでいてこんな感じ。左右のどっちを開いているかも違います。

cafelier (kinaba)
https://topcoder-g-hatena-ne-jp.jag-icpc.org/cafelier/20120703/1341324546.html

  int L = 0;
  int R = 10_0000_0000;
  while( R-L > 1 )
  {
     int C = (L+R)/2;
     (query(C) ? R : L) = C;
  }
  return R;

mametter
https://mametter.hatenablog.com/entry/20161117/p1

  l, r = 0, a.size
  while l < r
    m = l + (r - l) / 2
    if a[m] < c
      l = m + 1
    else
      r = m
    end
  end
  l

Copy link

Choose a reason for hiding this comment

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

まず何を探しているのでしょうか。
たとえば、
[0, 0, 1, 1, 2, 2] で 1 を探すのだったら何が欲しいのか。
2が欲しい場合、3が欲しい場合、4が欲しい場合、2-3どれかが欲しい場合。
あたりがあります。
それと、[0, 2] で 1 を探すのだったら何が欲しいのか。
-1 やエラーなどが欲しい場合。1 が欲しい場合。

Python の bisect_left は (2, 1)。bisect_right は (4, 1)。
Java の binarySearch は (2-3, ~insertion point)
C++ の upper_bound は (4, 1)。lower_bound は (2, 1)。

その上で、値なり境界なりをどう表現するのか、あることが確定したほうかないことが確定したほうか、境界ならば、多くは境界の右側でしょうが。だいたいここに4通りくらいあります。

Copy link

Choose a reason for hiding this comment

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

left = -1 から始まるパターンは、ここではあんまり見ませんね。ただ、そういう方法もあります。

Copy link
Owner Author

Choose a reason for hiding this comment

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

この「半開区間」もなかなか微妙で、何を探しているのかに対して意味が違うし、さらに左右の差もあるんですよ。

こちらはすみません、左が閉じていることを前提にした書き方になってました。
正しくは右半開区間ですね。

右が開いているのだとしても、
[0, 1, 2, 3, 4] に対して、2 を探しているのか、初めて2以上になる境界を探しているのかで、
前者ならば right は 3 で終了っぽいが、後者ならば、境界をその右側で表現して 2 で終了としたりします。

確かに思考のプロセスのコメントから、大事な部分が抜けているように思います。

色々な人の過去ログを読んで、以前よりは解像度が高くなった気はしているのですが、まだ自信が持てないので
もう少し詳細に考えていることを書いてみます。
お手数ですが、違和感がある部分をご指摘いただけると幸いです。

(以下は、色々と設定の仕方があると思いますが、今回はこのような方法で探索すると私が決めました)

まず、今回は最小値があるインデックスを探しています。
leftは探索範囲の左側でその場所を含みます。
rightは探索範囲の右側でその場所を含みません。

ピボット(middle)が指す要素が条件を満たすかどうかを判定していきます。
条件は、”最小値があるインデックスか、最小値があるインデックスより右のインデックス”にしました。
探索範囲の補集合について補足すると、leftより左(leftを含まない)は上記の条件を満たさない集合、right以上は上記の条件を満たす集合となっています。

探索範囲を上記条件を満たすように狭めていきます。
ピボットを(left + right) // 2で決めているので、毎回left <= middle < rightとなり探索区間は狭くなります。
left < rightの条件でループしているので、最終的にleft == rightとなってループを抜け、以下のような形で止まります。
(F, F, ..., F, [T), T, T, ..., T)

Copy link
Owner Author

Choose a reason for hiding this comment

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

設定を変えて、最小値があるインデックスの一つ前の場所を境界として返す場合は、一番右にあるFのインデックスを取ってくるように設計すれば良いと思います。

開区間で書くと以下のような感じでしょうか。
下のケースでは、left=2, right=3で止まり、left以下はF、right以上はTとなっています。
rightを取ってくれば、上で考えた最小値のインデックスを取得する問題に対応します。

nums = [3, 4, 5, 1, 2]
# nums = [1, 2, 3, 4, 5]

left = -1
right = len(nums)
while right - left > 1:
    middle = (left + right) // 2
    if nums[middle] <= nums[-1]:
        right = middle
    else:
        left = middle

print(left, nums[left]) 

Copy link
Owner Author

@TORUS0818 TORUS0818 Mar 16, 2025

Choose a reason for hiding this comment

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

left = -1 から始まるパターンは、ここではあんまり見ませんね。ただ、そういう方法もあります。

実は恥ずかしながら、この取り組みを始める前までは、(discordでもたまに出てくる)めぐる式2分探索という方法を(一応理解したつもりで)テンプレート化して脳死状態で2分探索を書いておりました。。

ただ、-1でインデックスアクセス出来てしまうのが何となく気持ち悪くなり、最近は右半開区間で書くことが多くなりました。

- 絞っていく条件がやや分かりにくいので、少し整理してみる
- numsを回転させて昇順にしたものを[a1, a2, a3, ..., an]とする(つまりai<aj(i<j)を満たす)
- 与えられた初期状態のnumsは[ak, ..., an, a1, ..., a(k-1)]
- [ak, ..., an]を領域1、[a1, ..., a(k-1)]を領域2とする
- middle(=(left + right) // 2)が領域1にあれば探索範囲はそこより右
- middleが領域2にあれば探索範囲はそこより左
- 領域1、2の判別は
- nums[middle] > nums[-1]なら領域1
- nums[middle] <= nums[-1]なら領域2
- あとは落ち着いてleftじゃなくてnums[left]を返す
- 何が目的だったか頭に置いておくのは結構大事だと思う

min()
```python
class Solution:
def findMin(self, nums: List[int]) -> int:
return min(nums)
```
思考ログ:
- 特別な要件でもない限りはこれでいい気がする

線形探索
```python
class Solution:
def findMin(self, nums: List[int]) -> int:
if nums[0] <= nums[-1]:
return nums[0]

for i in range(len(nums) - 1):
if nums[i] > nums[i + 1]:
return nums[i + 1]

raise Exception('unreachable.')

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.

seal-azarashi/leetcode#21 (comment)

以前、上記のような議論がありまして、ここに到達しないことが一目で分かるようにこのようにしました。

Choose a reason for hiding this comment

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

勉強になりました、ありがとうございます。
Javaだとコンパイルされるので必須ということだと思うんですが、Pythonでもたしかにそのほうが分かりやすいですね。

Copy link

Choose a reason for hiding this comment

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

Unreachable なところに raise を書くことですが、ありかもしれませんが、私はそこまで肯定的ではないです。結構微妙なところだと思います。

まず、一般的に、dead code は避けるものです。
また、Python の場合、返り値があって到達する場合はあってもなくても同じだが return None を書き、到達しない場合は書かないことで、unreachable かの意図は表現されるはずです。
それでは弱く、よほど気になるならば、コメントを一つ書いておくくらいが適切かもしれません。

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の要素の制約から、単調増加しなくなったポイントが最小値
- 既にソート済みの場合は全部探索してもこの条件に当てはまらないので、事前に判別した
- 要素が1つの場合もソート済みの特別な場合として含めた(ので```nums[0] <= nums[-1]```となっている)
- ただ狭義単調増加という条件を考えると、この等号は混乱の元になりそうなのでコメントを書いておくのがベターか

# Step2

講師役目線でのセルフツッコミポイント:
- 空配列への対処について

参考にした過去ログなど:
- https://github.com/hroc135/leetcode/pull/40
- https://github.com/Ryotaro25/leetcode_first60/pull/46
- チェックリスト、脳内で回してみる
> A 「2で割る処理がありますがこれは切り捨てでも切り上げでも構わないのでしょうか。」
> ○ middle = left + (right - left) / 2;
> × middle = left + (right - left + 1) / 2;
> B 「nums[middle] <= nums[right] とありますが、これは < でもいいですか。」
> ○ nums[middle] <= nums[right]
> × nums[middle] < nums[right]
> C 「nums[right] は、nums.back() でもいいですか。」
> ○ nums[right]
> × nums.back()
> D 「right の初期値は nums.size() でもいいですか。」
> ○ right = nums.size() - 1
> × right = nums.size()
- Aが×は全部ダメ
- (B, D) = (×, ×)もダメ
- 要素が1つのnumsを考えればいい
- (B, D) = (×, ○)はループに入らないので正しく動くのだが、個人的にはBは<=で持ちたい
- (C, D) = (○, ×)もダメ
- これ見落としていたが、ループの一回目で範囲外エラーになる
- (B, D)と(C, D)のNGパターンに重複((B, C, D) = (×, ○, ×))があることに注意すると、2^4 - 2^3 - (2 + 2 - 1) = 5通りがOK
- https://github.com/seal-azarashi/leetcode/pull/39
- https://github.com/Mike0121/LeetCode/pull/44
- 再帰の解法
- https://github.com/Yoshiki-Iwasa/Arai60/pull/35
- https://github.com/Yoshiki-Iwasa/Arai60/pull/35/files#r1699552857
- https://github.com/fhiyo/leetcode/pull/43
> 右端より大きい値で一番右側にあるものを探すと考えることもできる
> 右端とではなく、左端との比較とすることもできる。自分は最初できないんじゃないかと思ったが、ソート済みの状態を別で考えれば `x < nums[0]` をクエリにすれば `[False, ..., True, ...]` となるようにできる。
- https://github.com/Kitaken0107/arai60/pull/1
- https://github.com/sakupan102/arai60-practice/pull/43
- https://github.com/goto-untrapped/Arai60/pull/24
- 匿名クラスというのを知らなかった
- https://github.com/YukiMichishita/LeetCode/pull/9
- https://github.com/thonda28/leetcode/pull/7
- https://github.com/shining-ai/leetcode/pull/42
- https://github.com/hayashi-ay/leetcode/pull/45

2分探索(左と比較ver)
```python
class Solution:
def findMin(self, nums: List[int]) -> int:
def is_index_between_first_val_and_max_val(i: int) -> bool:
return nums[0] <= nums[i]

left = 0
right = len(nums)
while left < right:
middle = (left + right) // 2
if is_index_between_first_val_and_max_val(middle):
left = middle + 1
else:
right = middle

return nums[0] if len(nums) == left else nums[left]

Choose a reason for hiding this comment

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

私なら、この除外処理はループの前に
if nums[0]<=nums[-1]:
return nums[0]
とします

求めるべき最小値とそれより右の要素をfalse、それより左の要素をtrueとした時、is_index_between...は最初のリストが真に回転されてる場合はそのtrue/falseと一致して、最初からソートされてる場合には違うものになるからです。

Copy link
Owner Author

Choose a reason for hiding this comment

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

ありがとうございます。
はい、分けて考えるのも一つだと思います。

```
思考ログ:
- https://github.com/fhiyo/leetcode/pull/43
- 左と比較しても良い
- 対称性がある解法に関しては、(自分にとって直感的に理解しやすい)片方が理解できていればまあいいか、というマインドだったが、いざ実際に手を動かしてみると意外と出来なかったりする
- コードを読めるようになるためにも、もう少し積極的に考えてみるのが良さそう
- 参照先のコードのように初手でソートの確認をするのが良いと思うが、step1との対比のために敢えてこうしてみた
- 左を開区間にする手もある
- 3項演算子使わずに分けて書いた方がいいかも

再帰
```python
class Solution:
def findMin(self, nums: List[int]) -> int:
def find_min_helper(left: int, right: int) -> int:
if nums[left] <= nums[right]:
return nums[left]

middle = (left + right) // 2
if nums[middle] > nums[-1]:
return find_min_helper(middle + 1, right)
else:
return find_min_helper(left, middle)

return find_min_helper(0, len(nums) - 1)
```
- helper関数を作らないで、配列を渡す形にもできる
- n<=5000と小さいので再帰上限も問題ない

# Step3

かかった時間:3min

```python
class Solution:
def findMin(self, nums: List[int]) -> int:
def is_index_between_min_val_and_last_val(i: int) -> bool:
return nums[i] <= nums[-1]

assert len(nums) > 0

left = 0
right = len(nums)
while left < right:
middle = (left + right) // 2
if is_index_between_min_val_and_last_val(middle):

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.

if nums[i] <= nums[-1]と書かれた場合に、それがtrueの場合は何を意味するのか、個人的には直感的でないと思ったので関数名に情報を入れました。

right = middle
else:
left = middle + 1

return nums[left]
```
思考ログ:
- チェックリストの思考実験で少し理解が深まった気がする

# Step4

```python
```
思考ログ: