그래프를 탐색하는 방법에는 크게 깊이 우선 탐색(DFS)과 너비 우선 탐색(BFS)이 있다.
📌그래프란, 정점(vertex)과 그 사이를 연결하는 간선(edge)으로 이루어진 자료구조로,
그래프를 탐색한다는 것은 하나의 정점을 기준으로 차례대로 모든 정점들을 한 번씩 방문하는 것을 뜻한다.
끝을 봐야 물러난다
트리구조로 예를 들면,
기준 노드에서 다음 분기(branch)로 넘어가기 전 해당 분기를 완벽하게 탐색하는 방식
미로에서 한 방향으로 탈출로를 탐색하다
길이 막히면 그때 가장 가까운 갈림길로 돌아와
그 갈림길부터 탐색을 진행하는 것이 깊이 우선 탐색 방식이다.
⚠️ 탐색에 있어 '경로'에 대한 정보가 필요할 때 DFS방식으로 탐색한다.
나와 가까운 곳이 먼저야!
트리구조로 예를 들면,
기준 정점에서 인접한 노드를 먼저 탐색하는 방법으로,
시작 정점으로부터 가까운 정점들을 먼저 다 방문한 후 다음 레벨을 순회하는 방식
한 청년이 보물에 대한 소문을 듣고 보물섬을 찾아 나서려 한다.
지도도 없고 바다에 안개가 자욱해 자신이 위치 한 섬에 가장 인접한 섬들만 보인다.
위험을 느낀 청년은 안전하게 인접한 섬들부터 탐색하기 시작한다.
BFS 방식으로 보물섬을 찾아낸 청년은 이곳까지 온 경로는 모르지만,
길이 있다는 것과 자신의 거점과 얼마나 떨어져있는지는 알 수 있다!
⚠️ 두 정점 사이의 최단 경로를 알고자 할 때 BFS방식으로 탐색한다.
DFS 알고리즘에 경우 스텍과 재귀로 접근할 수 있는데,
재귀 함수를 이용하는 것이 가장 보편적이고 짧은 코드를 작성할 수 있다.
BFS 알고리즘은 큐(Queue)로 접근하는데,
방문할 정점을 순서대로 탐색하기때문에 큐가 알맞다.
문제
주어진 인접행렬에서 한 정점에서 다른 정점으로 이어지는 길이 존재하는지
입력
인자 1: matrix
Array 타입을 요소로 갖는 인접 행렬이 담긴 2차원 배열
인자 2: from
Number 타입의 시작 정점
인자 3: to
Number 타입의 도착 정점
출력
boolean 타입을 리턴해야 합니다
입출력 예시const result = getDirections( [ [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], [0, 1, 0, 0], ], 0, 2 ); console.log(result); // true
정점 0에서 2로 가는 길이 존재하는지 확인합니다.
0 --> 1 로 가는 간선이 존재하고, 1 --> 2 로 가는 간선이 존재하기 때문에 true를 반환합니다.
길의 경로를 묻는 것이 아닌, 그저 길이 존재하는지의 유무를 반환하는 것으로
bfs가 적합하다.
//bfs method -> 현 정점과 직접 이어진 모든 정점을 돌고 그 다음 레벨로 감
function getDirections(matrix, from, to) {
// 큐 구현 틀 만들기
let Q = []
let enQ = (vertex) => Q.push(vertex)
let deQ = () => Q.shift()
let isVisited = new Array(matrix.length).fill(false)
// 방문 여부를 boolean으로 명시하기 위한 배열 기본 설정.
enQ(from); // 큐에 시작 정점을 넣어주고
isVisited[from] = true; // 방문 여부 수정
while (Q.length) { // 방문 예정인 정점 목록들이 빌 때까지 반복
let now = deQ() // 현재 정점은 큐의 첫 번째 정점
if (now === to) return true // 현재 정점이 목표지라면 트루!
for (let next = 0; next < matrix[now].length; next++) {
// 목표지가 아니라면 현 정점과 연결된 다른 정점을 물색한다.
if (!isVisited[next] && matrix[now][next]) {
enQ(next)
isVisited[next] = true;
} // 현재 정점과 간선으로 이어져있고 방문한 적 없는 정점이라면
} // 방문 예정 목록인 큐에 넣고 방문 여부를 수정한다.
}
return false;
}
조금 더 간결하게 작성한 버전
function getDirections(matrix, from, to) {
let Q = [from] // 큐는 방문할 예정인 정점들의 목록.
let isVisited = [from] // 중복 방문을 피하기 위해 방문한 정점을 명시한다.
while (Q.length) {
let now = Q.shift()
if (now === to) return true
// 현 정점이 목표지점이면 도착한 것이다. 길이 있다!
for (let next = 0; next < matrix[now].length; next++) {
if (matrix[now][next] === 1 && !isVisited.includes(next)) {
Q.push(next)
// 간선이 있는데 방문하지 않은 곳이라면! 방문예정 목록에 넣어준다.
isVisited.push(next); // 그리고 방문했던 정점 목록에 추가한다.
}
}
}
return false;
}
// dfs 방식 -> 많은 간선 중에 하나에만 올인
function getDirections(matrix, from, to,
isvisited = new Array(matrix.length + 1).fill(false)) {
// 방문 여부 배열을 미리 파라미터로 주자.
const now = from // 현재 정점
isvisited[now] = true; // 현재 정점 방문도장
if (now === to) return true; // base case; 현재가 목적지라면 트루!
// 현재 정점의 간선을 찾아라!
for (let next = 0; next < matrix[now].length; next++) {
if (!isvisited[next] && matrix[now][next]) {
// 현재와 이어져있고 내가 가보지 않았다면
let recursion = getDirections(matrix, next, to, isvisited)
// 재귀로 동일한 일 반복.
if (recursion) return true;
// 자식노드를 파고들다 목적지에 도착했다면
// 다른 길은 보지도 말고 트루로 돌아오라.
}
}
return false;
}
중복 방문 체크는 그냥 정점 자체를 넣어주고
includes 메소드로 확인하는 것이 더 깔끔하다.
function getDirections(matrix, from, to, isVisited = []) {
let now = from
isVisited.push(now)
if (now === to) return true;
for (let next = 0; next < matrix[now].length; next++) {
if (matrix[now][next] && !isVisited.includes(next)) {
let recursion = getDirections(matrix, next, to, isVisited)
if (recursion) return true;
}
}
return false;
}