Skip to content
Open
Show file tree
Hide file tree
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
146 changes: 146 additions & 0 deletions 300-Longest-Increasing-Subsequence/note.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# 300. Longest Increasing Subsequence

https://leetcode.com/problems/longest-increasing-subsequence/

## step1(まず通す)

最初、

- `subsequence = []` を用意する
- リストの要素を前から見ていき、今見ている要素が
- subsequence の最後の要素より大きいなら末尾につけ加える
- subsequence の末尾より小さくて、その一つ前(後ろから2番目) より大きいなら subsequence の末尾と今見ている値を入れ替える

でできそう?と思ったが、`nums = [10,11,12, 1, 2, 3, 4]` で破綻する。(↑だと 3 になるが実際は 4)

他で思いついたのは、再帰的なやり方で

- リストが長さ1ならそのリストがLIS
- リストの長さが2以上のとき、
- もし最後の要素 > 「残りの部分におけるLIS」の最後の要素 → 「」と最後の要素を結合したものがLIS
- そうでないなら、「残りの部分におけるLIS」がもとの配列自体のLIS

ただこれも最初のものと同じで、成り立っていない。

```python
# 成り立っていない
class Solution:
def get_lis_len_and_end_num(self, nums: list[int]) -> tuple[int, int]:
if len(nums) == 1:
return 1, nums[0]

end_num = nums.pop()
rest_lenght, rest_end = self.get_lis_len_and_end_num(nums)
if rest_end < end_num:
return rest_lenght + 1, end_num
else:
return rest_lenght, rest_end

def lengthOfLIS(self, nums: list[int]) -> int:
length_of_lis, _ = self.get_lis_len_and_end_num(nums)
return length_of_lis
```

答えを見る。

> dp[i] を次のように定義します。
> dp[i] = nums[i] を最後の要素とする増加部分列の最大長
> nums[i] より前にある要素をすべて確認し、nums[j] < nums[i] なら、nums[j] で終わる部分列の後ろに nums[i] を追加できます。
> したがって dp[i] = max(dp[i], dp[j] + 1) となります。

上で考えていた方法は、「残りの部分」を見る時に暫定的な LIS だけを保持して他の情報を捨てていたのがよくなさそう。今一番長い列が将来も有利とは限らない、ということを理解できていない考え方だった。

ChatGPTによる解説

```
例の途中までを見ます。

[10, 11, 12, 1, 2, 3]

この時点では、長さ3の増加部分列が複数あります。

[10, 11, 12] 長さ3、末尾12
[1, 2, 3] 長さ3、末尾3

どちらも長さは同じですが、将来伸ばしやすいのは末尾が小さい [1, 2, 3] です。

次に 4 が来た場合、

[10, 11, 12] + 4 → 追加できない
[1, 2, 3] + 4 → [1, 2, 3, 4]

となります。

前半の再帰コードは、最初に [10, 11, 12] を選んだ時点で、後から始まる [1]、[1, 2]、[1, 2, 3] という可能性を捨てています。

## 「最長のものだけ覚える」では情報が足りない

コードが再帰から返している情報は、

(length_of_lis, そのLISの末尾)

の1組だけです。

しかし本当に必要なのは、たとえば次のような複数の候補です。

長さ1なら、末尾をどこまで小さくできるか
長さ2なら、末尾をどこまで小さくできるか
長さ3なら、末尾をどこまで小さくできるか
...

[10, 11, 12, 1] を処理した時点では、

長さ1の候補: [1] 末尾1
長さ2の候補: [10, 11] 末尾11
長さ3の候補: [10, 11, 12] 末尾12

という情報を残す必要があります。

長さ3の候補だけを見れば [10, 11, 12] が最長ですが、次に 2 が来たときは、長さ1の [1] が重要になります。

[1] + 2 → [1, 2]

さらに 3, 4 が来ることで、最終的に長さ4になります。
```

## step2(整形&他の人のコードを読む)

