(BFS에 대한 기본적인 지식이 있다고 가정하고 작성하였습니다. BFS에 대한 학습이 필요하면 여기를 참고해주세요)

 

BFS는 그래프 자료구조에서 유용하게 쓰이는 알고리즘이다.

백준 2178번에서 Time Out Error가 발생하여 이를 최적화한 과정을 기록하여 공유하고자 한다.

 

문제

 

전형적인 BFS 문제이며 로직은 다음과 같다.

  1. 시작점을 `queue`에 넣는다. ([row, col, 지금까지 거리] 형태로 넣어, 최소거리를 추적)
  2. `queue`의 길이가 0이 될때까지 다음 행동을 반복한다.
    • 해당 지점으로부터 4방향중 이동가능한 방향을 찾는다.
    • 이동가능한 방향을 큐에 넣는다.
  3. 이때, `queue`에서 꺼낸 위치가 최종 위치이면 최소값인지 판단하여 결과를 최신화한다.
// 최초 코드 (Time out Error)
const fs = require("fs");
const input = fs
  .readFileSync(process.platform === "linux" ? "/dev/stdin" : "../input.txt")
  .toString()
  .trim()
  .split("\\n");

const [N, M] = input.shift().split(" ").map(Number);
const arr = input.map((subarr) => subarr.trim().split(""));
// # 방향 변수
const dr = [0, 1, 0, -1];
const dc = [1, 0, -1, 0];

const queue = [[0, 0, 1]];
let visited = Array.from(new Array(N), () => new Array(M).fill(false));
visited[0][0] = true;
let min = -1;

while (queue.length) {
  const [row, col, cnt] = queue.shift();
  visited[nrow][ncol] = true;
  // # 도착 지점 도달
  if (row === N - 1 && col === M - 1) {
    min = min === -1 ? cnt : Math.min(min, cnt);
  }
  // # 도착 지점 이동중
  else {
    let i = 0;
    for (i; i < 4; i++) {
      const [nrow, ncol] = [row + dr[i], col + dc[i]];
      if (
        nrow >= 0 &&
        nrow < N &&
        ncol >= 0 &&
        ncol < M &&
        arr[nrow][ncol] === "1"
      ) {
        queue.push([nrow, ncol, cnt + 1]);
      }
    }
  }
}
console.log(min); // 결과 출력

Time out Error가 발생하였다.

로직에는 전혀 문제가 없어 보였고, 최적화가 필요한 것 같았다.

시스템적 최적화 외 실행 횟수를 줄이기 위한 최적화를 하기 위해서는 `queue`에 들어가는 좌표의 개수를 줄이는 방법 밖에 없다.

 

그래서, `visited`에 집중하기 시작했다.

`visited`는 지금까지 방문한 노드와 방문하지 않은 노드를 구분하기 위해 사용하였다.

실제, BFS에서는 방문한 노드를 다시 방문하지 않기 때문이다. (그림 참고)

출처: https://cdragon.tistory.com/entry/Algorithm-너비-우선-탐색BFS그래프-탐색

 

해당 문제는 모든 경로를 고려해야 한다고 판단하여, `visited[row][col] = true` 를 실제 방문했을 때 넣어주었던 것이다. 하지만, BFS의 개념에 대한 이해가 있었다면 이렇게 설정할 이유는 없다.

BFS는 DFS와 달리 같은 LEVEL의 노드들을 먼저 탐색한다.

즉, BFS를 진행하다가 어떤 좌표에 도달했다면, 해당 경로는 최소 경로가 된다.
따라서, 처음 최종 경로에 도달하는 경우가 최소 경로이다.

 

 

예를 들어,

출발 2 2 2
3 0 0 2
3 0 0 2
3 0 3 도착
3 3 3 0

이렇게 구성되어 있는 지도가 있다고 가정하자. (전체 좌표의 부분만을 예시로 설명합니다)

  1. 도착 위치까지 2로 이루어진 경로와 3로 이루어진 경로 2가지 경로가 존재한다.
  2. 2경로의 거리가 3경로의 거리보다 짧으며, BFS 로직 상 먼저 실행된다.
    참고로, BFS의 Queue에 좌표를 추가하는 행위는 "해당 좌표로부터 최소 거리를 찾겠습니다.” 를 의미한다.
  3. 결국, 나중에 도달한 방향(3경로)은 이미 최소 경로가 아니므로 고려할 필요가 없다.

설명이 길었습니다.

요약하자면

  1. `visited[row][col] = true`는 해당 위치가 valid한 위치라고 판단되었을때 바꿔주면 최적화가 가능합니다.
  2. 최종 위치에 도달하면 `break` 합니다.
const fs = require("fs");
const input = fs
  .readFileSync(process.platform === "linux" ? "/dev/stdin" : "../input.txt")
  .toString()
  .trim()
  .split("\n");

const [N, M] = input.shift().split(" ").map(Number);
const arr = input.map((subarr) => subarr.trim().split(""));
// # 방향 변수
const dr = [0, 1, 0, -1];
const dc = [1, 0, -1, 0];

const queue = [[0, 0, 1]];
let visited = Array.from(new Array(N), () => new Array(M).fill(false));
visited[0][0] = true;
let min = -1;

