Algorithm#알고리즘#DFS#BFS#격자탐색#그래프#최단경로#코딩테스트

DFS & BFS

그래프 표현 (인접 리스트/행렬) · DFS/BFS 기본 구현 · 연결 요소, 최단 경로 문제 · 격자(Grid) 탐색 패턴

2026년 3월 25일3분 읽기조회 11
공유

학습 목표: 그래프 표현 (인접 리스트/행렬) · DFS/BFS 기본 구현 · 연결 요소, 최단 경로 문제 · 격자(Grid) 탐색 패턴


1️⃣ 그래프 표현 방법

그래프란?

노드(정점, Node) 와 간선(Edge) 으로 이루어진 자료구조. 현실 세계의 지도, 소셜 네트워크, 도로망 등을 표현할 때 쓴다.

노드: 장소, 사람, 상태
간선: 연결, 관계, 이동 가능 여부

인접 행렬 (Adjacency Matrix)

2차원 배열로 표현. graph[i][j] = 1이면 i → j 연결.

# 노드 4개, 간선: 1-2, 1-3, 2-4, 3-4
n = 4
graph = [[0] * (n+1) for _ in range(n+1)]

edges = [(1,2), (1,3), (2,4), (3,4)]
for u, v in edges:
    graph[u][v] = 1
    graph[v][u] = 1  # 무방향 그래프면 양쪽 다

# graph[1][2] = 1, graph[1][3] = 1, ...

✅ 장점: 연결 여부를 O(1)로 확인
❌ 단점: 노드가 많으면 메모리 낭비 (N² 공간)


인접 리스트 (Adjacency List)

각 노드가 연결된 노드 목록만 저장. 실전에서 거의 이걸 쓴다!

from collections import defaultdict

graph = defaultdict(list)

edges = [(1,2), (1,3), (2,4), (3,4)]
for u, v in edges:
    graph[u].append(v)
    graph[v].append(u)  # 무방향 그래프

# graph = {1: [2, 3], 2: [1, 4], 3: [1, 4], 4: [2, 3]}

장점: 메모리 효율적 (실제 간선 수만큼만)
단점: 연결 여부 확인이 O(degree)

degree = 해당 노드에 연결된 간선의 수 예를 들어 노드 1이 2, 3, 4에 연결돼 있으면 degree = 3.


언제 뭘 써?

노드 수가 적고 (N ≤ 1000), 간선이 많으면   → 인접 행렬
노드 수가 많고, 간선이 적으면              → 인접 리스트 (대부분의 경우)
격자(Grid) 형태의 문제                  → 2차원 배열 그대로 사용

개념

한 방향으로 끝까지 파고들다가, 더 갈 곳이 없으면 되돌아와서 다른 방향 탐색.

🌲 비유: 미로에서 한 방향을 선택하고 막힐 때까지 직진, 막히면 갈림길로 되돌아가는 것!

탐색 순서 예시:
    1
   / \\
  2   3
 / \\
4   5

DFS: 1 → 2 → 4 → 5 → 3  (깊이 우선)
BFS: 1 → 2 → 3 → 4 → 5  (너비 우선)

DFS 구현 - 재귀 방식

def dfs(graph, node, visited):
    visited.add(node)
    print(node, end=' ')

    for neighbor in graph[node]:
        if neighbor not in visited:
            dfs(graph, neighbor, visited)

graph = {1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []}
dfs(graph, 1, set())
# 출력: 1 2 4 5 3

DFS 구현 - 스택 방식 (재귀 한도 초과 시)

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

    while stack:
        node = stack.pop()          # 스택에서 꺼내기
        if node not in visited:
            visited.add(node)
            print(node, end=' ')
            for neighbor in graph[node]:
                if neighbor not in visited:
                    stack.append(neighbor)

graph = {1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []}
dfs_stack(graph, 1)

⚠️ 파이썬 재귀 한도는 기본 1000!
노드가 많으면 sys.setrecursionlimit(10**6) 추가하거나 스택 방식으로 구현한다.


개념

시작점에서 가까운 노드부터 차례대로 탐색. 거리(레벨) 순으로 방문.

🌊 비유: 돌을 물에 던지면 파문이 동심원으로 퍼져나가는 것처럼, 시작점에서 거리 1, 2, 3… 순으로 탐색!

핵심: deque + visited 세트
시작 노드를 큐에 넣고 → 꺼내면서 이웃 탐색 → 방문 안 한 이웃을 큐에 추가

BFS 구현

from collections import deque

def bfs(graph, start):
    visited = set([start])
    queue = deque([start])

    while queue:
        node = queue.popleft()       # 큐에서 꺼내기
        print(node, end=' ')

        for neighbor in graph[node]:
            if neighbor not in visited:
                visited.add(neighbor)
                queue.append(neighbor)

graph = {1: [2, 3], 2: [4, 5], 3: [], 4: [], 5: []}
bfs(graph, 1)
# 출력: 1 2 3 4 5

BFS가 최단 경로를 보장하는 이유

