KELAS : 1C (sore)
NPM : 221055201115
JURUSAN : PRODI INFORMATIKA
PERTEMUAN : Ke – 2
1. Algoritma Rekursif
Salah satu konsep paling dasar dalam ilmu komputer dan pemrograman adalah pengunaan fungsi
sebagai abstraksi untuk kode-kode yang digunakan berulang kali. Kedekatan ilmu komputer
dengan matematika juga menyebabkan konsep-konsep fungsi pada matematika seringkali
dijumpai. Salah satu konsep fungsi pada matematika yang ditemui pada ilmu komputer adalah
fungsi rekursif: sebuah fungsi yang memanggil dirinya sendiri.
Kode berikut memperlihatkan contoh fungsi rekursif, untuk menghitung hasil kali dari dua
bilangan:
Bagaimana cara kerja fungsi rekursif ini? Sederhananya, selama nilai b bukan 1, fungsi akan terus
memanggil perintah a + kali(a, b - 1), yang tiap tahapnya memanggil dirinya sendiri
sambil mengurangi nilai b. Mari kita coba panggil fungsi kali dan uraikan langkah
pemanggilannya:
kali(2, 4)
-> 2 + kali(2, 3)
-> 2 + (2 + kali(2, 2))
-> 2 + (2 + (2 + kali(2, 1)))
-> 2 + (2 + (2 + 2))
-> 2 + (2 + 4)
-> 2 + 6
-> 8
def faktorial(n):
return n if n == 1 else n * faktorial(n - 1)
Fungsi faktorial memiliki cara kerja yang sama dengan fungsi kali. Mari kita panggil dan lihat
langkah pemanggilannya: faktorial(5)
-> 5 * faktorial(4)
-> 5 * (4 * faktorial(3))
-> 5 * (4 * (3 * faktorial(2)))
-> 5 * (4 * (3 * (2 * faktorial(1))))
-> 5 * (4 * (3 * (2 * 1)))
-> 5 * (4 * (3 * 2))
-> 5 * (4 * 6)
-> 5 * 24
-> 120
Dengan melihat kemiripan cara kerja serta kode dari fungsi faktorial dan kali, kita dapat
melihat bagaimana fungsi rekursif memiliki dua ciri khas:
1. Fungsi rekursif selalu memiliki kondisi yang menyatakan kapan fungsi tersebut berhenti.
Kondisi ini harus dapat dibuktikan akan tercapai, karena jika tidak tercapai maka kita tidak
dapat membuktikan bahwa fungsi akan berhenti, yang berarti algoritma kita tidak benar.
2. Fungsi rekursif selalu memanggil dirinya sendiri sambil mengurangi atau memecahkan
data masukan setiap panggilannya. Hal ini penting diingat, karena tujuan utama dari
rekursif ialah memecahkan masalah dengan mengurangi masalah tersebut menjadi
masalah-masalah kecil.
Setiap fungsi rekursif yang ada harus memenuhi kedua persyaratan di atas untuk memastikan
fungsi rekursif dapat berhenti dan memberikan hasil. Kebenaran dari nilai yang dihasilkan tentu
saja memerlukan pembuktian dengan cara tersendiri. Tetapi sebelum masuk ke analisa dan
pembuktian fungsi rekursif, mari kita lihat kegunaan dan contoh-contoh fungsi rekursif lainnya
lagi.
Fungsi Rekursif dan Iterasi
Pembaca yang jeli akan menyadari bahwa kedua contoh fungsi rekursif yang diberikan
sebelumnya, faktorial dan kali, dapat diimplementasikan tanpa menggunakan fungsi
rekursif. Berikut adalah contoh kode untuk perhitungan faktorial tanpa menggunakan rekursif:
def faktorial_iterasi(n):
hasil = 1
for i in range(1, n + 1):
hasil = hasil * i
return hasil
Binary Tree
Sebagai struktur rekursif, tentunya penelusuran binary tree akan lebih mudah dilakukan secara
rekursif dibandingkan iterasi. Hal ini sangat kontras dengan, misalnya, pencarian karakter di dalam
string. Sebagai data yang disimpan secara linear, pencarian karakter dalam string akan lebih
mudah untuk dilakukan secara iteratif.
Untuk mempermudah ilustrasi, mari kita lakukan perbandingan antara implementasi rekursif dan
iteratif untuk masalah yang lebih cocok diselesaikan dengan masing-masing pendekatan.
Misalnya, implementasi algoritma euclidean untuk menghitung faktor persekutuan terbesar (FPB)
yang lebih cocok untuk diimplementasikan dengan metode rekursif seperti berikut:
gcd(x,y)={xgcd(y,remainder(x,y))if y=0if y>0gcd(x,y)={xif y=0gcd(y,remainder(x,y))if y>0
tentunya implementasi secara rekursif lebih sederhana dan mudah dimengerti dibandingkan
dengan secara iterasi.
Sekarang mari kita lihat contoh algoritima yang lebih cocok diimplementasikan secara iteratif,
misalnya linear search. Implementasi standar linear search secara iteratif adalah sebagai berikut:
Perhatikan bagaimana diperlukan lebih banyak pengecekan pada fungsi rekursif, serta tambahan
parameter pos yang berguna untuk menyimpan posisi pengujian dan ditemukannya elemen yang
dicari. Jika menggunakan iterasi variabel pos tidak dibutuhkan lagi karena posisi ini akan
didapatkan secara otomatis ketika sedang menelusuri list. Dengan melihat jumlah argumen dan
pengecekan yang harus dilakukan, dapat dilihat bahwa implementasi linear search menjadi lebih
sederhana dan mudah dengan menggunakan metode iterasi.
2. Tail Call
Sesuai definisinya, dalam membuat fungsi rekursif pada satu titik kita akan harus memanggil
fungsi itu sendiri. Pemanggilan diri sendiri di dalam fungsi tentunya memiliki konsekuensi
tersendiri, yaitu pengunaan memori. Dengan memanggil dirinya sendiri, secara otomatis sebuah
fungsi akan memerlukan memori tambahan, untuk menampung seluruh variabel baru serta proses
yang harus dilakukan terhadap nilai-nilai baru tersebut. Penambahan memori ini seringkali
menyebabkan stack overflow ketika terjadi pemanggilan rekursif yang terlalu banyak.
Untuk menanggulangi kesalahan stack overflow ini, para pengembang bahasa pemrograman
mengimplementasikan apa yang dikenal dengan tail call optimization. Dalam merancang dan
menganalisa algoritma rekursif, pengertian akan tail call optimization merupakan hal yang sangat
penting. Jadi, apa itu tail call?
Tail call merupakan pemanggilan fungsi sebagai perintah terakhir di dalam fungsi lain.
Sederhananya, ketika kita memanggil sebuah fungsi pada bagian akhir dari fungsi lain, kita
melakukan tail call, seperti pada kode di bawah:
def fungsi(x):
y = x + 10
return fungsi_lain(y)
def tail_call(n):
if n == 0:
return fungsi_1(n + 1)
else:
return fungsi_2(n)
def bukan_tail_call(n):
result = fungsi_lain(n % 5)
return result * 10
yang bukan merupakan tail call, karena kode terakhir yang dieksekusi (result * 10e) adalah
sebuah operasi, bukan pemanggilan fungsi. Cara kerja ini tentunya juga dibawa ke fungsi rekursif,
di mana:
def faktorial(n):
if n == 1:
return 1
else:
return n * faktorial(n - 1)
def faktorial(n):
return n if n == 1 else n * faktorial(n - 1)
Salah satu informasi yang didapatkan di sini adalah kapan algoritma berhenti melakukan rekursif,
yaitu n == 1. Informasi lain yang kita miliki adalah berkurangnya jumlah data pada setiap
pemanggilan faktorial. Bagaimana kita melakukan analisis algoritma ini? Cara termudahnya
adalah dengan menggambarkan fungsi matematika dari faktorial terlebih dahulu:
faktorial(n)={1n∗faktorial(n−1)remainder(x,y))if n=1if n>1faktorial(n)={1if n=1n∗faktorial(n−1)
remainder(x,y))if n>1
Melalui fungsi matematika ini, kita dapat mulai melakukan penurunan untuk fungsi perhitungan
langkah fungsi faktorialfaktorial untuk kasus n>1n>1:
f(n)=1+f(n−1)=1+1+f(n−2)=1+1+1+f(n−3)...=n+f(n−k)f(n)=1+f(n−1)=1+1+f(n−2)=1+1+1+
f(n−3)...=n+f(n−k)
Dan tentunya kita dapat mengabaikan penambahan langkah nn di awal, serta dengan syarat
bahwa fungsi berhenti ketika n−k=1n−k=1:
n−kk=1=n−1
Maka dapat disimpulkan bahwa fungsi faktorial memiliki kompleksitas n−1n−1, atau O(n)O(n).
Ingat bahwa kunci dari perhitungan kompleksitas untuk algoritma rekursif terdapat pada fungsi
matematis algoritma dan definisi kapan berhentinya fungsi rekursif tersebut.
Kesimpulan
Fungsi rekursif merupakan fungsi yang memanggil dirinya sendiri. Terdapat dua komponen
penting dalam fungsi rekursif, yaitu kondisi kapan berhentinya fungsi dan pengurangan atau
pembagian data ketika fungsi memanggil dirinya sendiri. Optimasi fungsi rekursif dapat dilakukan
dengan menggunakan teknik tail call, meskipun teknik ini tidak selalu diimplementasikan oleh
semua bahasa pemrograman.
Selain sebagai fungsi, konsep rekursif juga terkadang digunakan untuk struktur data seperti binary
tree atau list.
Definisi formal dari rekursi
Rekursi dalam program perekaman layar, dengan suatu jendela paling kecil mengandung
foto keseluruhan layar.
Dalam matematika dan ilmu komputer, kelas dari objek atau metode memperlihatkan
perilaku rekursif bila mereka dapat didefinisikan oleh dua properti berikut:
Rasio Golden:
Faktorial:
Bilangan Fibonacci:
Bilangan Catalan: ,
3. Algoritma Divide and Conquer
print(total) # 255
Algoritma perulangan yang digunakan pada kode di atas memang sederhana dan memberikan
hasil yang benar, tetapi terdapat beberapa masalah pada kode tersebut, yaitu perhitungan
dilakukan secara linear, yang menghasilkan kompleksitas \(O(n)\). Hal ini tentunya cukup ideal
untuk ukuran list kecil, tetapi jika ukuran list menjadi besar (beberapa Milyar elemen) maka
perhitungan akan menjadi sangat lambat. Kenapa perhitungannya menjadi lambat? Karena nilai
dari total tergantung kepada kalkulasi nilai total sebelumnya. Kita tidak dapat melakukan
perhitungan total dari depan dan belakang list sekaligus, sehingga kita dapat mempercepat
perhitungan dua kali lipat. Dengan kode di atas, kita tidak dapat membagi-bagikan pekerjaan ke
banyak pekerja / CPU!
Lalu apa yang dapat kita lakukan? Langkah pertama yang dapat kita lakukan adalah menerapkan
teknik rekursif untuk membagi-bagikan masalah menjadi masalah yang lebih kecil. Jika awalnya
kita harus menghitung total keseluruhan list satu per satu, sekarang kita dapat melakukan
perhitungan dengan memecah-mecah list terlebih dahulu:
def sums(lst):
if len(lst) >= 1:
return lst[0]
mid = len(lst) // 2
left = sums(lst[:mid])
right = sums(lst[mid:])
print(sums(nums)) # 255
Singkatnya, setelah membagikan list menjadi dua bagian terus menerus sampai bagian
terkecilnya, kita menjumlahkan kedua nilai list tersebut, seperti pada gambar berikut:
Langkah-langkah
Langkah-langkah umum algoritme Divide and Conquer adalah: [1]
Divide and Conquer dulunya adalah strategi militer yang dikenal dengan nama divide ut imperes.
Sekarang strategi tersebut menjadi strategi fundamental di dalam ilmu komputer dengan nama
Divide and Conquer.
Definisi
• Divide: membagi persoalan menjadi beberapa upa-masalah yang memiliki kemiripan dengan
persoalan semula namun berukuran lebih kecil (idealnya berukuran hampir sama),
•Obyek persoalan yang dibagi : masukan (input) atau instances persoalan yang berukuran n
seperti:
- tabel (larik),
- matriks,
- eksponen,
• Tiap-tiap upa-masalah mempunyai karakteristik yang sama (the same type) dengan karakteristik
masalah asal
• sehingga metode Divide and Conquer lebih natural diungkapkan dengan skema rekursif.
r, k : integer
Algoritma
if n n0 then {ukuran masalah sudah cukup kecil }
else
• Ukuran tabel hasil pembagian dapat dibuat cukup kecil sehingga mencari minimum dan
maksimum dapat diselesaikan (SOLVE) secara trivial.
• Dalam hal ini, ukuran “kecil” yang dipilih adalah 1 elemen atau 2 elemen.
Algoritma:
sama, A1 dan A2
min2, maks2)
(c) COMBINE: if min1 < min2 then min min1 else min min2 if
{ Mengurutkan tabel A dengan metode Divide and Conquer Masukan: Tabel A dengan n elemen
Keluaran: Tabel A yang terurut }
Algoritma:
if Ukuran(A) > 1 then Bagi A menjadi dua bagian, A1 dan A2, masing-masing berukuran n1 dan n2
(n = n1 + n2)
Combine (A1, A2, A) { gabung hasil pengurutan bagian kiri dan bagian kanan } end
Merge Sort
Algoritma:
1. Untuk kasus n = 1, maka tabel A sudah terurut dengan sendirinya (langkah SOLVE).
(a) DIVIDE: bagi tabel A menjadi dua bagian, bagian kiri dan bagian kanan, masing-masing bagian
berukuran n/2 elemen. (
(c) MERGE: gabung hasil pengurutan kedua bagian sehingga diperoleh tabel A yang terurut.
Kompleksitas waktu:
Asumsi: n = 2k T(n) = jumlah perbandingan pada pengurutan dua buah upatabel + jumlah
perbandingan pada prosedur Merge 2 ( / 2) , 1
Penyelesaian:
T(n) = 2T(n/2) + cn
= ...
= 2k T(n/2k ) +kcn
n/2k = 1 k = 2 log n
sehingga
Contoh:
A 4 12 3 9 1 21 5 2
1.Mudah membagi, sulit menggabung (easy split/hard join) Tabel A dibagidua berdasarkan posisi
elemen:
Divide: A1 4 12 3 9 A2 1 21 5 2
Sort: A1 3 4 9 12 A2 1 2 5 21
Combine: A1 1 2 3 4 5 9 12 21
2.Sulit membagi, mudah menggabung (hard split/easy join) Tabel A dibagidua berdasarkan nilai
elemennya. Misalkan elemen-elemen A1 elemen-elemen A2.
A 4 12 3 9 1 21 5 2
Divide: A1 4 2 3 1 A2 9 21 5 12
Sort: A1 1 2 3 4 A2 5 9 12 21
Combine: A 1 2 3 4 5 9 12 21
Dynamic Programming (selanjutnya disebut “DP” saja) merupakan salah satu teknik perancangan
algoritma yang dikembangkan untuk menyelesaikan permasalahan yang sangat kompleks dengan
memecah permasalahan tersebut menjadi banyak sub-permasalahan. Perbedaan utama DP
dengan Divide and Conquer (selanjutnya disebut “D&C”) adalah pada DP kita menggunakan
kembali hasil kalkulasi sub-masalah yang telah dilakukan sebelumnya. Apa artinya?
Untuk mempermudah penjelasan, mari kita selesaikan masalah sederhana yang telah kita bahas
berkali-kali: perhitungan bilangan fibonacci. Algoritma untuk menyelesaikan perhitungan fibonacci
secara naif adalah seperti berikut:
def fibonacci(n):
if n <= 2:
hasil = 1
else:
hasil = fibonacci(n - 1) + fibonacci(n - 2)
return hasil
Algoritma fibonacci sederhana seperti di atas dapat dikatakan sebagai algoritma D&C, karena kita
membagikan perhitungan fibonacci ke dua fungsi fibonacci, sampai didapatkan nilai hasil
terkecilnya. Pemanggilan fungsi fibonacci di atas dapat digambarkan seperti berikut:
Pemanggilan Fungsi Fibonacci
Perhatikan bagaimana [Math Processing Error]f(n−2) dan [Math Processing
Error]f(n−3) dikalkulasikan sebanyak dua kali, dan semakin kita masuk ke dalam pohon
pemanggilan, kita akan melihat semakin banyak fungsi-fungsi yang dipanggil berkali-kali.
Pendekatan DP menghindari kalkulasi fungsi yang berulang kali seperti ini dengan
melakukan memoization, yaitu menyimpan hasil kalkulasi fungsi tersebut dan menggunakan nilai
yang disimpan ketika perhitungan yang sama dibutuhkan kembali. Dengan menyimpan hasil
kalkulasi seperti ini, tentunya jumlah total langkah perhitungan yang harus dilakukan menjadi
berkurang.
Misalnya, kita dapat menyimpan hasil kalkulasi dari fungsi fibonacci tersebut pada
sebuah dictionary, seperti berikut:
memo = dict()
def fibonacci_dp(n):
if n in memo.keys():
return memo[n]
elif n <= 2:
hasil = 1
else:
hasil = fibonacci_dp(n - 1) + fibonacci_dp(n - 2)
memo[n] = hasil
return hasil
Dengan menyimpan hasil kalkulasi dari fungsi yang telah ada, maka proses pemanggilan fungsi
akan menjadi seperti berikut
def fibonacci_dp_bu(n):
memo = dict()
for i in range(1, n + 1):
if i <= 2:
hasil = 1
else:
hasil = memo[i - 1] + memo[i - 2]
memo[i] = hasil
return memo[n]
Untuk melihat efek langsung dari ketiga fungsi tersebut, coba jalankan ketiga fungsi tersebut
untuk n yang sama, dan lihat perbedaan waktu eksekusinya! Sebagai latihan tambahan, hitung
juga kompleksitas dari ketiga fungsi perhitungan fibonacci tersebut.
Mari kita rangkum hal yang telah kita pelajari mengenai DP sejauh ini:
Dengan melakukan kalkulasi sederhana dari teks, tentunya kita bisa saja melakukan pemerataan
teks dengan mudah. Misalnya, untuk menghitung total teks yang dapat masuk ke dalam sebuah
baris tulisan, kita dapat menggunakan persamaan berikut:
[Math Processing Error]ukuran halaman←total ukuran kata+total ukuran spasi+total ukuran
simbol
Sehingga untuk membuat sebuah teks menjadi rata penuh (justified) kita dapat memasukkan
setiap kata, spasi, dan simbol satu demi satu sampai kita memenuhi sebuah baris. Jika kata
selanjutnya tidak lagi memiliki ukuran yang cukup, maka kita dapat menambahkan spasi di tengah-
tengah kata sebelumnya sampai baris penuh, dan lalu berpindah baris.
Secara sederhana, algoritma naif untuk melakukan rata penuh teks adalah seperti berikut:
1. Ambil satu elemen dalam teks, baik berupa kata, simbol, maupun spasi. Masukkan elemen
ini ke dalam baris.
4. Tambahkan ukuran baris sekarang dengan ukuran elemen berikutnya. Hasil pengukuran
ini selanjutnya akan disebut “Ukuran Baris Selanjutnya” atau UBS.
5. Cek nilai UBS:
1. Jika UBS masih lebih kecil dari lebar halaman, kembali ke langkah 1
2. Jika UBS sudah lebih dari lebar halaman:
1. Tambahkan spasi di antara setiap kata dalam baris sampai ukuran baris sama
dengan lebar halaman.
Secara kasar, algoritma di atas dapat diimplementasikan seperti kode berikut (yang jelas tidak
dapat dijalankan):
lines = [[next]]
current_line = 0
while(!text.empty()):
while(next_total_size < page_size):
total_size = next_total_size
next = text.get_next()
lines[current_line].push(next)
next_total_size = total_size + next.size()
current_line = current_line + 1
Hasil algoritma di atas kurang optimal, karena ketika terdapat kata-kata yang panjang dalam
sebuah kalimat, kita terpaksa harus memotong baris terlalu cepat, dan akhirnya menambahkan
banyak spasi. Contoh eksekusi dari algoritma di atas dapat dilihat pada gambar berikut:
Perhitungan nilai keburukan sendiri dapat dilakukan dengan menggunakan rumus sederhana
berikut:
[Math Processing Error]keburukan(i,j)={ukuran baris>lebar halaman∞(lebar halaman−ukuran
baris)3
dengan [Math Processing Error]i dan [Math Processing Error]j sebagai awal dan akhir dari kata
yang ingin dihitung tingkat keburukannya. Jika dijadikan kode program, algoritma tersebut dapat
dituliskan seperti berikut:
def length(word_lengths, i, j):
return sum(word_lengths[i- 1:j]) + j - i + 1
s = dict()
return s
Perlu dicatat bahwa kode di atas belum mengikut sertakan spasi dalam perhitungan, dan juga
belum membangun kembali baris-baris yang telah dipecah menjadi sebuah teks (paragraf).
Kesimpulan
Secara sederhana, teknik DP dapat dikatakan adalah sebuah teknik brute force yang pintar. Kita
memecah-mecah masalah menjadi sub-masalah, dan menyelesaikan seluruh sub-masalah
tersebut. Perbedaan utama dari DP dengan D&C adalah DP melakukan penyimpanan hasil
penyelesaian sub-masalah sehingga kita tidak perlu menyelesaikan sub-masalah yang sama
berulang kali
Contoh:
Karakteristikkan struktur solusi optimal
(c) • Maka rute yang dilalui adalah x1→x2→x3→x4 → x5=10 Pada persoalan ini,
(d) • Tahap (k) adalah proses memilih simpul tujuan berikutnya (ada 4 tahap).
(e) • Status (s) yang berhubungan dengan masing-masing tahap adalah simpulsimpul
di dalam graf multi-tahap.
4. ALGORITMA GREEDY
Algoritma greedy merupakan jenis algoritma yang menggunakan pendekatan penyelesaian masalah
dengan mencari nilai maksimum sementara pada setiap langkahnya. Nilai maksimum sementara ini
dikenal dengan istilah local maximum. Pada kebanyakan kasus, algoritma greedy tidak akan
menghasilkan solusi paling optimal, begitupun algoritma greedy biasanya memberikan solusi yang
mendekati nilai optimum dalam waktu yang cukup cepat.
Sebagai contoh dari penyelesaian masalah dengan algoritma greedy, mari kita lihat sebuah masalah
klasik yang sering dijumpai dalam kehidupan sehari-hari: mencari jarak terpendek dari peta.
Misalkan kita ingin bergerak dari titik A ke titik B, dan kita telah menemukan beberapa jalur dari
peta:
Jalur dari Titik A ke B
Jalur dari Titik A ke B
Dari peta yang ditampilkan di atas, dapat dilihat bahwa terdapat beberapa jalur dari titik A ke titik
B. Sistem peta pada gambar secara otomatis telah memilih jalur terpendek (berwarna biru). Kita
akan mencoba mencari jalur terpendek juga, dengan menggunakan algoritma greedy.
Langkah pertama yang harus kita lakukan tentunya adalah memilih struktur data yang tepat untuk
digunakan dalam merepresentasikan peta. Jika dilihat kembali, sebuah peta seperti pada gambar di
atas pada dasarnya hanya menunjukkan titik-titik yang saling berhubungan, dengan jarak tertentu
pada masing-masing titik tersebut. Misalnya, peta di atas dapat direpresentasikan dengan titik-titik
penghubung seperti berikut:
Graph Sederhana dari Titik A ke B
Graph Sederhana dari Titik A ke B
Dari gambar di atas, kita dapat melihat bagaimana sebuah peta jalur perjalanan dapat
direpresentasikan dengan menggunakan graph, spesifiknya Directed Graph (graph berarah). Maka
dari itu, untuk menyelesaikan permasalahan jarak terpendek ini kita akan menggunakan struktur
data graph untuk merepresentasikan peta. Berikut adalah graph yang akan digunakan:
Graph Berarah dari Titik A ke B
Graph Berarah dari Titik A ke B
Untuk mencari jarak terpendek dari A ke B, sebuah algoritma greedy akan menjalankan langkah-
langkah seperti berikut:
Kunjungi satu titik pada graph, dan ambil seluruh titik yang dapat dikunjungi dari titik sekarang.
Cari local maximum ke titik selanjutnya.
Tandai graph sekarang sebagai graph yang telah dikunjungi, dan pindah ke local maximum yang
telah ditentukan.
Kembali ke langkah 1 sampai titik tujuan didapatkan.
Jika mengapliikasikan langkah-langkah di atas pada graph A ke B sebelumnya maka kita akan
mendapatkan pergerakan seperti berikut:
Mulai dari titik awal (A). Ambil seluruh titik yang dapat dikunjungi.
Langkah Pertama Greedy
Langkah Pertama Greedy
Local maximum adalah ke C, karena jarak ke C adalah yang paling dekat.
Tandai A sebagai titik yang telah dikunjungi, dan pindah ke C.
Ambil seluruh titik yang dapat dikunjungi dari C.
Langkah Kedua Greedy
Langkah Kedua Greedy
Local maximum adaah ke D, dengan jarak 6.
Tandai C sebagai titik yang telah dikunjungi, dan pindah ke D.
Langkah Ketiga Greedy
Langkah Ketiga Greedy
(Langkah selanjutnya diserahkan kepada pembaca sebagai latihan).
Dengan menggunakan algoritma greedy pada graph di atas, hasil akhir yang akan didapatkan
sebagai jarak terpendek adalah A-C-D-E-F-B. Hasi jarak terpendek yang didapatkan ini tidak tepat
dengan jarak terpendek yang
sebenarnya (A-G-E-F-B). Algoritma greedy memang tidak selamanya memberikan solusi yang
optimal, dikarenakan pencarian local maximum pada setiap langkahnya, tanpa memperhatikan
solusi secara keseluruhan. Gambar berikut memperlihatkan bagaimana algoritma greedy dapat
memberikan solusi yang kurang optimal:
Solusi Kurang Optimal dari Greedy
Solusi Kurang Optimal dari Greedy
Tetapi ingat bahwa untuk kasus umum, kerap kali algoritma greedy memberikan hasil yang cukup
baik dengan kompleksitas waktu yang cepat. Hal ini mengakibatkan algoritma greedy sering
digunakan untuk menyelesaikan permasalahan kompleks yang memerlukan kecepatan jawaban,
bukan solusi optimal, misalnya pada game.
Implementasi Algoritma Greedy
Untuk memperdalam pengertian algoritma greedy, kita akan mengimplementasikan algoritma yang
telah dijelaskan pada bagian sebelumnya ke dalam kode program. Seperti biasa, contoh kode
program akan diberikan dalam bahasa pemrograman python. Sebagai langkah awal, tentunya kita
terlebih dahulu harus merepresentasikan graph. Pada implementasi yang kita lakukan, graph
direpresentasikan dengan menggunakan dictionary di dalam dictionary, seperti berikut:
DAG = {'A': {'C': 4, 'G': 9},
'G': {'E': 6},
'C': {'D': 6, 'H': 12},
'D': {'E': 7},
'H': {'F': 15},
'E': {'F': 8},
'F': {'B': 5}
# Hasil Representasi:
{'A': {'C': 4, 'G': 9},
'C': {'D': 6, 'H': 12},
'D': {'E': 7},
'E': {'F': 8},
'F': {'B': 5},
'G': {'E': 6},
'H': {'F': 15}}
Selanjutnya kita akan membuat fungsi yang mencari jarak terpendek dari graph yang dibangun,
dengan menggunakan algoritma greedy. Definisi dari fungsi tersebut sangat sederhana, hanya
sebuah fungsi yang mengambil graph, titik awal, dan titik akhir sebagai argumennya:
def shortest_path(graph, source, dest):
Jarak terpendek yang didapatkan akan dibangun langkah demi langkah, seperti pada algoritma
greedy yang mengambil nilai local maximum pada setiap langkahnya. Untuk hal ini, tentunya kita
akan perlu menyimpan jarak terpendek ke dalam sebuah variabel, dengan source sebagai isi awal
variabel tersebut. Jarak terpendek kita simpan ke dalam sebuah list, untuk menyederhanakan proses
penambahan nilai.
result = []
result.append(source)
Penelusuran graph sendiri akan kita lakukan melalui result, karena variabel ini merepresentasikan
seluruh node yang telah kita kunjungi dari keseluruhan graph. Variabel result pada dasarnya
merupakan hasil implementasi dari langkah 3 algoritma (“Tandai graph sekarang sebagai graph
yang telah dikunjungi”). Titik awal dari rute tentunya secara otomatis ditandai sebagai node yang
telah dikunjungi.
Selanjutnya, kita akan menelusuri graph sampai titik tujuan ditemukan, dengan menggunakan
iterasi:
while dest not in result:
current_node = result[-1]
dengan mengambil node yang sekarang sedang dicari local maximum-nya dari isi terakhir result.
Pencarian local maximum sendiri lebih memerlukan pengetahuan python daripada algoritma:
# Cari local maximum
local_max = min(graph[current_node].values())
# Ambil node dari local maximum,
# dan tambahkan ke result
# agar iterasi selanjutnya dimulai
# dari node sekarang.
for node, weight in graph[current_node].items():
if weight == local_max:
result.append(node)
Setelah seluruh graph ditelurusi sampai mendapatkan hasil, kita dapat mengembalikan result ke
pemanggil fungsi:
return result
Keseluruhan fungsi yang dibangun adalah sebagai berikut:
def shortest_path(graph, source, dest):
result = []
result.append(source)
while dest not in result:
current_node = result[-1]
local_max = min(graph[current_node].values())
for node, weight in graph[current_node].items():
if weight == local_max:
result.append(node)
return result
Perlu diingat bahwa fungsi ini masih banyak memiliki kekurangan, misalnya tidak adanya
penanganan kasus jika titik tujuan tidak ditemukan, atau jika terdapat node yang memiliki nilai
negatif (bergerak balik). Penanganan hal-hal tersebut tidak dibuat karena fungsi hanya bertujuan
untuk mengilustrasikan cara kerja algoritma greedy, bukan untuk digunakan pada aplikasi nyata.
Algoritma greedy merupakan algoritma yang besifat heuristik, mencari nilai maksimal sementara
dengan harapan akan mendapatkan solusi yang cukup baik. Meskipun tidak selalu mendapatkan
solusi terbaik (optimum), algoritma greedy umumnya memiliki kompleksitas waktu yang cukup
baik, sehingga algoritma ini sering digunakan untuk kasus yang memerlukan solusi cepat meskipun
tidak optimal seperti sistem real-time atau game.
Dari impementasi yang kita lakukan, dapat dilihat bagaimana algoritma greedy memiliki beberapa
fungsionalitas dasar, yaitu:
Fungsi untuk melakukan penelusuran masalah.
Fungsi untuk memilih local maximum dari pilihan-pilihan yang ada tiap langkahnya.
Fungsi untuk mengisikan nilai local maximum ke solusi keseluruhan.
Fungsi yang menentukan apakah solusi telah didapatkan.
Tentunya fungsi-fungsi di atas juga dapat digabungkan atau dipecah lebih lanjut lagi, menyesuaikan
dengan strategi greedy yang dikembangkan.
Misalkan kita ingin bergerak dari titik A ke titik I, dan kita telah menemukan beberapa jalur dari
peta:
Dari peta yang ditampilkan di atas, dapat dilihat bahwa terdapat beberapa jalur dari titik A ke titik I.
Sistem peta pada gambar secara otomatis telah memilih jalur terpendek (berwarna biru). Kita akan
mencoba mencari jalur terpendek juga, dengan menggunakan algoritma greedy.
Graph Sederhana dari Titik A ke I
Dari gambar di atas, kita dapat melihat bagaimana sebuah peta jalur perjalanan dapat
direpresentasikan dengan menggunakan graph, spesifiknya Directed Graph (graph berarah). Maka
dari itu, untuk menyelesaikan permasalahan jarak terpendek ini kita akan menggunakan struktur
data graph untuk merepresentasikan peta. Berikut adalah graph yang akan digunakan:
Graph Berarah dari Titik A ke I
Untuk mencari jarak terpendek dari A ke B, sebuah algoritma greedy akan menjalankan langkah-
langkah seperti berikut:
a. Kunjungi satu titik pada graph, dan ambil seluruh titik yang dapat dikunjungi dari titik sekarang.
b. Cari local maximum ke titik selanjutnya.
c. Tandai graph sekarang sebagai graph yang telah dikunjungi, dan pindah ke local maximum yang
telah ditentukan.
d. Kembali ke langkah 1 sampai titik tujuan didapatkan.
Dengan menggunakan algoritma greedy pada graph di atas, hasil akhir yang akan didapatkan
sebagai jarak terpendek adalah A-C-D-G-I. Hasi jarak terpendek yang didapatkan ini tidak tepat
dengan jarak terpendek yang sebenarnya (A-B-H-I). Algoritma greedy memang tidak selamanya
memberikan solusi yang optimal, dikarenakan pencarian local maximum pada setiap langkahnya,
tanpa memperhatikan solusi secara keseluruhan.
Di sini masalahnya adalah ukuran 'n', dan operasi dasarnya adalah tes
'jika' di mana item data dibandingkan dalam setiap iterasi. Tidak akan ada
perbedaan antara kasus terburuk dan terbaik karena jumlah swap selalu n-
1.
Brute Force String Matching
Jika semua karakter dalam pola unik, maka pencocokan string Brute
force dapat diterapkan dengan kompleksitas Big O(n) di mana n adalah
panjang string. Brute force Pencocokan string membandingkan pola
dengan substring karakter teks demi karakter hingga mendapatkan karakter
yang tidak cocok. Segera setelah ketidakcocokan ditemukan, karakter
substring yang tersisa dihapus, dan algoritme berpindah ke substring
berikutnya.
Kode semu di bawah ini menjelaskan logika pencocokan string. Di sini
algoritma mencoba mencari pola P[0…m-1] pada teks T[0….n-1].
Di sini kasus terburuk adalah ketika pergeseran ke substring lain tidak
dilakukan sampai perbandingan MTh.
Closest Pair
Rumusan masalah: Untuk mengetahui dua titik terdekat dalam
himpunan n titik pada bidang kartesius dua dimensi. Ada n jumlah
skenario di mana masalah ini muncul. Contoh kehidupan nyata adalah
dalam sistem kontrol lalu lintas udara di mana Anda harus memantau
pesawat yang terbang berdekatan satu sama lain, dan Anda harus
mencari tahu jarak minimum teraman yang harus dijaga oleh pesawat-
pesawat ini.
Algoritma brute force menghitung jarak antara setiap set titik yang
berbeda dan mengembalikan indeks titik yang jaraknya terkecil.
Brute force memecahkan masalah ini dengan kompleksitas waktu
[O(n2)] di mana n adalah jumlah titik.
Di bawah pseudo-code menggunakan algoritma brute force untuk
menemukan titik terdekat.
Convex Hull
Pernyataan Masalah: Lambung cembung adalah poligon terkecil
yang berisi semua titik. Lambung cembung dari himpunan titik s adalah
poligon cembung terkecil yang berisi s.
Mengevaluasi polinom
• Persoalan: Hitung nilai polinom
p(x) = an x n + an – 1 x n – 1 + … + a1 x + a0 pada x = t.
• Algoritma brute force: x k dihitung secara brute force (seperti pada
perhitungan a n ). Kalikan nilai x k dengan ak , lalu jumlahkan dengan
suku-suku lainnya.
function polinom(t : real)→real
{ Menghitung nilai p(x) pada x = t. Koefisien-koefisein polinom sudah
disimpan di dalam a[0..n].
Masukan: t
Keluaran: nilai polinom pada x = t. }
Deklarasi
i, j : integer
p, pangkat : real
Algoritma:
p0
for i n downto 0 do
pangkat 1
for j 1 to i do {hitung t i }
pangkat pangkat * t
endfor
p p + a[i] * pangkat
endfor
return p
Jumlah operasi perkalian: n(n + 1)/2 + (n + 1) Kompleksitas waktu
algoritma: O(n 2 ).
Perbaikan (improve): Nilai pangkat pada suku sebelumnya (x n – 1 )
digunakan untuk perhitungan pangkat pada suku sekarang
function polinom2(t : real)→real { Menghitung nilai p(x) pada x = t.
Koefisien-koefisein polinom sudah disimpan di dalam a[0..n].
Masukan: t
Keluaran: nilai polinom pada x = t. }
Deklarasi
i, j : integer
p, pangkat : real
Algoritma:
p a[0]
pangkat 1
for i 1 to n do
pangkat pangkat * t
p p + a[i] * pangkat
endfor
Jumlah operasi perkalian: 2n Kompleksitas algoritma ini adalah O(n).
Adakah algoritma perhitungan nilai polinom yang lebih mangkus
daripada brute force?
1. Travelling Salesperson Problem (TSP)
Contoh 3:
TSP dengan n = 4, simpul awal = a
(4 – 1)! = 3! = 6
buah rute perjalanan.
• Jika TSP diselesaikan dengan exhaustive search, maka kita harus
mengenumerasi sebanyak (n – 1)! buah sirkuit Hamilton,
menghitung bobot setiap sirkuitnya, lalu memilih sirkuit
Hamilton dengan bobot terkecil.
• Kompleksitas waktu algoritma exhaustive search untuk persoalan
TSP sebanding dengan (n – 1)! dikali dengan waktu untuk
menghitung bobot setiap sirkuit Hamilton.
• Menghitung bobot setiap sirkuit Hamilton membutuhkan waktu
O(n), sehingga kompleksitas waktu algoritma exhaustive search
untuk persoalan TSP adalah O(n n!).
• Perbaikan: setengah dari semua rute perjalanan adalah hasil
pencerminan dari setengah rute yang lain, yakni dengan
mengubah arah rute perjalanan
1 dan 6
2 dan 4
3 dan 5
6. Algoritma Becktracking
Algoritma Bactracking
Algoritma Backtracking merupakan salah satu bentuk algoritma yang banyak
digunakan oleh para programmer ataupun pengguna komputer ahli untuk
menyelesaikan suatu permasalahan komputasional pada perangkat komputer
yang mereka gunakan. Dalam programming algoritma backtracking, rekursi
adalah kunci dari programming backtracking. Rekursi sendiri merupakan
proses pengulangan suatu hal yang mencakup kesamaan-diri. Penggunaan
yang paling umum dari rekursi terdapat dalam kajian ilmu matematika dan
ilmu komputer.
Algoritma rekursi merupakan algoritma yang memanggil dirinya sendiri
secara berulang kali. Backtracking sebuah algoritma secara umum digunakan
untuk menemukan semua (atau beberapa) solusi terhadap sebuah
permasalahan komputasional. Proses backtracking dapat diaplikasikan hanya
pada beberapa permasalahan yang mengikuti konsep “solusi kandidat parsial”
dan juga sebuah tes yang cukup relatif cepat untuk menentukan kemungkinan
apakah solusi tersebut valid atau tidak.
Backtracking tidaklah berguna untuk menyelesaikan permasalahaan seperti
menentukan sebuah nilai yang diberikan pada sebuah tabel yang tidak
beraturan. Akan tetapi ketika diaplikasikan, backtracking biasanya lebih cepat
bila dibandingkan proses pemecahan masalah brute force yang mana harus
mencoba semua kandidat kemungkinannya.
Sebagai salah satu algoritma yang banyak digunakan oleh para programmer.
Backtracking merupakan sebuah alat yang penting untuk dapat menyelesaikan
permasalahan pemenuhan terbatas, seperti teka – teki silang, aritmatika
verbal, sudoku dan berbagai macam puzzle sejenisnya. Algoritma ini juga
dapat digunakan untuk menyelesaikan permasalahan komputasional seperti
memecahkan kata sandi atau password pada suatu program, membuat sistem
kerja atau mekanisme kerja dari suatu video game, ataupun sistem dasar dari
suatu simulasi komputer terhadap permasalahan di dunia nyata.
Dengan mempelejari hal ini lebih lanjut tentu akan membuat anda
merasakan manfaat mempelajari ilmu komputer. Begitu juga dengan
memahami cara kerja algoritma backtracking. Dengan memahami potensi
yang dimiliki oleh algoritma backtracking anda dapat menghasilkan sebuah
algoritma yang nantinya dapat mempermudah pekerjaan manusia dalam
menyelesaikan suatu masalah di masa depan.
Sejarah
Algoritma backtrack pertama kali diperkenalkan oleh D.H. Lehmer pada
tahun1950. Dalam perkembangannya beberapa ahli seperti Rwalker, Golomb,
dan Baumert menyajikan uraian umum tentang backtrack dan penerapannya
dalam berbagai persoalan dan aplikasi. Algoritma backtracking (runut balik)
merupakan salah satu metode pemecahan masalah yang termasuk dalam
strategi yang berbasis pencarian pada ruang status. Langkah-langkah
pencarian solusi dengan backtracking adalah sebagai berikut [7]: x Solusi
dicari dengan membentuk lintasan dari akar ke daun. Simpul yang sudah
dilahirkan dinamakan simpul hidup dan simpul hidup yang diperluas
dinamakan simpul-E
Fungsi Himmelblau adalah salah satu fungsi yang dapat digunakan untuk
mengoptimasi suatu permasalahan. Fungsi ini memiliki sebuah nilai
maksimum pada x = -0.270845, and y = -0.923039 dengan nilai fungsi sebesar
f(x,y) = 181.617, dengan asumsi bahwa rentang minimal dan maksimal dari
sebaran titik adalah -2 sampai dengan 2
Grafik fungsi Himmelblau yang normal, atau untuk sebaran titik tak terbatas
adalah sebagai berikut.
posisiAnak(i,:)=tmpPosisi;
nilaiFungsiAnak(i) = HitungNilaiFungsi(posisiAnak(i,:));
end
4g. Bandingkan nilai fungsi antara populasi anak dengan populasi sebenarnya
Cari indeks dimana populasi anak memiliki nilai fungsi lebih tinggi dari nilai
fungsi populasi sebenarnya
Ganti semua posisi dan nilai fungsi dari populasi sebenarnya pada indeks
tersebut sesuai dengan posisi dan nilai fungsi pada populasi anak
idxLebihBaik = nilaiFungsiAnak>nilaiFungsi;
nilaiFungsi(idxLebihBaik) = nilaiFungsiAnak(idxLebihBaik);
posisi(idxLebihBaik,:) = posisiAnak(idxLebihBaik,:);
4h. Hitung nilai fungsi terbaik yang terdapat pada populasi sebenarnya
[nilaiFungsiPopulasiTerbaik,idxTerbaik]=max(nilaiFungsi);
4i. Jika nilai fungsi tersebut ternyata lebih baik dari nilai fungsi terbaik secara
umum,
maka ambil posisi yang baru sebagai posisi yang terbaik
if nilaiFungsiPopulasiTerbaik > nilaiFungsiTerbaik,
posisiTerbaik = posisi(idxTerbaik,:);
nilaiFungsiTerbaik = nilaiFungsiPopulasiTerbaik;
end