LeetCode 300 - Longest Increasing Subsequence
題目
題目連結:https://leetcode.com/problems/longest-increasing-subsequence/
找到最長遞增子序列(LIS)的長度。
範例說明
Example 1:
1 | Input: nums = [10,9,2,5,3,7,101,18] |
Example 2:
1 | Input: nums = [0,1,0,3,2,3] |
Example 3:
1 | Input: nums = [7,7,7,7,7,7,7] |
想法
O(N^2)
有一種常見的 O(N^2)
動態規劃。考慮動態規劃的狀態與轉移,定義狀態 dp(i)
等於以第 i
個數字為結尾的序列的 LIS 長度,轉移即為:
如此的狀態轉移十分直觀,以第 i
個數字為結尾的序列,代表我們要選擇 nums[i]
,那麼 nums[i]
可以放在 nums[j]
後面,如果 j < i && nums[j] < nums[i]
,因此以 nums[i]
為結尾的最長遞增子序列即為所有符合條件的 j
中,dp[j]
最大的值再加一。若沒有符合的 j
,則以 nums[i]
結尾的最長遞增子序列至少也為 1。
由於狀態有 N
種,每次轉移都要花 O(N)
的時間,因此總時間複雜度為 O(N^2)
。
O(NlogN)
要達到 O(NlogN)
的想法,首先考慮另一種 O(N^2)
的動態規劃。一樣考慮狀態與轉移:
定義狀態 dp(j)
代表長度為 j
的最長遞增子序列結束在 dp(j)
這個數字,若有多個滿足,則選數字最小的。
接著對 nums[0]
, nums[1]
… 進行 N 次的轉移:
假設 dp(1) = 1, dp(2) = 3, dp(3) = 5, dp(4) = 7
,則當要對 nums[i] = 4
進行轉移時,數字 4
能產生的最長遞增子序列為 3,因為我們知道 dp(2) = 3
,所以數字 4
可以插入在數字 3
之後形成長度 3 的遞增子序列。但是 dp(3)
已經有一個數字 5
了,因此要選數字較小的來當 dp(3)
的值,所以要使 dp(3)=nums[i]
,還需要有一個條件 nums[i] < dp(3)
。
因此轉移如下:
為了方便實作,可以另 dp(0)=-INF
且當 i > 0
,初始化 dp(i)=INF
。
1 | const INF = 1e9 + 10; |
最後,對於所有的 dp(i)
且 dp(i) != INF
的 i
即為最長遞增子序列的長。
因此總時間複雜度為 O(N^2)
。
觀察上述的轉移與程式碼,可以得到兩個性質:
dp(i-1) < dp(i)
。nums[i]
每次恰好更新一個數字。
由於這兩個性質,可以發現只要在 dp
陣列上,利用二分搜找到大於等於 nums[i] 的第一個數字並取代之,即可將時間複雜度減低到 O(NlogN)
。
實作細節
實作上,可以不用先開好長度為 N + 1
的 dp
陣列,只在需要的時候增加 dp
陣列的長度(也就是不多紀錄值為 INF
的部分):
對於上述的程式碼,當 nums[i]
更新的數字是 INF
,代表多了一個數字需要紀錄,這時才需要增加 dp
陣列的長度,而此種情況代表 nums[i]
比最後一個非 INF
的數字還要大。
如此一來,最後陣列的長度即為最長遞增子序列的長度減一(需要扣除第零個數字 -INF
)。
另外,找大於等於 nums[i] 的第一個數字,可以利用 C++ 的 lower_bound
來實作。
程式碼
1 | /** |