哈嘍,大家好!我是花花。今天我們一起來看一下兩個僅有一"字"之差的經典的動態規劃問題:
最後我們會以一道LeetCode題目為例子,看看如何在特定情況下把LCS歸約成LIS從而降低時間複雜度。
在講具體問題之前我們先來舉個例子看一下什麼是子序列(subsequence)以及它和子數組(subarray)和子集(subset)之間的區別。
假設我們有一個數組:[1, 2, 3, 4, 5]。
[1,2,3]是一個子序列,同時也是一個子數組。
[1,3,5]是一個子序列,但卻不是一個子數組,因為子數組要求元素連續。
[3,1,5]是一個子集,但既不是一個子序列,也不是一個子數組。因為後兩者都要元素順序不能改變。
給定兩個數組A和B,找出兩者共同擁有的子序列中長度最長的一個。通常只需要返回長度即可。
A: [6,4,8,1,3,2]
B: [4,7,6,2,3,8,6,1]
LCS(A, B) = [4,8,1]([6,8,1]也是一個解)
我們可以這樣來定義dp數組:
dp[i][j] := LCS(A[0~i], B[0~j])
當A[i] == B[j]時,子序列的長度增加1。
dp[i][j] = dp[i-1][j-1] + 1
如果A[i] != B[j],跳過當前匹配,在dp[i][j-1]和dp[i-1][j]中取一個最大值。
合在一起的話我們可以得到下面的轉移方程:
dp數組初始化成0
假設len(A) = m, len(B) = n
答案就存儲在dp[m][n]中(加了padding)
空間複雜度O(mn)
時間複雜度O(mn)
參考代碼:
# Author: Huahua class Solution: def LCS(self, A: List[int], B: List[int]) -> int: m, n = len(A), len(B) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(1, m+1): for j in range(1, n+1): dp[i][j] = max((dp[i-1][j], dp[i][j-1], dp[i-1][j-1] + int(A[i-1] == B[j-1]))) return dp[m][n]和最長公共子序列不同,最長遞增子序列的輸入只有一個數組。要求返回該數組最長的一個子序列並且子序列中所有的元素嚴格單調遞增。
A: [1,4,2,3,6,5,9,4]
LIS(A) = [1,2,3,5,9] 長度是5,如下圖所示:
由於輸入只有一個數組,所以我們的dp數組也變成了一維:
dp[i] := 以A[i]結尾的LIS的長度
轉移方程比較簡單:
dp[i] = max(dp[j]) + 1, 0 <= j < i and A[i] > A[j]
如果A[i] > A[j],那麼A[i]就可以接在A[j]後面擴展以A[j]結尾的最長遞增子序列。所以對於i,我們枚舉所有比i小的索引j然後找一個長度最長的子序列去擴展。
比如元素9,我們找到它之前的元素5,9>5,而且以5結尾的LIS = [1,2,3,5],長度最長為4。所以我們可以把9追加進這個子序列得到最長的子序列[1,2,3,5,9],長度為5。
dp數組初始化成1,因為每個元素自己就是一個合法的、長度為1的子序列。
時間複雜度:O(n^2)
空間複雜度:O(n)
利用LIS自己本身是單調遞增的特性,我們可以進一步使用貪心+二分搜索來降低算法的時間複雜度。我們需要改變一下dp數組的定義。
dp[i] := 長度為i的LIS的末尾元素的最小值
讀起來有些拗口,別急,我們先來比較幾個子序列然後再來看為什麼要這麼定義dp數組。
ex1: A = [1, 4, 6], B = [1, 2, 3] 這兩個子序列誰比較好?當然是B好,因為如果後面出現一個數組5,就可以接在B後面構成更長的LIS,但卻不能接在A後面。所以如果長度相同,我們希望末尾元素越小越好。
ex2: A = [1, 2, 6], B = [1, 4, 6] 這兩個子序列誰比較好?答案是一樣好。因為末尾元素相同,不管後面的元素是什麼樣子,擴展A還是擴展B的結果是相同的。對於相同長度的子序列,只有末尾元素會影響決策,而和前面的元素沒有關係。
所以對於一個給定的長度LIS,我們只需要記錄該長度的子序列的末尾元素的最小值,即dp[i]。
有了這些基本思想之後我們來看一個這個算法的具體過程。
對於每個元素A[i]:
1. 如果它把當前最長的LIS的末尾元素要大,我們可以用它去擴展LIS
2. 否則我們找一個長度最長的並且末尾元素>=A[i]的LIS,去替換它的末尾元素,因為我們希望末尾元素越小越好。
由於dp數組是按照長度存儲LIS的末尾元素的,再加上其本身也是單調遞增的,所以我們可以使用二分搜索找到可以替換的元素的index。
我們直接找一個例子看一下
A: [1,4,2,3,6,5,9,4]
a
dp1
[1] // [1] 擴展長度為0的LIS
4
[1,4] // [*,4] 擴展長度為1的LIS2
[1,2] // [*,2] 更新長度為2的LIS
3
[1,2,3] // [*,*,3] 擴展長度為2的LIS
6
[1,2,3,6] // [*,*,*,6] 擴展長度為3的LIS
5
[1,2,3,5] // [*,*,*,5] 更新長度為4的LIS
9
[1,2,3,5,9] // [*,*,*,*,9] 擴展長度為4的LIS4
[1,2,3,4,9] // [*,*,*,4] 更新長度為4的LIS第一行A[i] = 1,一開始dp為空,我們直接擴展長度為0的LIS,得到長度為1的LIS = [1]。