LeetCode #300 Longest Increasing Subsequence - 刷題之旅
1 題目描述
![]()
給定一個整數數組nums,找到一個最長的遞增子序列的長度。
2 解法
2.1 Recursion
這題是DP問題,假設我們有一組數字[1, 2, 3, 7, 4, 5, 6] 我們需要比較當前的值 nums[i]是否比上一個值 last還要大,當滿足條件的時候,我們就要思考一個問題到底是**要把當前的值考量進去,還是跳過當前的?**這就會牽涉到兩個公式,類似背包問題,偷與不偷,那這裡就是塞與不塞:
- 塞:
1塞(當前值) + 未來的最佳解 - 不塞:
不塞 + 未來的最佳解
以上述的例子來說,如果我們走到 7 的時候,我們可以選擇塞或不塞
- 如果塞的話,未來的最佳解都是 0,因為沒有比
7大的值了 - 如果不塞的話,後面還有
4, 5, 6可以選擇
這樣的話我們可以得出一個公式,假設i是當前的值:
- 塞:
1 + helper(nums[i+1:]) - 不塞:
helper(nums[i+1:])
但是還有一個問題需要考慮,就是我們在往後比較的時候,需要知道被比較的對象是誰,舉例來說一樣從7開始
- 塞:那後續與
4, 5, 6比較的就是7 - 不塞:那後續與
4比較的就是3,與5比較的就是4以此類推,會隨著i的變化而變化
而上面這個被後續比較的對象就是last,所以我們的公式就變成: - 塞:
1 + helper(nums[i], nums[i+1:]) - 不塞:
helper(last, nums[i+1:])
1 | def lengthOfLIS(self, nums: List[int]) -> int: |
2.1 Recursion with Memoization
上面的解法是遞迴,但是遞迴的問題就是會有很多重複的計算,所以我們可以用memoization來解決這個問題,這樣就不用重複計算了。我們從上面可以發現i是慢慢增長的,所以我們如果可以儲存不同長度且不同last時的最佳解,那就可以避免重複計算。
我們儲存dp[(last, i)]=max(take, not_take),這樣就可以知道在不同長度的狀況下,最佳解是多少。
1 | def lengthOfLIS(self, nums: List[int]) -> int: |
2.2 Iterative + Tabulation
接下來我們需要使用迭代的方式來解決這個問題。這題我是參考Youtube | Neetcode介紹得非常詳細。首先,他是一個拿與不拿的問題,因此我們使用比較簡單的例子[1,2,4,3]先畫個樹狀圖來看一下有哪些狀況,不用全部畫出來,我們就先繪製出第一個到底的狀況就好如下圖:
![]()
首先你會發現:
LTS[3]因為i=3已經是最底了,基本上從3往後看只會有自己一個長度,因此LTS[3]=1是無庸置疑的。LTS[2]因為i=2,所以有兩種可能max(1, 1+LTS[3])但是1+LTS[3]只有在nums[2]>nums[3]的情況下才會成立,所以仍然是LTS[2]=1LST[1]因為i=1後面有三種可能max(1, 1+LTS[2], 1+LTS[3])就有點像是max(自己, 自己+id2, 自己+id3),而i=2因為比後面的數字都還要小,所以自己+id2或自己+id3都成立。因此我們會得到max(1, 1+1, 1+1)=2LST[0]因為i=0後面有四種狀況max(1, 1+LTS[1], 1+LTS[2], 1+LTS[3]),而i=1因為比後面的數字都還要小,所以自己+id1或自己+id2或自己+id3都成立。因此我們會得到max(1, 1+2, 1+1, 1+1)=3
如果你繼續嘗試把其他可能都畫出來,你會發現都已經是重複的狀況:
![]()
從前述講的順序中,我們會發現可以從後面往前面推,然後使用雙層for迴圈,並且時間複雜度是O(n^2)。
最外層i是從n-1到0,內層j是從i+1到n,這樣就可以得到所有的狀況,並且可以得到最佳解。
![]()
最後程式碼如下,就解出來了:
1 | dp = [1]*len(nums) # 至少可以包含自己,一定會有最小值1 |
時間複雜度:O(n^2)
空間複雜度:O(n)
2.3 O(NlogN) Patience Sort
接下來題目要求試試看O(NlogN)其實就要使用Binary Search,但是在使用Binary Search之前,我們需要先了解一下一個超級神奇的演算法!叫做Patience Sort,他是一個卡牌遊戲,可以幫助我們找到最長的遞增子序列。我們先來介紹卡牌遊戲的規則:
首先,我們有一組卡牌,規則是:
- 如果當前的卡牌比piles的最後一張卡牌還要大,那就直接放到最後一張卡牌的後面
- 如果當前的卡牌比piles的最後一張卡牌還要小,那就要找到一張比當前卡牌還要大的卡牌,然後放在上面,如果找不到的話,就要開一個新的pile


最後你會發現,每個piles最上面的卡牌由左到右是遞增的
![]()
更神奇的事情發生了,這個piles的數量就是最長的遞增子序列的長度!因為每個piles一定可以找到一張牌可以排出遞減的牌組
![]()
![]()
所以我們用這個卡牌的遊戲規則其實跟我們的地目很像
- 有一個無序的卡牌
- 然後我們想要知道最長的遞增子序列
如果我們改一下題目的規則:
- 如果當前的卡牌比piles的最後一張卡牌還要小,就要覆蓋該piles的最後一張卡牌
- 如果當前的卡牌比piles的最後一張還要大,就建立新的piles
來重新看一下題目,這樣我們到時候堆疊出來的piles就會長這樣:
![]()
1 | nums = [10,9,2,5,3,7,101,18] |
用python來實現如下
1 | """ |
但是這樣還是O(N^2)因此我們可以用二分查找來解決這個問題,這樣就可以達到O(NlogN)的時間複雜度。
1 | def lengthOfLIS(self, nums: List[int]) -> int: |
或是直接使用套件bisect來解決這個問題
1 | import bisect from bisect_left |
3 結論
這題真的是我花了2天慢慢想,完全沒想過Patience Sort這種方式來解題目,太神奇拉!!!超愛這題!