while (queue.length) {
  const [row, col, cnt] = queue.shift();

  // # 도착 지점 도달
  if (row === N - 1 && col === M - 1) {
    min = cnt; 
	break; // 한가지 밖에 없습니다.
  }
  // # 도착 지점 이동중
  else {
    let i = 0;
    for (i; i < 4; i++) {
      const [nrow, ncol] = [row + dr[i], col + dc[i]];
      if (
        nrow >= 0 &&
        nrow < N &&
        ncol >= 0 &&
        ncol < M &&
        arr[nrow][ncol] === "1" &&
        !visited[nrow][ncol]
      ) {
        queue.push([nrow, ncol, cnt + 1]);
        visited[nrow][ncol] = true;
      }
    }
  }
}
console.log(min);

추가

만약에, 모든 경로를 구해야 하는 경우라면 처음에 했던 코드대로 진행해야 합니다.

참고 문헌

https://cdragon.tistory.com/entry/Algorithm-너비-우선-탐색BFS그래프-탐색

'PS > BFS+DFS' 카테고리의 다른 글

[PS] 파이썬 BFS / DFS 구현방법  (2) 2024.04.12

🔎DFS (Depth First Search)

깊이 우선 탐색으로, 스택을 사용해 구현한다.

구현 방법

  1. graph를 배열의 형태로 완성한다.
    → 각 index에 해당 index번째 노드와 연결된 간선의 번호를 적는다.
    cf) 양방향인 경우, 양쪽에 적어야한다.
  2. 탐색 시작 노드를 스택에 삽입하고 방문처리한다.
  3. 스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면 해당 노드를 스택에 넣고 방문 처리한다.
    → 방문하지 않은 인접 노드가 없으면 다시 스택에서 최상단 노드를 꺼낸다.
  4. 2번이 불가능 할때까지 반복한다.
# 방문 정보를 리스트 자료형으로 표현
# 노드는 9개지만, index 0번 노드는 사용하지 않는다.
visited =[False] * 10

# 각 노드가 연결된  정보를 리스트 자료형으로 표현 (2차원 리스트)
graph = [
	[],                 # 빈 자리
  [3, 4, 5],          # 1번 노드와 연결된 노드들
  [2, 3, 8],          # 2번 노드와 연결된 노드들
  [1, 7],             # 3번 노드와 연결된 노드들
  [1, 4, 5],          # 4번 노드와 연결된 노드들
  [3, 5],             # 5번 노드와 연결된 노드들
  [3, 4],             # 6번 노드와 연결된 노드들
  [7],                # 7번 노드와 연결된 노드들
  [2, 6, 8],          # 8번 노드와 연결된 노드들
  [1, 7]              # 9번 노드와 연결된 노드들
]

def DFS(graph, visited, start):
  # 현재 노드를 방문 처리
  visited[start] = True
  print(start, end = ' ')

  for i in graph[start]:
    if not visited[i]:
      DFS(graph, visited, i) // 재귀함수!!

# 현재 노드와 연결된 다른 노드를 재귀적으로 방문
DFS(graph, visited, 1)

🔎BFS (Breadth First Search)

너비 우선 탐색으로, deque를 사용하여 구현

 

구현 방법

  1. graph를 배열의 형태로 완성한다.cf) 양방향인 경우, 양쪽에 적어야한다.
    → 각 index에 해당 index번째 노드와 연결된 간선의 번호를 적는다.
  2. 탐색 시작 노드를 큐에 삽입하고 방문 처리한다.
  3. 큐에서 노드를 꺼내 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 큐에 삽입하고 방문 처리한다.
  4. 2번이 불가능 할때까지 반복한다.
from collections import deque

# BFS
def BFS(graph, visited, start):
  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

# 노드는 9개지만, index 0번 노드는 사용하지 않는다.
visited =[False] * 10

# 각 노드가 연결된  정보를 리스트 자료형으로 표현 (2차원 리스트)
graph = [
  [],                 # 빈 자리
  [],                 # 1번 노드와 연결된 노드들
  [2, 3, 8],          # 2번 노드와 연결된 노드들
  [1, 7],             # 3번 노드와 연결된 노드들
  [1, 4, 5],          # 4번 노드와 연결된 노드들
  [3, 5],             # 5번 노드와 연결된 노드들
  [3, 4],             # 6번 노드와 연결된 노드들
  [7],                # 7번 노드와 연결된 노드들
  [2, 6, 8],          # 8번 노드와 연결된 노드들
  [1, 7]              # 9번 노드와 연결된 노드들
]

BFS(graph, visited, 1)

✏️팁

  • 둘다 O(NlogN)의 시간 복잡도를 지닌다.
  • True / False를 적극 활용하기
visited =[False] * (N+1)
  • ⭐노드가 1부터 시작하는 경우, graph와 visited도 1부터 시작하기 → 헷갈림 방지
  • DFS의 재귀가 이미 visited를 모두 방문했음에도 실행하고 있을 때를 대비하여, 해 호출마다 check_end 함수를 넣을 수 있음.
  • 일반적으로, BFS가 DFS보다 빠름.

+ Recent posts