最長遞增子序列


大家好,我是程序員學長。

今天我們來聊一聊最長遞增子序列這個問題。

如果喜歡,記得點個關注喲~

問題描述

給你一個整數數組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,51,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)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM