Graph algorithm adalah kumpulan teknik untuk menjelajahi dan menganalisis struktur graph. Empat algoritma paling fundamental adalah BFS (Breadth-First Search) untuk traversal berlapis, DFS (Depth-First Search) untuk traversal mendalam, Dijkstra untuk shortest path berbobot non-negatif, dan topological sort untuk mengurutkan tugas dengan prasyarat. Pada chapter ini kita akan mengimplementasikan keempatnya di Kotlin menggunakan adjacency list generik dan ArrayDeque sebagai antrian. Pembaca akan melihat mengapa BFS menghasilkan jalur terpendek pada graph tak berbobot, dan bagaimana priority queue membuat Dijkstra memilih simpul dengan jarak terkecil di setiap langkah.

BFS menjelajahi graph dengan mengunjungi semua tetangga terdekat sebelum berpindah ke level berikutnya. Algoritma memakai queue FIFO—simpul yang pertama masuk adalah yang pertama diproses. Pada graph tak berbobot, BFS menemukan jalur terpendek karena setiap edge memiliki bobot yang sama.

AspekBFSDFS
Struktur dataQueue (FIFO)Stack (LIFO)
Pola jelajahBerlapis, level demi levelMendalam, sejauh mungkin
Shortest path (unweighted)YaTidak menjamin
MemoriO(V) untuk queueO(h) untuk stack rekursi

DFS menjelajahi satu cabang sampai ujung sebelum mundur dan mencoba cabang lain. Implementasi bisa iteratif (memakai stack) atau rekursif. DFS berguna untuk mendeteksi siklus, menemukan komponen terhubung, dan topological sort versi rekursif.

Dijkstra: Shortest Path Berbobot #

Dijkstra memperluas BFS untuk graph berbobot non-negatif. Algoritma memakai priority queue yang selalu memilih simpul dengan jarak terkecil. Setiap kali tetangga ditemukan dengan jarak lebih kecil, priority queue diperbarui. Kompleksitas O((V + E) log V) dengan priority queue berbasis heap.

Implementasi Graph Algorithms #

kotlin
// Demo Graph Algorithms: BFS, DFS, Dijkstra, topological sort.

// === Graph adjacency list berarah ===
class GraphAL<T>(val directed: Boolean = false) {
    private val adj = mutableMapOf<T, MutableList<T>>()

    fun addEdge(from: T, to: T) {
        adj.getOrPut(from) { mutableListOf() }.add(to)
        if (from !in adj) adj[from] = mutableListOf()
        if (!directed) adj.getOrPut(to) { mutableListOf() }.add(from)
        if (to !in adj) adj[to] = mutableListOf()
    }

    fun neighbors(v: T): List<T> = adj[v] ?: emptyList()
    fun vertices(): List<T> = adj.keys.toList()
}

// === BFS: Breadth-First Search ===
fun <T> bfs(graph: GraphAL<T>, start: T): List<T> {
    val visited = mutableSetOf<T>()
    val queue = ArrayDeque<T>()
    val result = mutableListOf<T>()
    queue.addLast(start)
    visited.add(start)
    while (queue.isNotEmpty()) {
        val curr = queue.removeFirst()
        result.add(curr)
        for (next in graph.neighbors(curr)) {
            if (next !in visited) {
                visited.add(next)
                queue.addLast(next)
            }
        }
    }
    return result
}

// === BFS: shortest path (unweighted) ===
fun <T> bfsShortestPath(graph: GraphAL<T>, start: T, target: T): List<T>? {
    val visited = mutableSetOf<T>()
    val queue = ArrayDeque<T>()
    val parent = mutableMapOf<T, T?>()
    queue.addLast(start)
    visited.add(start)
    parent[start] = null
    while (queue.isNotEmpty()) {
        val curr = queue.removeFirst()
        if (curr == target) {
            val path = mutableListOf<T>()
            var node: T? = curr
            while (node != null) {
                path.add(node)
                node = parent[node] as? T
            }
            return path.reversed()
        }
        for (next in graph.neighbors(curr)) {
            if (next !in visited) {
                visited.add(next)
                parent[next] = curr
                queue.addLast(next)
            }
        }
    }
    return null
}

