最長上升子序列(Longest Increasing Subsequence, LIS)是動態規劃與貪心策略完美結合的經典問題。本文提供 O(n²) 標準 DP、O(n log n) 尾值數組+二分優化兩種核心解法,並延伸至路徑重構、非嚴格遞增、在線處理等變體。
n 的數列 a[0..n-1]n=5, a={4,2,3,1,5} → LIS={2,3,5} 長度 3dp[i] = 以 a[i] 結尾的最長上升子序列長度
dp[i] = 1 + max{ dp[j] | 0 ≤ j < i 且 a[j] < a[i] }
// 無滿足條件 j 時 max 為 0,dp[i] = 1
#include <stdio.h>
#include <algorithm>
#include <climits>
#define MAX_N 10000
int n;
int a[MAX_N];
int dp[MAX_N];
int lis_dp() {
int res = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
res = std::max(res, dp[i]);
}
return res;
}
int main() {
n = 5;
int a1[] = {4, 2, 3, 1, 5};
memcpy(a, a1, sizeof(a1));
printf("result = %d\n", lis_dp()); // 3
return 0;
}
維護數組 tails[k] = 長度為 k+1 的所有上升子序列中,最小結尾元素。
tails 單調遞增 → 可二分查找。
a[i]tails[0..len-1] 中二分查找第一個 ≥ a[i] 的位置 pospos == len,a[i] 可延長最長子序列 → tails[len++] = a[i]tails[pos] = a[i](用更小值優化同長度子序列結尾)len 即 LIS 長度#include <stdio.h>
#include <algorithm>
#include <climits>
#define MAX_N 10000
int n;
int a[MAX_N];
int tails[MAX_N]; // tails[i] = 長度 i+1 的 LIS 最小結尾值
int lis_nlogn() {
int len = 0;
for (int i = 0; i < n; i++) {
// lower_bound: 第一個 ≥ a[i] 的位置
int pos = std::lower_bound(tails, tails + len, a[i]) - tails;
tails[pos] = a[i];
if (pos == len) len++;
}
return len;
}
// 手寫二分版(無 STL 環境)
int lower_bound_manual(int *arr, int len, int target) {
int l = 0, r = len;
while (l < r) {
int mid = (l + r) >> 1;
if (arr[mid] >= target) r = mid;
else l = mid + 1;
}
return l;
}
// O(n²) DP 版天然支持路徑重構
int pre[MAX_N]; // 前驅索引
int lis_dp_with_path() {
int res = 0, last = -1;
for (int i = 0; i < n; i++) {
dp[i] = 1; pre[i] = -1;
for (int j = 0; j < i; j++) {
if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
pre[i] = j;
}
}
if (dp[i] > res) {
res = dp[i];
last = i;
}
}
// 回溯輸出
int seq[MAX_N], cnt = 0;
for (int cur = last; cur != -1; cur = pre[cur])
seq[cnt++] = a[cur];
for (int i = cnt-1; i >= 0; i--)
printf("%d ", seq[i]);
printf("\n");
return res;
}
| 變體 | 修改點 | 複雜度 |
|---|---|---|
| 非嚴格遞增 (≤) | lower_bound → upper_bound (第一個 > target) | O(n log n) |
| 最長不降子序列 | 同上 | O(n log n) |
| 最長下降子序列 (LDS) | 遍歷時取反 -a[i] 或反向比較 | O(n log n) |
| 在線流式處理 | 同 O(n log n) 算法,逐個處理即可 | O(n log n) |
| 輸出所有 LIS | 需圖論建模 + DFS 回溯,指數級輸出 | 指數 |
| 算法 | 時間 | 空間 | 路徑重構 | 適用場景 |
|---|---|---|---|---|
| O(n²) DP | O(n²) | O(n) | 直接支持 | n ≤ 5000、教學、需路徑 |
| O(n log n) 貪心+二分 | O(n log n) | O(n) | 需額外數組輔助 | n ≤ 10⁶、競賽、面試 |
// 返回第一個 ≥ target 的下標 [0, len]
int lower_bound(int *a, int len, int target) {
int l = 0, r = len;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] >= target) r = mid;
else l = mid + 1;
}
return l;
}
// 返回第一個 > target 的下標 [0, len]
int upper_bound(int *a, int len, int target) {
int l = 0, r = len;
while (l < r) {
int mid = l + (r - l) / 2;
if (a[mid] > target) r = mid;
else l = mid + 1;
}
return l;
}
# 單調遞增 5 1 2 3 4 5 → 5 # 單調遞減 5 5 4 3 2 1 → 1 # 隨機 8 10 9 2 5 3 7 101 18 → 4 (2,3,7,101 或 2,5,7,101) # 重複元素 (嚴格遞增) 6 1 2 2 3 3 4 → 4 (1,2,3,4) # 非嚴格遞增 6 1 2 2 3 3 4 → 6 (1,2,2,3,3,4)
LIS 問題展示了「狀態定義決定複雜度」的算法哲理:O(n²) DP 狀態「以 i 結尾」自然但冗餘;O(n log n) 狀態「長度 k 的最小結尾」極致壓縮信息。徹底掌握 tails 數組單調性證明與 lower_bound 替換邏輯,是演算法面試與競賽的必殺技。