相較於最長上升子串問題,LIS問題並不嚴格要求連續的子串,其求解難度也有所提升。
在嘗試將該問題由規模縮減時,我們不光要考慮規模的,還需要考慮所以更小規模的子問題。因爲對於任何位置結尾的最長子串,無法確定其上一個數字在原始數組的什麼位置。據此,我們定義如下的狀態變量和狀態轉移函數。
狀態變量dp[i]: 以數組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]
方法四:貪婪算法
前面遞歸算法的時間複雜度爲,而DP算法的時間複雜度爲,那有沒有複雜度更低的算法呢?
在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:]
該算法仍包含兩層循環,時間複雜度仍爲。不難注意到對於length數組而言,其是單調遞增的,所以可以引入二分搜索進行快速定位,從而將時間複雜度降低爲,具體見下。
方法五 貪婪算法+二分搜索
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:]