【動態規劃】數組最長上升子序列問題(LIS)

相較於最長上升子串問題,LIS問題並不嚴格要求連續的子串,其求解難度也有所提升。

在嘗試將該問題由nn規模縮減時,我們不光要考慮n1n-1規模的,還需要考慮所以更小規模的子問題。因爲對於任何位置結尾的最長子串,無法確定其上一個數字在原始數組的什麼位置。據此,我們定義如下的狀態變量和狀態轉移函數。

狀態變量dp[i]: 以數組nums[i]爲結尾的最長子串長度
狀態轉移函數: dp[i]=max(dp[j]+1,1)ifj<i&nums[j]<nums[i]dp[i]=\max(dp[j]+1,1) \quad if\quad j<i\&nums[j]<nums[i]

在LIS具體的求解時,我們可以利用遞歸或者DP的方式進行。這裏分別介紹多種方法:

方法一:原始的遞歸方法

def LIS_RE(array):
    """
    基於遞歸的算法
    """
    if len(array) == 1:
        return 1

    maxLength = 1
    for i in range(len(array)-1):
        if array[i] < array[-1]:
            maxLength = max(LIS_RE(array[:i+1])+1, maxLength)

    return maxLength

方法二:遞歸方法+備忘錄
不難發現,在方法一中有大量重複計算的子問題,我們可以通過構建一張Hashtable記錄這些子問題的結果。

def LIS_RE2(array, cache=dict()):
    """
    在上面的遞歸方法中,不難發現有很多重複的子問題,所以通過hash的形式保存備忘錄
    cache({index:value})爲數組array[index]爲子序列末尾的最長子序列長度
    """
    if len(array) == 1:
        return 1

    maxLength = 1
    for i in range(len(array)-1):
        if array[i] < array[-1]:
            if i in cache:
                submaxLength = cache.get(i)
            else:
                submaxLength = LIS_RE(array[:i+1])
            maxLength = max(submaxLength+1, maxLength)

    return maxLength

方法三:原始的DP算法

def LIS_DP(array):
    """
    基於DP的算法
    定義狀態: dp[i]包括第i個數在內的最長上升子串長度
    定義轉移方程: dp[i] = max(dp[i], dp[j] + 1)  if j <i and nums[j]<nums[i]
    通過設置父節點,可以追溯具體的子序列
    :param array:
    :return:
    """
    dps = [1 for _ in range(len(array))]     # 初始值
    parents = [-1 for _ in range(len(array))]     # 前序節點處值
    for i in range(1, len(array)):
        for j in range(0, i):
            if array[j] < array[i]:
                if dps[j] + 1 > dps[i]:
                    dps[i] = dps[j] + 1
                    parents[i] = j

    # 發現最大子串長度
    maxLength = 0
    maxEndIndex = -1
    for i in range(len(array)):
        if dps[i] > maxLength:
            maxLength = dps[i]
            maxEndIndex = i

    # 回溯子序列數值
    index = maxEndIndex
    numsList =[]
    while index != -1:
        numsList.append(array[index])
        index = parents[index]

    return maxLength, numsList[::-1]

方法四:貪婪算法

前面遞歸算法的時間複雜度爲O(2n)O(2^n),而DP算法的時間複雜度爲O(n2)O(n^2),那有沒有複雜度更低的算法呢?

在DP算法中,我們每找以nums[i]爲結果的子串,均需要遍歷前面的所有0-(i-1)組子串,其實這是沒必要的。我們不妨定義一個length數組,用來記錄各最長子串長度時的各子串末尾數值的最小值,這樣每增加一個新的數字,我們可以視其與length各元素的大小關係來決定拓展length數組或者更新其中的元素。

其具體算法細節和說明見下:

def LIS_greedy(array):
    """
    基於貪心思想的算法
    定義數組lengths: lengths[i]表示長度爲i+1的上升子串中對應的最小值, 最終的length長度即爲最長序列長度
    其更新思想爲:若array[j]>length[-1],則添加array[j], 否則找到第一個大於該值的length[k]進行替換
    注意返回的length序列並非真實的最短序列
    :param array:
    :return:
    """
    lengths = [-np.inf]            # 定義一個哨崗位
    for i in range(len(array)):
        if array[i] > lengths[-1]:
            lengths.append(array[i])

        else:
            j = len(lengths) - 1
            while j > 0:
                if lengths[j] > array[i]:
                    j -= 1
                else:
                    break
            lengths[j+1] = array[i]

    return len(lengths) - 1, lengths[1:]

該算法仍包含兩層循環,時間複雜度仍爲O(n2)O(n^2)。不難注意到對於length數組而言,其是單調遞增的,所以可以引入二分搜索進行快速定位,從而將時間複雜度降低爲O(nlogn)O(nlogn),具體見下。

方法五 貪婪算法+二分搜索

def LIS_greedy2(array):
    """
    在上述貪心算法基礎上,注意到lengths序列本身是有序的,可以用二分法加速搜索,從而使算法的時間複雜度降至O(nlog(n))
    定義數組lengths: lengths[i]表示長度爲i+1的上升子串中對應的最小值, 最終的length長度即爲最長序列長度
    其更新思想爲:若array[j]>length[-1],則添加array[j], 否則找到第一個大於該值的length[k]進行替換
    注意返回的length序列並非真實的最短序列
    :param array:
    :return:
    """
    lengths = [-np.inf]
    for i in range(len(array)):
        if array[i] > lengths[-1]:
            lengths.append(array[i])

        else:
            lo = 1
            hi = len(lengths) - 1
            while lo < hi:        # 該值必然存在
                mid = lo + (hi - lo)//2
                if lengths[mid] > array[i]:
                    hi = mid
                else:
                    lo = mid + 1

            lengths[lo] = array[i]

    return len(lengths) - 1, lengths[1:]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章