> うーん、読みにくいと思ったんですが、理由が
> lengths
> の意味がパズルになっているからだと思いました。
>
> 日本語で長々と書くと、「lengths[i] とは、仮に nums[i] がシーケンスの最後であると仮定した場合に可能な、最も長いシーケンスの長さ」ですよね。
>
> まあ、「長さ(複数)」であることには間違いないですが、「長さ」とだけいわれて、ああ「仮に nums[i] がシーケンスの最後であると仮定した場合に可能な、最も長いシーケンスの長さ」ってことね、とならず、それを推測するパズルになっています。
>
> これを前提とすると、内側のループは、関数にすることができるはずで、「i よりも左で、nums[i] 未満で終わる最大のシーケンスの長さ」を返させればいいですね。

https://discord.com/channels/1084280443945353267/1200089668901937312/1209563502407065602

step1 の解説を読むと、

> しかし本当に必要なのは、たとえば次のような複数の候補です。
> 長さ1なら、末尾をどこまで小さくできるか
> 長さ2なら、末尾をどこまで小さくできるか
> 長さ3なら、末尾をどこまで小さくできるか

といいながら、末尾ごとに最大長さを持っている = 長さの重複があって情報が冗長、という感じがする。

素直に、「長さごとの最小末尾要素」を持つようにするとどうなるか。

> 2番の解法は、上のリンクで書いた私の考えに近いものです。
>
> 2番というのは、
> 頭から舐めていって k 番目まで見たときに、
> 「nums[k] までを使って、すべての長さ1のシーケンスを考えたときに、そのシーケンスの一番お尻の最小値」
> 「nums[k] までを使って、すべての長さ2のシーケンスを考えたときに、そのシーケンスの一番お尻の最小値」
> 「nums[k] までを使って、すべての長さ3のシーケンスを考えたときに、そのシーケンスの一番お尻の最小値」
> ……
> というものです。これは昇順に並んでいるはずで、nums[k + 1] を使って、これを更新することは可能ですね。
>
> これは、sorted array なので二分探索で、どこを更新するかは絞れます。

https://discord.com/channels/1084280443945353267/1200089668901937312/1209563502407065602

## step3(10分以内にさっとかける \* 3回)
11 changes: 11 additions & 0 deletions 300-Longest-Increasing-Subsequence/step1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
length_from_begin = [1 for i in range(len(nums))]
for i in range(len(nums)):
for j in range(i):
if nums[i] > nums[j]:
length_from_begin[i] = max(
length_from_begin[i], length_from_begin[j] + 1
)

return max(length_from_begin)
14 changes: 14 additions & 0 deletions 300-Longest-Increasing-Subsequence/step2-bin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from bisect import bisect_left


class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
smallest_end_for_lis_lentgh: list[int] = []

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

自分なら length_to_min_tails と名付けるのですが、好みの問題かもしれません。

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.

ありがとうございます
自分でも dict だとそのような key_to_value 型の命名をよくするので、それと同じようなものだと思えばそう名づけるのも良さそうです

for i in range(len(nums)):
length_to_update = bisect_left(smallest_end_for_lis_lentgh, nums[i])
if length_to_update == len(smallest_end_for_lis_lentgh):
smallest_end_for_lis_lentgh.append(nums[i])
else:
smallest_end_for_lis_lentgh[length_to_update] = nums[i]

return len(smallest_end_for_lis_lentgh)
16 changes: 16 additions & 0 deletions 300-Longest-Increasing-Subsequence/step2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
lis_length_ending_at = [1 for i in range(len(nums))]

def max_extendable_lis_length_before(index: int) -> int:
max_length = 0
for i in range(index):
if nums[i] < nums[index]:
max_length = max(max_length, lis_length_ending_at[i])

return max_length

for i in range(len(nums)):
lis_length_ending_at[i] = max_extendable_lis_length_before(i) + 1

return max(lis_length_ending_at)
14 changes: 14 additions & 0 deletions 300-Longest-Increasing-Subsequence/step3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from bisect import bisect_left


class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
smallest_end_for_lengths = []
for i in range(len(nums)):
length_to_update = bisect_left(smallest_end_for_lengths, nums[i])
if length_to_update == len(smallest_end_for_lengths):
smallest_end_for_lengths.append(nums[i])
else:
smallest_end_for_lengths[length_to_update] = nums[i]

return len(smallest_end_for_lengths)