그래프 탐색 알고리즘 DFS/BFS

탐색(search) 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정

대표적인 그래프 탐색 알고리즘으로는 DFSBFS가 있다.

DFS/BFS는 코딩 테스트에서 매우 자주 등장하는 유형이므로 반드시 숙지해야 한다.

DFS/BFS를 배우기 전에 반드시 스택는 반드시 알아두어야 할 자료구조이다.



Stack

  • 먼저 들어 온 데이터가 나중에 나가는 형식의 자료구조 (선입후출) (FILO)
  • 입구와 출구가 동일한 형태로 스택을 시각화할 수 있다.
  • ex) 쌓아올린 박스

스택 구현 예제

파이썬에서는 append()pop()의 시간복잡도가 상수. 즉 O(1)이기 때문에

리스트로 스택을 구현하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
stack = []

# 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
stack.append(5)
stack.append(2)
stack.append(3)
stack.append(7)
stack.pop()
stack.append(1)
stack.append(4)
stack.pop()

print(stack[::-1]) # 최상당 원소부터 출력
print(stack) # 최하단 원소부터 출력

# output
>>> [1, 3, 2, 5]
>>> [5, 2, 3, 1]



Queue

  • 먼저 들어온 데이터가 먼저 나가는 형식의 자료구조 (선입선출) (FIFO)
  • 입구와 출구가 모두 뚫려 있는 터널과 같은 형태
  • ex) 은행 창구, 터널 등

큐 구현 예제

파이썬에서는 큐를 구현할 때 deque를 사용하여 구현한다.

list자료형으로 기능적으로는 큐를 구현할 수 있지만 시간복잡도가 더 높아서 비효율적으로 동작한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from collections import deque

queue = deque()

# 삽입(5) - 삽입(2) - 삽입(3) - 삽입(7) - 삭제() - 삽입(1) - 삽입(4) - 삭제()
queue.append(5)
queue.append(2)
queue.append(3)
queue.append(7)
queue.popleft()
queue.append(1)
queue.append(4)
queue.popleft()

print(queue) # 먼저 들어온 순서대로 출력
queue.reverse() # 역순으로 바꾸기
print(queue) # 나중에 들어온 원소부터 출력

# output
>>> deque([3, 7, 1, 4])
>>> deque([4, 1, 7, 3])



재귀 함수

재귀 함수(Recursive Function) 자기 자신을 다시 호출하는 함수를 의미

DFS를 실질적으로 구현하고자 할 때 자주 사용하는 방법 중 하나이므로 이해가 필요하다.

1
2
3
4
5
def recursive_func():
    print('재귀 함수 호출')
    recursive_func()

recursive_func()

파이썬에서는 재귀를 호출하는 과정에서의 깊이 제한이 있기 때문에 별다른 설정을 하지 않고 함수를 재귀적으로 호출하게 되면 오류가 발생한다.

1
2
3
4
5
6
7
8
def recursive_func():
    if i == 100:
        return
    print(i, '번째 재귀함수에서', i+1, '번째 재귀함수 호출')
    recursive_function(i+1)
    print(i, '번째 재귀함수 종료')

recursive_func(1)

재귀 함수를 문제 풀이에서 사용할 때는 재귀 함수의 종료 조건을 반드시 명시해야 한다.

재귀함수 구현 예제(팩토리얼)

n! = 1 x 2 x 3 x ... x (n-1) x n

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 반복적으로 구현한 n!
def factorial_iterative(n):
    result = 1
    # 1부터 n까지 차례대로 곱하기
    for i in range(1, n+1):
        result += i
    return result

# 재귀적으로 구현한 n!
def factorial_recursive(n):
    if n <= 1: # n이 1이하인 경우 1 반환 -> 0!과 1!의 값은 1
        return 1 
    # n! = n * (n-1)!
    return n * factorial_recursive(n-1)

print('반복적으로 구현:', factorial_iterative(5))
print('재귀적으로 구현:', factorial_recursive(5))

# output
>>> 반복적으로 구현: 120
>>> 재귀적으로 구현: 120

재귀 함수 사용시 유의 사항

  • 재귀 함수를 잘 활용하면 복잡한 알고리즘을 간결하게 작성할 수 있지만 오히려 다른사람이 이해하기 어려운 형태의 코드가 될 수도 있으므로 신중하게 사용
  • 모든 재귀 함수는 반복문을 이용하여 동일한 기능을 구현할 수 있음
  • 재귀 함수가 반복문보다 유리할 경우도 있고 불리한 경우도 있음
  • 컴퓨터가 함수를 연속적으로 호출하면 컴퓨터 메모리 내부의 스택 프레임에 쌓임. 그래서 스택을 사용할 때 구현상 스택 라이브러리 대신에 재귀 함수를 이용하는 경우가 많음.




