大家好,我是程序員學長。
今天我們來聊一聊最長遞增子序列這個問題。
如果喜歡,記得點個關注喲~
問題描述
給你一個整數數組nums,找到其中最長嚴格遞增子序列的長度。
子序列是由數組派生而來的序列,刪除(或不刪除)數組中的元素而不改變其余元素的順序。例如,[3,6,2,7] 是數組 [0,3,1,6,2,2,7] 的子序列。
示例:
輸入:nums = [2,1,6,3,5,4]
輸出:3
解釋:最長遞增子序列是 [1,3,4],因此長度為 3。
分析問題
對於以第i個數字結尾的最長遞增之序列的長度來說,它等於以第j個數字結尾的最長遞增子序列的長度的最大值+1,其中 0<j<i,並且nums[j] < nums[i]。例如,對於以5結尾的最長遞增子序列的長度,他等於以3結尾的最長遞增子序列的長度+1。
所以,我們定義一個數組dp,其中dp[i]表示以第i個元素結尾的最長遞增子序列的長度。則可以很容易的知道狀態轉移方程為:dp[i]=max(dp[j])+1,其中0<j<i 且 nums[j]<nums[i]。即考慮dp[0...i-1]中最長的遞增子序列的后面添加一個元素nums[i],使得新生成的子序列滿足遞增的條件。
最后,整個數組的最長遞增子序列的長度為數組dp中的最大值。
下面我們來看一下代碼的實現。
def lengthOfLIS(nums):
#如果數組為空,直接返回
if not nums:
return 0
dp = []
#從頭遍歷數組中的元素
for i in range(len(nums)):
dp.append(1)
#在dp中尋找滿足條件的最長遞增子序列
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
print(lengthOfLIS([2,1,6,3,5,4]))
時間復雜度是O(n^2),其中n為數組nums的長度。因為對於數組nums的每個元素,我們都需要O(n)的時間去遍歷dp中的元素。
空間復雜度是O(n),其中n為數組nums的長度。
優化
這里,我們也可以使用貪心的思想來解決。由於題目是求最長的遞增子序列,要想使得遞增子序列的長度足夠長,就需要讓序列上升的盡可能的慢,因此我們希望每次在上升子序列最后加上的那個數盡可能的小。
我們維護一個數組d,其中d[i]表示長度為i的遞增子序列的末尾元素的最小值,比如對於序列[2,1,6,3,5,4]來說,子序列1,3,5和1,3,4都是它的最長的遞增子序列,則d[3]=4,因為4<5。
同時,我們也可以注意到數組d是單調遞增的,即對於j<i ,那么d[j]<d[i]。我們可以使用反證法來證明,假設存在j<i時,d[j]>=d[i],我們考慮從長度為i的最長子序列的末尾刪除i-j個元素,那么這個序列的長度變為j,且第j個元素x必然是小於d[i]的(因為是遞增子序列,d[i]在x的后面,所以d[i]>x),又因為d[j]>d[i]的,所以可以得出x<d[j]的。那么我們就找到了一個長度為j的子序列,並且末尾元素比d[j]小,這與題設矛盾,從而可以證明數組d是單調遞增的。
我們依次遍歷數組中的元素,並更新數組d和len的值。如果nums[i] > d[len],則len=len+1,否則在數組d中,找到第一個比nums[i]小的數d[k],並更新d[k+1]=nums[i]。
def lengthOfLIS(nums):
d = []
#遍歷數組中的元素
for n in nums:
#如果n比數組d的最后一個元素大,則加入數組中
#否則,在d中尋找第一個小於n的元素的位置
if not d or n > d[-1]:
d.append(n)
else:
l = 0
r = len(d) - 1
k = r
while l <= r:
mid = (l + r) // 2
if d[mid] >= n:
k = mid
r = mid - 1
else:
l = mid + 1
d[k] = n
return len(d)
該算法的時間復雜度是O(nlogn)。我們依次遍歷數組nums,然后用數組中的元素去更新數組d,而更新數組d時,我們采用二分查找的方式來定位要更新的位置,所以時間復雜度是O(nlogn)。由於需要一個額外的數組d來保存,所以空間復雜度是O(n)。
進階
下面我們把題目再修改一下,給定數組nums,設長度為n,輸出nums的最長遞增子序列。(如果有多個答案,請輸出其中按數值進行比較的字典序最小的那個)。
示例:
輸入:[1,2,8,6,4]
返回值:[1,2,4]
說明:其最長遞增子序列有3個,(1,2,8)、(1,2,6)、(1,2,4)其中第三個按數值進行比較的字典序最小,故答案為(1,2,4)
由於題目要求輸出最長遞增子序列中數值最小的那個,所以我們要在上一題的基礎上進行修改,這里引入一個數組maxlen,用來記錄以元素nums[i]結尾的最長遞增子序列的長度。
在得到數組maxlen和數組d之后,我們可以知道該序列的最長遞增子序列的長度是len(d)。然后從后遍歷數組maxlen,如果maxlen[i]=len(d),我們將對於元素返回結果res中,依次類推,直到遍歷完成。
Tips:為什么要從后往前遍歷數組maxlen呢?假設我們得到的maxlen為[1,2,3,3,3],最終的輸出結果為res(字典序最小的最長遞增子序列),那么res的最后一個元素在nums中位置為maxlen(i)==3對於的下標i,此時數組nums中有三個元素對應的最長遞增子序列的長度為3,即nums[2]、nums[3]和nums[4],那到底是哪一個呢?如果是nums[2],那么nums[2] < nums[4] ,則maxlen[4]=4,與已知條件相悖,因此我們應該取nums[4]放在res的最后一個位置。所以需要從后先前遍歷。
def lengthOfLIS(nums):
#最長遞增子序列
d = []
#記錄以nums[i]結尾的最長遞增子序列的長度
maxlen = []
#遍歷數組中的元素
for n in nums:
#如果n比數組d的最后一個元素大,則加入數組中
#否則,在d中尋找第一個小於n的元素的位置
if not d or n > d[-1]:
#更新最長遞增子序列
d.append(n)
#更新以n為結尾元素的最長遞增子序列
maxlen.append(len(d))
else:
l = 0
r = len(d) - 1
k = r
while l <= r:
mid = (l + r) // 2
if d[mid] >= n:
k = mid
r = mid - 1
else:
l = mid + 1
#更新最長遞增子序列
d[k] = n
#更新以n為結尾元素的最長遞增子序列
maxlen.append(k+1)
#求解按字典序最小的結果
#此時我們知道最長長度為len(d),從后向前遍歷maxLen,
#遇到第一個maxLen[i]==len(d)的下標i處元素arr[i]即為所求
lens = len(d)
res = [0] * lens
for i in range(len(maxlen)-1,-1,-1):
if maxlen[i]==lens:
res[lens-1]=nums[i]
lens=lens-1
return res
print(lengthOfLIS([1,2,8,6,4]))
該算法的時間復雜度是O(nlogn),空間復雜度是O(n)。