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
209 changes: 209 additions & 0 deletions problem43/memo.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
## 取り組み方
- step1: 5分以内に空で書いてAcceptedされるまで解く + テストケースと関連する知識を連想してみる
- step2: 他の方の記録を読んで連想すべき知識や実装を把握した上で、前提を置いた状態で最適な手法を選択し実装する
- step3: 10分以内に1回もエラーを出さずに3回連続で解く

## step1
numsを前半(末尾の数より大きい部分)と後半(末尾の数以下の部分)に分け、
targetが末尾より大きい場合は、前半を探して、targetが末尾以下なら後半を探す。

後半の開始を返す関数と、与えられたnumsの範囲にtargetがあるかを探す関数を用意する方針で実装する。
エッジケースとして考えたいのは、

- ソートされたものが回転されていない場合 -> これは今回対応しない
- 全て末尾以下の数になる場合
- 重複したnumがある

```py
class Solution:
def search(self, nums: List[int], target: int) -> int:
def search_min_index() -> int:
if nums[0] < nums[-1]:
return 0
left = 0
right = len(nums)

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

def search_target_index(left: int, right: int) -> int:
while left < right:
mid = (left + right) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid
return -1

min_index = search_min_index()
if min_index == 0:
return search_target_index(0, len(nums))
if target > nums[-1]:
return search_target_index(0, min_index)
else:
return search_target_index(min_index, len(nums))
```

## step2
### 読んだコード
- https://github.com/sakupan102/arai60-practice/pull/44/files
- https://github.com/fhiyo/leetcode/pull/44/files
- https://github.com/fuga-98/arai60/pull/43/files
- https://discord.com/channels/1084280443945353267/1233295449985650688/1239594872697262121

### 感想
- ソート順を定義して1回の二分探索で解く方法もあったが、個人的には課題を分解して2回の二分探索を行う方が自然な操作に思えた
- とはいえ、探索順を再定義するという発想は役に立ちそうな気もする
- 2つ目の二分探索の処理から求めたいindex or -1を返す方法の選択肢もいくつかあったが、今の書き方でも「回転されていない配列(or 360度*整数の回転)の対応をしていること」や「targetがどちらに属するかでどう処理を変えるか」が伝わりやすいので良いかとも思った
- step1の実装だと、targetが複数あったときに、規則を持ったindexを返せない構造になっているので、一番左のindexを返すor右を返すようにした方が汎用性が高そう

### step1の改良
- targetが複数あったときに、左端を返すように改良
- numsが空のケースを弾く

```py
class Solution:
def search(self, nums: List[int], target: int) -> int:
def search_min_index() -> int:
if nums[0] <= nums[-1]:
return 0
left = 0
right = len(nums)
while left < right:
mid = (left + right) // 2
if nums[mid] > nums[-1]:
left = mid + 1
else:
right = mid
return left

def search_target_index(left: int, right: int) -> int:
while left < right:
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1
else:
right = mid
if left < len(nums) and nums[left] == target:
return left
return -1

if nums is None:
Copy link

Choose a reason for hiding this comment

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

  • numsが空のケースを弾く

空が空配列のことなら弾けてないですね。Noneと[]は別物なので。

$ ipython
Python 3.11.3 (main, Jun  5 2023, 00:49:13) [Clang 14.0.3 (clang-1403.0.22.14.1)]
Type 'copyright', 'credits' or 'license' for more information
IPython 8.14.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: [] is None
Out[1]: 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.

ご指摘、検証ありがとうございます。
空配列とNoneを混同していました。

以下のようにすべきですね。

    if nums is None:
        raise TypeError("Input cannot be None")
    if len(nums) == 0:
        raise ValueError("Input cannot be empty array")

Copy link

Choose a reason for hiding this comment

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

numsの型がList[int]と書いてある (List[int] | None ではない) のでNoneかどうかだけわざわざチェックするのはちょっと違和感ありました。じゃあnumsがfloatとかdictのときとかをスルーしてるの何でだろうと思うので。
自分があえてやるなら not isinstance(nums, list) とか type(nums) is not list でTypeError出すかなぁと思いました (サブタイプも受け入れたいから前者かな)。

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がfloatとかdictのときとかをスルーしてるの何でだろうと思うので。

おっしゃる通りです。
今回は、if len(nums) == 0:だけしておけばよさそうですね。

自分があえてやるなら not isinstance(nums, list) とか type(nums) is not list でTypeError出すかなぁと思いました (サブタイプも受け入れたいから前者かな)。

確かに、型が一致するか(サブタイプ含めて一致するかみるならtype、そうでないならisinstanceを選ぶ)を見る方が、適切ですね。

return -1
min_index = search_min_index()
if min_index == 0:
return search_target_index(0, len(nums))
if target > nums[-1]:
return search_target_index(0, min_index)
else:
return search_target_index(min_index, len(nums))
```