DFS

DFS 깊이 우선 탐색 (Depth-First Search)

그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘

DFS는 스택이나 재귀함수를 이용하며 구체적인 동작 과정은 다음과 같다.

  1. 탐색 시작 노드스택에 삽입하고 방문 처리를 한다.
  2. 스택의 최상단 노드에 방문하지 않은 인접한 노드가 하나라도 있으면 그 노드를 스택에 넣고 방문 처리하고, 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다.
  3. 더 이상 2번의 과정을 수행할 수 없을 때까지 반복한다.

즉, 매번 최상단 원소를 기준으로 해서 방문하지 않은 인접 노드가 있으면 그 인접한 노드로도 방문을 수행하는 것이다.

그래프에서 가장 깊은 부분을 우선적으로 탐색하고 가장 깊게 들어갔다가 더 이상 들어갈 수 없다면 다른 방향으로 깊게 들어가는 방식을 반복 하는 것이다.

DFS 동작 예시

간선은 방향이 없는 무방향 그래프

DFS는 인접한 노드가 여러개 일 수 있기 때문에 어떤 노드부터 방문할지를 결정하기 위한 기준이 필요

이러한 방문 기준은 문제에서 요구하는 내용에 따라서 조금씩 달라질 수 있고 어떤 노드부터 방문하더라도 상관이 없는 경우도 있다.


[STEP 0] 그래프를 준비한다. (방문 기준: 번호가 낮은 인접 노드부터)

  • 시작노드: 1 IMG_0206

[STEP 1] 시작노드인 ‘1’을 스택에 삽입하고 방문 처리한다.

  • STACK = [1]

[STEP 2] 스택의 최상단 노드인 ‘1’에 방문하지 않은 인접 노드 ‘2’, ‘3’, ‘8’ 중 가장 작은 노드인 ‘2’를 스택에 넣고 방문 처리한다.

  • STACK = [2, 1]

[STEP 3] 스택의 최상단 노드인 ‘2’에 방문하지 않은 인접 노드 ‘7’번 노드를 스택에 넣고 방문 처리한다.

  • STACK = [7, 2, 1]

[STEP 4] 스택의 최상단 노드인 ‘7’에 방문하지 않은 인접 노드 ‘6’, ‘8’ 중 작은 노드인 ‘6’을 스택에 넣고 방문 처리한다.

  • STACK = [6, 7, 2, 1]

[STEP 5] 스택의 최상단 노드인 ‘6’에 방문하지 않은 인접 노드가 없다. 따라서 스택에서 ‘6’번 노드를 꺼낸다.

  • STACK = [7, 2, 1]

[STEP 6] 스택의 최상단 노드인 ‘7’에 방문하지 않은 인접 노드 ‘8’번 노드를 스택에 넣고 방문 처리한다.

  • STACK = [8, 7, 2, 1]

[STEP 7] 스택의 최상단 노드인 ‘8’에 방문하지 않은 인접 노드가 없으므로 ‘8’번 노드를 꺼낸다.

  • STACK = [7, 2, 1]

[STEP 8] 스택의 최상단 노드인 ‘7’에 방문하지 않은 인접 노드가 없으므로 ‘7’번 노드를 꺼낸다.

  • STACK = [2, 1]

[STEP 9] 스택의 최상단 노드인 ‘2’에 방문하지 않은 인접 노드가 없으므로 ‘2’번 노드를 꺼낸다.

  • STACK = [1]

[STEP 10] 스택의 최상단 노드인 ‘1’에 방문하지 않은 인접 노드 ‘3’번 노드를 스택에 넣고 방문 처리한다.

  • STACK = [3, 1]

[STEP 11] 스택의 최상단 노드인 ‘3’에 방문하지 않은 인접 노드 ‘4’, ‘5’ 중 작은 노드인 ‘4’번 노드를 스택에 넣고 방문 처리한다.

  • STACK = [4, 3, 1]

[STEP 11] 스택의 최상단 노드인 ‘4’에 방문하지 않은 인접 노드 ‘5’번 노드를 스택에 넣고 방문 처리한다.

  • STACK = [5, 4, 3, 1]


이러한 과정을 반복하였을 때 전체 노드의 탐색 순서(스택에 들어간 순서)는

1 -> 2 -> 7 -> 6 -> 8 -> 3 -> 4 -> 5


DFS 소스코드 예제 (Python)

DFS 메서드 정의

1
2
3
4
5
6
7
8
def dfs(graph, v, visited):
    # 현재 노드를 방문 처리
    visited[v] = True
    print(v, end=' ') # 방문한 노드 번호 출력
    # 현재 노드와 연결된 다른 노드를 재귀적으로 방문
    for i in graph[v]:
        if not visited[i]:
            dfs(graph, i, visited)

