Dynamic Programming (DP) adalah teknik penyelesaian masalah yang memecah masalah menjadi submasalah yang tumpang tindih lalu menyimpan hasil submasalah agar tidak dihitung ulang. DP menjembatani recursion murni yang lambat karena overlapping subproblems dan greedy yang tidak selalu optimal. Pada chapter ini kita akan membandingkan dua gaya implementasi di Kotlin—memoization (top-down) dan tabulation (bottom-up)—lalu menerapkannya pada Fibonacci, 0/1 knapsack, Longest Common Subsequence, dan minimum coin change. Pembaca akan melihat bagaimana DP mengubah kompleksitas Fibonacci dari O(2ⁿ) menjadi O(n).
Dua Pendekatan DP #
| Aspek | Memoization (Top-down) | Tabulation (Bottom-up) |
|---|---|---|
| Arah | Dari masalah utama ke base case | Dari base case ke masalah utama |
| Implementasi | Recursion + cache | Loop iteratif + tabel |
| Komputasi | Hanya submasalah yang dibutuhkan | Semua submasalah sampai target |
| Memori call stack | Berisiko overflow untuk n besar | Aman (tanpa recursion) |
| Keterbacaan | Lebih dekat ke rumus rekursif | Lebih eksplisit |
// Top-down: simpan hasil di map, panggil rekursif
val memo = mutableMapOf<Int, Long>()
fun fibMemo(n: Int): Long {
if (n <= 1) return n.toLong()
if (n in memo) return memo[n]!!
val r = fibMemo(n - 1) + fibMemo(n - 2)
memo[n] = r
return r
}
// Bottom-up: isi tabel dari awal
fun fibTab(n: Int): Long {
val dp = LongArray(n + 1)
dp[0] = 0; dp[1] = 1
for (i in 2..n) dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
}Implementasi Dynamic Programming #
// Demo Dynamic Programming: memoization vs tabulation, dengan contoh Fibonacci, knapsack, longest common subsequence.
// === Top-down: Fibonacci dengan memoization ===
val memoFib = mutableMapOf<Int, Long>()
fun fibMemo(n: Int): Long {
if (n <= 1) return n.toLong()
if (memoFib.containsKey(n)) return memoFib[n]!!
val result = fibMemo(n - 1) + fibMemo(n - 2)
memoFib[n] = result
return result
}
// === Bottom-up: Fibonacci dengan tabulation ===
fun fibTab(n: Int): Long {
if (n <= 1) return n.toLong()
val dp = LongArray(n + 1)
dp[0] = 0
dp[1] = 1
for (i in 2..n) dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
}
// === Bottom-up dengan space optimization ===
fun fibOptimized(n: Int): Long {
if (n <= 1) return n.toLong()
var prev2 = 0L
var prev1 = 1L
for (i in 2..n) {
val curr = prev1 + prev2
prev2 = prev1
prev1 = curr
}
return prev1
}
// === 0/1 Knapsack DP ===
fun knapsack01(berat: IntArray, nilai: IntArray, kapasitas: Int): Int {
val n = berat.size
// dp[i][w] = nilai maksimum memilih dari item 0..i-1 dengan kapasitas w
val dp = Array(n + 1) { IntArray(kapasitas + 1) }
for (i in 1..n) {
for (w in 0..kapasitas) {
dp[i][w] = if (berat[i - 1] <= w) {
maxOf(dp[i - 1][w], dp[i - 1][w - berat[i - 1]] + nilai[i - 1])
} else {
dp[i - 1][w]
}
}
}
return dp[n][kapasitas]
}
// === Longest Common Subsequence (LCS) ===
fun lcs(a: String, b: String): Int {
val m = a.length
val n = b.length
val dp = Array(m + 1) { IntArray(n + 1) }
for (i in 1..m) {
for (j in 1..n) {
dp[i][j] = if (a[i - 1] == b[j - 1]) {
dp[i - 1][j - 1] + 1
} else {
maxOf(dp[i - 1][j], dp[i][j - 1])
}
}
}
return dp[m][n]
}
// === Coin change: minimum koin untuk jumlah tertentu ===
fun minCoinDP(amount: Int, pecahan: IntArray): Int {
val dp = IntArray(amount + 1) { Int.MAX_VALUE }
dp[0] = 0
for (i in 1..amount) {
for (p in pecahan) {
if (p <= i && dp[i - p] != Int.MAX_VALUE) {
dp[i] = minOf(dp[i], dp[i - p] + 1)
}
}
}
return if (dp[amount] == Int.MAX_VALUE) -1 else dp[amount]
}
fun main() {
println("=== Demo Dynamic Programming ===")
println("--- Fibonacci ---")
println("Top-down (memoization):")
for (n in listOf(10, 20, 30, 40, 50)) {
memoFib.clear()
println(" fib($n) = ${fibMemo(n)}")
}
println()
println("Bottom-up (tabulation):")
for (n in listOf(10, 20, 30, 40, 50)) {
println(" fib($n) = ${fibTab(n)}")
}
println()
println("Bottom-up optimized (space O(1)):")
for (n in listOf(50, 90)) {
println(" fib($n) = ${fibOptimized(n)}")
}
println()
println("--- 0/1 Knapsack ---")
val berat = intArrayOf(2, 3, 4, 5)
val nilai = intArrayOf(3, 4, 5, 6)
val kapasitas = 5
println("Berat: ${berat.toList()}, Nilai: ${nilai.toList()}, Kapasitas: $kapasitas")
println("Nilai maksimum: ${knapsack01(berat, nilai, kapasitas)}")
println()
println("--- Longest Common Subsequence ---")
val pairs = listOf(
"ABCBDAB" to "BDCAB",
"kotlin" to "otin",
"AGGTAB" to "GXTXAYB"
)
pairs.forEach { (a, b) ->
println("LCS(\"$a\", \"$b\") = ${lcs(a, b)}")
}
println()
println("--- Coin change (minimum koin) ---")
val pecahan = intArrayOf(1, 3, 4)
for (amt in listOf(6, 7, 10, 11)) {
println("Amount $amt dengan pecahan ${pecahan.toList()}: ${minCoinDP(amt, pecahan)} koin")
}
println()
println("--- Perbandingan: greedy vs DP untuk pecahan [1,3,4], target 6 ---")
println("Greedy (akan salah): 3 koin (4+1+1)")
println("DP (optimal): ${minCoinDP(6, intArrayOf(1, 3, 4))} koin (3+3)")
}Output yang Dihasilkan:
=== Demo Dynamic Programming ===
--- Fibonacci ---
Top-down (memoization):
fib(10) = 55
fib(20) = 6765
fib(30) = 832040
fib(40) = 102334155
fib(50) = 12586269025
Bottom-up (tabulation):
fib(10) = 55
fib(20) = 6765
fib(30) = 832040
fib(40) = 102334155
fib(50) = 12586269025
Bottom-up optimized (space O(1)):
fib(50) = 12586269025
fib(90) = 2880067194370816120
--- 0/1 Knapsack ---
Berat: [2, 3, 4, 5], Nilai: [3, 4, 5, 6], Kapasitas: 5
Nilai maksimum: 7
--- Longest Common Subsequence ---
LCS("ABCBDAB", "BDCAB") = 4
LCS("kotlin", "otin") = 4
LCS("AGGTAB", "GXTXAYB") = 4
--- Coin change (minimum koin) ---
Amount 6 dengan pecahan [1, 3, 4]: 2 koin
Amount 7 dengan pecahan [1, 3, 4]: 2 koin
Amount 10 dengan pecahan [1, 3, 4]: 3 koin
Amount 11 dengan pecahan [1, 3, 4]: 3 koin
--- Perbandingan: greedy vs DP untuk pecahan [1,3,4], target 6 ---
Greedy (akan salah): 3 koin (4+1+1)
DP (optimal): 2 koin (3+3)Bedah Kode Baris-per-Baris #
Mari kita amati bagian kunci:
fibMemo(baris 5-11): CachememoFibmenyimpan hasil perhitungan per indeks. Sebelum menjumlahkanfibMemo(n-1) + fibMemo(n-2), function memeriksa apakahnsudah ada di map. Bila ada, hasil langsung dikembalikan tanpa rekursi tambahan—kompleksitas turun dariO(2ⁿ)menjadiO(n).fibTab(baris 14-21): Membangun arraydpdari indeks 0 hingga n secara berurutan. Setiapdp[i]dihitung daridp[i-1] + dp[i-2]yang sudah pasti tersedia. Tanpa recursion, tidak ada risiko stack overflow dan overhead pemanggilan function lebih rendah.fibOptimized(baris 24-34): Karena Fibonacci hanya butuh dua nilai sebelumnya, array penuh tidak perlu. Cukup dua variabelprev1danprev2yang diperbarui tiap iterasi. Memori turun dariO(n)menjadiO(1).knapsack01(baris 37-51): Tabeldp[i][w]menyimpan nilai maksimum yang dapat dicapai dengan memilih dari item pertama hingga ke-i-1dan kapasitasw. Transisi: bila berat item ke-imuat, pilih maksimum antara tidak ambil (dp[i-1][w]) atau ambil (dp[i-1][w-berat[i-1]] + nilai[i-1]). KompleksitasO(n * kapasitas).lcs(baris 54-68): Tabeldp[i][j]menyimpan panjang LCS daria[0..i-1]danb[0..j-1]. Bilaa[i-1] == b[j-1], tambah 1 kedp[i-1][j-1]. Bila beda, ambil maksimumdp[i-1][j](buang karaktera) ataudp[i][j-1](buang karakterb). KompleksitasO(m * n).minCoinDP(baris 71-82): Inisialisasidp[0] = 0(0 koin untuk jumlah 0) dan sisanyaInt.MAX_VALUE. Untuk setiap jumlahi, coba setiap pecahanp; biladp[i-p]valid,dp[i] = min(dp[i], dp[i-p] + 1). Hasil2 koinuntuk amount 6 dengan pecahan[1,3,4]membuktikan DP menemukan solusi optimal yang greedy lewatkan.
Memoization atau Tabulation? #
Memoization lebih intuitif karena langsung mengikuti rumus rekursif dan hanya menghitung submasalah yang benar-benar dibutuhkan—efisien untuk masalah dengan state jarang dicapai. Tabulation lebih cepat secara konstan karena tidak ada overhead pemanggilan function, dan aman dari stack overflow. Untuk produksi, tabulation umumnya lebih disukai kecuali struktur transisi sangat kompleks dan sulit diiterasikan.
Penutup #
Dynamic Programming mengubah algoritma eksponensial menjadi polinomial dengan menyimpan hasil submasalah. Pilihan antara memoization dan tabulation bergantung pada kebutuhan keterbacaan, efisiensi konstan, dan kedalaman recursion. Pada chapter berikutnya kita akan membahas algoritma graph seperti BFS, DFS, dan Dijkstra yang juga banyak memanfaatkan prinsip DP.
Referensi #
Referensi Lanjutan #
- Kotlin Collections overview
— pengantar koleksi Kotlin yang menyediakan
ListdanMutableListsebagai tabel memoisasi