Skip to content

Conversation

@Kaichi-Irie
Copy link
Owner

問題へのリンク

300. Longest Increasing Subsequence

言語

Python

自分の解法

step1

class Solution:
    def lengthOfLIS(self, nums: list[int]) -> int:
        if not nums:
            return 0

        max_lengths_so_far = [1] * len(nums)

        for i in range(1, len(nums)):
            length = 0
            for j in range(i):
                if nums[j] < nums[i]:
                    length = max(length, max_lengths_so_far[j])
            max_lengths_so_far[i] = length + 1
        return max(max_lengths_so_far)
  • 変数名がうまくつけられなかった
  • 時間計算量も最適でない

numsの要素数をnとすると、

  • 時間計算量:O(n^2)
  • 空間計算量:O(n)

step2

class Solution:
    def lengthOfLIS(self, nums: list[int]) -> int:
        if not nums:
            return 0

        # max_length[i] is the maximum length of increasing subsequences that ends at nums[i]
        max_lengths = [1] * len(nums)
        for i in range(len(nums)):
            max_previous_length = 0
            for j in range(i):
                if nums[j] < nums[i]:
                    max_previous_length = max(max_previous_length, max_lengths[j])
            max_lengths[i] = max_previous_length + 1
        return max(max_lengths)

step3

step3_bruteforce.py

class Solution:
    def lengthOfLIS(self, nums: list[int]) -> int:
        left_max_lengths = [1] * len(nums)
        for i in range(len(nums)):
            for j in range(i):
                if nums[j] < nums[i]:
                    left_max_lengths[i] = max(
                        left_max_lengths[i], left_max_lengths[j] + 1
                    )

        return max(left_max_lengths)
  • こっちはすらすら書ける

step3_binary_search.py

class Solution:
    def bisect_left(self, nums: list[int], target: int) -> int:
        if not nums:
            return 0
        left = -1
        right = len(nums)
        while right - left > 1:
            mid = (right + left) // 2
            if target <= nums[mid]:
                right = mid
            else:
                left = mid
        return right

    def lengthOfLIS(self, nums: list[int]) -> int:
        if not nums:
            return 0
        tails = []
        for num in nums:
            index = self.bisect_left(tails, num)
            if index == len(tails):
                tails.append(num)
                continue
            tails[index] = min(tails[index], num)
        return len(tails)
  • tailsは常に昇順にソートされているので、numtailsのどこに入るかを二分探索。これは覚えていないと書けない
  • left=-1, right=len(nums)から始めると、mid=-1mid=len(nums)になるのではないかと不安に思っていたが、while right - left > 1の条件でループするので、ループの中ではright - leftは常に2以上になるので、left < mid < rightが保証される。返り値はrightなので、len(nums)が返ることはある。
  • ちなみにbisect_rightの実装は
def bisect_right(nums: list[int], target: int) -> int:
    left = -1
    right = len(nums)
    while right - left > 1:
        mid = (right + left) // 2
        if target < nums[mid]:
            right = mid
        else:
            left = mid
    return right

step4 (FB)

別解・模範解答

min_tails_linear.py

class Solution:
    def lengthOfLIS(self, nums: list[int]) -> int:
        # min_tails[i] is the minimum number of the tails of increasing subsequences with length (i+1)
        if not nums:
            return 0
        min_tails = [nums[0]]
        for num in nums[1:]:
            if min_tails[-1] < num:
                min_tails.append(num)
                continue
            for i, tail in enumerate(min_tails):
                if num <= tail:
                    min_tails[i] = num
                    break
        return len(min_tails)
  • min_tails[i]は長さi + 1の増加部分列の最小の末尾要素を表す
  • 非常にエレガントだが、なぜこれで正しいのか直感的に理解するのは難しい
  • 時間計算量:O(n^2)
  • 空間計算量:O(n)
    • 最悪、numsが昇順にソートされている場合、tailsの長さはnになる

mins_tailsは常に昇順にソートされているので、nummin_tailsのどこに入るかを二分探索で探せる時間計算量はO(log n)になる

min_tails_binary_search_bisect.py

import bisect


class Solution:
    def lengthOfLIS(self, nums: list[int]) -> int:
        if not nums:
            return 0
        min_tails = [nums[0]]
        for num in nums[1:]:
            j = bisect.bisect_left(min_tails, num)
            if j == len(min_tails):
                min_tails.append(num)
            else:
                min_tails[j] = num
        return len(min_tails)
  • 時間計算量:O(log n)
  • 空間計算量:O(n)

