본문으로 건너뛰기

1. 깊이 우선 탐색(DFS) 개요

  • 깊이 우선 탐색(Depth-First Search, DFS)은 그래프나 트리 구조에서 가능한 한 깊이 들어가면서 탐색을 진행하는 알고리즘입니다.
  • 미로 찾기를 할 때 한 방향으로 계속 가다가 막히면 다시 돌아와서 다른 방향으로 가는 것과 유사합니다.

1.1 DFS의 특징

  • 한 경로를 끝까지 탐색한 후 다음 경로로 이동
  • 재귀 함수나 스택으로 구현 가능
  • 백트래킹이 필요한 문제에 적합
  • 모든 노드를 방문하는 것이 목표일 때 사용

2. DFS 동작 원리

2.1 기본 알고리즘

  1. 시작 노드를 방문 처리
  2. 현재 노드와 연결된 미방문 노드를 찾아 방문
  3. 더 이상 방문할 노드가 없으면 이전 노드로 돌아감
  4. 모든 노드를 방문할 때까지 2-3 반복
DFS vs BFS

DFS는 깊이를 우선으로 하는 반면, BFS(너비 우선 탐색)는 너비를 우선으로 탐색합니다. 미로에서 DFS는 한 길을 끝까지 가보는 전략이고, BFS는 현재 위치에서 갈 수 있는 모든 길을 한 칸씩 가보는 전략입니다.

3. DFS 구현 방법

3.1 재귀를 이용한 구현

기본 구조

def dfs(graph, v, visited):
# 현재 노드를 방문 처리
visited[v] = True
print(v, end=' ')

# 현재 노드와 연결된 다른 노드를 재귀적으로 방문
for i in graph[v]:
if not visited[i]:
dfs(graph, i, visited)

# 그래프를 인접 리스트로 표현
graph = [
[], # 0번 노드는 사용하지 않음
[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번 노드와 연결된 노드들
]

# 방문 여부를 저장할 리스트
visited = [False] * 9

# DFS 호출
dfs(graph, 1, visited) # 1번 노드부터 탐색 시작

3.2 스택을 이용한 구현

반복문 구조

def dfs_iterative(graph, start):
visited = []
stack = [start]

while stack:
v = stack.pop()
if v not in visited:
visited.append(v)
stack.extend(reversed(graph[v]))

return visited

4. DFS 활용 사례

4.1 미로 탐색 문제

def solve_maze(maze, start, end):
def dfs(x, y):
if x < 0 or x >= len(maze) or y < 0 or y >= len(maze[0]):
return False

if maze[x][y] == 1: # 길이 있는 경우
maze[x][y] = 0 # 방문 처리

# 상하좌우 탐색
dfs(x-1, y)
dfs(x+1, y)
dfs(x, y-1)
dfs(x, y+1)
return True

return False

return dfs(start[0], start[1])

4.2 연결 요소 찾기

def count_components(n, edges):
def dfs(node):
visited[node] = True
for neighbor in graph[node]:
if not visited[neighbor]:
dfs(neighbor)

# 그래프 생성
graph = [[] for _ in range(n)]
for a, b in edges:
graph[a].append(b)
graph[b].append(a)

visited = [False] * n
count = 0

# 모든 노드에 대해 DFS 수행
for i in range(n):
if not visited[i]:
dfs(i)
count += 1

return count
  • 연결 요소란 그래프 내에서 서로 연결된 부분 그래프를 의미합니다.

5. DFS 시간 복잡도

5.1 인접 행렬을 사용할 경우

  • 시간 복잡도: O(V²)
    • 각 정점을 방문할 때마다(V)
    • 해당 정점과 연결된 정점을 찾기 위해 모든 정점을 확인해야 함(V)
  • V는 정점의 개수

5.2 인접 리스트를 사용할 경우

  • 시간 복잡도: O(V + E)
    • 각 정점은 정확히 한 번씩 방문됨(V)
    • 각 정점 방문 시 해당 정점의 인접 리스트를 확인 모든 정점의 인접 리스트를 합하면 총 간선 수(E)
  • V는 정점의 개수, E는 간선의 개수
성능 최적화 팁
  • 방문 체크는 set 자료구조 사용
  • 재귀 깊이가 깊어질 경우 시스템 재귀 한도 조정
  • 가지치기(pruning)를 통한 불필요한 탐색 방지
  • 그래프가 밀집되어 있다면(간선이 많다면) 인접 행렬이 더 효율적일 수 있습니다.
  • 그래프가 희소하다면(간선이 적다면) 인접 리스트가 더 효율적입니다.
  • 대부분의 실제 그래프는 희소 그래프이므로 인접 리스트를 더 많이 사용합니다.

6. 코딩 테스트에서의 DFS

6.1 자주 나오는 문제 유형

  • 경로 찾기
  • 연결 요소 개수 세기
  • 사이클 판별
  • 백트래킹이 필요한 문제

6.2 문제 해결 전략

  1. 그래프 표현 방식 결정 (인접 리스트 vs 인접 행렬)
  2. 방문 처리 방법 선택
  3. 종료 조건 명확히 설정
  4. 가지치기 조건 고려
주의사항
  • 재귀 깊이 제한에 주의
  • 무한 루프 방지를 위한 방문 체크
  • 메모리 사용량 고려
  • 스택 오버플로우 주의

6.3 추천 문제

7. 마치며

DFS는 그래프와 트리 탐색의 기본이 되는 알고리즘입니다. 재귀적 사고방식을 기르고 백트래킹 문제를 해결하는 데 필수적인 도구이므로, 코딩 테스트 준비나 실제 개발에서 자주 활용됩니다.

학습 추천사항
  1. 기본적인 그래프 탐색 문제부터 시작
  2. 재귀와 반복 두 가지 방식 모두 구현해보기
  3. 실제 코딩 테스트 문제 풀이 연습
  4. 다양한 응용 문제에 적용해보기