// === DFS: Depth-First Search (iteratif) ===
fun <T> dfs(graph: GraphAL<T>, start: T): List<T> {
    val visited = mutableSetOf<T>()
    val stack = ArrayDeque<T>()
    val result = mutableListOf<T>()
    stack.addLast(start)
    while (stack.isNotEmpty()) {
        val curr = stack.removeLast()
        if (curr in visited) continue
        visited.add(curr)
        result.add(curr)
        for (next in graph.neighbors(curr)) {
            if (next !in visited) stack.addLast(next)
        }
    }
    return result
}

// === DFS rekursif ===
fun <T> dfsRec(graph: GraphAL<T>, start: T): List<T> {
    val visited = mutableSetOf<T>()
    val result = mutableListOf<T>()
    fun visit(node: T) {
        if (node in visited) return
        visited.add(node)
        result.add(node)
        for (next in graph.neighbors(node)) visit(next)
    }
    visit(start)
    return result
}

// === Dijkstra: shortest path weighted (non-negative) ===
class WGraph<T> {
    private val adj = mutableMapOf<T, MutableList<Pair<T, Int>>>()
    fun addEdge(from: T, to: T, weight: Int) {
        adj.getOrPut(from) { mutableListOf() }.add(to to weight)
        if (from !in adj) adj[from] = mutableListOf()
        if (to !in adj) adj[to] = mutableListOf()
    }
    fun neighbors(v: T): List<Pair<T, Int>> = adj[v] ?: emptyList()
    fun vertices(): List<T> = adj.keys.toList()
}

fun <T> dijkstra(graph: WGraph<T>, start: T): Map<T, Int> {
    val dist = mutableMapOf<T, Int>()
    dist[start] = 0
    val visited = mutableSetOf<T>()
    // Simple priority queue using sorted list
    val pq = java.util.PriorityQueue<Pair<Int, T>>(compareBy { it.first })
    pq.add(0 to start)
    while (pq.isNotEmpty()) {
        val (d, u) = pq.poll()
        if (u in visited) continue
        visited.add(u)
        for ((v, w) in graph.neighbors(u)) {
            val newDist = d + w
            if (newDist < (dist[v] ?: Int.MAX_VALUE)) {
                dist[v] = newDist
                pq.add(newDist to v)
            }
        }
    }
    return dist
}

// === Topological sort (Kahn's algorithm) ===
fun topologicalSort(graph: GraphAL<String>): List<String> {
    val inDegree = mutableMapOf<String, Int>()
    for (v in graph.vertices()) inDegree[v] = 0
    for (v in graph.vertices()) {
        for (u in graph.neighbors(v)) inDegree[u] = (inDegree[u] ?: 0) + 1
    }
    val queue = ArrayDeque<String>()
    for ((v, d) in inDegree) if (d == 0) queue.addLast(v)
    val result = mutableListOf<String>()
    while (queue.isNotEmpty()) {
        val curr = queue.removeFirst()
        result.add(curr)
        for (next in graph.neighbors(curr)) {
            inDegree[next] = (inDegree[next] ?: 0) - 1
            if (inDegree[next] == 0) queue.addLast(next)
        }
    }
    return result
}

fun main() {
    println("=== Demo Graph Algorithms ===")

    val g = GraphAL<Char>()
    listOf(
        'A' to 'B', 'A' to 'C',
        'B' to 'D', 'B' to 'E',
        'C' to 'F',
        'D' to 'G', 'E' to 'G', 'F' to 'G'
    ).forEach { g.addEdge(it.first, it.second) }

    println("--- BFS dari A ---")
    println("Order: ${bfs(g, 'A')}")

    println()
    println("--- BFS shortest path A ke G ---")
    val path = bfsShortestPath(g, 'A', 'G')
    println("Path: $path")

    println()
    println("--- DFS iteratif dari A ---")
    println("Order: ${dfs(g, 'A')}")

    println()
    println("--- DFS rekursif dari A ---")
    println("Order: ${dfsRec(g, 'A')}")

    println()
    println("--- Dijkstra shortest path (weighted) ---")
    val wg = WGraph<Char>()
    listOf(
        Triple('A', 'B', 4),
        Triple('A', 'C', 2),
        Triple('B', 'C', 5),
        Triple('B', 'D', 10),
        Triple('C', 'E', 3),
        Triple('E', 'D', 4),
        Triple('D', 'F', 11),
        Triple('E', 'F', 8)
    ).forEach { wg.addEdge(it.first, it.second, it.third) }
    val dist = dijkstra(wg, 'A')
    println("Jarak terpendek dari A:")
    dist.forEach { (v, d) -> println("  A -> $v = $d") }

    println()
    println("--- Topological sort (DAG) ---")
    val dag = GraphAL<String>(directed = true)
    listOf(
        " fundamentals" to "oop",
        "fundamentals" to "data_structures",
        "data_structures" to "algorithms",
        "oop" to "design_patterns",
        "algorithms" to "design_patterns",
        "design_patterns" to "architecture"
    ).forEach { dag.addEdge(it.first.trim(), it.second.trim()) }
    println("Topological order: ${topologicalSort(dag)}")
}

