DSA Interview Prep · Pelajaran

Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah

Kesan kitaran dalam graf tidak berarah dengan menjejak ibu bapa dan dalam graf berarah dengan pewarnaan DFS (tiga keadaan lawatan putih/kelabu/hitam).

Pelajaran 4 daripada 413 langkah

Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah ialah pelajaran DSA Interview Prep percuma di CoddyKit. Ini ialah pelajaran 4 daripada 4. Sebanyak 3 pelajaran dalam laluan pembelajaran ini boleh dibaca sepenuhnya secara percuma — selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan praktikal dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran DSA Interview Prep, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus DSA Interview Prep merangkumi sejumlah 4 pelajaran.

Mengapa Pengesanan Kitaran Penting

Kitaran dalam graf ialah laluan yang bermula dan berakhir pada nod yang sama. Pengesanan kitaran amat penting dalam banyak algoritma: pengisihan topologi gagal pada graf berkitaran, penyelesaian kebergantungan mesti mengesan kebergantungan bulat, dan pengesanan kebuntuan dalam penjadualan OS memerlukan pencarian kitaran dalam graf peruntukan sumber. Pendekatannya berbeza antara graf tidak berarah dan berarah — kedua-duanya memerlukan algoritma yang berbeza secara asasnya.

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')

Pengesanan Kitaran Tidak Berarah dengan DFS

Dalam graf tidak berarah, kitaran wujud jika DFS melawati nod yang sudah berada dalam laluan semasa (bukan sekadar nod yang telah dilawati). Cabarannya: setiap sisi muncul dalam kedua-dua arah, jadi apabila kita melawati nod anak, senarai jirannya turut mengandungi nod semasa kita (induknya). Kita mesti menjejaki induk setiap nod untuk mengelakkan sisi kembali kepada induk daripada ditandai secara palsu sebagai kitaran. Jika kita menemui nod yang telah dilawati tetapi bukan induk kita, kita telah menemui kitaran.

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

Kitaran Tidak Berarah dengan BFS

Pengesanan kitaran BFS dalam graf tidak berarah juga menjejaki induk bagi setiap nod yang telah dilawati. Semasa memproses jiran sesuatu nod, jika jiran itu telah dilawati dan bukan induk nod semasa, maka wujud kitaran. Gunakan kamus untuk menyimpan induk. Pendekatan O(V + E) ini mengelakkan kebimbangan tentang had rekursi dan merupakan alternatif lelaran yang lebih sesuai untuk graf 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

Kitaran Berarah: Mengapa Penjejakan Induk Gagal

Dalam graf berarah, penjejakan induk tidak mencukupi. Pertimbangkan A→C dan B→C: nod C mempunyai dua 'induk' tetapi tiada kitaran. Pendekatan yang betul menggunakan pewarnaan tiga keadaan: putih (belum dilawati), kelabu (dalam laluan/tindanan DFS semasa), dan hitam (telah diproses sepenuhnya). Kitaran wujud jika kita menemui nod kelabu semasa DFS — ini bermakna kita telah menemui sisi belakang kepada leluhur dalam laluan semasa.

# 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)')

Pengesanan Kitaran Berarah dengan DFS Tiga Keadaan

Gunakan tatasusunan state[] dengan nilai 0 (putih/belum dilawati), 1 (kelabu/dalam tindanan), dan 2 (hitam/selesai). Mulakan DFS, tandakan nod sebagai kelabu semasa masuk dan hitam semasa keluar. Jika DFS sampai ke nod kelabu, sisi belakang telah ditemui — terdapat kitaran. Jika ia sampai ke nod hitam, laluan itu telah diterokai sepenuhnya dan bebas kitaran, jadi langkaunya.

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

Jadual Kursus: Kitaran dalam DAG

Jadual Kursus (LeetCode #207) meminta anda menentukan sama ada semua kursus boleh diselesaikan berdasarkan prasyarat yang diberikan. Modelkan kursus sebagai nod dan prasyarat sebagai sisi berarah. Semua kursus boleh diselesaikan jika dan hanya jika graf itu ialah DAG (tiada kitaran). Gunakan pengesanan kitaran DFS tiga keadaan — jika kitaran ditemui, kembalikan palsu; jika tidak, kembalikan benar.

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

Pengesanan Kitaran dengan Algoritma Kahn (BFS)

Pendekatan alternatif untuk mengesan kitaran dalam graf berarah menggunakan pengisihan topologi BFS Kahn. Kira darjah masuk semua nod. Masukkan nod yang mempunyai darjah masuk 0 ke dalam baris gilir. Proses setiap nod: kurangkan darjah masuk jiran, kemudian masukkan nod yang mencapai 0 ke dalam baris gilir. Jika bilangan nod yang diproses sama dengan V, tiada kitaran; jika tidak, wujud kitaran (nod yang belum diproses membentuk kitaran). Pendekatan O(V + E) ini mudah difahami dan diingati berbanding 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

Cari Kitaran: Mengumpulkan Nod Kitaran

Kadangkala anda perlu mengenal pasti nod yang menjadi sebahagian daripada kitaran, bukan sekadar mengesan kewujudannya. Semasa DFS tiga keadaan, apabila sisi belakang ditemui, jejak semula tindanan panggilan (atau tindanan laluan) untuk mengumpulkan semua nod antara leluhur dengan nod semasa. Tindanan laluan yang dikekalkan bersama tatasusunan keadaan merekodkan laluan DFS semasa, lalu membolehkan pembinaan semula kitaran dalam O(panjang kitaran).

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]

