0Pricing
DSA Interview Prep · 강의

방향 그래프와 무방향 그래프의 순환 탐지

부모 추적으로 무방향 그래프의 순환을 탐지하고, DFS 색상 표시(흰색·회색·검은색의 세 상태 방문 표시)로 방향 그래프의 순환을 탐지합니다.

방향 그래프와 무방향 그래프의 순환 탐지은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

사이클 탐지가 중요한 이유

그래프에서 사이클은 같은 노드에서 시작하고 끝나는 경로입니다. 사이클 탐지는 여러 알고리즘에서 중요합니다. 위상 정렬은 사이클이 있는 그래프에서 실패하고, 의존성 해결에서는 순환 의존성을 감지해야 하며, OS 일정 관리의 교착 상태 탐지에서는 자원 할당 그래프의 사이클을 찾아야 합니다. 무방향 그래프와 방향 그래프에서는 접근법이 다르며, 근본적으로 서로 다른 알고리즘이 필요합니다.

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

DFS를 사용한 무방향 사이클 탐지

무방향 그래프에서는 DFS가 현재 경로에 이미 있는 노드를 방문하면 사이클이 존재합니다. 단순히 방문한 적이 있는지만 확인해서는 안 됩니다. 모든 간선이 양방향으로 나타나므로, 자식 노드를 방문할 때 그 이웃 목록에는 현재 노드인 부모도 포함됩니다. 부모로 되돌아가는 간선을 사이클로 잘못 표시하지 않으려면 각 노드의 부모를 추적해야 합니다. 방문한 노드가 부모가 아니라면 사이클을 찾은 것입니다.

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

BFS를 사용한 무방향 사이클 탐지

무방향 그래프에서 BFS로 사이클을 탐지할 때도 방문한 각 노드의 부모를 추적합니다. 노드의 이웃을 처리할 때 이웃이 이미 방문되었고 현재 노드의 부모가 아니라면 사이클이 존재합니다. 부모를 저장하려면 사전을 사용합니다. 이 O(V + E) 접근법은 재귀 제한 문제를 피하므로, 큰 그래프에서 선호되는 반복 방식입니다.

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

방향 그래프 사이클: 부모 추적이 실패하는 이유

방향 그래프에서는 부모 추적만으로 충분하지 않습니다. A→C와 B→C를 생각해 보십시오. 노드 C에는 '부모'가 두 개 있지만 사이클은 없습니다. 올바른 접근법은 3가지 상태 색칠을 사용하는 것입니다. 흰색은 방문하지 않음, 회색은 현재 DFS 경로/스택에 있음, 검은색은 완전히 처리됨을 뜻합니다. DFS 중 회색 노드를 만나는 순간 사이클이 존재합니다. 현재 경로의 조상으로 향하는 역방향 간선을 찾았다는 의미입니다.

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

3가지 상태 DFS를 사용한 방향 사이클 탐지

state[] 배열에 0(흰색/방문하지 않음), 1(회색/스택에 있음), 2(검은색/완료)의 값을 사용합니다. DFS를 시작할 때 노드를 회색으로 표시하고, 탐색을 마칠 때 검은색으로 표시합니다. DFS가 회색 노드에 도달하면 역방향 간선을 찾은 것이므로 사이클이 존재합니다. 검은색 노드에 도달한 경우에는 해당 경로를 이미 완전히 탐색했으며 사이클이 없으므로 건너뜁니다.

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

DAG의 사이클: 수강 일정

수강 일정 (LeetCode #207)은 선수 과목 조건이 주어졌을 때 모든 과목을 이수할 수 있는지 묻습니다. 과목을 노드로, 선수 과목 조건을 방향 간선으로 모델링합니다. 모든 과목을 이수할 수 있는 필요충분조건은 그래프가 DAG(사이클이 없는 방향 그래프)인 것입니다. 3가지 상태 DFS 사이클 탐지를 사용하여 사이클을 찾으면 거짓을 반환하고, 그렇지 않으면 참을 반환합니다.

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

칸 알고리즘(BFS)을 사용한 사이클 탐지

방향 그래프의 또 다른 사이클 탐지 방법으로 칸의 BFS 위상 정렬을 사용할 수 있습니다. 모든 노드의 진입 차수를 셉니다. 진입 차수가 0인 노드를 큐에 넣습니다. 각 노드를 처리하면서 이웃의 진입 차수를 줄이고, 0이 된 노드를 큐에 넣습니다. 처리한 노드 수가 V와 같으면 사이클이 없고, 그렇지 않으면 사이클이 존재합니다(처리되지 않은 노드들이 사이클을 이룹니다). 이 O(V + E) 접근법은 직관적이며 3가지 상태 DFS보다 기억하기 쉽습니다.

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

사이클 찾기: 사이클 노드 수집

때로는 사이클의 존재 여부만 탐지하는 것이 아니라 사이클에 속한 노드가 무엇인지 식별해야 합니다. 3가지 상태 DFS 중 역방향 간선을 찾으면 호출 스택(또는 경로 스택)을 거슬러 올라가 조상 노드와 현재 노드 사이의 모든 노드를 수집합니다. 상태 배열과 함께 유지하는 경로 스택은 현재 DFS 경로를 저장하므로 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]

