# 刷題筆記 - LeetCode 300. Longest Increasing Subsequence https://leetcode.com/problems/longest-increasing-subsequence ## Thoughts 給一個整數的 array `nums`,回傳最長的嚴格遞增子序列 subsequence != subarray 也就是說,有的數字是可以跳過的,只要最後回來的會是符合順序的序列 Example 1: >Input: nums = [10,9,2,5,3,7,101,18] Output: 4 Explanation: The longest increasing subsequence is [2,3,7,101], therefore the length is 4. Example 2: >Input: nums = [0,1,0,3,2,3] Output: 4 Example 3: >Input: nums = [7,7,7,7,7,7,7] Output: 1 以例子 1 來看:` [10,9,2,5,3,7,101,18]` 2 開頭,5 可以跳過,所以最長的子序列長度會是 4 - `[2,3,7,101]` or `[2,3,7,18]` ## Solution 1 - DP ### Logic 反過來思考 >如果以 `dp[i]` 為結尾的嚴格遞增子序列長度會是多少? 假設一個 DP array ``` dp = [1] * len(nums) ``` 初始值為 1,因為最短的子序列長度會是 1 (自己) \ 如果要檢查 **自己** 為結尾的子序列長度為何 那就是把自己的位置當作終點,從頭開始檢視(第二層迴圈: `0 ~ i`) 第二層迴圈的 index 假設用 `j`,第一層 index 假設用 `i` 如果 `nums[j] < nums[i]` => **表示 `nums[i]` 可以接在 `nums[j]` 後面遞增** 而 `dp[j]` 代表的是**以位置 `j` 的元素作為結尾的子序列長度** 若可以再增加一個 `nums[i]` 那就表示**以位置 `i` 的元素作為結尾的子序列長度,`dp[i]` = `dp[j] + 1`** => 即**之前的子序列長度加上我所在的這個元素(長度 1)** 邏輯可以寫成 ``` if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) ``` 因為第二層迴圈會去檢查 `i` 之前的所有位置,也就是會有多個 `j` 我們要選擇最長的那個子序列 ### Code ```python!= class Solution: def lengthOfLIS(self, nums: List[int]) -> int: dp = [1] * len(nums) for i in range(len(nums)): for j in range(0, i): if nums[j] < nums[i]: dp[i] = max(dp[i], dp[j] + 1) return max(dp) ``` 因 `dp[i]` 代表的是以該位置為結尾的子序列最大的長度,所以答案就是`max(dp)` ### Time & Space complexity - Time: O(n^2) - Space: O(n) ## Solution 2 - Binary Search ### Logic 如果照順序遍歷 `nums`,實際上做的事就是確認誰是最小的數字,加到某個 sub array 中,接下來看下一個數字是不是比之前的數字大,如果比之前的數字還小,就找位置替換掉它,比之前的數字大,就可以塞進 sub array e.g. ``` nums = [10, 9, 2, 5, 3, 7, 101, 18] ``` 維護一個 array `sub` ``` [10] → 加入 10 [9] → 9 替換 10(因為 9 比 10 小) [2] → 2 替換 9 [2, 5] → 5 > 2 → 塞進 sub [2, 3] → 3 替換 5 [2, 3, 7] → 7 > 3 → 塞進 sub [2, 3, 7, 101] → 101 > 7 → 塞進 sub [2, 3, 7, 18] → 18 替換 101 ``` 整個過程其實是維持 `sub` 的遞增性,去找可以插入 `nums[i]` 的地方 遍歷完整個 `nums`,`sub` 的長度就會是最長的嚴格遞增子序列 \ 所以重點在,遍歷 `nums` 時,我要怎麼找到正確的位置來替換原本 `sub` 中的元素,然後在什麼條件下,我可以將 `nums[i]` 塞進 `sub` 中? \ 這邊就要利用 Binary Search,找出目標 array 中,可以塞進 target number 的位置,也就是要找到第一個 array[x] >= target number 這邊放上 Python 的套件 `bisect.bisect_left` 的用法 ``` bisect.bisect_left(arr, target) ``` > 在排序好的 `arr` 中,回傳第一個 `>= target` 的位置 e.g. ```python! import bisect arr = [2, 3, 5, 7] print(bisect.bisect_left(arr, 4)) # 回傳 2 → arr[2] = 5, 為第一個 >= 4 的位置 print(bisect.bisect_left(arr, 5)) # 回傳 2 → arr[2] = 5, 為第一個 >= 5 的位置 print(bisect.bisect_left(arr, 8)) # 回傳 4 → 沒有任何元素 >= 8,回傳 len(arr) 也就是插入的位置為最後 ``` 因此過程就是 1. 遍歷整個 `nums`,利用二分搜尋法去找 `sub` 中,可以插入目前數字的位置 2. 若使用 `bisect_left` 回傳的值是 `len(sub)`,那就代表這個數字可以被塞進 `sub` 中 3. 若不符合 2.,代表回傳的值是**替換**的位置,就把 `sub` 中該位置換成現在的數字 ### Code ```python!= import bisect class Solution: def lengthOfLIS(self, nums: List[int]) -> int: sub = [] for num in nums: position = bisect.bisect_left(sub, num) # current number is larger than all number in the sub array if position == len(sub): sub.append(num) else: # replace the sub[position] with current number sub[position] = num return len(sub) ``` 如果不使用 `bisect`,自己寫 ```python!= class Solution: def lengthOfLIS(self, nums: List[int]) -> int: # Find the insertion point for target in arr to maintain sorted order # find a position x, where it is the 1st position that arr[x] >= target def binary_search_left(arr, target): l, r = 0, len(arr) while l < r: m = l + (r - l) // 2 if arr[m] < target: l = m + 1 else: r = m return l sub = [] for num in nums: position = binary_search_left(sub, num) if position == len(sub): sub.append(num) else: sub[position] = num return len(sub) ``` ### Time & Space complexity - Time: O(n * log(n)) - n: 遍歷 nums - log(n): 在 `sub` 中用 binary search 找目標位置,`sub` 最長長度為 n - Space: O(n) - 使用 `sub` 來儲存最長的嚴格遞增子序列,最長長度為 n