Cari Nod Selamat Akhir

Cari Nod Selamat Akhir (LeetCode #802) meminta anda menentukan nod yang akhirnya membawa kepada nod terminal (tiada sisi keluar) tanpa tersekat dalam kitaran. Sesuatu nod adalah 'selamat' jika semua laluan daripadanya membawa kepada nod terminal. Gunakan DFS tiga keadaan: nod yang berwarna hitam (diproses sepenuhnya tanpa mengesan kitaran) adalah selamat. Nod yang menjadi sebahagian daripada kitaran atau membawa kepada kitaran tidak selamat.

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]

Sambungan Berlebihan dalam Graf Tidak Berarah

Sambungan Berlebihan (LeetCode #684) mencari sisi yang mewujudkan kitaran apabila ditambahkan pada graf tidak berarah yang pada asalnya bebas kitaran. Walaupun masalah ini boleh diselesaikan dengan pengesanan kitaran DFS, penyelesaian paling kemas menggunakan Gabung-Cari (DSU): proses sisi satu demi satu; jika kedua-dua titik hujungnya sudah bersambung (komponen yang sama), sisi semasa mewujudkan kitaran dan itulah jawapannya. DSU memberikan O(alpha(n)) bagi setiap operasi — secara berkesan 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 Pengesanan Kitaran

Untuk meringkaskan himpunan alat pengesanan kitaran: bagi graf tidak berarah, gunakan DFS dengan penjejakan induk atau Gabung-Cari. Bagi graf berarah, gunakan DFS tiga keadaan (putih/kelabu/hitam) atau pengisihan topologi BFS Kahn. Pilih Gabung-Cari apabila anda menambahkan sisi satu demi satu (dalam talian). Pilih Kahn apabila anda juga memerlukan susunan topologi. Pilih DFS tiga keadaan apabila anda perlu mengenal pasti nod kitaran tertentu. Sentiasa nyatakan perbezaan antara graf berarah dengan tidak berarah apabila membincangkan pengesanan kitaran dalam temu duga.

# 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')

Semakan Ringkas

Uji pemahaman anda tentang konsep Struktur Data & Algoritma — Persediaan Temu Duga Pengekodan daripada pelajaran ini.

Imbas Kembali Pelajaran

Dalam pelajaran ini anda mempelajari: pengesanan kitaran tidak berarah dengan DFS yang menjejaki induk, pengesanan kitaran berarah dengan pewarnaan tiga keadaan putih/kelabu/hitam, alternatif BFS Kahn untuk graf berarah, serta aplikasi termasuk jadual kursus, sambungan berlebihan dan nod selamat akhir. Seterusnya kita akan menyelami asas pengaturcaraan dinamik.

Percuma untuk bermula

Pelajari Python dengan tutor kecerdasan buatan — percuma

Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.

Kursus
30
Pelajaran
120

Soalan Lazim

Adakah pelajaran “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah” percuma?

Ya — sebanyak 3 pelajaran dalam laluan pembelajaran DSA Interview Prep, termasuk “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah”, boleh dibaca sepenuhnya secara percuma di web ini. Selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan interaktif dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Kursus DSA Interview Prep merangkumi sejumlah 4 pelajaran.

Apakah yang akan saya pelajari dalam “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah”?

Kesan kitaran dalam graf tidak berarah dengan menjejak ibu bapa dan dalam graf berarah dengan pewarnaan DFS (tiga keadaan lawatan putih/kelabu/hitam). Anda berlatih DSA Interview Prep menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.

Adakah saya memerlukan pengalaman untuk memulakan DSA Interview Prep?

Tiada pengalaman terdahulu diperlukan. Pembelajaran DSA Interview Prep di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 4 daripada 4.

Berapa lamakah pelajaran “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah” diambil?

Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.

Bolehkah saya menulis dan menjalankan kod dalam pelajaran DSA Interview Prep ini?

Ya. Setiap pelajaran DSA Interview Prep menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.

Semua pelajaran dalam kursus ini

  1. Perwakilan Graf dan Persediaan Lintasan
  2. BFS: Laluan Terpendek dan Lintasan Mengikut Aras
  3. DFS: Komponen Bersambung dan Isi Banjir
  4. Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah
← Kembali ke DSA Interview Prep