Divide and conquer adalah paradigma algoritma yang bekerja dengan tiga langkah: divide (pecah jadi sub-masalah), conquer (selesaikan tiap sub-masalah, biasanya rekursif), dan combine (gabungkan hasilnya). Banyak algoritma klasik — merge sort, quick sort, binary search, closest pair — adalah implementasi paradigma ini. Pada Chapter ini kita akan membedah struktur umum divide and conquer, menyegarkan Master Theorem untuk analisis kompleksitas, dan menerapkannya pada satu contoh non-trivial: maximum subarray sum dengan strategi divide and conquer.
Tiga Langkah Paradigma #
Aturan #
Setiap algoritma divide and conquer memiliki struktur berikut:
solve(problem P):
if (P cukup kecil):
return solusi langsung // base case
(P1, P2, ..., Pk) = divide(P) // pecah
for each Pi:
Si = solve(Pi) // conquer (rekursif)
return combine(S1, S2, ..., Sk) // combineMekanisme: Mengapa Bekerja #
Kompleksitas ditentukan oleh tiga parameter:
T(n)— waktu untuk masalah ukurann.a— jumlah sub-masalah yang dibuat.b— rasio ukuran sub-masalah terhadap masalah asli (sub-masalah ukurann/b).f(n)— waktu untukdivide+combine.
Persamaan recurrences: T(n) = a · T(n/b) + f(n).
Master Theorem (Segar) #
Aturan #
Master Theorem memberi solusi tertutup untuk recurrences berbentuk T(n) = a · T(n/b) + f(n):
| Kasus | Kondisi | Solusi |
|---|---|---|
| 1 | f(n) = O(n^c) dengan c < log_b(a) | T(n) = Θ(n^(log_b a)) |
| 2 | f(n) = Θ(n^c) dengan c = log_b(a) | T(n) = Θ(n^c · log n) |
| 3 | f(n) = Ω(n^c) dengan c > log_b(a) | T(n) = Θ(f(n)) |
Contoh Penting #
| Algoritma | a | b | f(n) | T(n) |
|---|---|---|---|---|
| Binary search | 1 | 2 | O(1) | O(log n) |
| Merge sort | 2 | 2 | O(n) | O(n log n) |
| Karatsuba multiply | 3 | 2 | O(n) | O(n^1.585) |
| Strassen matrix mul | 7 | 2 | O(n²) | O(n^2.807) |
Contoh: Maximum Subarray Sum (Divide and Conquer) #
Soal #
Diberikan array a berisi integer (boleh negatif), cari jumlah maksimum subarray kontigu. Subarray boleh kosong (jumlah 0).
Strategi D&C #
Bagi array menjadi dua bagian sama besar: left = a[0..mid], right = a[mid+1..n-1]. Maka subarray maksimum berada di salah satu dari:
- Sepenuhnya di kiri → selesaikan rekursif di
left. - Sepenuhnya di kanan → selesaikan rekursif di
right. - Memotong titik tengah → menjorok ke kiri dari
middan ke kanan darimid+1. Cari “suffix maksimum” di kiri + “prefix maksimum” di kanan.
Kode: Implementasi D&C #
#include <iostream>
#include <vector>
#include <algorithm>
#include <limits>
long long cross_sum(const std::vector<int>& a, int lo, int mid, int hi) {
// Suffix maksimum dari a[lo..mid] berakhir di mid
long long left_sum = 0, best_left = 0;
for (int i = mid; i >= lo; --i) {
left_sum += a[i];
best_left = std::max(best_left, left_sum);
}
// Prefix maksimum dari a[mid+1..hi] dimulai di mid+1
long long right_sum = 0, best_right = 0;
for (int i = mid + 1; i <= hi; ++i) {
right_sum += a[i];
best_right = std::max(best_right, right_sum);
}
return best_left + best_right;
}
long long max_subarray(const std::vector<int>& a, int lo, int hi) {
if (lo > hi) return 0;
if (lo == hi) return std::max(0, a[lo]);
int mid = lo + (hi - lo) / 2;
long long left = max_subarray(a, lo, mid);
long long right = max_subarray(a, mid + 1, hi);
long long cross = cross_sum(a, lo, mid, hi);
return std::max({left, right, cross});
}
long long max_subarray(const std::vector<int>& a) {
if (a.empty()) return 0;
return max_subarray(a, 0, static_cast<int>(a.size()) - 1);
}
int main() {
std::vector<std::vector<int>> tests = {
{-2, 1, -3, 4, -1, 2, 1, -5, 4},
{1, 2, 3, 4, 5},
{-1, -2, -3, -4},
{5, -2, 3, 4, -1, 7, -8, 2, 1}
};
for (const auto& a : tests) {
std::cout << "max subarray = " << max_subarray(a) << '\n';
}
return 0;
}Output yang Dihasilkan:
max subarray = 6
max subarray = 15
max subarray = 0
max subarray = 16- Fungsi
cross_sum: menghitung jumlah subarray yang memotong titik tengah. Subarray ini berbentuka[i..j]dengani ≤ mid < j. - Baris
for (int i = mid; i >= lo; --i): iterasi darimidke kiri, mengakumulasi suffix. Simpan nilai maksimumbest_left. - Baris
for (int i = mid + 1; i <= hi; ++i): iterasi darimid+1ke kanan, mengakumulasi prefix. Simpan nilai maksimumbest_right. - Baris
return best_left + best_right;: subarray melintasi tengah = suffix maks kiri + prefix maks kanan. - Baris
int mid = lo + (hi - lo) / 2;: hindari overflowlo + hi. Base caselo == himengembalikanmax(0, a[lo])(subarray boleh kosong). - Baris
long long cross = cross_sum(a, lo, mid, hi);: hitung subarray yang melintasi tengah. - Baris
return std::max({left, right, cross});: jawaban adalah maksimum dari tiga kemungkinan. - Hasil
6untuk{-2,1,-3,4,-1,2,1,-5,4}: subarray{4,-1,2,1}= 6. Sesuai (ini soal LeetCode klasik). - Hasil
0untuk{-1,-2,-3,-4}: semua negatif, pilih subarray kosong → 0. - Hasil
16untuk{5,-2,3,4,-1,7,-8,2,1}: subarray{5,-2,3,4,-1,7}= 16. Sesuai.
Divide and Conquer pada Segment Tree #
Salah satu penggunaan D&C yang sangat praktis adalah segment tree — pohon biner di mana setiap node menyimpan agregat dari sebuah rentang. Struktur ini memungkinkan query rentang dalam O(log n). Pembangunan pohon awal adalah D&C murni: bagi array, rekursif ke kiri-kanan, gabungkan.
Setiap query “jumlah elemen di [l, r]” dijawab dengan menyusuri O(log n) node yang persis menutupi rentang tersebut.
Ringkasan #
- Divide and conquer = divide → conquer (rekursif) → combine. Base case menangani masukan kecil.
- Master Theorem: untuk
T(n) = a·T(n/b) + f(n), solusi bergantung pada perbandinganf(n)dengann^(log_b a). - Merge sort dan binary search adalah contoh kanonik D&C; keduanya mengikuti pola “bagi dua, rekursif, gabung”.
- Contoh maximum subarray via D&C menghasilkan O(n log n), lebih lambat dari Kadane O(n) tetapi merupakan dasar untuk segment tree.
- Segment tree adalah D&C statis yang siap pakai untuk range query — kuasai pola umumnya untuk soal-soal lanjutan.