def bfs_shortest(graph, start, end):
    visited = {start: 0}   # {노드: 거리}
    queue = deque([start])

    while queue:
        node = queue.popleft()
        if node == end:
            return visited[end]

        for neighbor in graph[node]:
            if neighbor not in visited:
                visited[neighbor] = visited[node] + 1
                queue.append(neighbor)

    return -1  # 도달 불가

# BFS는 거리 순으로 탐색하기 때문에
# 처음 목적지에 도달했을 때가 반드시 최단 거리!

💡 핵심: BFS는 레벨(거리) 순서로 탐색하기 때문에,
가중치 없는 그래프에서 최단 경로 = BFS 가 공식임.


4️⃣ DFS vs BFS - 언제 뭘 써야 해?

📌 DFS를 써야 할 때

경로 자체를 탐색하거나, 모든 경우를 탐색해야 할 때

  • 연결 요소(Connected Component) 개수 세기
  • 사이클 감지
  • 백트래킹 문제 (경로에 특정 조건이 있을 때)
  • 그래프가 크고 목표가 깊은 곳에 있을 때
모든 정점 방문이 목표  → DFS
경로의 특징 저장 필요  → DFS

📌 BFS를 써야 할 때

시작점에서 최단 거리/최소 횟수를 구해야 할 때

  • 최단 경로 문제 (가중치 없는 그래프)
  • 레벨 탐색 (몇 단계 만에 도달 가능한지)
  • 격자에서 최소 이동 횟수 구하기
최단 거리 필요       → BFS
레벨 단위 탐색 필요  → BFS
"몇 번 만에 가능?"   → BFS

✅ 한 줄 요약

모든 경로 탐색, 연결 확인    → DFS
최단 거리, 최소 횟수        → BFS
모르겠으면                → 일단 BFS 먼저 고려 (최단 경로 보장하니까)

5️⃣ 연결 요소 (Connected Components)

개념

그래프에서 서로 이어진 노드 그룹의 수를 세는 것. 섬 개수 세기, 단지 개수 세기 등에 활용.

