Beberapa masalah secara natural menghasilkan dua atau lebih sub-masalah per pemanggilan — contoh klasik: Fibonacci (fib(n-1) + fib(n-2)), tree traversal (kiri dan kanan), atau generate all subsets. Rekursi jenis ini disebut rekursi bercabang. Meskipun ekspresi kodenya elegan, kompleksitas waktunya bisa meledak eksponensial: fib(50) naif butuh miliaran pemanggilan. Pada Chapter ini kita akan membedah Fibonacci dan Tribonacci, mengukur ledakan eksponensial secara empiris, dan melihat sekilas solusinya: memoization yang menjadi pintu masuk ke dynamic programming.
Aturan: Dua atau Lebih Panggilan Rekursif #
Fungsi rekursif bercabang memanggil dirinya dua atau lebih kali dalam satu tubuh fungsi. Setiap pemanggilan membuat recursion tree — pohon dengan root fungsi awal, anak-anaknya adalah panggilan rekursif, dan seterusnya hingga daun (base case).
Untuk Fibonacci naif fib(n):
fib(n) = fib(n-1) + fib(n-2)untukn > 1fib(0) = 0,fib(1) = 1(base case)
Pohon rekursif untuk fib(5):
Perhatikan fib(3) dihitung dua kali, fib(2) dihitung tiga kali. Inilah sumber inefisiensi.
Mekanisme: Ledakan Eksponensial #
Jumlah panggilan rekursif T(n) untuk fib(n) mengikuti recurrence:
T(0) = T(1) = 1(base case: 1 panggilan)T(n) = T(n-1) + T(n-2) + 1(dua sub-panggilan + 1 untuk diri sendiri)
Solusi: T(n) = Θ(φ^n) di mana φ = (1 + √5)/2 ≈ 1.618 (golden ratio). Artinya fib(50) butuh sekitar 1.618^50 ≈ 2.4 × 10^10 panggilan rekursif — detik menit.
Kode: Fibonacci Naif #
#include <iostream>
long long fib_naive(int n) {
if (n <= 0) return 0;
if (n == 1) return 1;
return fib_naive(n - 1) + fib_naive(n - 2);
}
int main() {
for (int n : {0, 1, 2, 5, 10, 20}) {
std::cout << "fib(" << n << ") = " << fib_naive(n) << '\n';
}
return 0;
}Output yang Dihasilkan:
fib(0) = 0
fib(1) = 1
fib(2) = 1
fib(5) = 5
fib(10) = 55
fib(20) = 6765- Baris
if (n <= 0) return 0;: base case pertama untukn = 0atau negatif. - Baris
if (n == 1) return 1;: base case kedua untukn = 1. - Baris
return fib_naive(n - 1) + fib_naive(n - 2);: dua panggilan rekursif. Inilah yang menyebabkan ledakan eksponensial. - Hasil
fib(20) = 6765: benar secara matematis, tetapi perhatikan bahwafib(50)tidak akan selesai dalam waktu yang wajar.
Mengukur Jumlah Panggilan #
Kode: Hitung Jumlah Panggilan Rekursif #
#include <iostream>
long long g_calls = 0;
long long fib_counted(int n) {
++g_calls;
if (n <= 0) return 0;
if (n == 1) return 1;
return fib_counted(n - 1) + fib_counted(n - 2);
}
int main() {
for (int n : {5, 10, 15, 20, 25, 30}) {
g_calls = 0;
long long result = fib_counted(n);
std::cout << "fib(" << n << ") = " << result
<< " | panggilan rekursif: " << g_calls << '\n';
}
return 0;
}Output yang Dihasilkan:
fib(5) = 5 | panggilan rekursif: 15
fib(10) = 55 | panggilan rekursif: 177
fib(15) = 610 | panggilan rekursif: 1973
fib(20) = 6765 | panggilan rekursif: 21891
fib(25) = 75025 | panggilan rekulsif: 242785
fib(30) = 832040 | panggilan rekursif: 2692537g_calls: variabel global yang menghitung setiap pemanggilan fungsi.- Pola pertumbuhan: 15 → 177 → 1973 → 21891 → 242785 → 2692537. Setiap kenaikan
n5 satuan, jumlah panggilan kira-kira ×11 (karenaφ^5 ≈ 11.09). fib(30) ≈ 2.7 juta panggilan: masih bisa dihitung dalam puluhan milidetik. Tapifib(50)akan butuh ~2.9 miliar panggilan — menit-menit.- Konfirmasi eksponensial: rasio
T(n+5) / T(n) ≈ 11, sesuaiφ^5 ≈ 11.09.
Tribonacci: Tiga Cabang #
Tribonacci adalah generalisasi Fibonacci dengan tiga suku sebelumnya:
trib(n) = trib(n-1) + trib(n-2) + trib(n-3)untukn > 2trib(0) = 0,trib(1) = 0,trib(2) = 1(base case)
Sekarang ada tiga panggilan rekursif per node → ledakan lebih cepat.
Kode: Tribonacci Naif #
#include <iostream>
long long g_calls = 0;
long long trib(int n) {
++g_calls;
if (n < 2) return 0;
if (n == 2) return 1;
return trib(n - 1) + trib(n - 2) + trib(n - 3);
}
int main() {
for (int n : {5, 10, 15, 20, 25}) {
g_calls = 0;
long long result = trib(n);
std::cout << "trib(" << n << ") = " << result
<< " | panggilan: " << g_calls << '\n';
}
return 0;
}Output yang Dihasilkan:
trib(5) = 4 | panggilan: 13
trib(10) = 81 | panggilan: 289
trib(15) = 1705 | panggilan: 6094
trib(20) = 35890 | panggilan: 128287
trib(25) = 755476 | panggilan: 2700421- Baris
trib(n-1) + trib(n-2) + trib(n-3): tiga panggilan rekursif. Setiap node memiliki 3 anak. - Pertumbuhan: 13 → 289 → 6094 → 128287 → 2700421. Rasio per kenaikan 5: ~22×, ~21×, ~21×, ~21× — sesuai dengan basis eksponensial baru.
- Basis eksponensial Tribonacci:
λ ≈ 1.839(lebih besar dariφ ≈ 1.618Fibonacci). Ledakan lebih cepat dari Fibonacci. trib(25)butuh 2.7 juta panggilan — untuk Fibonacci,fib(25)hanya butuh 243 ribu. Tribonacci meledak ~11x lebih cepat.
Memoization: Solusi Ledakan Eksponensial #
Akar masalah: sub-masalah yang sama dihitung berulang kali. Solusi: simpan hasil yang sudah dihitung, gunakan kembali bila diminta lagi. Teknik ini disebut memoization.
Kode: Fibonacci dengan Memoization #
#include <iostream>
#include <vector>
long long g_calls = 0;
std::vector<long long> g_memo;
long long fib_memo(int n) {
++g_calls;
if (n <= 0) return 0;
if (n == 1) return 1;
if (g_memo[n] != -1) return g_memo[n];
g_memo[n] = fib_memo(n - 1) + fib_memo(n - 2);
return g_memo[n];
}
int main() {
int n = 40;
g_memo.assign(n + 1, -1);
g_calls = 0;
long long result = fib_memo(n);
std::cout << "fib_memo(" << n << ") = " << result << '\n';
std::cout << "panggilan rekursif: " << g_calls << '\n';
return 0;
}Output yang Dihasilkan:
fib_memo(40) = 102334155
panggilan rekursif: 79- Baris
if (g_memo[n] != -1) return g_memo[n];: bila hasil sudah ada di memo, langsung kembalikan. Tidak ada panggilan rekursif tambahan. - Baris
g_memo[n] = fib_memo(n - 1) + fib_memo(n - 2);: hitung sekali, simpan ke memo. fib(40) = 102334155: dengan memoization, hanya butuh 79 panggilan rekursif. Tanpa memo,fib(40)butuh ~331 juta panggilan.- Kompleksitas: O(n) waktu dan O(n) ruang. Memoization mengubah eksponensial menjadi linier.
Versi Iteratif Fibonacci #
Bila hanya butuh nilai fib(n), versi iteratif paling efisien: O(n) waktu, O(1) ruang.
Kode: Fibonacci Iteratif O(1) Space #
#include <iostream>
long long fib_iter(int n) {
if (n <= 0) return 0;
if (n == 1) return 1;
long long prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; ++i) {
long long cur = prev1 + prev2;
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
int main() {
for (int n : {10, 20, 50, 90}) {
std::cout << "fib_iter(" << n << ") = " << fib_iter(n) << '\n';
}
return 0;
}Output yang Dihasilkan:
fib_iter(10) = 55
fib_iter(20) = 6765
fib_iter(50) = 12586269025
fib_iter(90) = 2880067194370816120- Variabel
prev2,prev1: hanya menyimpan dua suku terakhir. Tidak ada array, tidak ada rekursi. - Loop dari 2 hingga n: update
prev1danprev2setiap iterasi. - Kompleksitas: O(n) waktu, O(1) ruang. Paling efisien untuk Fibonacci.
fib(90) ≈ 2.88 × 10^18: masih muat dilong long(maks ~9.2 × 10^18).
Rekursi Bercabang Lain: Kombinasi dan Subsets #
Rekursi bercabang tidak hanya untuk Fibonacci. Contoh lain:
- Subsets: setiap elemen bisa masuk atau tidak → 2 cabang per elemen →
2^ntotal. - Permutations: setiap posisi bisa diisi oleh elemen yang tersisa →
n!total. - Tree DFS: 2 cabang per node (kiri, kanan) →
O(n)total bila tidak ada subtree duplikat.
Perbedaan dengan Fibonacci: tree DFS mengunjungi setiap node tepat sekali, sehingga tetap O(n). Fibonacci naif mengunjungi sub-masalah yang sama berulang kali → eksponensial.
Ringkasan #
- Rekursi bercabang memanggil dirinya dua atau lebih kali per pemanggilan → membentuk recursion tree.
- Fibonacci naif: 2 cabang per node → O(φ^n) ≈ O(1.618^n).
fib(50)tidak praktis. - Tribonacci: 3 cabang → O(λ^n) ≈ O(1.839^n). Lebih cepat meledak dari Fibonacci.
- Memoization menyimpan hasil sub-masalah → transformasi dari eksponensial ke polinomial.
- Versi iteratif Fibonacci: O(n) waktu, O(1) ruang — paling efisien.
- Rekursi bercabang tidak selalu eksponensial: bila setiap sub-masalah unik (misal tree DFS), total tetap O(n).