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
215 changes: 215 additions & 0 deletions 競技プロ就活部PR用/300. Longest Increasing Subsequence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
参照:
https://github.com/TORUS0818/leetcode/pull/33#discussion_r1826882598
https://github.com/kazukiii/leetcode/pull/32#discussion_r1790191654
https://github.com/seal-azarashi/leetcode/pull/28#pullrequestreview-2344638688
https://github.com/Ryotaro25/leetcode_first60/pull/34#discussion_r1743810106
https://github.com/Yoshiki-Iwasa/Arai60/pull/46#discussion_r1716197814
https://github.com/Exzrgs/LeetCode/pull/18
https://github.com/rossy0213/leetcode/pull/15#discussion_r1611781523
https://github.com/goto-untrapped/Arai60/pull/18
https://github.com/shining-ai/leetcode/pull/31
https://github.com/hayashi-ay/leetcode/pull/27/commits/b53ce7bfa1c3cf30970c94356aab268597e70fea
https://discord.com/channels/1084280443945353267/1200089668901937312/1209827519352668170

外部参照:
https://ei1333.github.io/luzhiled/snippets/dp/longest-increasing-subsequence.html


類題:
1235. Maximum Profit in Job Scheduling
https://leetcode.com/problems/maximum-profit-in-job-scheduling/description/
962. Maximum Width Ramp
https://leetcode.com/problems/maximum-width-ramp/description/


## DPによる解法
### 1回目 (14m52s)
時間計算量: O(N**2)
空間計算量: O(N)

* 最初全探索を考えたが、subsequence(連続とは限らない)ため、再帰的に解く必要があり計算量が膨大(O(2^n))になる。
* DPとして解く際、値に何を入れるか考え、言語化すると"ある地点leftまでにできる最大の増加部分列の長さ"。

```python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
num_subsequences = [1] * len(nums)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

num_subsequences という変数名は、サブシーケンスの数というニュアンスに感じました。 最大の増加部分列の長さという意味に近づけるため、 max_subsequence_lengths または max_lengthsはいかがでしょうか?

Copy link
Copy Markdown
Owner Author

@Mike0121 Mike0121 Feb 23, 2025

Choose a reason for hiding this comment

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

@nodchip さん
ありがとうございます。max_subsequence_lengthsが個人的に良いと思いました。また、numは少しニュアンスが異なるのも同意です。

for left in range(len(nums) - 1, -1, -1):
for right in range(left, len(nums)):
if nums[left] < nums[right]:
num_subsequences[left] = max(num_subsequences[left], num_subsequences[right] + 1)

return max(num_subsequences)
```

### 2回目
* rightのループを始める時に、right == leftの場合の比較は不必要。
* num_subsequencesが最適解かはあまり自信がない。できれば"ある地点leftまでにできる最大の増加部分列の長さ"の要素を入れても良いと思ったが、長くなりすぎる。

```python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
num_subsequences = [1] * len(nums)

for left in range(len(nums) - 1, -1, -1):
for right in range(left + 1, len(nums)):
if nums[left] < nums[right]:
num_subsequences[left] = max(num_subsequences[left], num_subsequences[right] + 1)

return max(num_subsequences)
```


