18~20강 :: DFS 알고리즘, BFS 알고리즘
👉🏻 DFS(Depth-First Search)
- DFS는 깊이 우선 탐색이라고도 부르며 그래프에서 깊은 부분을 우선적으로 탐색하는 알고리즘이다
- DFS는 스택 자료구조(혹은 재귀함수)를 이용한다
- 구체적 동작 과정
- 탐색 시작 노드를 스택에 삽입하고 방문 처리를 한다
- 스택의 최상단 노드에 방문하지 않은 인접한 노드가 하나라도 있으면 그 노드를 스택에 넣고 방문처리한다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼낸다
- 더이상 2번의 과정을 수행할 수 없을 때까지 반복한다
- 매번 최 상단 원소를 기준으로 해서 방문하지 않은 인접 노드가 있으면 그 인접한 노드로 방문을 수행
👉🏻 DFS 동작 예시
- 방향이 없는 무방향 그래프
- 인접한 노드가 여러개일 수 있다
- 어떤 노드부터 방문할지에 대한 기준이 있어야 한다
- 방문 기준은 문제에서 요구하는 내용에 따라 달라지거나 상관 없기도 하다
- 매번 스택의 최상단 노드 확인
- 인접한 노드 중에서 방문하지 않은 것에 방문
- 인접&방문X 노드가 여러개라면 조건에 맞게 방문
- 이제 2를 기준으로 시작
- 1번과 7번이 있는데
- 1번은 이미 방문했고 7번은 방문한 적이 없기 때문에 7번으로
- 인접한 6번과 8번중 기준에 맞는 6번에 방문
- 이처럼 가장 깊은 부분부터 탐색
- 가장 깊게 들어갔다가 더 들어갈 수 없다면 다시 돌아와서 다른 방향으로 깊게 들어가는 방식
👉🏻 DFS 예제
- 파이썬에서는 그래프를 표현하기 위해 2차원 리스트를 사용할 수 있다
- 일반적으로 그래프 문제가 출제되면 노드 번호가 1번부터 시작하는 경우가 많기 때문에 index 0에 대한 내용은 비워둔다
- index 1번부터 해당 노드의 인접한 노드가 무엇인지 리스트 형태로 담아줄 수 있다
- 1번 노드와 연결돼있는 건 2, 3, 8번이라는 뜻
- 2번 노드와 연결돼있는 건 1, 7번이라는 뜻
- 방문처리를 위해 1차원 리스트를 만듦
- 모든 값을 False로 초기화 → 모든 노드를 하나도 방문하지 않은 것으로 처리
- 1번 노드 ~ 8번노드를 가지고 있기 때문에 index0번은 사용하지 않기 위해서 일부러 하나 더 큰 크기로 만든 것
- 코드 상에서 각 노드에서 -1을 적용해 처리할 수 도 있지만
0은 사용하지 않는 방식으로 하는 게 번호를 그대로 맵핑해 직관적이다
- dfs 함수는 그래프 정보, 방문 처리 여부가 기록된 리스트를 이용
- 해당 노드를 방문처리
- 그 노드를 방문했다는 의미로 노드 번호 출력
- stack의 최상단에 있는 원소와 연결된 다른 노드를 하나씩 확인하며 인접한 노드가 방문되지 않은 상태라면 재귀함수를 이용해 방문 진행
- 재귀적으로 방문하지 않은 노드를 방문함
- 깊이 우선으로 최대한 깊게 그래프를 탐색
* C++ ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/8.cpp
* Java ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/8.java
👉🏻 BFS (Breadth-First-Search)
- BFS는 너비 우선 탐색이라고도 부르며, 그래프에서 가까운 노드부터 우선적으로 탐색하는 알고리즘이다
- BFS는 큐 자료구조를 이용
- 구체적 동작 과정
- 탐색 시작 노드를 큐에 삽입하고 방문처리를 한다
- 큐에서 노드를 꺼낸 뒤에 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 큐에 삽입하고 방문처리한다
- 더이상 2번의 과정을 수행할 수 없을 때 까지 반복한다
- DFS수행시에서는 인접하지 않은 노드에 대해서? 다시 한번씩 큐에 넣으면서 방문한다는 특징
(인접한 노드 중 방문하지 않은 노드에 대해서가 맞는 설명이겠지..?)
- BFS는 해당 시점에서 인접한 노드를 한 번에 전부 큐에 넣는다는 점이 다르다
- DFS와 마찬가지로 다양한 대기업 문제에서 출제되고 있다
- 특정 조건에서의 최단경로 문제 해결 위한 목적으로 효과적으로 사용
- 큐 자료구조를 사용하기 때문에 언어마다 큐 자료구조를 어떻게 구현하는지 알아야해
- 방향성 없는 무방향 그래프
- 특정 노드에 대해서 인접한 노드가 여러개
- 1번 노드 방문처리
- 큐에 넣음 (위에서 들어와서 아래로 나간다)
- 큐에 들어 있던 원소인 1을 꺼내서 1에 대해 처리
- 현재 처리 중인 노드는 하늘색
- 방문처리 수행한 노드는 회색
- 2, 3, 8을 차례대로 큐에 넣고 방문처리
- 작은 번호부터 넣음
- 가장 밑에 있는 2를 꺼내 2에 대해 처리
- 인접한 노드 1과 7이 있는데 1은 이미 방문처리 되었기 때문에 7을 큐에 넣어준다
- 다음으로 3을 꺼내 3에대해 처리
- 3번 노드와 인접한 노드이 1, 4, 5 중에
1은 방문처리 되었기 때문에 4, 5를 순서대로 큐에 넣어 방문처리한다
- 8번 노드를 꺼내 처리함
- 8번 노드의 인접 노드는 1, 7이 있으나 이미 모두 방문처리 되었기 때문에 8만 꺼내고 무시한다
- 거리가 가까운 것 부터 탐색됨
- 1번의 경우 2, 3, 8이 거리가 1이기 때문에 시작 노드와 가까운 것들 부터 처리된 것을 볼 수 있다.
- 거리가 가장 먼 6번 노드가 가장 마지막에 처리
- 이러한 특징 때문에 각 간선의 비용이 모두 동일한 상황에서 최단거리 문제를 해결하기 위한 목적으로 활용되기도 함
👉🏻 BFS 예제
- 큐를 위해 덱 라이브러리 불러오기
- 그래프에서 0번 노드에 대한 내용은 비워 1~8에 대해서만 처리하도록 한다
- 각 노드와 연결된 인접 노드 표현
- 방문 처리를 위해 visited 리스트를 만든다
- bfs 함수는
- 실행 노드를 큐에 넣고, 해당 노드를 방문처리
- 큐가 빌 때까지 실행
- 큐에서 하나의 원소를 뽑는다. 이때는 먼저 들어온 원소를 꺼낸다
- 해당 노드의 번호를 출력하고
- 해당 노드의 인접 노드 중 아직 방문하지 않은 원소를 큐에 모두 삽입해 방문 처리를 수행한다
* C++ ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/9.cpp
* Java ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/11.java
문제1️⃣. 음료수 얼려 먹기
🧩 문제 설명
- 연결 요소(connected component)찾기
🧩 문제 조건
- 전체 얼음틀의 공간은 100만개 이하
💡 스스로 풀어보기
DFS를 활용할 방법을 생각했지만 코드로 구현하지는 못함😔
🧩 문제 해결 아이디어
- 그래프 형태로 모델링
- 각 위치에서 상하좌우로 연결된 위치들은 서로 인접한 노드로 표현할 수 있다
- 이 때 특정 지점에서 DFS / BFS를 수행해 이동 가능한 모든 경로에 대해서 모두 방문처리를 함
- DFS를 이용해 모든 위치에 대해서 현재 방문한 위치와 연결된 인접한 노드에 대해서도 반복적으로 DFS를 호출해 연결된 모든 노드들을 방문
💡 예시 답안
- n과 m 그리고 이차원 리스트의 맵 정보를 입력받는다
- 그래프는 공백 없는 0과 1로 이루어진 문자열로 주어지기 때문에
한 줄을 입력 받고 각 원소를 정수로 바꿔 리스트로 만든다
- n × m 크기의 각 위치에서 dfs를 수행한다
- 현재 위치에서 DFS를 수행해 방문처리가 되었다면 카운트를 진행한다
- dfs 함수에서
- 주어진 범위를 넘어갈 경우 종료되게 한다 (0~n-1 ,0~m-1)
- 범위를 넘지 않았고, 현재 노드를 아직 방문하지 않았다면, 방문처리를 한다 (1로 바꿔줌)
- 결과적으로 True 값을 반환
- 현재 위치에 대해서 처음 dfs가 수행된 것이기 때문에 그 위치에 대해서 result 값을 증가
- 연결된 모든 노드에 대해서 방문처리를 진행할 수 있도록 하기 위해서
상하좌우 모든 위치에 대해 재귀적으로 dfs 함수를 호출
- 여기서 호출되는 내용들은 리턴값을 사용하지 않기 때문에
단순히 연결된 모든 노드에 대해 방문처리를 수행하기 위한 목적으로 사용된다
- dfs는 한 번 수행되면 해당 노드와 연결된 모든 노드들도 방문처리 할 수 있도록 하고
그 시작점 노드가 방문처리됐다면 (= 처음 방문하는 것이라면) 그때만 result 값을 증가시킴
- def dfs(x, y):: DFS 함수를 정의합니다. 시작 노드의 좌표를 매개변수로 받습니다.
- if x <= -1 or x >= n or y <= -1 or y >= m:: 주어진 범위를 벗어나는 경우 함수를 즉시 종료합니다.
- 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: 현재 노드에서 DFS를 시작하여 연결된 모든 노드를 방문했음을 의미하는 True를 반환합니다.
- return False: 현재 노드가 이미 방문된 경우에는 False를 반환합니다.
- n, m = map(int, input().split()): N과 M을 공백을 기준으로 입력받습니다.
- graph = []: 빈 리스트 graph를 생성합니다.
- for i in range(n):: N번 반복하며,
- graph.append(list(map(int, input()))): 각 행에 대한 0과 1로 이루어진 리스트를 입력받아 graph에 추가합니다.
- result = 0: 결과값을 저장할 변수 result를 초기화합니다.
- for i in range(n):: N번 반복하며,
- for j in range(m):: M번 반복하며,
- if dfs(i, j) == True:: 현재 위치에서 DFS를 수행하고, 반환값이 True인 경우 (연결된 모든 노드를 방문한 경우),
- result += 1: 결과값을 1 증가시킵니다.
- print(result): 최종 결과값을 출력합니다.
- 사용자에게 N과 M 값을 입력받습니다. 이때, N은 행의 개수, M은 열의 개수를 나타냅니다.
- 빈 리스트 graph를 생성합니다. 이 리스트는 2차원 배열의 형태로 입력된 데이터를 저장할 공간입니다.
- for i in range(n):: N번 반복하면서 행에 대한 정보를 입력받습니다.
a. graph.append(list(map(int, input()))): 각 행에 대한 0과 1로 이루어진 리스트를 입력받아 graph에 추가합니다. 이때, map(int, input())는 사용자로부터 입력받은 문자열을 각 문자를 정수로 변환하여 리스트로 만드는 역할을 합니다. - 결과값을 저장할 변수 result를 초기화합니다.
- for i in range(n):: N번 반복하면서,
a. for j in range(m):: M번 반복하면서,
i. `if dfs(i, j) == True:`: 현재 위치에서 DFS를 수행하고, 반환값이 True인 경우 (연결된 모든 노드를 방문한 경우),
ii. `result += 1`: 결과값을 1 증가시킵니다.
6. print(result): 최종 결과값을 출력합니다.
이제 각 단계를 정리하면 사용자가 입력한 N과 M에 따라 2차원 배열의 데이터를 입력받고, 그래프의 각 노드를 DFS를 통해 방문하여 연결된 모든 노드를 찾습니다. 이후에 찾은 영역의 개수를 최종적으로 출력합니다.
* C ++ ver
* Java ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/10.java
문제2️⃣. 미로 탈출
🧩 문제 설명
🧩 문제 조건
💡 스스로 풀어보기
모르겠다..
🧩 문제 해결 아이디어
- BFS 문제는 간선의 비용이 모두 같을 때 최단 거리를 탐색하기 위해 사용할 수 있는 알고리즘
- 첫 번째 위치인 (1,1)에서 BFS를 수행한다
- 인접한 노드 중에서 1인 노드로만 이동
- 큐에 원소를 넣어 방문처리할 때 원소의 값이 1인 원소에 한해서만 BFS를 진행한다
- 이번에 방문한 노드까지의 거리를 2로 기록
- 이 문제에서는 시작 위치와 마지막 위치를 포함해서 마지막 위치까지 도달하는 최단 경로에 포함된 노드의 수를 출력
- 현재 노드도 큐에 담기게 되고
해당 노드를 꺼내 상하좌우를 확인하고 인접한 1을 가진 노드까지 방문을 수행
- 다음 노드를 큐에 넣을 때 방문 처리를 수행하고 이 노드까지는 거리가 3으로 기록
- 매번 새로운 지점을 방문할 때 이전 지점까지의 최단 거리 +1 하는 방식
💡 예시 답안
- 입력받기
- 네 가지 방향 벡터를 정의한다
- bfs 함수를 호출
- deque를 이용해 큐를 구현
- 초기에 x와 y로 이루어진 튜플 데이터를 담는다
- 큐가 빌 때 까지 bfs를 수행한다
- 반복시마다 큐에 있는 원소 하나를 꺼내 현재 위치에서 네 가지 방향으로의 위치를 확인
- 연결된 위치가 공간을 벗어난 경우와 괴물이 있는 경우는 무시한다
- 해당 노드를 처음 방문할 때만 최단 거리를 기록할 수 있도록 한다
- 직전 노드 위치에서의 최단거리 +1 값을 넣는다
- 다음 이동 위치까지는 1만큼 거리가 더 먼 곳이기 때문에 큐에 데이터를 넣어 거리 값을 1 증가
- 결과적으로 bfs 수행한 뒤 가장 오른쪽 아래까지의 최단거리를 반환
- def bfs(x, y):: BFS 함수를 정의합니다. 시작 노드의 좌표를 매개변수로 받습니다.
- queue = deque(): BFS에서 사용할 큐를 선언합니다. deque는 양쪽에서 빠르게 삽입, 삭제할 수 있는 큐 구조를 제공합니다.
- queue.append((x, y)): 시작 노드의 좌표를 큐에 추가합니다.
- while queue:: 큐가 빌 때까지 반복합니다.
a. x, y = queue.popleft(): 큐에서 좌표를 하나 꺼내옵니다.
b. for i in range(4):: 상, 하, 좌, 우 네 방향에 대해서 확인합니다.
i. `nx = x + dx[i]`: 다음 위치의 x 좌표를 계산합니다.
ii. `ny = y + dy[i]`: 다음 위치의 y 좌표를 계산합니다.
iii. `if nx < 0 or nx >= n or ny < 0 or ny >= m: continue`: 다음 위치가 미로 찾기 공간을 벗어난 경우 무시합니다.
iv. `if graph[nx][ny] == 0: continue`: 다음 위치가 벽인 경우 무시합니다.
v. `if graph[nx][ny] == 1:`: 다음 위치를 처음 방문하는 경우에만 최단 거리를 기록하고 큐에 추가합니다.
- `graph[nx][ny] = graph[x][y] + 1`: 다음 위치의 최단 거리를 현재 위치의 거리 + 1로 갱신합니다.
- `queue.append((nx, ny))`: 다음 위치의 좌표를 큐에 추가합니다.
- return graph[n - 1][m - 1]: 가장 오른쪽 아래까지의 최단 거리를 반환합니다.
- from collections import deque: 덱(Deque)을 사용하기 위해 라이브러리를 불러옵니다.
- n, m = map(int, input().split()): N과 M을 공백을 기준으로 입력받습니다.
- graph = []: 빈 리스트 graph를 생성합니다.
- for i in range(n):: N번 반복하며,
a. graph.append(list(map(int, input()))): 각 행에 대한 0과 1로 이루어진 리스트를 입력받아 graph에 추가합니다. - dx = [-1, 1, 0, 0]과 dy = [0, 0, -1, 1]: 이동할 네 가지 방향을 정의합니다. 위, 아래, 왼쪽, 오른쪽 순서입니다.
- print(bfs(0, 0)): BFS 함수에 출발점 (0, 0)을 넣고 실행한 결과를 출력합니다.
- 사용자로부터 N과 M 값을 입력받습니다. 이때, N은 행의 개수, M은 열의 개수를 나타냅니다.
- 빈 리스트 graph를 생성합니다. 이 리스트는 2차원 배열의 형태로 입력된 데이터를 저장할 공간입니다.
- for i in range(n):: N번 반복하면서 행에 대한 정보를 입력받습니다.
a. graph.append(list(map(int, input()))): 각 행에 대한 0과 1로 이루어진 리스트를 입력받아 graph에 추가합니다. - 이동할 네 가지 방향을 정의합니다. dx = [-1, 1, 0, 0]과 dy = [0, 0, -1, 1]는 상, 하, 좌, 우 방향으로의 이동을 나타냅니다.
- def bfs(x, y):에서 BFS 함수를 정의합니다. 시작 노드의 좌표를 매개변수로 받습니다.
- 큐를 선언하고 시작 노드의 좌표를 큐에 추가합니다.
- 큐가 빌 때까지 반복합니다..
a. 큐에서 좌표를 하나 꺼내옵니다.
b. 상, 하, 좌, 우 네 방향에 대해서 확인합니다
i. 다음 위치가 미로의 범위를 벗어나면 무시합니다.
ii. 다음 위치가 벽이면 무시합니다.
iii. 다음 위치가 아직 방문하지 않은 위치인 경우,
- 최단 거리를 갱신하고 큐에 다음 위치를 추가합니다.
- BFS 함수가 끝나면, 가장 오른쪽 아래까지의 최단 거리를 반환합니다.
- BFS 함수에 출발점 (0, 0)을 넣고 실행한 결과를 출력합니다.
이렇게 코드는 네 가지 방향으로 이동하면서 최단 거리를 계산하는데, 큐에는 현재 위치에서 갈 수 있는 모든 이동 가능한 위치가 차례로 추가되어 BFS가 진행됩니다. 이런 방식으로 BFS는 출발점에서 목적지까지의 최단 거리를 탐색합니다.
* C++ ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/11.cpp
* Java ver
https://github.com/ndb796/python-for-coding-test/blob/master/5/11.java
출처: https://youtu.be/1vLqC1rItM8?si=duiCuBUMP06oRoLH
https://youtu.be/CJiF-muKz30?si=J3e7cF3Sv2dum_Oc
https://youtu.be/e7_H8SLZlHY?si=BQXmAwwFApenKvGI