DFS
DFS는 Depth-First Search, 깊이 우선 탐색이라고도 부르며, 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘이다.
DFS는 스택 자료구조에 기초한다는 점에서 구현이 간단하다. 실제로는 스택을 쓰지 않아도 되며 탐색을 수행함에
있어서 데이터의 개수가 N개인 경우 O(N)의 시간이 소요된다는 특징이 있다.
또한 DFS는 스택을 이용하는 알고리즘이기 때문에 실제 구현을 재귀 함수를 이용했을 때 매우 간결하게 구현할 수 있다.
DFS 예제
# DFS 메서드 정의
def dfs(graph, v, visited):
# 현재 노드를 방문처리
visited[v] = True
print(v, end=' ')
# 현재 노드와 연결된 다른 노드를 재귀적으로 방문
for i in graph[v]:
if not visited[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]
]
# 각 노드가 방문된 정보를 리스트 자료형으로 표현(1차원 리스트)
visited = [False] * 9
# 정의된 DFS 함수 호출
dfs(graph, 1, visited)
다음 예제를 분석해보자. 먼저 2차원 리스트인 graph를 선언해주었다. graph에는 각 노드가 연결된 정보가 저장되어
있다.
일반적으로 그래프 문제가 출제되면 노드의 번호가 1번부터 시작되는 경우가 많기 때문에 index[0]에 대한 정보는
비워둘 수 있도록 하고 1번 인덱스부터 해당 노드에 인접한 노드가 무엇인지 리스트 형태로 담아주자.
1번 노드에 연결된 노드는 2번, 3번, 8번이라고 리스트를 초기화해주었고 마찬가지로 2번 노드와 연결된 노드는
1번, 7번이다. (인접 리스트 방식)
방문 처리를 위해 visited라는 1차원 리스트를 선언해주었는데 기본값을 False로 해주어 처음에는 모든 노드를
방문하지 않은 것으로 처리했다. 다만 여기서 1번 노드부터 8번 노드까지 가지고 있고 index[0]은 사용하지 않도록
하기 위해 노드 수보다 하나 더 큰 크기로 1차원 배열을 선언해주었다.
dfs 함수는 먼저 해당 노드를 방문 처리하고 방문했다는 의미로 해당 노드의 번호를 출력한다.
이어서 현재 확인하고있는 스택에 최상단에 있는 원소와 연결된 다른 노드를 하나씩 확인하면서 만약 인접한 노드가
방문되지 않은 상태라면 그 노드에 대해서도 마찬가지로 재귀 함수를 이용해 방문을 진행한다.
이처럼 재귀적으로 방문하지 않은 노드들을 계속해서 방문한다는 점에서 깊이 우선으로 최대한 깊게 그래프를 탐색할 수
있는 것이다. 실행 결과를 확인해보면 책에서 그림으로 진행했던 DFS와 결과가 같다.
BFS
BFS (Breadth First Search) 알고리즘은 '너비 우선 탐색'이라는 의미를 가진다. 쉽게 말해 가까운 노드부터 탐색하는
알고리즘이다. DFS는 최대한 멀리 있는 노드를 우선으로 탐색하는 방식으로 동작한다고 했는데, BFS는 그 반대다.
그렇다면 BFS는 실제로 어떤 방식으로 구현할 수 있을까?
BFS 구현에서는 선입선출 방식인 큐 자료구조를 이용하는 것이 정석이다. 인접한 노드를 반복적으로 큐에 넣도록
알고리즘을 작성하면 자연스럽게 먼저 들어온 것이 나가게 되어, 가까운 노드부터 탐색을 진행하게 된다.
파이썬으로 큐를 구현할 때는 collections 모듈에서 제공하는 deque 자료구조를 활용하자. deque는 스택과 큐의 장점을 모두 채택한 것인데 데이터를 넣고 빼는 속도가 리스트 자료형에 비해 효율적이며 queue 라이브러리를 이용하는 것보다
더 간단하다.
너비 우선 탐색 알고리즘인 BFS는 큐 자료구조에 기초한다는 점에서 구현이 간단하다. 실제로 구현함에 있어 앞서
언급한 대로 deque 라이브러리를 사용하는 것이 좋으며 탐색을 수행함에 있어 O(N)의 시간이 소요된다. 일반적인 경우에 실제 수행 시간은 DFS보다 좋은 편이라는 점까지만 추가적으로 기억하자.
BFS 예제
from collections import deque
# BFS 메서드 정의
def bfs(graph, start, visited):
# 큐(Queue) 구현을 위해 deque 라이브러리 사용
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]
]
# 각 노드가 방문된 정보를 리스트 자료형으로 표현(1차원 리스트)
visited = [False] * 9
# 정의된 BFS 함수 호출
bfs(graph, 1, visited)
파이썬을 이용해 BFS를 구현하는 예시는 다음과 같다. 먼저 queue를 위해서 하나의 deque라이브러리를 불러와준다.
graph 부분에서는 DFS와 마찬가지로 0번 인덱스는 비워준다. 노드의 수는 8개지만 graph리스트의 원소는 9개이다.
bfs 메서드를 살펴보자. 먼저 시작 노드를 큐에 넣어준다. 그다음에 시작 노드를 방문 처리한 뒤에 큐가 빌 때까지
while문을 반복한다. while문에서는 매번 큐에서 하나의 원소를 꺼낸다. 파이썬에서 deque을 이용해 queue를 구현할
때는 코드와 같이 popleft를 사용해 가장 먼저 들어왔던 원소를 꺼낼 수 있다. 큐에서 원소를 꺼낸 뒤 해당 노드의 번호를 출력해주고 그 꺼낸 노드와 인접한 노드를 확인하면서 아직 방문하지 않은 노드가 있다면 큐에 다 넣어줄 수 있도록 한다. 그다음 방문처리를 해준다.
예제 1. 음료수 얼려먹기 (책 149 페이지)
# N,M을 공백으로 구분하여 입력받기
n, m = map(int, input().split())
# 2차원 리스트의 맵 정보 입력받기
graph = []
for _ in range(n):
graph.append(list(map(int, input())))
# DFS로 특정한 노드를 방문한 뒤에 연결된 모든 노드들도 방문
def dfs(x,y):
# 주어진 범위를 벗어나는 경우에는 즉시 종료
if x <= -1 or x >= n or y <= -1 or y >= m:
return False
# 현재 노드를 아직 방문하지 않았다면
if graph[x][y] == 0:
# 해당 노드 방문 처리
graph[x][y] = 1
# 상, 하, 좌, 우의 위치도 모두 재귀적으로 호출
dfs(x -1, y)
dfs(x, y -1)
dfs(x +1, y)
dfs(x, y+1)
return True
return False
# 모든 노드(위치)에 대하여 음료수 채우기
result = 0
for i in range(n):
for j in range(m):
# 현재 위치에서 DFS 수행
if dfs(i,j) == True:
result += 1
print(result)
예제 2. 미로탈출
from collections import deque
# N, M을 공백으로 구분하여 입력받기
n, m = map(int, input().split())
# 2차원 리스트의 맵 정보 입력받기
graph = []
for _ in range(n):
graph.append(list(map(int, input())))
# 이동할 네 방향 정의 (상, 하, 좌, 우)
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
# BFS 소스코드 구현
def bfs(x, y):
# 큐(Queue) 구현을 위해 deque 라이브러리 사용
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 graph[nx][ny] == 0:
continue
# 해당 노드를 처음 방문하는 경우에만 최단 거리 기록
if graph[nx][ny] == 1:
graph[nx][ny] = graph[x][y] + 1
queue.append((nx, ny))
return graph[n - 1][m - 1]
print(bfs(0, 0))
'Python 코딩테스트 > 이것이 취업을 위한 코딩 테스트다' 카테고리의 다른 글
다이나믹 프로그래밍(동적 계획법) (0) | 2021.09.12 |
---|---|
이진 탐색: 탐색 범위를 반으로 좁혀가며 빠르게 탐색하는 알고리즘 (0) | 2021.08.31 |
정렬 알고리즘: 선택 정렬, 삽입 정렬, 퀵 정렬, 계수 정렬, sort (0) | 2021.08.23 |
구현: 아이디어를 코드로 바꾸는 구현 (0) | 2021.07.23 |
그리디: 현재 상황에서 가장 좋아 보이는 것만을 선택하는 알고리즘 (0) | 2021.06.25 |