그래프 표현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 각 노드가 연결된 정보를 2차원 리스트로 표현
graph = [
    [],
    [2, 3, 8], # 1번 노드와 인접한 노드
    [1, 7],    # 2번 노드와 인접한 노드
    [1, 4, 5], # 3번 노드와 인접한 노드
    [3, 5],    # 4번 노드와 인접한 노드
    [3, 4],    # 5번 노드와 인접한 노드
    [7],       # 6번 노드와 인접한 노드
    [2, 6, 8], # 7번 노드와 인접한 노드
    [1, 7]     # 8번 노드와 인접한 노드
]

# 각 노드가 방문된 정보를 1차원 리스트로 표현
visited = [False] * 9

DFS 함수 호출

1
2
3
4
dfs(graph, 1, visited)

# output 
>>> 1 2 7 6 8 3 4 5




BFS

BFS 너비 우선 탐색 (Breadth-First Search)

그래프에서 가까운 노드부터 우선적으로 탐색하는 알고리즘

BFS는 큐 자료구조를 이용하며, 구체적인 동작 과정은 다음과 같다.

  1. 탐색 시작 노드큐에 삽입하고 방문 처리 한다.
  2. 큐에서 노드를 꺼낸 뒤에 해당 노드의 인접 노드 중에서 방문하지 않은 노드모두 큐에 삽입하고 방문 처리 한다.
  3. 더 이상 2번의 과정을 수행할 수 없을 때까지 반복한다.

BFS 알고리즘은 특정 조건에서의 최단 경로 문제를 해결하기 위한 목적으로도 효과적으로 사용될 수 있다.

BFS 동작 예시

DFS와 동일하게 방향성이 없는 무방향 그래프

매번 큐에서 원소를 꺼내서 그 꺼낸 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 방문 처리 하는 것을 반복하면 된다.

[STEP 0] 그래프를 준비한다. (방문 기준: 번호가 낮은 인접 노드부터)

  • 시작노드: 1 IMG_0206

[STEP 1] 시작노드인 ‘1’을 큐에 삽입하고 방문 처리한다.

  • Queue = [1]

[STEP 2] 큐에서 노드 ‘1’을 꺼내 방문하지 않은 인접 노드 ‘2’, ‘3’, ‘8’을 큐에 삽입하고 방문 처리한다.

  • Queue = [8, 3, 2]

[STEP 3] 큐에서 노드 ‘2’를 꺼내 방문하지 않은 인접 노드 ‘7’을 큐에 삽입하고 방문 처리한다.

  • Queue = [7, 8, 3]

[STEP 4] 큐에서 노드 ‘3’을 꺼내 방문하지 않은 인접 노드 ‘4’, ‘5’를 큐에 삽입하고 방문 처리한다.

  • Queue = [5, 4, 7, 8]

[STEP 5] 큐에서 노드 ‘8’을 꺼내고 방문하지 않은 인접 노드가 없으므로 무시한다.

  • Queue = [5, 4, 7]

[STEP 6] 큐에서 노드 ‘7’을 꺼내 방문하지 않은 인접 노드 ‘6’을 큐에 삽입하고 방문 처리한다.

  • Queue = [6, 5, 4]


이러한 과정을 반복하였을 때 전체 노드의 탐색 순서(큐에 들어간 순서)는

1 -> 2 -> 3 -> 8 -> 7 -> 4 -> 5 -> 6


BFS 소스코드 예제 (Python)

BFS 메서드 정의

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from collections import deque

def bfs(graph, start, visited):
    queue = deque([start])
    visited[start] = True # 현재 노드 방문 처리
    # 큐가 빌 때까지 반복
    while queue:
        # 큐에서 하나의 원소를 뽑아 출력
        v = queue.popleft()
        print(v, end=' ')
        # 아직 방문하지 않은 인접한 원소들을 큐에 삽입
        for i in graph[v]:
            if not visited[i]:
                queue.append(i)
                visited[i] = True

그래프 표현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 각 노드가 연결된 정보를 2차원 리스트로 표현
graph = [
    [],
    [2, 3, 8], # 1번 노드와 인접한 노드
    [1, 7],    # 2번 노드와 인접한 노드
    [1, 4, 5], # 3번 노드와 인접한 노드
    [3, 5],    # 4번 노드와 인접한 노드
    [3, 4],    # 5번 노드와 인접한 노드
    [7],       # 6번 노드와 인접한 노드
    [2, 6, 8], # 7번 노드와 인접한 노드
    [1, 7]     # 8번 노드와 인접한 노드
]

# 각 노드가 방문된 정보를 1차원 리스트로 표현
visited = [False] * 9

BFS 함수 호출

1
2
3
4
bfs(graph, 1, visited)

# output
>>> 1 2 3 8 7 4 5 6

Leave a comment