DSA Interview Prep · Pelajaran

Deteksi Siklus pada Graf Berarah dan Tak Berarah

Deteksi siklus pada graf tak berarah dengan melacak induk dan pada graf berarah dengan pewarnaan DFS (tiga status kunjungan putih/abu-abu/hitam).

Pelajaran 4 dari 413 langkah

Deteksi Siklus pada Graf Berarah dan Tak Berarah adalah pelajaran DSA Interview Prep gratis di CoddyKit. Ini adalah pelajaran 4 dari 4. Kamu bisa membaca pelajaran lengkapnya di bawah secara gratis — lalu praktikkan langsung di browser dengan editor kode bawaan dan tutor AI 24/7. Ini adalah bagian dari jalur belajar DSA Interview Prep, dan progresmu tersinkronisasi di web dan aplikasi CoddyKit. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Mengapa Deteksi Siklus Penting

Sebuah siklus dalam graf adalah jalur yang dimulai dan berakhir pada simpul yang sama. Deteksi siklus sangat penting dalam banyak algoritme: pengurutan topologis gagal pada graf yang memiliki siklus, penyelesaian dependensi harus mendeteksi dependensi melingkar, dan deteksi kebuntuan dalam penjadwalan OS memerlukan pencarian siklus dalam graf alokasi sumber daya. Pendekatannya berbeda antara graf tak berarah dan graf berarah — keduanya memerlukan algoritme yang pada dasarnya berbeda.

from collections import defaultdict

# Undirected cycle: A-B-C-A (triangle)
undirected = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
    undirected[u].append(v)
    undirected[v].append(u)

# Directed cycle: A->B->C->A
directed = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
    directed[u].append(v)  # one direction only

# Key difference:
# Undirected: edge A-B appears as both A->B and B->A
# Must track parent to distinguish cycle from back-edge to parent
print('Undirected and directed cycles need different detection')

Deteksi Siklus Tak Berarah dengan DFS

Dalam graf tak berarah, sebuah siklus ada jika DFS mengunjungi simpul yang sudah berada dalam jalur saat ini (bukan sekadar sudah dikunjungi). Tantangannya: setiap sisi muncul dalam kedua arah, sehingga ketika kita mengunjungi simpul anak, daftar tetangganya menyertakan simpul kita saat ini (induknya). Kita harus melacak induk setiap simpul agar tidak keliru menganggap sisi yang kembali ke induk sebagai siklus. Jika menemukan simpul yang sudah dikunjungi dan bukan induk kita, berarti kita telah menemukan siklus.

