개념
Depth-First Search - 깊이 우선 탐색
한 방향으로 끝까지 탐색한 후 되돌아와 다른 방향 탐색
시간복잡도
표현 방법 | 시간복잡도 |
---|---|
인접 리스트 | O(V + E) |
인접 행렬 | O(V²) |
V: 정점 수, E: 간선 수
특징
동작 방식
그래프:
1
/ \
2 3
/ \
4 5
탐색 순서: 1 → 2 → 4 → 5 → 3
(왼쪽 끝까지 간 후 되돌아옴)
장점
- 메모리 효율적: 현재 경로만 저장
- 경로 찾기: 목표까지의 경로 추적 가능
- 백트래킹: 되돌아가며 탐색
단점
- 최단 경로 보장 안 됨: 먼저 찾은 경로가 최단 아닐 수 있음
- 무한 루프 가능: 방문 체크 필수
구현 방법
1. 재귀 (Recursive)
def dfs_recursive(graph, start, visited=None):
if visited is None:
visited = set()
# 현재 노드 방문
visited.add(start)
print(start, end=' ')
# 인접 노드 방문
for neighbor in graph[start]:
if neighbor not in visited:
dfs_recursive(graph, neighbor, visited)
return visited
# 테스트
graph = {
1: [2, 3],
2: [1, 4, 5],
3: [1],
4: [2],
5: [2]
}
dfs_recursive(graph, 1) # 1 2 4 5 3
# 시간복잡도: O(V + E)
# 공간복잡도: O(V) - 재귀 스택 + visited
2. 스택 (Iterative)
def dfs_iterative(graph, start):
visited = set()
stack = [start]
while stack:
vertex = stack.pop()
if vertex not in visited:
visited.add(vertex)
print(vertex, end=' ')
# 인접 노드를 스택에 추가 (역순으로 추가하면 정순 방문)
for neighbor in reversed(graph[vertex]):
if neighbor not in visited:
stack.append(neighbor)
return visited
# 테스트
dfs_iterative(graph, 1) # 1 2 4 5 3
# 시간복잡도: O(V + E)
# 공간복잡도: O(V)
3. 경로 반환
def dfs_path(graph, start, end, path=None):
if path is None:
path = []
path = path + [start]
# 목표 도달
if start == end:
return path
# 인접 노드 탐색
for neighbor in graph[start]:
if neighbor not in path:
new_path = dfs_path(graph, neighbor, end, path)
if new_path:
return new_path
return None
# 테스트
print(dfs_path(graph, 1, 5)) # [1, 2, 5]
4. 모든 경로 찾기
def dfs_all_paths(graph, start, end, path=None, paths=None):
if path is None:
path = []
if paths is None:
paths = []
path = path + [start]
if start == end:
paths.append(path)
else:
for neighbor in graph[start]:
if neighbor not in path:
dfs_all_paths(graph, neighbor, end, path, paths)
return paths
# 테스트
graph2 = {
1: [2, 3],
2: [4],
3: [4],
4: []
}
print(dfs_all_paths(graph2, 1, 4)) # [[1, 2, 4], [1, 3, 4]]
자주 나오는 문제 유형
1. 연결 요소 개수
문제: 그래프의 연결 요소(Connected Component) 개수
def count_components(n, edges):
# 그래프 생성
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = [False] * n
count = 0
def dfs(node):
visited[node] = True
for neighbor in graph[node]:
if not visited[neighbor]:
dfs(neighbor)
# 모든 노드에서 DFS 시도
for i in range(n):
if not visited[i]:
dfs(i)
count += 1
return count
# 테스트
print(count_components(5, [[0,1], [1,2], [3,4]])) # 2
# 시간복잡도: O(V + E)
핵심: 방문하지 않은 노드에서 DFS 시작할 때마다 count++
2. 섬의 개수
문제: 2D 그리드에서 1로 연결된 섬의 개수
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(r, c):
# 범위 체크 및 물 체크
if (r < 0 or r >= rows or c < 0 or c >= cols or
grid[r][c] == '0'):
return
# 방문 표시
grid[r][c] = '0'
# 4방향 탐색
dfs(r + 1, c)
dfs(r - 1, c)
dfs(r, c + 1)
dfs(r, c - 1)
for i in range(rows):
for j in range(cols):
if grid[i][j] == '1':
dfs(i, j)
count += 1
return count
# 테스트
grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
print(num_islands(grid)) # 3
# 시간복잡도: O(rows × cols)
핵심: 땅(1)을 만나면 DFS로 연결된 모든 땅을 0으로 변경
3. 사이클 탐지 (무방향 그래프)
문제: 그래프에 사이클이 있는지 확인
def has_cycle(graph, n):
visited = [False] * n
def dfs(node, parent):
visited[node] = True
for neighbor in graph[node]:
if not visited[neighbor]:
if dfs(neighbor, node):
return True
elif neighbor != parent:
# 방문했고 부모가 아니면 사이클
return True
return False
for i in range(n):
if not visited[i]:
if dfs(i, -1):
return True
return False
# 테스트
graph = {
0: [1, 2],
1: [0, 2],
2: [0, 1]
}
print(has_cycle(graph, 3)) # True (0-1-2-0)
# 시간복잡도: O(V + E)
핵심: 방문한 노드를 다시 만났는데 부모가 아니면 사이클
4. 사이클 탐지 (방향 그래프)
문제: 방향 그래프에서 사이클 탐지
def has_cycle_directed(graph, n):
WHITE, GRAY, BLACK = 0, 1, 2
color = [WHITE] * n
def dfs(node):
if color[node] == GRAY:
return True # 사이클 발견
if color[node] == BLACK:
return False # 이미 처리됨
color[node] = GRAY # 방문 중
for neighbor in graph[node]:
if dfs(neighbor):
return True
color[node] = BLACK # 처리 완료
return False
for i in range(n):
if color[i] == WHITE:
if dfs(i):
return True
return False
# 테스트
graph = {
0: [1],
1: [2],
2: [0]
}
print(has_cycle_directed(graph, 3)) # True (0→1→2→0)
# 시간복잡도: O(V + E)
핵심: 3색 DFS - WHITE(미방문), GRAY(방문 중), BLACK(완료)
5. 위상 정렬 (Topological Sort)
문제: 방향 비순환 그래프(DAG)의 위상 정렬
def topological_sort(graph, n):
visited = [False] * n
stack = []
def dfs(node):
visited[node] = True
for neighbor in graph[node]:
if not visited[neighbor]:
dfs(neighbor)
stack.append(node)
for i in range(n):
if not visited[i]:
dfs(i)
return stack[::-1] # 역순
# 테스트
graph = {
0: [1, 2],
1: [3],
2: [3],
3: []
}
print(topological_sort(graph, 4)) # [0, 2, 1, 3] 또는 [0, 1, 2, 3]
# 시간복잡도: O(V + E)
핵심: DFS 완료 순서의 역순이 위상 정렬
6. 단절점 (Articulation Point)
문제: 그래프에서 제거하면 연결 요소가 증가하는 정점
def find_articulation_points(graph, n):
visited = [False] * n
discovery = [0] * n
low = [0] * n
parent = [-1] * n
ap = [False] * n
timer = [0]
def dfs(u):
children = 0
visited[u] = True
discovery[u] = low[u] = timer[0]
timer[0] += 1
for v in graph[u]:
if not visited[v]:
children += 1
parent[v] = u
dfs(v)
low[u] = min(low[u], low[v])
# 루트 노드: 자식이 2개 이상
if parent[u] == -1 and children > 1:
ap[u] = True
# 비루트 노드: low[v] >= discovery[u]
if parent[u] != -1 and low[v] >= discovery[u]:
ap[u] = True
elif v != parent[u]:
low[u] = min(low[u], discovery[v])
for i in range(n):
if not visited[i]:
dfs(i)
return [i for i in range(n) if ap[i]]
# 시간복잡도: O(V + E)
핵심: Tarjan 알고리즘 - low, discovery 값 비교
추천 연습 문제
기초
중급
고급
핵심 요약
개념 | 설명 |
---|---|
탐색 방식 | 끝까지 탐색 후 되돌아옴 |
구현 | 재귀 또는 스택 |
시간복잡도 | O(V + E) |
활용 | 경로 찾기, 사이클 탐지, 위상 정렬 |
DFS 템플릿
기본 템플릿
def dfs(node, visited):
visited.add(node)
# 현재 노드 처리
process(node)
# 인접 노드 방문
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor, visited)
2D 그리드 템플릿
def dfs_grid(r, c):
# 범위 체크
if r < 0 or r >= rows or c < 0 or c >= cols:
return
# 방문 체크
if visited[r][c]:
return
visited[r][c] = True
# 4방향 탐색
dfs_grid(r+1, c)
dfs_grid(r-1, c)
dfs_grid(r, c+1)
dfs_grid(r, c-1)
경로 추적 템플릿
def dfs_with_path(node, end, path):
path.append(node)
if node == end:
return path
for neighbor in graph[node]:
if neighbor not in path:
result = dfs_with_path(neighbor, end, path[:])
if result:
return result
return None
DFS vs BFS
특성 | DFS | BFS |
---|---|---|
자료구조 | 스택 (재귀) | 큐 |
탐색 방식 | 깊이 우선 | 너비 우선 |
최단 경로 | 보장 안 됨 | 보장됨 (가중치 없을 때) |
메모리 | O(h) - 높이 | O(w) - 너비 |
활용 | 경로, 사이클, 위상 정렬 | 최단 거리, 레벨 탐색 |
주의사항
1. 방문 체크 필수
# 무한 루프 방지
visited = set()
if node not in visited:
visited.add(node)
2. 재귀 깊이 제한
import sys
sys.setrecursionlimit(10**6) # Python 기본 1000
3. 그리드에서 방향 배열 활용
# 4방향
dx = [0, 0, 1, -1]
dy = [1, -1, 0, 0]
for i in range(4):
nx, ny = x + dx[i], y + dy[i]
dfs(nx, ny)
# 8방향
dx = [0, 0, 1, -1, 1, 1, -1, -1]
dy = [1, -1, 0, 0, 1, -1, 1, -1]