NlogN 求最長不下降子序列(LIS)

Ofnoname發表於2024-04-26

最長不下降子序列是一道非常經典的題目,我們假設題目如下:

有一個陣列 $ a $,現在要從中抽取一個嚴格上升的子序列(子序列就是你可以從原序列裡刪除一些數,保留一部分數得到的新數列)(嚴格上升也就是不能相等只能遞增),現在要求出這個子序列最長有多長?

舉例來說,假設有一個陣列 a = [10, 9, 2, 5, 3, 7, 101, 18]。這個陣列的最長上升子序列是 [2, 3, 7, 101],這是從原陣列中第3, 5, 6, 7個位置選取得到的,長度為4。(當然最後一個數字換成 18 也可以)

平方做法

通常來講,平方做法很容易想到,我們開一個陣列 $ f $ ,用 $ f_i $ 表示以 $ a_i $ 結尾的 LIS 的長度。我們從左往右計算每一個 $ f_i $ ,每一個 $ f_i $ 可能由 \(f_1, f_2...f_{i-1}\)得到。比方說假如 $ a_i > a_j $,那麼 \(a_i\) 就可以接到 $ f_{i-1} $ 表示的序列後面,長度加一,列舉所有之前的 f,取最大值,最終就可以計算出來。

#include <vector>
#include <algorithm>
using namespace std;

int longestIncreasingSubsequence(const vector<int>& a) {
    int n = a.size();
    vector<int> f(n, 1);  // 初始化每個元素的LIS長度為1
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (a[i] > a[j]) {
                f[i] = max(f[i], f[j] + 1);
            }
        }
    }
    return *max_element(f.begin(), f.end());  // 返回f中的最大值,即最長上升子序列的長度
}

int main() {
    vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18};
    int result = longestIncreasingSubsequence(a);
    return result;
}

N log N 做法

更高效的方法是透過維護一個陣列 $ tail $ 來實現,其中 $ tail_i $ 儲存長度為 i 的所有上升子序列中末尾最小的元素(比如 $ tail_4 $ 表示所有長度為 4 的 LIS 裡最後一個數字最小能達到多少)。在更新過程中,我們使用二分搜尋來確定一個元素應當放置在 $ tail $ 中的位置。

我們還是從左向右來列舉,對於每個元素 $ a_i $,在 \(tail\) 中找到第一個大於 $ a_i $ 的位置。

  • 如果找到這樣的位置,即表示之前有一個序列的末尾可以被替換,所以修改其 $ tail $ 值為新 $ a_i $
  • 如果沒有找到,說明 $ a_i $ 比之前所有數字都大,那麼把 $ a_i $ 追加到 $ tail $ 末尾,最長的上升序列長度成功加1.

對於 $ tail $ 來說,下標越大,條件越難滿足,則能達到的最小值越大,所以 $ tail $ 顯然是嚴格遞增的,這不難理解,而每一次操作也維持了 $ tail $ 的單調性。

舉例來說,現在\(tail = [3, 5, 6, 19, 25]\),而當前 $ a_i $ 為 8,可以找到 $ tail_4 = 19$ 這表明在我之前出現過一個長度為4,以 19 結尾的序列,以及一個長度為3,以小於8結尾的序列,那麼這個長度為3的序列可以接上8,替換掉原來的19,一定更優

#include <vector>
#include <algorithm>
using namespace std;

int longestIncreasingSubsequence(const vector<int>& a) {
    vector<int> tail;
    for (int x : a) {
        auto it = lower_bound(tail.begin(), tail.end(), x);
        if (it == tail.end()) {
            tail.push_back(x);  // 如果x大於tail中所有元素,追加到末尾
        } else {
            *it = x;  // 替換為更小的值,保持序列最小
        }
    }
    return tail.size();  // tail的大小就是最長上升子序列的長度
}

int main() {
    vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18};
    int result = longestIncreasingSubsequence(a);
    return result;
}

獲得具體的數列

除了得到最長長度,此方法也是支援把序列獲取到的,增加一個陣列,每一次更新 tail 時,來記錄每個元素的前一個元素的索引,這裡附加上 chatgpt 的程式碼:

def longest_non_decreasing_subsequence(arr):
    from bisect import bisect_right

    if not arr:
        return 0, []

    dp = []  # 存放各長度最長不下降子序列的最小結尾元素的值
    predecessor = [-1] * len(arr)  # 前驅元素的索引
    indices = []  # 存放各長度最長不下降子序列的最小結尾元素的索引

    for i, x in enumerate(arr):
        pos = bisect_right([arr[idx] for idx in indices], x)
        if pos < len(indices):
            indices[pos] = i
            predecessor[i] = indices[pos-1] if pos > 0 else -1
        else:
            indices.append(i)
            predecessor[i] = indices[-2] if len(indices) > 1 else -1

    # 回溯找到最長不下降子序列
    n = len(indices)
    sequence = [0] * n
    k = indices[-1]
    for j in range(n-1, -1, -1):
        sequence[j] = arr[k]
        k = predecessor[k]

    return n, sequence

# 示例
arr = [10, 9, 2, 5, 3, 7, 101, 18]
length, sequence = longest_non_decreasing_subsequence(arr)
print("Length:", length)  # 輸出長度
print("Sequence:", sequence)  # 輸出序列

嚴格上升和不下降

要從求最長嚴格上升子序列變為求最長不下降子序列(也就是允許序列中的元素相等),主要修改的部分就是二分查詢部分。在嚴格上升的場景中,tail 不會出現相等值,我們尋找的是第一個大於等於當前元素的位置,而在不下降的場景中,要找到的是第一個大於當前元素的位置。

舉例子來說,當要求變為嚴格不下降,$ tail $ 就有可能出現相等值,假設 $ tail = [1, 2, 2, 3, 3, 3, 5] $,且下一個 \(a_i\) 為 2。那麼應該更新 \(a_2\) 第一個3。而擴充套件 $ tail $ 的條件也放寬為大於等於,具體程式碼就不再展示。

相關文章