def has_cycle_undirected(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()

    def dfs(node, parent):
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                if dfs(nb, node):  # recurse with current as parent
                    return True
            elif nb != parent:     # visited and not parent = CYCLE
                return True
        return False

    for node in range(n):
        if node not in visited:
            if dfs(node, -1):  # -1 = no parent for root
                return True
    return False

print(has_cycle_undirected(4, [(0,1),(1,2),(2,3),(3,1)]))  # True
print(has_cycle_undirected(3, [(0,1),(1,2)]))               # False

Siklus Tak Berarah dengan BFS

Deteksi siklus dengan BFS pada graf tak berarah juga melacak induk setiap simpul yang telah dikunjungi. Saat memproses tetangga sebuah simpul, jika tetangga tersebut sudah dikunjungi dan bukan induk simpul saat ini, berarti ada siklus. Gunakan kamus untuk menyimpan induk. Pendekatan O(V + E) ini menghindari kekhawatiran tentang batas rekursi dan merupakan alternatif iteratif yang lebih disukai untuk graf berukuran besar.

from collections import deque, defaultdict

def has_cycle_bfs_undirected(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()

    for start in range(n):
        if start in visited:
            continue
        visited.add(start)
        parent = {start: -1}
        queue = deque([start])
        while queue:
            node = queue.popleft()
            for nb in graph[node]:
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = node
                    queue.append(nb)
                elif parent[node] != nb:  # visited and not parent = CYCLE
                    return True
    return False

print(has_cycle_bfs_undirected(4, [(0,1),(1,2),(2,0)]))  # True

Siklus Berarah: Mengapa Pelacakan Induk Gagal

Dalam graf berarah, pelacakan induk tidak memadai. Perhatikan A→C dan B→C: simpul C memiliki dua 'induk', tetapi tidak memiliki siklus. Pendekatan yang benar menggunakan pewarnaan tiga keadaan: putih (belum dikunjungi), abu-abu (berada dalam jalur/tumpukan DFS saat ini), dan hitam (telah selesai diproses sepenuhnya). Siklus ada jika kita menemukan simpul abu-abu saat DFS — artinya kita menemukan sisi balik menuju leluhur dalam jalur saat ini.

# Three-state DFS coloring:
# WHITE (0): not yet visited
# GRAY  (1): currently being visited (in DFS stack)
# BLACK (2): fully visited (all descendants processed)

# Why parent fails for directed graphs:
# A -> C  (no cycle)
# B -> C  (no cycle)
# If we DFS from A, mark C gray
# Then DFS from B finds C is gray -- but this is NOT a cycle!
# C is gray from A's path, not B's path.
# Parent tracking only works when the back-edge goes to the IMMEDIATE parent.
print('Directed graph: use 3-state coloring (white/gray/black)')

Deteksi Siklus Berarah dengan DFS Tiga Keadaan

Gunakan array state[] dengan nilai 0 (putih/belum dikunjungi), 1 (abu-abu/berada dalam tumpukan), dan 2 (hitam/selesai). Mulai DFS, tandai simpul sebagai abu-abu saat masuk dan hitam saat keluar. Jika DFS mencapai simpul abu-abu, sisi balik ditemukan — berarti ada siklus. Jika mencapai simpul hitam, jalur tersebut sudah sepenuhnya ditelusuri dan bebas siklus, sehingga lewati simpul itu.

def has_cycle_directed(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    state = [0] * n  # 0=white, 1=gray, 2=black

    def dfs(node):
        state[node] = 1  # mark gray (in stack)
        for nb in graph[node]:
            if state[nb] == 1:  # gray = back edge = CYCLE
                return True
            if state[nb] == 0:  # white = unvisited
                if dfs(nb):
                    return True
        state[node] = 2  # mark black (fully processed)
        return False

    for node in range(n):
        if state[node] == 0:
            if dfs(node):
                return True
    return False

print(has_cycle_directed(4, [(0,1),(1,2),(2,0),(2,3)]))  # True (0->1->2->0)
print(has_cycle_directed(3, [(0,1),(1,2)]))               # False

Jadwal Mata Kuliah: Siklus dalam DAG

Jadwal Mata Kuliah (LeetCode #207) menanyakan apakah semua mata kuliah dapat diselesaikan jika diberikan prasyaratnya. Modelkan mata kuliah sebagai simpul dan prasyarat sebagai sisi berarah. Semua mata kuliah dapat diselesaikan jika dan hanya jika graf tersebut merupakan DAG (tanpa siklus). Gunakan deteksi siklus DFS tiga keadaan — jika ditemukan siklus, kembalikan False; jika tidak, kembalikan True.

from collections import defaultdict

def can_finish(num_courses, prerequisites):
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)  # b is prerequisite for a: b -> a

    state = [0] * num_courses

    def dfs(course):
        if state[course] == 1: return False  # cycle!
        if state[course] == 2: return True   # already verified
        state[course] = 1  # mark as in-progress
        for next_course in graph[course]:
            if not dfs(next_course):
                return False
        state[course] = 2  # mark as done
        return True

    return all(dfs(i) for i in range(num_courses) if state[i] == 0)

print(can_finish(2, [[1,0]]))        # True: take 0 then 1
print(can_finish(2, [[1,0],[0,1]]))  # False: circular dependency

Deteksi Siklus dengan Algoritme Kahn (BFS)

Alternatif deteksi siklus untuk graf berarah menggunakan pengurutan topologis BFS Kahn. Hitung derajat masuk semua simpul. Masukkan simpul dengan derajat masuk 0 ke dalam antrean. Proses setiap simpul: kurangi derajat masuk tetangganya dan masukkan tetangga yang derajat masuknya mencapai 0 ke antrean. Jika jumlah simpul yang diproses sama dengan V, tidak ada siklus; jika tidak, berarti ada siklus (simpul yang belum diproses membentuk siklus). Pendekatan O(V + E) ini intuitif dan lebih mudah diingat daripada DFS tiga keadaan.

from collections import defaultdict, deque

def has_cycle_kahn(n, edges):
    graph = defaultdict(list)
    in_degree = [0] * n
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1

    # Start with all zero in-degree nodes
    queue = deque(i for i in range(n) if in_degree[i] == 0)
    processed = 0
    while queue:
        node = queue.popleft()
        processed += 1
        for nb in graph[node]:
            in_degree[nb] -= 1
            if in_degree[nb] == 0:
                queue.append(nb)

    return processed != n  # if not all processed, cycle exists

print(has_cycle_kahn(4, [(0,1),(1,2),(2,0),(2,3)]))  # True
print(has_cycle_kahn(3, [(0,1),(1,2)]))               # False

Temukan Siklus: Mengumpulkan Simpul Siklus

Kadang-kadang Anda perlu mengidentifikasi simpul mana yang merupakan bagian dari siklus, bukan hanya mendeteksi keberadaannya. Selama DFS tiga keadaan, ketika sisi balik ditemukan, telusuri mundur melalui tumpukan pemanggilan (atau tumpukan jalur) untuk mengumpulkan semua simpul di antara leluhur dan simpul saat ini. Tumpukan jalur yang dikelola bersama array keadaan menangkap jalur DFS saat ini, sehingga memungkinkan rekonstruksi siklus dengan biaya O(cycle_length).

def find_cycle_nodes(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    state = [0] * n
    path = []  # current DFS path
    cycle = []

    def dfs(node):
        state[node] = 1
        path.append(node)
        for nb in graph[node]:
            if state[nb] == 1:  # back edge -> found cycle
                start = path.index(nb)
                cycle.extend(path[start:])
                return True
            if state[nb] == 0 and dfs(nb):
                return True
        path.pop()
        state[node] = 2
        return False

    for i in range(n):
        if state[i] == 0 and dfs(i):
            break
    return cycle

print(find_cycle_nodes(4, [(0,1),(1,2),(2,0),(2,3)]))  # [0, 1, 2]

Temukan Status Aman Akhir

Temukan Status Aman Akhir (LeetCode #802) menanyakan simpul mana yang pada akhirnya menuju simpul terminal (tanpa sisi keluar) tanpa terjebak dalam siklus. Sebuah simpul 'aman' jika semua jalur dari simpul tersebut menuju simpul terminal. Gunakan DFS tiga keadaan: simpul yang berwarna hitam (telah selesai diproses sepenuhnya tanpa mendeteksi siklus) bersifat aman. Simpul yang merupakan bagian dari siklus atau menuju siklus tidak aman.

def eventual_safe_nodes(graph):
    n = len(graph)
    state = [0] * n  # 0=unvisited, 1=visiting, 2=safe

    def dfs(node):
        if state[node] == 1:  # currently visiting = cycle
            return False
        if state[node] == 2:  # already verified safe
            return True
        state[node] = 1  # mark as visiting
        for nb in graph[node]:
            if not dfs(nb):
                return False  # leads to cycle, not safe
        state[node] = 2  # mark as safe
        return True

    return [i for i in range(n) if dfs(i)]

# [[1,2],[2,3],[5],[0],[5],[],[]] means:
# 0->[1,2], 1->[2,3], 2->[5], 3->[0] (cycle!), 4->[5], 5->[], 6->[]
print(eventual_safe_nodes([[1,2],[2,3],[5],[0],[5],[],[]]))
# [2, 4, 5, 6]

Koneksi Berlebih pada Graf Tak Berarah

Koneksi Berlebih (LeetCode #684) menemukan sisi yang menciptakan siklus ketika ditambahkan ke graf tak berarah yang sebelumnya tidak memiliki siklus. Meskipun hal ini dapat diselesaikan dengan deteksi siklus DFS, solusi yang paling bersih menggunakan Penggabungan-Pencarian (DSU): proses sisi satu per satu; jika kedua ujungnya sudah terhubung (berada dalam komponen yang sama), sisi saat ini menciptakan siklus dan merupakan jawabannya. DSU memberikan O(alpha(n)) untuk setiap operasi — pada praktiknya O(1).

def find_redundant_connection(edges):
    n = len(edges)
    parent = list(range(n + 1))
    rank = [0] * (n + 1)

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])  # path compression
        return parent[x]

    def union(x, y):
        px, py = find(x), find(y)
        if px == py:
            return False  # already connected = cycle!
        if rank[px] < rank[py]: px, py = py, px
        parent[py] = px
        if rank[px] == rank[py]: rank[px] += 1
        return True

    for u, v in edges:
        if not union(u, v):
            return [u, v]  # this edge creates the cycle
    return []

print(find_redundant_connection([[1,2],[1,3],[2,3]]))  # [2,3]
print(find_redundant_connection([[1,2],[2,3],[3,4],[1,4],[1,5]]))  # [1,4]

Ringkasan Strategi Deteksi Siklus

Untuk merangkum kumpulan strategi deteksi siklus: untuk graf tak berarah, gunakan DFS dengan pelacakan induk atau Penggabungan-Pencarian. Untuk graf berarah, gunakan DFS tiga keadaan (putih/abu-abu/hitam) atau pengurutan topologis BFS Kahn. Pilih Penggabungan-Pencarian ketika Anda menambahkan sisi satu per satu. Pilih Kahn jika Anda juga memerlukan urutan topologis. Pilih DFS tiga keadaan jika Anda perlu mengidentifikasi simpul siklus tertentu. Saat membahas deteksi siklus dalam wawancara, selalu nyatakan perbedaan antara graf berarah dan tak berarah.

# Cycle detection summary:
# Graph type  | Algorithm            | Complexity
# ------------|----------------------|-----------
# Undirected  | DFS + parent track   | O(V + E)
# Undirected  | Union-Find (DSU)     | O(E * alpha(V))
# Directed    | DFS 3-state (W/G/B)  | O(V + E)
# Directed    | Kahn's BFS topo sort | O(V + E)

# When to choose:
# Online (edges added one at a time): Union-Find
# Need topological order too: Kahn's BFS
# Need cycle nodes identified: 3-state DFS with path stack
# Simple existence check: any of the above
print('Always clarify directed vs undirected before coding')

Uji Cepat

Uji pemahaman Anda tentang konsep Struktur Data & Algoritma — Persiapan Wawancara Pemrograman dari pelajaran ini.

Ringkasan Pelajaran

Dalam pelajaran ini Anda mempelajari: deteksi siklus tak berarah dengan DFS yang melacak induk, deteksi siklus berarah dengan pewarnaan tiga keadaan putih/abu-abu/hitam, alternatif BFS Kahn untuk graf berarah, serta penerapannya pada jadwal mata kuliah, koneksi berlebih, dan status aman akhir. Berikutnya kita akan mendalami dasar-dasar pemrograman dinamis.

Gratis untuk memulai

Belajar Python dengan tutor AI — gratis

Tulis dan jalankan kode asli di browser kamu, dapatkan bantuan instan dari tutor AI 24/7, dan lanjutkan di mana kamu tinggalkan di web atau aplikasi.

Kursus
30
Pelajaran
120

Pertanyaan yang Sering Diajukan

Apakah pelajaran “Deteksi Siklus pada Graf Berarah dan Tak Berarah” gratis?

Ya — teks lengkap “Deteksi Siklus pada Graf Berarah dan Tak Berarah” gratis dibaca di sini di web. Untuk praktiknya secara interaktif (editor kode bawaan dan tutor AI 24/7) dan buka sisa kursus DSA Interview Prep, upgrade ke CoddyKit PRO. Kursus DSA Interview Prep mencakup 4 pelajaran total.

Apa yang akan aku pelajari di “Deteksi Siklus pada Graf Berarah dan Tak Berarah”?

Deteksi siklus pada graf tak berarah dengan melacak induk dan pada graf berarah dengan pewarnaan DFS (tiga status kunjungan putih/abu-abu/hitam). Kamu berlatih DSA Interview Prep dengan kode praktik yang langsung kamu jalankan di browser, dan tutor AI 24/7 menjawab pertanyaanmu saat kamu mengerjakan pelajaran ini.

Apakah aku perlu pengalaman untuk memulai DSA Interview Prep?

Tidak diperlukan pengalaman sebelumnya. DSA Interview Prep di CoddyKit dirancang untuk pemula hingga pelajar tingkat lanjut, jadi kamu bisa memulai di sini atau dari awal dan belajar sesuai kecepatan kamu sendiri. Ini adalah pelajaran 4 dari 4.

Berapa lama pelajaran “Deteksi Siklus pada Graf Berarah dan Tak Berarah” memakan waktu?

Sebagian besar pelajaran CoddyKit memakan waktu sekitar 5–10 menit. Setiap pelajaran ringkas dan interaktif, jadi kamu membuat kemajuan stabil dan melanjutkan dari tempat kamu tinggalkan di web dan aplikasi.

Bisakah aku menulis dan menjalankan kode dalam pelajaran DSA Interview Prep ini?

Ya. Setiap pelajaran DSA Interview Prep menyertakan editor kode bawaan, jadi kamu menulis dan menjalankan kode nyata langsung di browser dan mendapatkan umpan balik AI instan — tidak diperlukan penyiapan lokal.

Semua pelajaran dalam kursus ini

  1. Representasi Graf dan Persiapan Traversal
  2. BFS: Jalur Terpendek dan Traversal Level
  3. DFS: Komponen Terhubung dan Flood Fill
  4. Deteksi Siklus pada Graf Berarah dan Tak Berarah
← Kembali ke DSA Interview Prep