def count_components(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()
    count = 0

    def dfs(node):
        visited.add(node)
        for neighbor in graph[node]:
            if neighbor not in visited:
                dfs(neighbor)

    for node in range(1, n+1):
        if node not in visited:
            dfs(node)        # 새로운 연결 요소 발견!
            count += 1

    return count

# 노드 5개, 간선: (1,2), (3,4)
print(count_components(5, [(1,2),(3,4)]))  # 3 (1-2 묶음, 3-4 묶음, 5 혼자)

6️⃣ 격자(Grid) 탐색 패턴

개념

2차원 배열(지도, 미로)을 그래프처럼 탐색하는 패턴. 코딩테스트에서 가장 자주 나오는 형태!

# 상하좌우 이동 방향 벡터 (격자 탐색의 국룰)
dr = [-1, 1, 0, 0]   # 행 변화
dc = [0, 0, -1, 1]   # 열 변화

# 범위 체크 (이것도 국룰)
def in_bound(r, c, rows, cols):
    return 0 <= r < rows and 0 <= c < cols

격자 DFS - 연결된 영역 크기 구하기

def dfs_grid(grid, r, c, visited):
    rows, cols = len(grid), len(grid[0])
    visited[r][c] = True
    size = 1

    dr = [-1, 1, 0, 0]
    dc = [0, 0, -1, 1]

    for i in range(4):
        nr, nc = r + dr[i], c + dc[i]
        if 0 <= nr < rows and 0 <= nc < cols:
            if not visited[nr][nc] and grid[nr][nc] == 1:
                size += dfs_grid(grid, nr, nc, visited)

    return size

grid = [
    [1, 1, 0, 0],
    [1, 0, 0, 1],
    [0, 0, 1, 1],
]
visited = [[False]*4 for _ in range(3)]
print(dfs_grid(grid, 0, 0, visited))  # 3 (연결된 1의 개수)

격자 BFS - 최단 거리 구하기

from collections import deque

def bfs_grid(grid, start_r, start_c, end_r, end_c):
    rows, cols = len(grid), len(grid[0])
    visited = [[False]*cols for _ in range(rows)]
    queue = deque([(start_r, start_c, 0)])  # (행, 열, 거리)
    visited[start_r][start_c] = True

    dr = [-1, 1, 0, 0]
    dc = [0, 0, -1, 1]

    while queue:
        r, c, dist = queue.popleft()

        if r == end_r and c == end_c:
            return dist

        for i in range(4):
            nr, nc = r + dr[i], c + dc[i]
            if 0 <= nr < rows and 0 <= nc < cols:
                if not visited[nr][nc] and grid[nr][nc] != 1:  # 벽이 아니면
                    visited[nr][nc] = True
                    queue.append((nr, nc, dist + 1))

    return -1  # 도달 불가

🎯 격자 BFS 핵심 3가지:

1. visited 처리는 큐에 넣을 때 (꺼낼 때 하면 중복 방문 발생!)
2. 범위 체크 + 벽 체크를 동시에
3. 큐에 (행, 열, 거리)를 함께 저장

7️⃣ 그래프 탐색 5문제 풀이

✅ 문제 1 - DFS와 BFS 동시 구현

import sys
from collections import deque

input = sys.stdin.readline

def dfs(graph, v, visited):
    visited[v] = True
    print(v, end=' ')
    for i in sorted(graph[v]):  # 번호 작은 것부터
        if not visited[i]:
            dfs(graph, i, visited)

def bfs(graph, start, n):
    visited = [False] * (n+1)
    queue = deque([start])
    visited[start] = True

    while queue:
        v = queue.popleft()
        print(v, end=' ')
        for i in sorted(graph[v]):
            if not visited[i]:
                visited[i] = True
                queue.append(i)

✅ 문제 2 - 단지 개수 세기 (격자 DFS)

def solution(grid):
    n = len(grid)
    visited = [[False]*n for _ in range(n)]
    groups = []

    def dfs(r, c):
        visited[r][c] = True
        count = 1
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0 <= nr < n and 0 <= nc < n and not visited[nr][nc] and grid[nr][nc] == 1:
                count += dfs(nr, nc)
        return count

    for i in range(n):
        for j in range(n):
            if grid[i][j] == 1 and not visited[i][j]:
                groups.append(dfs(i, j))

    groups.sort()
    return len(groups), groups

✅ 문제 3 - 미로 최단 경로 (격자 BFS)

from collections import deque

def shortest_path(maze):
    n, m = len(maze), len(maze[0])
    queue = deque([(0, 0, 1)])  # (행, 열, 거리)
    visited = [[False]*m for _ in range(n)]
    visited[0][0] = True

    while queue:
        r, c, dist = queue.popleft()
        if r == n-1 and c == m-1:
            return dist
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0 <= nr < n and 0 <= nc < m and not visited[nr][nc] and maze[nr][nc] == 1:
                visited[nr][nc] = True
                queue.append((nr, nc, dist+1))
    return -1

✅ 문제 4 - 토마토 익히기 (다중 시작점 BFS)

from collections import deque

def tomato(grid):
    n, m = len(grid), len(grid[0])
    queue = deque()

    # 익은 토마토 전부 큐에 넣기 (다중 시작점!)
    for i in range(n):
        for j in range(m):
            if grid[i][j] == 1:
                queue.append((i, j, 0))

    max_day = 0
    while queue:
        r, c, day = queue.popleft()
        max_day = max(max_day, day)
        for dr, dc in [(-1,0),(1,0),(0,-1),(0,1)]:
            nr, nc = r+dr, c+dc
            if 0 <= nr < n and 0 <= nc < m and grid[nr][nc] == 0:
                grid[nr][nc] = 1
                queue.append((nr, nc, day+1))

    # 아직 안 익은 토마토 있으면 -1
    for row in grid:
        if 0 in row:
            return -1
    return max_day

💡 다중 시작점 BFS: 시작점이 여러 개일 때 전부 큐에 미리 넣고 시작!
토마토, 불 번지기, 바이러스 퍼짐 같은 문제에서 자주 씀.


✅ 문제 5 - 연결 요소 개수 (그래프 DFS)

import sys
sys.setrecursionlimit(10**6)

def count_connected_components(n, edges):
    graph = [[] for _ in range(n+1)]
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = [False] * (n+1)
    count = 0

    def dfs(v):
        visited[v] = True
        for neighbor in graph[v]:
            if not visited[neighbor]:
                dfs(neighbor)

    for i in range(1, n+1):
        if not visited[i]:
            dfs(i)
            count += 1

    return count

4주차 핵심 요약

그래프 표현
   → 인접 행렬: O(1) 탐색, 메모리 많이 씀
   → 인접 리스트: 메모리 효율적, 대부분의 경우 이걸 써

 DFS
   → 재귀 or 스택으로 구현
   → 연결 요소, 백트래킹, 경로 탐색에 사용

 BFS
   → deque로 구현 (list.pop(0)은 O(n)이라 느림!)
   → 가중치 없는 그래프의 최단 경로 보장

 격자 탐색
   → dr, dc 방향 벡터 + 범위 체크가 국룰
   → visited는 큐에 넣을 때 처리

 DFS vs BFS 선택 기준
   → 최단 거리 필요 → BFS
   → 모든 경로 탐색 → DFS

DFS vs BFS - 언제 뭘 쓰냐

연결 요소 개수 세기 어느 쪽이든 가능하지만 → DFS가 재귀로 구현이 더 직관적

미로 최단 거리 DFS로 풀면 최단 보장 안 됨 → 반드시 BFS

섬 넓이 구하기 넓이를 누적해야 하니까 → DFS (재귀로 누적하기 편함)

N단계 이내 도달 가능한 노드 수 레벨 기반 탐색 → BFS

바이러스 / 불 번지기 (다중 시작점) 시작점 여러 개를 큐에 한꺼번에 → 다중 시작점 BFS

관련 포스트

댓글

방명록을 남겨주세요