최종 안전 상태 찾기

최종 안전 상태 찾기 (LeetCode #802)는 사이클에 갇히지 않고 결국 종단 노드(나가는 간선이 없는 노드)로 이어지는 노드가 무엇인지 묻습니다. 어떤 노드에서 시작하는 모든 경로가 종단 노드로 이어지면 그 노드는 '안전'합니다. 3가지 상태 DFS를 사용합니다. 사이클 없이 완전히 처리되어 검은색이 된 노드는 안전합니다. 사이클에 속하거나 사이클로 이어지는 노드는 안전하지 않습니다.

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]

무방향 그래프의 중복 간선

중복 연결 (LeetCode #684)은 원래 사이클이 없는 무방향 그래프에 추가했을 때 사이클을 만드는 간선을 찾습니다. DFS 사이클 탐지로도 해결할 수 있지만, 가장 깔끔한 방법은 유니온-파인드 (DSU)를 사용하는 것입니다. 간선을 하나씩 처리하면서 양 끝점이 이미 연결되어 있다면(같은 연결 요소라면) 현재 간선이 사이클을 만들므로 정답입니다. DSU는 연산당 O(alpha(n))을 제공하며, 사실상 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]

요약: 사이클 탐지 전략

사이클 탐지 도구를 요약하면 다음과 같습니다. 무방향 그래프에는 부모 추적 DFS 또는 유니온-파인드를 사용합니다. 방향 그래프에는 3가지 상태 DFS(흰색/회색/검은색) 또는 칸의 BFS 위상 정렬을 사용합니다. 간선을 한 번에 하나씩 추가하는 온라인 상황에서는 유니온-파인드를 선택합니다. 위상 순서도 필요하다면 칸 알고리즘을 선택합니다. 특정 사이클 노드를 식별해야 한다면 3가지 상태 DFS를 선택합니다. 면접에서 사이클 탐지를 설명할 때는 방향 그래프와 무방향 그래프의 차이를 항상 밝혀야 합니다.

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

빠른 확인

이 레슨에서 다룬 자료 구조 및 알고리즘 & 코딩 면접 대비 개념에 대한 이해도를 확인합니다.

학습 내용 요약

이번 레슨에서는 부모 추적 DFS를 사용한 무방향 사이클 탐지, 3가지 상태인 흰색/회색/검은색 색칠을 사용한 방향 사이클 탐지, 방향 그래프를 위한 칸의 BFS 대안, 그리고 수강 일정, 중복 연결, 최종 안전 상태 찾기 등의 응용을 배웠습니다. 다음에는 동적 계획법의 기초를 자세히 살펴봅니다.

자주 묻는 질문

“방향 그래프와 무방향 그래프의 순환 탐지” 강의는 무료인가요?

네 — “방향 그래프와 무방향 그래프의 순환 탐지” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.

“방향 그래프와 무방향 그래프의 순환 탐지”에서 뭘 배우나요?

부모 추적으로 무방향 그래프의 순환을 탐지하고, DFS 색상 표시(흰색·회색·검은색의 세 상태 방문 표시)로 방향 그래프의 순환을 탐지합니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.

DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?

사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.

“방향 그래프와 무방향 그래프의 순환 탐지” 강의는 얼마나 걸리나요?

대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.

이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?

네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.

이 강의의 모든 강의

  1. 그래프 표현과 순회 설정
  2. BFS: 최단 경로와 레벨 순회
  3. DFS: 연결 요소와 플러드 필
  4. 방향 그래프와 무방향 그래프의 순환 탐지
← DSA Interview Prep(으)로 돌아가기