### 1回の二分探索
FFFFTTTとなる規則を作って、境界を見つける二分探索の問題として捉える。
規則の考え方は、targetとnums[-1]の位置関係で場合分けし、

- 前半の山にtargetがあると予想される時は、numが前半にいるときはtargetとの大小関係を見る必要があり、後半は全てTrueとして返す
- 後半の山にtargetがあると予想される時は、numが前半にいるときは全てFaelseとして返し、後半は大小関係を見る必要がある

のように考えれば良い。

```py
class Solution:
def search(self, nums: List[int], target: int) -> int:
def is_in_target_range(num: int) -> bool:
if nums[-1] < target: # targetが前半の山にいると思われる
if nums[-1] < num: # numが前半の山にいる
return target <= num
else: # numが後半の山にいる
return True
if target <= nums[-1]: # targetが後半の山にいると思われる
Copy link

Choose a reason for hiding this comment

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

else: にしてもよいと思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。
確かにこれくらいの距離ならelseでも条件を覚えておけそうですね。

if nums[-1] < num: # numが前半の山にいる
return False
else: # numが後半の山にいる
return target <= num

left = 0
right = len(nums)
while left < right:
mid = (left + right) // 2
if is_in_target_range(nums[mid]):
right = mid
else:
left = mid + 1
if left < len(nums) and target == nums[left]:
return left
return -1
```

### 1回の二分探索をさらに工夫したもの
(numが後半の山に属するか、numがtarget以上か)というタプルについて考えると、

1. numが前半の山に属し、target未満 -> (False, False)
2. numが前半の山に属し、target以上 -> (False, True)
3. numが後半の山に属し、target未満 -> (True, False)
4. numが後半の山に属し、target以上 -> (True, True)

のように辞書順でソートされる。
また、bisect_left が 二つの辞書順を考慮した以下のような二分探索ができる。

```py
data = [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'c'), (3, 'a')]
bisect_left(data, (2, 'b')) # 結果: 3
```

この二つを組み合わせて、実装できる。
初見はよくわからなかったが、ここまで整理できた段階でみると、
bisect_left自然な実装な気もして来た。

```py
class Solution:
def search(self, nums: List[int], target: int) -> int:
def get_priority(num: int) -> Tuple[bool, bool]:
"""最小値未満、以上で前半と後半のソート済み配列とした時、(後半に属するか、target以上か)を返す。
具体的な使用イメージは以下。
1. numが前半に属し、target未満 -> (False, False)
2. numが前半に属し、target以上 -> (False, True)
<- targetが前半ならこれを満たす初めを得る
3. numが後半に属し、target未満 -> (True, False)
4. numが後半に属し、target以上 -> (True, True)
<- targetが後半ならこれを満たす初めを得る
"""
return (num <= nums[-1], target <= num)
Copy link

Choose a reason for hiding this comment

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

あんまり考えていないのですが、これ、単に
(num <= nums[-1], num)
でもいいですかね。

Copy link
Owner Author

Choose a reason for hiding this comment

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

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

これ指摘されて今気づけました。

前半と後半のどちらにいるかを分けた後に、target以上になる位置を探したいので、
(num <= nums[-1], num)で問題ないですね。


priority_target = get_priority(target)
i = bisect_left(nums, priority_target, key=get_priority)
if i < len(nums) and nums[i] == target:
return i
return -1
```

## step3
```py
class Solution:
def search(self, nums: List[int], target: int) -> int:
def is_in_target_range(num: int) -> bool:
if target > nums[-1]: # targetが前半に存在
if num > nums[-1]: # numが前半に存在
return num >= target
else: # numが後半に存在
return True
if target <= nums[-1]: # targetが後半に存在
if num > nums[-1]: # numが前半に存在
return False
else: # numが後半に存在
return num >= target

candidate_index = bisect_left(nums, True, key=is_in_target_range)
if candidate_index < len(nums) and nums[candidate_index] == target:
return candidate_index
return -1
```