: 많은 양의 데이터 중에서 원하는 데이터를 찾는 과정
프로그래밍에서는 그래프, 트리 등의 자료구조 안에서 '탐색'을 하는 문제를 자주 다룬다. 대표적인 탐색 알고리즘으로, 코딩테스트에서도 자주 출제되는 유형인 DFS와 BFS에 대해 알아보고자 한다. 일단, DFS와 BFS를 이해하기 위해서는 기본 자료구조인 '스택'과 '큐' 그리고 '재귀함수'에 대해 알고 있어야 한다.
스택과 큐, 재귀함수에 대한 설명은 아래 링크에 따로 정리해두었으니 참고하도록 하자.
자료구조 - 스택 (Stack)
자료구조 - 큐 (Queue)
[Python - 재귀함수 (Recursive function)]
: 하나의 노드를 시작으로 다수의 노드를 방문하는 것
: 특정 지점
: 노드와 노드를 연결하는 선
: 2차원 배열에 각 노드가 연결된 형태를 기록하는 방식
## 인접 행렬 방식 예제
INF = 999999999 # 연결되지 않은 노드에 대한 무한의 비용 선언
graph = [
[0, 7, 5],
[7, 0, INf],
[5, INF, 0]]
print(graph)
→ [[0, 7, 5], [7, 0, 999999999], [5, 999999999, 0]]
: 모든 노드에 연결된 노드에 대한 정보를 차례대로 연결하여 저장하는 방식
## 인접 리스트 방식 예제
graph = [[] for _ in range(3)]
# 노드 0에 연결된 노드 정보(노드, 거리) 저장
graph[0].append((1, 7))
graph[0].append((2, 5))
# 노드 1에 연결된 노드 정보(노드, 거리) 저장
graph[1].append((0, 7))
# 노드 2에 연결된 노드 정보(노드, 거리) 저장
graph[2].append((0, 5))
print(graph)
→ [[(1, 7), (2, 5)], [(0, 7)], [(0, 5)]]
: 그래프에서 특정한 경로로 탐색하다가 특정한 상황에서 최대한 깊숙이 들어가서 노드를 방문한 후, 다시 돌아가 다른 경로로 탐색하는 알고리즘
스택
자료구조를 이용하는 알고리즘이기 때문에 실제 구현은 재귀 함수
를 이용해서 더 간결하게 나타낼 수 있다.데이터의 개수가 개인 경우 시간 복잡도 :
탐색 시작 노드를 스택에 삽입하고 방문처리를 한다.
스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면, 그 인접 노드를 스택에 넣고 방문처리를 한다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다.
2번 과정을 더 이상 수행할 수 없을 때 까지 반복한다.
방문처리 : 스택에 한 번 삽입되어 처리된 노드가 다시 삽입되지 않게 체크하는 것
아래 그림으로 좀 더 자세히 살펴보자. 아래의 그래프에서 회색 노드는 방문 처리된 노드, 하늘색 노드는 현재 처리하는 스택의 최상단 노드이다.
위 그림을 코드로 구현해보자.
# DFS 메서드 정의
def dfs(graph, v, visited):
visited[v] = True # 현재 노드를 방문 처리
print(v, end = ' ') # 탐색된 노드 출력
for i in graph[v]: # 현재 노드와 연결된 다른 노드를 재귀적으로 방문
if not visited[i]: # i번째 노드가 방문하지 않은 인접노드인 경우
dfs(graph, i, visited)
# 각 노드가 연결된 정보를 2차원 리스트 자료형으로 표현
graph = [
[],
[2, 3, 8],
[1, 7],
[1, 4, 5],
[3, 5],
[3, 4],
[7],
[2, 6, 8],
[1, 7]
]
# 각 노드의 방문 정보 False로 초기화 (1차원 리스트로 표현)
visited = [False] * 9
# DFS 함수 호출 (v : 최상단 노드)
dfs(graph, 1, visited)
→ 1 2 7 6 8 3 4 5
: 가까운 노드부터 탐색하는 알고리즘
큐
자료구조를 이용한다.아래 그림으로 좀 더 자세히 살펴보자. 아래의 그래프에서 회색 노드는 방문 처리된 노드, 하늘색 노드는 큐에서 꺼내 현재 처리하는 노드이다.
위 그림을 코드로 구현해보자.
from collections import deque
# BFS 메서드 정의
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 # 방문처리
# 각 노드가 연결된 정보를 2차원 리스트 자료형으로 표현
graph = [
[],
[2, 3, 8],
[1, 7],
[1, 4, 5],
[3, 5],
[3, 4],
[7],
[2, 6, 8],
[1, 7]
]
# 각 노드의 방문 정보 False로 초기화 (1차원 리스트로 표현)
visited = [False] * 9
# BFS 함수 호출 (start : 시작 노드)
Bfs(graph, 1, visited)
→ 1 2 3 8 7 4 5 6
구멍이 뚫린 부분은 0, 칸막이가 존재하는 부분은 1로 표시된 N*M 크기의 얼음틀이 있다. 구멍이 뚫려 있는 부분끼리 상, 하, 좌, 우로 붙어 있는 경우 서로 연결되어 있는 것으로 간주한다. 얼음 틀의 모양이 주어졌을 때, 생성되는 총 아이스크림의 개수를 구하는 프로그램을 작성하시오.
💡 해결 아이디어
1. 특정 노드의 주변 상, 하, 좌, 우를 살펴본 뒤 주변 노드 중 값이 0이면서 아직 방문하지 않은 노드가 있다면 해당 노드를 방문한다.
2. 방문한 노드에서 다시 상, 하, 좌, 우를 살펴보면서 방문을 다시 진행하면, 연결된 모든 노드를 방문할 수 있다.
3. 1번과 2번 과정을 모든 노드에 반복하며 방문하지 않은 노드의 개수를 센다.
<## 모범답안
n, m = map(int, input().split())
ice = [list(map(int, input())) for _ in range(n)]
def dfs(x, y):
# 주어진 얼음틀의 범위를 벗어나는 경우 즉시 종료
if x <= -1 or x >= n or y <= -1 or y >= m:
return False
# 현재 노드를 아직 방문하지 않았다면
if ice[x][y] == 0:
ice[x][y] = 1 # 해당 노드 방문처리
# 상,하,좌,우의 위치도 모두 재귀적으로 호출(리턴값을 사용하지 않기 때문에 단순히 연결된 모든 노드에 대해서 방문처리를 하기 위한 목적으로 수행된다)
dfs(x-1, y) # 현재 노드 기준 '왼쪽'방향 탐색
dfs(x, y-1) # 현재 노드 기준 '아래쪽'방향 탐색
dfs(x+1, y) # 현재 노드 기준 '오른쪽'방향 탐색
dfs(x, y+1) # 현재 노드 기준 '위쪽'방향 탐색
return True # 현재 위치에서 처음 DFS가 수행 => True
# 얼음틀에 1밖에 없을때 즉시 종료
return False
cnt = 0 # 카운트 할 아이스크림 개수
# 모든 노드에 대해서 음료수 채우기
for i in range(n):
for j in range(m):
if dfs(i, j) == True: # 현재 위치가 처음으로 방문처리된 노드라면
cnt += 1 # 아이스크림 개수 카운트
# 결과출력
print(cnt)
4 5
00110
00011
11111
00000
3
모범답안에서 가장 헷갈렸던 부분이 바로 이 부분이었다.
cnt = 0 # 카운트 할 아이스크림 개수
# 모든 노드에 대해서 음료수 채우기
for i in range(n):
for j in range(m):
if dfs(i, j) == True: # 현재 위치가 처음으로 방문처리된 노드라면
cnt += 1 # 아이스크림 개수 카운트
내가 이해가 안됐던 부분은 'dfs 메소드에 따라 방문처리된 노드가 여러개일 테고, 그렇게 되면 만들어지는 아이스크림 개수가 존재하는 노드만큼 많아지지 않을까?' 였다.
하지만 DFS 메소드 부분을 잘 살펴보면,
N x M 크기의 직사각형 형태의 미로에 갇혀 있다. 미로에는 여러 마리의 괴물이 있어 이를 피해 탈출해야 한다. 현재 위치는 (1,1) 이고 미로의 출구는 (N,M)의 위치에 존재하며 한 번에 한 칸씩 이동할 수 있다. 이때 괴물이 있는 부분은 0, 괴물이 없는 부분은 1로 표시되어 있다. 미로가 반드시 탈출할 수 있는 형태로 제시될 때, 탈출하기 위해 움직여야 하는 최소 칸의 개수를 구하시오. (칸을 셀 때는 시작 칸과 마지막 칸을 모두 포함해서 계산하며, 시작 칸과 마지막 칸은 항상 1이다.)
💡 해결 아이디어
1. 시작 지점에서 가까운 노드부터 차례대로 그래프의 모든 노드를 탐색하는 방법인 BFS를 사용한다.
2. (1, 1) 지점에서부터 BFS를 수행하여 모든 노드의 값을 거리 정보로 넣는다.
3. 특정한 노드를 방문하면 그 이전 노드의 거리에 1을 더한 값을 리스트에 넣는다.
## 모범답안
from collections import deque
n, m = map(int, input().split())
maze = [list(map(int, input())) for _ in range(n)]
# 이동할 네 방향 정의 (좌, 우, 하, 상)
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
# bfs : 간선의 비용이 모두 같을 때 최단거리를 탐색하기 위해 사용할 수 있는 알고리즘
def bfs(x, y):
queue = deque()
queue.append((x, y))
# 큐가 빌 때 까지 반복
while queue:
x, y = queue.popleft()
# 현재 위치에서 상,하,좌,우 방향으로 위치 확인
for i in range(4):
nx = x + dx[i]
ny = y + dy[i]
# 미로를 벗어난 경우 무시
if nx < 0 or ny < 0 or nx >= n or ny >= m :
continue
# 괴물 때문이 이동할 수 없는 경우 무시
if maze[nx][ny] == 0:
continue
# 해당 노드를 처음 방문하는 경우에만 최단 거리 기록
if maze[nx][ny] == 1:
maze[nx][ny] = maze[x][y] + 1 # 바로 직전 노드에서의 최단거리값에 1을 더한 값 = 지금까지 온 최단거리
queue.append((nx, ny))
# 가장 오른쪽 아래(출구)까지의 최단 거리 반환
return maze[n-1][m-1]
# 결과출력
print(bfs(0, 0))
5 6
101010
111111
000001
111111
111111
10