-
[알고리즘][개념] DFS(깊이 우선 탐색) & BFS(너비 우선 탐색)개발 이야기/알고리즘 및 코테 준비 2024. 9. 18. 15:28
https://devuna.tistory.com/32, 튜나 개발일기를 참고하였습니다.
여러 자료구조 중 그래프를 탐색하는 방법에는 크게 깊이 우선 탐색, DFS와 너비 우선 탐색인 BFS가 존재한다.
여기서 말하는 그래프란 node와 edge로 구성된 자료구조를 말한다. 이 중 방향성이 있는 비순환 그래프를 트리라고 말한다.
이런 그래프를 탐색한다는 의미는 하나의 정점으로부터 시작하여 차례대로 모든 정점을 하나씩 방문하는 것을 의미한다.
코테 문제 중 일부는 이런 search 기법을 잘 구현해야하기에 DFS와 BFS의 개념을 알아보고 python으로 주어진 그래프를 간단히 탐색해보는 예제코드를 작성해보려고 한다.
깊이 우선 탐색(DFS, Depth-First Search)
: 우선 최대한 깊이 내려간 뒤에, 더 이상 갈 수 없다면 옆으로 가자!
깊이 우선 탐색이란 위의 이미지에서 볼 수 있듯이, 루트 노드(혹은 임의의 노드)에서 시작해서 다음 분기로 넘어가기 전에 해당 분기를 완벽하게(깊숙히 끝까지) 탐색하는 방법이다.
스택 또는 재귀함수로 구현한다.
아래의 특징이 있다.
1. 모든 노드를 방문하고자 하는 경우에 이 방법을 선택함
2. 깊이 우선 탐색(DFS)이 너비 우선 탐색(BFS)보다 좀 더 간단함
3. 검색 속도 자체는 너비 우선 탐색(BFS)에 비해서 느림너비 우선 탐색(BFS, Breadth-First Search): 1층 1층 보자..!(layer 단으로 보자)
루트 노드(혹은 다른 임의의 노드)에서 시작해석 인접한 노드를 먼저 탐색하는 방법으로, 시작 정점으로부터 가까운 정점들, 즉 이웃 노드들을 우선 방문하는 방법이다. 큐를 이용해서 구현한다.
주로 두 노드 사이의 최단 경로를 찾고 싶을 때 이 방법을 선택한다.
DFS와 BFS 비교
DFS, BFS은 특징에 따라 사용에 더 적합한 문제 유형들이 있습니다.
1) 그래프의 모든 정점을 방문하는 것이 주요한 문제단순히 모든 정점을 방문하는 것이 중요한 문제의 경우 DFS, BFS 두 가지 방법 중 어느 것을 사용하셔도 상관없습니다.
둘 중 편한 것을 사용하시면 됩니다.
2) 경로의 특징을 저장해둬야 하는 문제예를 들면 각 정점에 숫자가 적혀있고 a부터 b까지 가는 경로를 구하는데 경로에 같은 숫자가 있으면 안 된다는 문제 등, 각각의 경로마다 특징을 저장해둬야 할 때는 DFS를 사용합니다. (BFS는 경로의 특징을 가지지 못합니다)
3) 최단거리 구해야 하는 문제미로 찾기 등 최단거리를 구해야 할 경우, BFS가 유리합니다.
왜냐하면 깊이 우선 탐색으로 경로를 검색할 경우 처음으로 발견되는 해답이 최단거리가 아닐 수 있지만,
너비 우선 탐색으로 현재 노드에서 가까운 곳부터 찾기 때문에 경로를 탐색 시 먼저 찾아지는 해답이 곧 최단거리기 때문입니다.
이밖에도
- 검색 대상 그래프가 정말 크다면 DFS를 고려
- 검색대상의 규모가 크지 않고, 검색 시작 지점으로부터 원하는 대상이 별로 멀지 않다면 BFS간단한 코드 구현(python)
트리 구현
class TreeNode: def __init__(self, value): self.value = value self.children = [] def add_child(self, node): self.children.append(node) # 트리 생성 root = TreeNode(1) child_1 = TreeNode(2) child_2 = TreeNode(3) child_3 = TreeNode(4) root.add_child(child_1) root.add_child(child_2) root.add_child(child_3) child_1.add_child(TreeNode(5)) child_3.add_child(TreeNode(6)) child_3.add_child(TreeNode(7))
구현된 트리는 아래와 같습니다.
1 / | \ 2 3 4 / / \ 5 6 7
DFS 알고리즘:
1. 재귀함수를 이용한 구현
def dfs(node): if node is None: return print(node.value, end=" ") # 현재 노드 출력 for child in node.children: dfs(child) # 자식 노드에 대해 재귀 호출
2. 스택을 이용한 구현
스택의 경우 LIFO를 따름으로 이를 이용. 아래의 방식으로 작동
- 루트 노드를 스택에 추가합니다.
- 스택에서 노드를 꺼내고 그 노드를 방문합니다.
- 꺼낸 노드의 자식들을 스택에 추가하는데, 자식을 오른쪽에서 왼쪽 순서로 넣어야 스택에서 꺼낼 때 왼쪽 자식부터 방문하게 됩니다.
- 스택이 비어있을 때까지 위 과정을 반복합니다.
def dfs_stack(root): if root is None: return stack = [root] # 스택에 루트 노드를 넣음 while stack: node = stack.pop() # 스택에서 가장 위의 노드를 꺼냄 print(node.value, end=" ") # 현재 노드 출력 # 자식 노드를 스택에 추가 (반대로 추가해야 왼쪽부터 탐색됨) # 자식들을 오른쪽에서 왼쪽 순으로 스택에 넣어야 왼쪽부터 탐색됨 for child in reversed(node.children): stack.append(child)
1 -> 2 -> 5 -> 3 -> 4 -> 6 -> 7 순서로 탐색함
BFS 알고리즘:
큐를 이용하여 구현. 큐의 경우 FIFO 임으로 탐색할 노드를 큐에 넣고, 이후 그 다음 자식 노드들을 큐에 차례대로 넣어가며 탐색한다.
from collections import deque def bfs(root): if root is None: return queue = deque([root]) # 큐에 루트 노드를 삽입 while queue: node = queue.popleft() # 큐의 앞에서 노드를 꺼내고 출력 print(node.value, end=" ") queue.extend(node.children) # 현재 노드의 자식들을 큐에 삽입
BFS는 트리의 레벨 순서대로 탐색합니다. 먼저 루트 노드 1을 방문하고, 그 다음 레벨에서 2, 3, 4를 방문한 후, 그다음 레벨에서 5, 6, 7을 방문합니다. BFS 탐색 순서는 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7입니다
'개발 이야기 > 알고리즘 및 코테 준비' 카테고리의 다른 글
[Softeer][21년 재직자 대회예선] 이미지 프로세싱 (1) 2024.09.23 [알고리즘][개념] Selection sort, Insertion Sort, Merge Sort, Quick sort (0) 2024.09.18 [Softeer][21년 재직자 대회 예선] 좌석관리 (0) 2024.09.09 [Softeer][21년 재직자 예선] 회의실 예약 (1) 2024.09.09 [Softeer][21년 재직자 대회 예선] 전광판 문제 (0) 2024.09.04