Output yang Dihasilkan:

text
=== Demo Graph Algorithms ===
--- BFS dari A ---
Order: [A, B, C, D, E, F, G]

--- BFS shortest path A ke G ---
Path: [A, B, D, G]

--- DFS iteratif dari A ---
Order: [A, C, F, G, E, B, D]

--- DFS rekursif dari A ---
Order: [A, B, D, G, E, F, C]

--- Dijkstra shortest path (weighted) ---
Jarak terpendek dari A:
  A -> A = 0
  A -> B = 4
  A -> C = 2
  A -> E = 5
  A -> D = 9
  A -> F = 13

--- Topological sort (DAG) ---
Topological order: [fundamentals, oop, data_structures, algorithms, design_patterns, architecture]

Bedah Kode Baris-per-Baris #

Mari kita amati bagian kunci:

  1. bfs (baris 19-36): Queue ArrayDeque<T> menyimpan simpul yang akan dikunjungi. Set visited mencegah simpul diproses dua kali. Setiap simpul di-dequeue, ditambahkan ke result, lalu semua tetangga yang belum dikunjungi di-enqueue dan ditandai. Urutan traversal berlapis: level 0 (start), level 1 (tetangga langsung), level 2 (tetangga dari tetangga), dst.
  2. bfsShortestPath (baris 39-66): Mirip dengan bfs tetapi menyimpan parent[next] = curr untuk setiap simpul yang baru ditemukan. Begitu target ditemukan, jalur direkonstruksi dengan menelusuri parent dari target kembali ke start, lalu di-reversed() agar urut dari start ke target. Pada graph tak berbobot, jalur ini pasti terpendek karena BFS menjelajah level demi level.
  3. dfs iteratif (baris 69-84): Stack ArrayDeque dipakai sebagai LIFO. Karena simpul bisa ditambahkan ke stack beberapa kali sebelum diproses, perlu cek if (curr in visited) continue di awal loop. Ini memastikan setiap simpul hanya diproses sekali.
  4. dfsRec (baris 87-98): Local function visit memanggil dirinya sendiri untuk setiap tetangga yang belum dikunjungi. Versi rekursif lebih ringkas tetapi berisiko stack overflow untuk graph dalam.
  5. dijkstra (baris 112-132): PriorityQueue<Pair<Int, T>> diurutkan berdasarkan jarak (compareBy { it.first }). Setiap iterasi mengambil simpul dengan jarak terkecil. Untuk setiap tetangga, bila d + w < dist[v], perbarui dist[v] dan tambahkan ke priority queue. Cek if (u in visited) continue menghindari pemrosesan ulang simpul yang sudah final. Output menunjukkan A → D = 9 melalui jalur A → C → E → D (2 + 3 + 4 = 9), bukan A → B → D (4 + 10 = 14).
  6. topologicalSort (baris 135-153): Algoritma Kahn menghitung in-degree setiap simpul. Simpul dengan in-degree 0 ditambahkan ke queue. Setiap iterasi mengeluarkan simpul dari queue, menambahkan ke hasil, lalu mengurangi in-degree setiap tetangga. Simpul yang in-degree-nya turun ke 0 ditambahkan ke queue. Hasilnya urutan topologis yang sah: fundamentals → oop → data_structures → algorithms → design_patterns → architecture.

Penutup #

Empat algoritma graph ini menjadi dasar banyak aplikasi—dari pencarian rute di peta hingga penjadwalan tugas dengan prasyarat. Pada chapter berikutnya kita akan membahas cara menulis algoritma secara idiomatic menggunakan fitur Kotlin seperti extension function, sealed class, dan sequence.

Referensi #

Referensi Lanjutan #