### 3回目
* 好みの問題であるが、for文の向きを逆にしても良いので3回目はそっちで書いてみる。
* 「数直線で考えたとき、右側が大きくなるよう、 nums[left] < nums[right] としたい」 by nodchipさん
(https://github.com/shining-ai/leetcode/pull/31/commits/c838d53b1643a92161bbc54f80fe6d5b3c5f6edf)
* 個人的にはfor文の進み方のせいかこっちの方がわかりやすかった。
Comment on lines +64 to +67
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, leftは小さい値からスタートするこちらの方が直感的だなと感じました。

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.

ありがとうございます、どちらでも良い場合は順方向がやはりイメージしやすいですね。


```python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
num_subsequences = [1] * len(nums)

for right in range(1, len(nums)):
for left in range(right):
if nums[left] < nums[right]:
num_subsequences[right] = max(num_subsequences[right], num_subsequences[left] + 1)

return max(num_subsequences)
```


## 二分探索(bisect_left)による解法
時間計算量: O(NlogN)
空間計算量: O(N)

ロジック:
MAX_INT = 10 ** 4 + 1とした時、numsと同等の長さの配列を下記の様に作成する。
increasing_subsequence = [10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1 ...]
ここに、各値を左から順にnums = [0,1,0,3,2,3]をbisect_leftで見つかったindexに代入していく。
n = 0
increasing_subsequence = [0, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1]

n = 1
increasing_subsequence = [0, 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1]

n = 0
increasing_subsequence = [0, 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1]

n = 3
increasing_subsequence = [0, 1, 3, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1]

n = 2
increasing_subsequence = [0, 1, 2, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1]

n = 3
increasing_subsequence = [0, 1, 2, 3, 10 ** 4 + 1, 10 ** 4 + 1, 10 ** 4 + 1]

最終的に、再度increasing_subsequenceに対してMAX_INTを代入した場合の位置(3とMAX_INTの境目)が
できるsubsequenceの最大の長さ。

最悪時間計算量は、O(NlogN)。二分探索(O(logN))をN回行う。

### 1回目
```python
from bisect import bisect_left
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
MAX_INT = 10 ** 4 + 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.

MAX_INT は、普通は int の最大値、Python2 では sys.maxint 3 では sys.maxsize C では INT_MAX のことだと考えるでしょう。違うものが入っているのは好ましくないです。

入力の制約を満たさない入力、異常な入力への対処

現実には、おそらく、このコードがプロダクションで使われていたところ、ある日、まったく違う事情でここにアルファベットでない文字が流れるようになって、予期せぬ動作をして、原因探しの旅に出てここに行き着くことになります。そのときにどのような動作をこのコードはしていますか。みたいな想像をしています。

https://docs.google.com/document/d/11HV35ADPo9QxJOpJQ24FcZvtvioli770WWdZZDaLOfg/edit?tab=t.0#heading=h.jdtk9v35bca4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

数字の場合は、だんだんいろいろな事情で大きな数字が入るようになってきて、気がついたら、ここにその数が流れてきて、10001 を超えていたという事が起きるでしょう。で、事故を起こして、顧客に代表が謝罪をしているときに、2年前にこのコードを書いたときには、10000までしか来ないって言われていたので、このコードは悪くありません、となるかということですね。

つまり、そういう可能性を見ながらコードを書けるかなのです。そういう風に話が変わったときにどれくらい柔軟であるべきかなども俎上に載せて計算して書いています。

もちろん、この入力が「年齢」ならば大きな問題はないでしょう。そこらへんは状況次第です。

ただ、たとえば、64ビット符号付き整数の最大値を限界にしておいたら、事故を起こしたときに、他のところも事故を起こしている可能性が高いので、テストなどに引っかかってここが原因になりにくいですね。

Copy link
Copy Markdown
Owner Author

@Mike0121 Mike0121 Feb 24, 2025

Choose a reason for hiding this comment

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

@oda さん
コメントありがとうございます。なるほどです、Leetcodeの入力制約で言っているから or とりあえず64ビット符号付き整数の最大値にしておく、ではなく考え方そのものがそこにあることが大事かなと感じました。頭に入れておいて、意識します。

increasing_subsequence = [MAX_INT] * len(nums)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

アルゴリズムの形は理解しているもの、それがなぜ正しく動いているのか、仕組みを理解していないような印象を受けました。念のため、なぜ正しく動いているか、仕組みを説明してみていただけますか?特に、リストのインデックスと、その位置の値がどのような関係性を持つかに着目して説明するとよいと思います。

また、仕組みを理解しているのであれば、 increasing_subsequence に代入される各要素が、 increasing_subsequence という変数名で表されるものとは異なるということが分かると思います。適切な名前を付け直してみていただけますか?

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.

ありがとうございます。
少し質問の理解に自信がなく("アルゴリズムの形"と"なぜ正しく動くか"の違い)、検討違いな答えになっていたらご指摘お願い致します。
リストのインデックスとその位置の値に関しては、リストの各インデックスの値は、increasing subsequenceの末尾として考えられる最小値、という理解です。
ロジック全体として、各値を元のリスト(nums)の順番通りに値: MAX_INTで初期化されたリストに入れて行った際、

  1. ある値nが部分列の末尾候補より小さいものがある:
    その位置にnを代入し、より小さい値で更新を行う。これで、より長いincreasing subsequenceを検討することが可能。
  2. ある値nが部分列のどの末尾候補より大きい:
    まだ更新されていないMAX_INTがある位置を返すため、新たな増加部分列(長さが1つ伸びたもの)の候補として、nを記録する。
    これを繰り返し、最終的に部分配列の最初のMAX_INTの位置(numsの値で更新された部分長さ + 1)が最終的に作ることが可能な最大のincresing_subsequenceの長さとなります。

ここまでを言語化して、increasing_subsequenceの変数名のほか候補としては、
min_end_values , 'min_last_vals'などかなと思いました。

間違っている部分もあるかもしれませんが、ご一読いただけますと幸いです。

Copy link
Copy Markdown

@nodchip nodchip Feb 23, 2025

Choose a reason for hiding this comment

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

前半部分のなぜ正しく動くかの部分は正しく理解できているように思いました。

後半部分の変数名については、悪くないように思いました。インデックスと、その値の対応関係が、より明確に変数名に表現されているとよいと感じました。例えば、リスト自体を 1-based に変更したうえで、 length_to_min_last_values としてしまうなどを考えました。

Copy link
Copy Markdown
Owner Author

@Mike0121 Mike0121 Feb 24, 2025

Choose a reason for hiding this comment

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

ありがとうございます、よかったです。
リスト自体を 1-based に変更したうえで、 length_to_min_last_valuesも良いなと思いました。また、appendしていく方法で、length_to_min_last_valuesと名前つけるのが一番直感的に変数名と合うのかなとも思いました。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

空のリストから始め、 bisect_left() の結果が len(increasing_subsequence) だった場合、末尾に要素を追加するという方法もあります。最後は return len(increasing_subsequence) することになります。入力によってはこちらの方法のほうがメモリ使用量が少なくなるため、良いと思います。


for n in nums:
index = bisect_left(increasing_subsequence, n)
increasing_subsequence[index] = n

return bisect_left(increasing_subsequence, MAX_INT)
```

### 2回目
* bisectを自前実装
* increasing_subsequenceはより良い名前がありそう。

```python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
def bisect_left_original(arr: List[int], val: int) -> int:
left, right = 0, len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] < val:
left = mid + 1
else:
right = mid
return left

MAX_INT = 10**4 + 1
increasing_subsequence = [MAX_INT] * len(nums)
for num in nums:
insert_index = bisect_left_original(increasing_subsequence, num)
increasing_subsequence[insert_index] = num

return bisect_left_original(increasing_subsequence, MAX_INT)
Comment on lines +134 to +152
Copy link
Copy Markdown

@olsen-blue olsen-blue Feb 23, 2025

Choose a reason for hiding this comment

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

bisect_leftの実装は良いと思いました。以前整理したことがあるので、こちら参考になるかもしれません。
https://github.com/olsen-blue/Arai60/blob/41171886bb6299943cdcdf8e92e08bdc2833580f/349.%20Intersection%20of%20Two%20Arrays.md#bisect_left

increasing_subsequenceは、[]で初期値にしておいて、appendしながら構築すると、最後の答えの値を len(increasing_subsequence)で取得できるので、こちらの方がシンプルかもしれません。ご検討ください。
(上でnodchipさんも同じこと言ってましたね...すみません。ご放念ください。)

Copy link
Copy Markdown
Owner Author

@Mike0121 Mike0121 Feb 24, 2025

Choose a reason for hiding this comment

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

ありがとうございます〜。全然、同じことでもその分皆さんが気になる点かと思うので、参考になります。復習時にこちらの方法で書いてみます。

```

### 3回目
* 変更なし

```python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
def bisect_left_original(arr: List[int], val: int) -> int:
left, right = 0, len(arr)
while left < right:
mid = (left + right) // 2
if arr[mid] < val:
left = mid + 1
else:
right = mid
return left

MAX_INT = 10 ** 4 + 1
increasing_subsequence = [MAX_INT] * len(nums)
for num in nums:
index = bisect_left_original(increasing_subsequence, num)
increasing_subsequence[index] = num

return bisect_left_original(increasing_subsequence, MAX_INT)
```

### 4回目
* icreasing_subsequenceを[]で初期化
* この場合は変数名は`icreasing_subsequence`で良いと思った。

```python
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
def bisect_left_original(val: int, arr: List[int]) -> int:
if not arr: return 0

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

increasing_subsequence = []
for num in nums:
index = bisect_left_original(num, increasing_subsequence)
if index > len(increasing_subsequence):
raise ValueError(f"Index should not exceed length of increasing_subsequence: {index} (max allowed: {len(increasing_subsequence)})")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

このエラー処理を入れた理由が気になりました。自分は不要ではないかと思いました。また、入れるとするなら index < 0 の場合はなぜ処理しないのかというツッコミどころもあると思いました。


if index == len(increasing_subsequence):
increasing_subsequence.append(num)
else:
increasing_subsequence[index] = num

return len(increasing_subsequence)
```

## セグメントツリーによる解法
一旦スキップ。