1. 程式人生 > 遊戲攻略 >《原神攻略》逐月節肅霜之路100%完成度收集路線

《原神攻略》逐月節肅霜之路100%完成度收集路線

大家好,我是程式設計師學長。

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

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

問題描述

給你一個整數陣列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)。

最後

到此為止,我們就把這道題聊完了。

原創不易,各位覺得文章不錯的話,不妨點贊、在看、轉發三連走起!

你知道的越多,你的思維也就越開闊,我們下期再見。