DFS란?
자료의 검색, 트리나 그래프를 탐색하는 방법. 한 노드를 시작으로 인접한 다른 노드를 재귀적으로 탐색하고 끝까지 탐색하면 다시 위로 와서 다음을 탐색함여 검색한다.
BFS란?
한 노드를 시작으로 인접한 모든 정점들을 우선 방문하는 방법. 더 이상 방문하지 않은 정점이 없을 때까지 방문하지 않은 모든 정점들에 대해서도 넓이 우선 검색을 적용한다.
왜 사용하는 걸까?
모든 경우의 수를 전부 탐색해야할 경우에 쓰인다. 예시로 알파고가 있다.
모든 대국의 수를 계산하고 예측하여 최적의 수를 계산해내기 위해 모든 수를 전부 탐색해야 한다.
DFS 와 BFS 는 그 탐색하는 순서에서 차이가 있다.
DFS는 끝까지 파고드는 것이고 BFS는 갈라진 모든 경우의 수를 탐색해보고 오는 것이다.
실제 구현해보기
DFS
- 노드를 방문하고 깊이 우선으로 인접한 노드를 방문한다.
- 또 그 노드를 방문해서 깊이 우선으로 인접한 노드를 방문한다.
- 만약 끝에 도달했다면 리턴한다.
즉, 깊이를 우선 깊게 들어간 후에 끝이 날 경우 1번과 근접한 노드 중 방문하지 않은 노드로 반복하여 방문하는 것
방문하지 않은 노드라는 것을 어떻게 알 수 있을까?
이는 visited라는 배열에 방문한 노드를 기록해 두면 될 것이다.
예시코드
# 위의 그래프를 예시로 삼아서 인접 리스트 방식으로 표현했습니다!
graph = {
1: [2, 5, 9],
2: [1, 3],
3: [2, 4],
4: [3],
5: [1, 6, 8],
6: [5, 7],
7: [6],
8: [5],
9: [1, 10],
10: [9]
}
visited = []
#1. 시작 노드인 1부터 시작한다
#2. 현재 방문한 노드를 visiteed_array 에 추가한다
#3. 현재 방문한 노드와 인접한 노드 중 방문하지 않은 노드를 방문한다.
def dfs_recursion(adjacent_graph, cur_node, visited_array):
visited_array.append(cur_node)
print(adjacent_graph[cur_node])
print("visited_array : " , visited_array)
#하나하나 꺼내본 다음에 visited 배열에 없다면, dfs_recursion 함수를 호출하여, 2,3의 행위를 반복한다.
for i in adjacent_graph[cur_node] :
print("i = " , i)
if i not in visited: #방문하지 않았다면
dfs_recursion(adjacent_graph, i , visited_array)
return
dfs_recursion(graph, 1, visited) # 1 이 시작노드입니다!
print(visited) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 이 출력되어야 합니다!
그러나, 이런식으로 무한정 깊어지는 노드가 있는 경우에는 에러가 발생할 수 있다.
탈출 조건없이 반복하다보면 RecursionError라는 것이 발생할 수 있기 때문이다.
따라서 다른 방법으로 해결해야한다.
가장 마지막에 넣은 노드들을 꺼내서 탐색만 하면되는데 이는 스택을 이용하면 DFS 를 재귀 없이 구현할 수 있다.
1. 루트 노드를 스택에 넣는다
2. 현재 스택의 노드를 빼서 visited에 추가한다.
3. 현재 방문한 노드와 인접한 노드 중 방문하지 않은 노드를 스택에 추가한다
4. 2번부터 반복한다
5. 스택이 비면 탐색을 종료한다.
1. 현재 스택에서 가장 마지막에 넣은 1을 빼서, visited 에 추가합니다.
# stack -> [] visited -> [1]
2. 인접한 노드들인 [2, 5, 9] 에서 방문하지 않은 것들인 [2, 5, 9]를 stack 에 추가합니다.
# stack -> [2, 5, 9] visited -> [1]
3. 현재 스택에서 가장 마지막에 넣은 9을 빼서, visited 에 추가합니다.
# stack -> [2, 5] visited -> [1, 9]
4. 인접한 노드들인 [1, 10] 에서 방문하지 않은 것들인 [10] 을 stack 에 추가합니다.
# stack -> [2, 5, 10] visited -> [1, 9]
5. 현재 스택에서 가장 마지막에 넣은 10을 빼서, visited 에 추가합니다.
# stack -> [2, 5] visited -> [1, 9, 10]
6. 인접한 노드들인 [9] 에서 방문하지 않은 노드들이 없으니 추가하지 않습니다.
# stack -> [2, 5] visited -> [1, 9, 10]
7. 현재 스택에서 가장 마지막에 넣은 5를 빼서, visited 에 추가합니다.
# stack -> [2] visited -> [1, 9, 10, 5]
8. 인접한 노드들인 [1, 6, 8] 에서 방문하지 않은 것들인 [6, 8]를 stack 에 추가합니다.
# stack -> [2, 6, 8] visited -> [1, 9, 10, 5]
9. 현재 스택에서 가장 마지막에 넣은 8를 을 빼서, visited 에 추가합니다.
# stack -> [2, 6] visited -> [1, 9, 10, 5, 8]
10. 인접한 노드들인 [5] 에서 방문하지 않은 노드들이 없으니 추가하지 않습니다.
# stack -> [2, 6] visited -> [1, 9, 10, 5, 8]
11. 현재 스택에서 가장 마지막에 넣은 6을 빼서, visited 에 추가합니다.
# stack -> [2] visited -> [1, 9, 10, 5, 8, 6]
12. 인접한 노드들인 [5, 7] 에서 방문하지 않은 것들인 [7] 을 stack 에 추가합니다.
# stack -> [2, 7] visited -> [1, 9, 10, 5, 8, 6]
13. 현재 스택에서 가장 마지막에 넣은 7를 을 빼서, visited 에 추가합니다.
# stack -> [2] visited -> [1, 9, 10, 5, 8, 6, 7]
14. 인접한 노드들인 [6] 에서 방문하지 않은 노드들이 없으니 추가하지 않습니다.
# stack -> [2] visited -> [1, 9, 10, 5, 8, 6, 7]
15. 현재 스택에서 가장 마지막에 넣은 2를 빼서, visited 에 추가합니다.
# stack -> [] visited -> [1, 9, 10, 5, 8, 6, 7, 2]
16. 인접한 노드들인 [1, 3] 에서 방문하지 않은 것들인 [3] 을 stack 에 추가합니다.
# stack -> [3] visited -> [1, 9, 10, 5, 8, 6, 7, 2]
17. 현재 스택에서 가장 마지막에 넣은 3을 빼서, visited 에 추가합니다.
# stack -> [] visited -> [1, 9, 10, 5, 8, 6, 7, 2, 3]
18. 인접한 노드들인 [2, 4] 에서 방문하지 않은 것들인 [4] 를 stack 에 추가합니다.
# stack -> [4] visited -> [1, 9, 10, 5, 8, 6, 7, 2, 3, 4]
19. 현재 스택에서 가장 마지막에 넣은 4를 빼서, visited 에 추가합니다.
# stack -> [] visited -> [1, 9, 10, 5, 8, 6, 7, 2, 3, 4]
20. 인접한 노드들인 [3] 에서 방문하지 않은 노드들이 없으니 추가하지 않습니다.
21. 현재 스택에서 꺼낼 것이 없습니다. DFS 가 끝났습니다.
스택을 이용해서 구현해보자
# 위의 그래프를 예시로 삼아서 인접 리스트 방식으로 표현했습니다!
graph = {
1: [2, 5, 9],
2: [1, 3],
3: [2, 4],
4: [3],
5: [1, 6, 8],
6: [5, 7],
7: [6],
8: [5],
9: [1, 10],
10: [9]
}
#1. 시작 노드를 스택에 넣는다.
#2. 현재 스택의 노드를 빼서 visited 에 추가한다.
#3. 현재 방문한 노드와 인접한 노드 중 방문하지 않은 노드를 스택에 추가한다.
#위의 과정을 스택이 빌 때까지 반복한다.
def dfs_stack(adjacent_graph, start_node):
stack = [start_node]
visted = []
print("현재 스택에 있는 것 = " , stack )
while stack :
current_node = stack.pop()
visted.append(current_node)
print("current_node = " , current_node)
#3. 현재 방문한 노드와 인접한 노드 중 방문하지 않은 노드를 스택에 추가한다.
for i in adjacent_graph[current_node] :
print("현재 노드와 연관된 노드들 = " , i , "방문한 적 있는 노드들 : " , visted )
if i not in visted :
stack.append(i)
print("stack에 들어가 있는 것들 = " , stack)
return visted
print(dfs_stack(graph, 1)) # 1 이 시작노드입니다!
# [1, 9, 10, 5, 8, 6, 7, 2, 3, 4] 이 출력되어야 합니다!
BFS
- 현재 인접한 노드 먼저 방문한다
- 인접하지 않은 노드들은 따로 저장해두고, 가장 처음에 넣은 노드를 꺼내서 탐색한다.
- 만약 끝에 도달했다면 리턴한다.
즉, 가장 처음에 넣은 노드를 꺼내서 구현을 해야한다.
'큐' 를 이용해서 BFS 을 구현해야 한다.
1. 루트 노드를 큐에 넣습니다.
2. 현재 큐의 노드를 빼서 visited 에 추가한다.
3. 현재 방문한 노드와 인접한 노드 중 방문하지 않은 노드를 큐에 추가한다.
4. 2부터 반복한다.
5. 큐가 비면 탐색을 종료한다.
# 위의 그래프를 예시로 삼아서 인접 리스트 방식으로 표현했습니다!
graph = {
1: [2, 3, 4],
2: [1, 5],
3: [1, 6, 7],
4: [1, 8],
5: [2, 9],
6: [3, 10],
7: [3],
8: [4],
9: [5],
10: [6]
}
visited = [] # 방문한 걸 저장하기 위한 배열
queue = [1] # 시작 노드인 1을 넣어둔다.
1. 현재 큐에서 가장 처음에 넣은 1을 빼서, visited 에 추가합니다.
# queue -> [] visited -> [1]
2. 인접한 노드들인 [2, 3, 4] 에서 방문하지 않은 것들인 [2, 3, 4]를 queue 에 추가합니다.
# queue -> [2, 3, 4] visited -> [1]
3. 현재 큐에서 가장 처음에 넣은 2을 빼서, visited 에 추가합니다.
# queue -> [3, 4] visited -> [1, 2]
4. 인접한 노드들인 [1, 5] 에서 방문하지 않은 것들인 [5]를 queue 에 추가합니다.
# queue -> [3, 4, 5] visited -> [1, 2]
5. 현재 큐에서 가장 처음에 넣은 3을 빼서, visited 에 추가합니다.
# queue -> [4, 5] visited -> [1, 2, 3]
6. 인접한 노드들인 [1, 6, 7] 에서 방문하지 않은 것들인 [6, 7]를 queue 에 추가합니다.
# queue -> [4, 5, 6, 7] visited -> [1, 2, 3]
7. 현재 큐에서 가장 처음에 넣은 4을 빼서, visited 에 추가합니다.
# queue -> [5, 6, 7] visited -> [1, 2, 3, 4]
8. 인접한 노드들인 [1, 8] 에서 방문하지 않은 것들인 [8]를 queue 에 추가합니다.
# queue -> [5, 6, 7, 8] visited -> [1, 2, 3, 4]
9. 현재 큐에서 가장 처음에 넣은 5을 빼서, visited 에 추가합니다.
# queue -> [6, 7, 8] visited -> [1, 2, 3, 4, 5]
10. 인접한 노드들인 [2, 9] 에서 방문하지 않은 것들인 [9]를 queue 에 추가합니다.
# queue -> [6, 7, 8, 9] visited -> [1, 2, 3, 4, 5]
11. 현재 큐에서 가장 처음에 넣은 6을 빼서, visited 에 추가합니다.
# queue -> [7, 8, 9] visited -> [1, 2, 3, 4, 5, 6]
12. 인접한 노드들인 [3, 10] 에서 방문하지 않은 것들인 [10]를 queue 에 추가합니다.
# queue -> [7, 8, 9, 10] visited -> [1, 2, 3, 4, 5, 6]
13. 현재 큐에서 가장 처음에 넣은 7을 빼서, visited 에 추가합니다.
# queue -> [8, 9, 10] visited -> [1, 2, 3, 4, 5, 6, 7]
14. 인접한 노드들인 [3] 에서 방문하지 않은 것들이 없으니 추가하지 않습니다.
# queue -> [8, 9, 10] visited -> [1, 2, 3, 4, 5, 6, 7]
15. 현재 큐에서 가장 처음에 넣은 8을 빼서, visited 에 추가합니다.
# queue -> [9, 10] visited -> [1, 2, 3, 4, 5, 6, 7, 8]
16. 인접한 노드들인 [4] 에서 방문하지 않은 것들이 없으니 추가하지 않습니다.
# queue -> [9, 10] visited -> [1, 2, 3, 4, 5, 6, 7, 8]
17. 현재 큐에서 가장 처음에 넣은 9을 빼서, visited 에 추가합니다.
# queue -> [10] visited -> [1, 2, 3, 4, 5, 6, 7, 8, 9]
18. 인접한 노드들인 [5] 에서 방문하지 않은 것들이 없으니 추가하지 않습니다.
# queue -> [10] visited -> [1, 2, 3, 4, 5, 6, 7, 8, 9]
19. 현재 큐에서 가장 처음에 넣은 10을 빼서, visited 에 추가합니다.
# queue -> [] visited -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
20. 인접한 노드들인 [6] 에서 방문하지 않은 것들이 없으니 추가하지 않습니다.
# queue -> [] visited -> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
21. 현재 큐에서 꺼낼 것이 없습니다. BFS 가 끝났습니다.
큐를 사용하기 위해서 deque를 사용한다
from collections import deque
deq = deque()
# Add element to the start
deq.appendleft(10)
# Add element to the end
deq.append(0)
# Pop element from the start
deq.popleft()
# Pop element from the end
deq.pop()
- deque.append(item): item을 데크의 오른쪽 끝에 삽입한다.
- deque.appendleft(item): item을 데크의 왼쪽 끝에 삽입한다.
- deque.pop(): 데크의 오른쪽 끝 엘리먼트를 가져오는 동시에 데크에서 삭제한다.
- deque.popleft(): 데크의 왼쪽 끝 엘리먼트를 가져오는 동시에 데크에서 삭제한다.
- deque.extend(array): 주어진 배열(array)을 순환하면서 데크의 오른쪽에 추가한다.
- deque.extendleft(array): 주어진 배열(array)을 순환하면서 데크의 왼쪽에 추가한다.
- deque.remove(item): item을 데크에서 찾아 삭제한다.
- deque.rotate(num): 데크를 num만큼 회전한다(양수면 오른쪽, 음수면 왼쪽).
# 위의 그래프를 예시로 삼아서 인접 리스트 방식으로 표현했습니다!
from collections import deque
graph = {
1: [2, 3, 4],
2: [1, 5],
3: [1, 6, 7],
4: [1, 8],
5: [2, 9],
6: [3, 10],
7: [3],
8: [4],
9: [5],
10: [6]
}
#1.시작 노드를 큐에 넣는다
#2. 큐에 노드를 빼서 visited에 넣는다
#3. 현재 방문한 노드와 인접한 노드 중 방문하지 않는 노드를 큐에 추가한다
#4. 2~3번의 과정을 큐가 빌 때까지 반복한다.
#queue 를 사용하기 위해서 deque로 함으로써 큐로 사용할 수 있게 한다.
def bfs_queue(adj_graph, start_node):
que = deque([start_node])
visited = []
while que :
current_node = que.popleft()
print("현재 방문한 노드 : " , current_node)
visited.append(current_node)
for i in adj_graph[current_node] :
print("현재 노드에서 인접한 애들을 출력 =" , i)
if i not in visited :
que.append(i)
return visited
print(bfs_queue(graph, 1)) # 1 이 시작노드입니다!
# [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 이 출력되어야 합니다!
현재 방문한 노드 : 1
현재 노드에서 인접한 애들을 출력 = 2
현재 노드에서 인접한 애들을 출력 = 3
현재 노드에서 인접한 애들을 출력 = 4
현재 방문한 노드 : 2
현재 노드에서 인접한 애들을 출력 = 1
현재 노드에서 인접한 애들을 출력 = 5
현재 방문한 노드 : 3
현재 노드에서 인접한 애들을 출력 = 1
현재 노드에서 인접한 애들을 출력 = 6
현재 노드에서 인접한 애들을 출력 = 7
현재 방문한 노드 : 4
현재 노드에서 인접한 애들을 출력 = 1
현재 노드에서 인접한 애들을 출력 = 8
현재 방문한 노드 : 5
현재 노드에서 인접한 애들을 출력 = 2
현재 노드에서 인접한 애들을 출력 = 9
현재 방문한 노드 : 6
현재 노드에서 인접한 애들을 출력 = 3
현재 노드에서 인접한 애들을 출력 = 10
현재 방문한 노드 : 7
현재 노드에서 인접한 애들을 출력 = 3
현재 방문한 노드 : 8
현재 노드에서 인접한 애들을 출력 = 4
현재 방문한 노드 : 9
현재 노드에서 인접한 애들을 출력 = 5
현재 방문한 노드 : 10
현재 노드에서 인접한 애들을 출력 = 6
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
'알고리즘' 카테고리의 다른 글
[알고리즘] Dynamic Programming (0) | 2025.02.24 |
---|---|
[알고리즘] 그래프 (0) | 2025.02.23 |
[알고리즘] 트리, 힙 (0) | 2025.02.23 |
[알고리즘] 점근 표기법 (0) | 2025.02.23 |
[알고리즘] 1주차 시간 복잡도 & 공간복잡도 (0) | 2025.02.22 |