想定されるフォローアップ質問

  • もし bisect_left ではなく bisect_right を使った場合、結果は変わりますか?変わる場合、どのような入力で変わりますか?変わらない場合、その理由は何ですか?
    • 本問では、bisect_leftbisect_right のどちらを使用しても結果は変わらない。なぜなら、求めるLISは"strictly increasing"であり、tails配列に同じ値が存在することはないからである。しかし、もし問題が"non-decreasing"なLISを求めるものであれば、bisect_rightを使用することで、同じ値を持つ要素がtailsに追加される可能性があり、結果が変わることになる。その場合はbisect_rightを使用することで、同じ値を持つ要素がLISに含まれることを許容することになる。
  • このアルゴリズムではLISの『長さ』しか分かりませんが、実際の部分列そのものを復元するには、どのような変更が必要になりますか?
    • このアルゴリズムでは、実際にはLISの「長さ」に加えて「末尾の要素」もわかる。そのため、末尾から

次に解く問題の予告

tails.append(num)
continue
tails[index] = min(tails[index], num)
return len(tails)
Copy link

Choose a reason for hiding this comment

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

読みやすいと思います。

class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
left_max_lengths = [1] * len(nums)
for i in range(len(nums)):
Copy link

@potrue potrue Sep 15, 2025

Choose a reason for hiding this comment

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

参考までに、このような書き方もあると思いました。

        for i in range(len(nums)):
            left_max_lengths[i] = max(
                [left_max_lengths[j] + 1 for j in range(i) if nums[j] < nums[i]],
                default=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.

レビューありがとうございます。
確かにその書き方も明瞭ですね。参考にさせていただきます。

# @lc code=start
class Solution:
def lengthOfLIS(self, nums: list[int]) -> int:
left_max_lengths = [1] * len(nums)
Copy link

Choose a reason for hiding this comment

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

leftだけだと少しわかりづらいかもしれないと思いました。個人的には、ending_index_to_LIS_lengthぐらいにしても良いと思いました。
このツリーが少し参考になるかもしれません。
https://github.com/h1rosaka/arai60/pull/33/files#r2312432424

Copy link
Owner Author

Choose a reason for hiding this comment

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

そうですね、このあたりの変数名は自分でもなかなか良いのが思いつかず、難しく感じていました。ツリーもありがとうございます。ending_index_to_LIS_length, end_index_to_lengthあたりが良いなと感じました。

return 0
min_tails = [nums[0]]
for num in nums[1:]:
j = bisect_left(min_tails, num)

Choose a reason for hiding this comment

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

好みだと思いますが、インデックスという意味ならここは i を使うかなと思いました。

Copy link
Owner Author

Choose a reason for hiding this comment

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

レビューありがとうございます。おっしゃる通り、jよりもi、更にindex_to_insertがよりベターですね。

return 0
subsequence_min_tails = []
for num in nums:
index_to_insert = bisect.bisect_left(subsequence_min_tails, num)

Choose a reason for hiding this comment

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

一文字変数より index_to_insert の方が読みやすく感じました。

if array[-1] < x:
return len(array)
left = -1
right = len(array) - 1
Copy link

Choose a reason for hiding this comment

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

right = len(array)

でないと、 len(array) が返らず、下の min_tails.append(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.

確かにその通りですね。ご指摘ありがとうございます。
配列のどの要素よりも大きい値を渡した時に、bisect_leftlen(nums)を返すべきですが、それを返せていませんね。修正します。

return max(max_lengths_so_far)
```

- 変数名がうまくつけられなかった
Copy link

Choose a reason for hiding this comment

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

max_lengths_so_far のことであれば、自分なら index_to_max_length と付けると思います。 list のインデックスから、その値を末尾としたときに最大の長さへのマッピング、というニュアンスをこめています。

Copy link
Owner Author

Choose a reason for hiding this comment

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

確かにわかりやすいと感じました。max_length_by_indexmax_length_ending_at_indexあたりもありかもしれません。
リストや辞書の命名に詰まったら*s(複数形)と、*to**by*(マッピング)をどちらも検討してみます。

if index == len(tails):
tails.append(num)
continue
tails[index] = min(tails[index], num)

Choose a reason for hiding this comment

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

勘違いだったら申し訳ないのですが、ここは上でも書かれているように tails[index] = 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.

確かにその通りですね。bisect_leftで求めたindexなので、tails[index] >= numが保証されていますね。ご指摘ありがとうございます。

@h1rosaka
Copy link

読みやすかったです。

`step3_binary_search.py`
```python
class Solution:
def bisect_left(self, nums: list[int], target: int) -> int:
Copy link

Choose a reason for hiding this comment

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

これでも回るコードになっているので OK と思います。読むときにはこう書いてくれる保証がないので、それよりも広いものを読めるようにしておく必要はあるでしょう。私が読み取る必要があると思っているのは以下です。

  1. 引数の制約はなにか。
  2. ループの不変条件はなにか。
  3. middle の制約はなにか。
  4. 更新によって不変条件が守られるか。
  5. ループの終了条件とその時に欲しいものが見つかっているか。
  6. 有限回で終了するか。

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants