Longest Increasing Subsequence (LIS) adalah masalah klasik dengan dua solusi berbeda: DP O(n²) yang natural, dan O(n log n) dengan binary search yang elegan. Selain panjang, kita sering ingin merekonstruksi subsequence itu sendiri. LIS muncul di banyak aplikasi: scheduling, analisis sequence, dan persoalan geometri. Pada Chapter ini kita akan membedah kedua pendekatan, algoritma patience sorting, dan rekonstruksi.
Aturan: Subsequence Naik Terpanjang #
Diberikan array a[0..n-1], cari subsequence terpanjang (tidak harus contiguous) sehingga setiap elemen lebih besar dari elemen sebelumnya. Variasi umum: “strictly increasing” (a[i] < a[j]) atau “non-decreasing” (a[i] <= a[j]).
Mekanisme 1: DP O(n²) #
State: dp[i] = panjang LIS yang berakhir tepat di indeks i.
Recurrence:
dp[i] = 1 + max(dp[j] for j < i if a[j] < a[i])
(jika tidak ada j valid, dp[i] = 1)Setiap dp[i] adalah best dari semua predecessor valid yang lebih kecil dari a[i], ditambah 1 untuk elemen i itu sendiri.
Jawaban: max(dp[i] for i = 0..n-1).
Kode: DP O(n²) #
#include <iostream>
#include <vector>
#include <algorithm>
int lis_dp(const std::vector<int>& a) {
int n = static_cast<int>(a.size());
if (n == 0) return 0;
std::vector<int> dp(n, 1);
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (a[j] < a[i]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
}
return *std::max_element(dp.begin(), dp.end());
}
int main() {
std::vector<int> a = {3, 10, 2, 1, 20};
std::cout << "LIS length = " << lis_dp(a) << '\n';
return 0;
}Output yang Dihasilkan:
LIS length = 3- Baris
std::vector<int> dp(n, 1);: setiap elemen sendiri adalah LIS sepanjang 1. Base case implisit. - Baris
for (int j = 0; j < i; ++j): cek semua predecessorjdarii. - Baris
if (a[j] < a[i]): syarat — predecessor harus lebih kecil (strictly increasing). - Baris
dp[i] = std::max(dp[i], dp[j] + 1);: perpanjang LIS yang berakhir dijdengan elemeni. - Hasil
3: LIS{3, 10, 20}atau{2, ...}→ panjang 3.
Rekonstruksi LIS (O(n²)) #
#include <iostream>
#include <vector>
#include <algorithm>
std::vector<int> lis_reconstruct(const std::vector<int>& a) {
int n = static_cast<int>(a.size());
if (n == 0) return {};
std::vector<int> dp(n, 1), parent(n, -1);
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (a[j] < a[i] && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
parent[i] = j;
}
}
}
int best_end = static_cast<int>(
std::max_element(dp.begin(), dp.end()) - dp.begin());
std::vector<int> result;
int cur = best_end;
while (cur != -1) {
result.push_back(a[cur]);
cur = parent[cur];
}
std::reverse(result.begin(), result.end());
return result;
}
int main() {
std::vector<int> a = {3, 10, 2, 1, 20};
auto seq = lis_reconstruct(a);
std::cout << "LIS = ";
for (int x : seq) std::cout << x << ' ';
std::cout << '\n';
return 0;
}Output yang Dihasilkan:
LIS = 3 10 20- Baris
std::vector<int> dp(n, 1), parent(n, -1);: tambahkan arrayparentuntuk traceback.parent[i]menyimpan indeks predecessor terbaik. - Baris
parent[i] = j;: catat predecessor setiap kalidp[i]diperbarui. - Baris
while (cur != -1): mulai daribest_end(indeks dengandpmaksimum), ikutiparenthingga ke awal. - Hasil
3 10 20: salah satu LIS yang valid.
Mekanisme 2: Patience Sorting O(n log n) #
Ide kunci: maintain array tails[k] = elemen akhir terkecil yang bisa dicapai oleh LIS sepanjang k+1. Untuk setiap elemen a[i], lakukan binary search di tails untuk menemukan posisi a[i].
- Bila
a[i]lebih besar dari semuatails, append — LIS bertambah panjang. - Bila tidak, ganti elemen pertama di
tailsyang>= a[i]dengana[i]— ini menjagatailstetap minimal tanpa mengubah panjang LIS.
Panjang tails = panjang LIS. Catatan: tails itu sendiri bukan selalu LIS yang valid, hanya panjangnya yang benar.
Kode: Patience Sorting O(n log n) #
#include <iostream>
#include <vector>
#include <algorithm>
int lis_fast(const std::vector<int>& a) {
std::vector<int> tails;
for (int x : a) {
auto it = std::lower_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) {
tails.push_back(x);
} else {
*it = x;
}
}
return static_cast<int>(tails.size());
}
int main() {
std::vector<int> a = {3, 10, 2, 1, 20};
std::cout << "LIS fast = " << lis_fast(a) << '\n';
std::vector<int> b = {10, 9, 2, 5, 3, 7, 101, 18};
std::cout << "LIS fast = " << lis_fast(b) << '\n';
return 0;
}Output yang Dihasilkan:
LIS fast = 3
LIS fast = 4- Baris
auto it = std::lower_bound(tails.begin(), tails.end(), x);: cari elemen pertama ditailsyang>= x. Kompleksitas O(log k). - Baris
if (it == tails.end()) tails.push_back(x);: bilaxlebih besar dari semua, perpanjang LIS. - Baris
*it = x;: ganti elemen pertama yang>= xdenganx. Ini menjagatailstetap minimal, sehingga peluang perpanjangan di masa depan meningkat. - Hasil
3untuk{3,10,2,1,20}: LIS{3,10,20}atau{2,...}. - Hasil
4untuk{10,9,2,5,3,7,101,18}: LIS{2,5,7,101}atau{2,3,7,18}.
Rekonstruksi LIS dengan O(n log n) #
Untuk merekonstruksi, simpan indeks ke tails setiap kali update, lalu traceback:
#include <iostream>
#include <vector>
#include <algorithm>
std::vector<int> lis_fast_reconstruct(const std::vector<int>& a) {
int n = static_cast<int>(a.size());
if (n == 0) return {};
std::vector<int> tails, tails_idx, parent(n, -1);
for (int i = 0; i < n; ++i) {
auto it = std::lower_bound(tails.begin(), tails.end(), a[i]);
int pos = static_cast<int>(it - tails.begin());
if (it == tails.end()) {
tails.push_back(a[i]);
tails_idx.push_back(i);
} else {
*it = a[i];
tails_idx[pos] = i;
}
parent[i] = (pos > 0) ? tails_idx[pos - 1] : -1;
}
// Traceback
std::vector<int> result;
int cur = tails_idx.back();
while (cur != -1) {
result.push_back(a[cur]);
cur = parent[cur];
}
std::reverse(result.begin(), result.end());
return result;
}
int main() {
std::vector<int> a = {10, 9, 2, 5, 3, 7, 101, 18};
auto seq = lis_fast_reconstruct(a);
std::cout << "LIS = ";
for (int x : seq) std::cout << x << ' ';
std::cout << '\n';
return 0;
}Output yang Dihasilkan:
LIS = 2 3 7 18- Baris
tails_idx.push_back(i);: simpan indeks elemen yang menjadi ekortails. - Baris
parent[i] = (pos > 0) ? tails_idx[pos - 1] : -1;: predecessor adalah ekortailssebelum posisipos. Inilah elemen yang menjadi “basis” untuka[i]. - Baris
int cur = tails_idx.back();: mulai dari ekor terakhir — ini pasti elemen akhir dari sebuah LIS. - Hasil
2 3 7 18: LIS yang valid.
Variasi #
| Variasi | Modifikasi |
|---|---|
| Non-decreasing (mengizinkan sama) | Ganti lower_bound dengan upper_bound |
| Longest decreasing | Negasi elemen, lalu LIS |
| 2D LIS (Russian doll) | Sortir satu dimensi, lalu LIS pada dimensi lain |
| Count number of LIS | Tambahkan array count[] untuk menghitung cara mencapai panjang tertentu |
Tabel Kompleksitas #
| Pendekatan | Waktu | Ruang | Rekonstruksi |
|---|---|---|---|
| DP O(n²) | O(n²) | O(n) | Mudah dengan parent[] |
| Patience sorting | O(n log n) | O(n) | Perlu tails_idx + parent[] |
Ringkasan #
- LIS O(n²): state
dp[i]= LIS berakhir dii, transisi1 + max(dp[j])untukj < iyanga[j] < a[i]. - LIS O(n log n) dengan patience sorting: maintain
tails[k]= ekor minimum LIS sepanjangk+1. - Rekonstruksi: simpan
parent[]untuk traceback. - Variasi: non-decreasing (pakai
upper_bound), decreasing (negasi), 2D (sortir lalu LIS). - Pilih DP O(n²) bila
nkecil dan butuh kesederhanaan; pakai patience sorting untuknbesar.