
BFS는 breadth-first-search의 줄임말로, 그래프에서 너비를 우선으로 탐색하는 알고리즘이다.
DFS는 한 경로를 끝까지 파고 들어가지만, BFS는 현재 위치에서 가까운 요소들을 우선적으로 탐색한다.
만약 위의 그래프를 BFS로 탐색한다면 2 -> 7 -> 5 -> 2 -> 6 -> 9 -> 5 -> 11 -> 4의 순서로 탐색하게 될 것이다.
BFS는 항상 가까운 요소부터 탐색하기 때문에 최단 경로를 구할 때 많이 쓰인다.
모든 요소를 다 탐색하지 않고도 가장 가까운 거리/횟수로 특정 요소를 찾아야 할 때 유용하다.
BFS는 큐를 이용해 구현할 수 있다.
큐는 가장 먼저 들어온 요소가 가장 먼저 탐색되는(FIFO) 자료구조이기 때문에, 가까운 요소를 먼저 탐색해야 하는 BFS의 성질과 동일하다.
shift()로 맨 앞의 요소를 꺼내 탐색하고, 연결된 요소는 push()로 맨 뒤에 추가한다.
또한, DFS와 동일하게 방문 여부를 체크하여 이미 방문한 곳은 재방문 하지 않도록 한다.
function bfs(start) {
const queue = [];
queue.push(start);
visited[start] = true;
while (queue.length) {
const cur = queue.shift();
for (const next of graph[cur]) {
if (!visited[next]) {
visited[next] = true;
queue.push(next);
}
}
}
}
BFS로 보드를 전부 순환하며 벽 개수를 세서 마지막 위치에 도달했을 때 가장 적은 벽 개수를 반환하도록 하려고 했는데 일반 BFS로 풀면 시간 초과가 발생했다.
이 문제를 일반 BFS로 풀면 안 되는 이유는 이동하는 비용이 다르기 때문이다.
빈 방은 비용이 0이지만 벽은 비용이 1이다.
일반 BFS는 모든 비용이 동일할 때만 최단 거리를 보장한다. (최단 경로 도착 !== 최소 벽 개수)
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const [M, N] = input.shift().split(' ').map(Number);
const board = input.map(i => i.split('').map(Number));
const visited = Array.from({length: N+1}, () => Array(M+1).fill(false));
const move = [[0,1], [0,-1], [-1,0], [1,0]];
let answer = M * N;
const queue = [];
queue.push([0,0,0]);
while(queue.length){
const [x,y,count] = queue.shift();
visited[x][y] = true;
if(x === N - 1 && y === M - 1 && count < answer){
answer = count;
}
for(let [dx, dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx >= 0 && nx < N && ny >= 0 && ny < M && !visited[nx][ny]){
queue.push([nx, ny, count+Number(board[nx][ny])]);
}
}
}
console.log(answer);
이런 경우에는 비용이 적은 것을 먼저 처리하고, 비용이 많은 것을 나중에 처리하면 가장 적은 비용으로 도달하는 경우를 빠르게 구할 수 있다.
이를 위해 덱을 이용한다. (비용이 0인 경우 앞에 넣고, 1인 경우 뒤에 넣기 -> 맨 앞부터 꺼내서 처리)
또한, 단순히 좌표의 방문 여부를 처리하는 것이 아닌 좌표별 최소 비용을 저장하도록 한다.
이때 최소 비용을 보장하기 위해 예정되는 비용(dist[x][y] + board[nx][ny])이 원래 비용(dist[nx][ny])더 낮을 경우에만 deque에 값을 추가한다.
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const [M, N] = input.shift().split(' ').map(Number);
const board = input.map(i => i.split('').map(Number));
const dist = Array.from({ length: N }, () => Array(M).fill(Infinity));
const move = [[0,1], [0,-1], [-1,0], [1,0]];
const deque = [];
deque.push([0,0]);
dist[0][0] = 0;
while(deque.length){
const [x,y] = deque.shift();
for(let [dx, dy] of move){
const nx = x+dx;
const ny = y+dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
const cost = dist[x][y] + board[nx][ny];
if(cost < dist[nx][ny]){
dist[nx][ny] = cost;
if(board[nx][ny] === 0){
deque.unshift([nx,ny])
}else{
deque.push([nx,ny])
}
}
}
}
console.log(dist[N-1][M-1]);
위의 문제랑 벽/빈 방 숫자만 반대일뿐 동일하므로 빈 방을 0, 벽을 1로 반전시켜서 최소 벽 개수를 구한다.
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const N = +input.shift();
const board = input.map(i => i.split('').map(n => 1 - +n));
const move = [[0,1], [0,-1], [-1,0], [1,0]];
const dist = Array.from({length: N}, () => Array(N).fill(Infinity));
const deque = [];
deque.push([0,0]);
dist[0][0] = 0;
while(deque.length){
const [x,y] = deque.shift();
for(let [dx,dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx < 0 || nx >= N || ny < 0 || ny >= N) continue;
const cost = dist[x][y] + board[nx][ny];
if(cost < dist[nx][ny]){
dist[nx][ny] = cost
if(board[nx][ny] === 0){
deque.unshift([nx,ny])
}else{
deque.push([nx,ny])
}
}
}
}
console.log(dist[N-1][N-1]);
즉, 0은 이동 비용이 0이고 1은 이동 비용이 1이다.
이동 비용이 동일하지 않기 때문에 위의 문제와 마찬가지로 일반 BFS로 풀면 안 되고, 각 좌표별 거리를 저장해야 한다.
0인 부분을 먼저 탐색하기 위해 다음 값이 0일 경우 앞에 넣고, 1일 경우 뒤에 넣는다. (덱 필요)
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const [N,M] = input[0].split(' ').map(Number);
const [X1,Y1,X2,Y2] = input[1].split(' ').map(Number);
const move = [[0,1], [0,-1], [-1,0], [1,0]];
const dist = Array.from({length: N}, () => Array(M).fill(Infinity));
const board = [];
for(let i=2;i<2+N;i++){
const row = [];
for(let j=0;j<M;j++){
if(input[i][j] === "#" || input[i][j] === "1"){
row.push(1)
}else{
row.push(0)
}
}
board.push(row)
}
const deque = [];
deque.push([X1-1,Y1-1]);
dist[X1-1][Y1-1] = 0;
while(deque.length){
const [x,y] = deque.shift();
for(let [dx,dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
const cost = dist[x][y] + board[nx][ny];
if(cost < dist[nx][ny]){
dist[nx][ny] = cost
if(board[nx][ny] === 0){
deque.unshift([nx,ny])
}else{
deque.push([nx,ny])
}
}
}
}
console.log(dist[X2-1][Y2-1])
주의 : 대각선 범위로 값 채울 때 주어지는 값이 항상 왼쪽 위 -> 오른쪽 아래 순서로 주어지지 않으므로 크기 비교해서 범위 잘 정하기!
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
let idx = 0;
const N = +input[idx++];
const SIZE = 500;
const board = Array.from({length: SIZE+1}, () => Array(SIZE+1).fill(0));
if(N === 0 && +input[1] === 0){
console.log(0);
return;
}
// 위험 구역
while(N > 0 && idx <= N){
const [X1,Y1,X2,Y2] = input[idx++].split(' ').map(Number);
let sx = X1 < X2 ? X1 : X2;
let lx = X1 > X2 ? X1 : X2;
let sy = Y1 < Y2 ? Y1 : Y2;
let ly = Y1 > Y2 ? Y1 : Y2;
for(let j=sx;j<=lx;j++){
for(let k=sy;k<=ly;k++){
board[j][k] = 1;
}
}
}
const M = +input[idx++];
// 죽음 구역
while(M > 0 && idx <= N+M+1){
const [X1,Y1,X2,Y2] = input[idx++].split(' ').map(Number);
let sx = X1 < X2 ? X1 : X2;
let lx = X1 > X2 ? X1 : X2;
let sy = Y1 < Y2 ? Y1 : Y2;
let ly = Y1 > Y2 ? Y1 : Y2;
for(let j=sx;j<=lx;j++){
for(let k=sy;k<=ly;k++){
board[j][k] = -1;
}
}
}
const move = [[0,1], [0,-1], [-1,0], [1,0]];
const dist = Array.from({length: SIZE+1}, () => Array(SIZE+1).fill(Infinity));
const deque = [];
deque.push([0,0]);
dist[0][0] = 0;
while(deque.length){
const [x,y] = deque.shift();
for(let [dx,dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx < 0 || nx >= SIZE+1 || ny < 0 || ny >= SIZE+1) continue;
if(board[nx][ny] === -1) continue; // 죽음 영역 건너뛰기
const cost = dist[x][y] + board[nx][ny];
if(cost < dist[nx][ny]){
dist[nx][ny] = cost;
if(board[nx][ny] === 0){
deque.unshift([nx,ny])
}else{
deque.push([nx,ny])
}
}
}
}
console.log(dist[SIZE][SIZE] === Infinity ? -1 : dist[SIZE][SIZE]);
위처럼 풀었을 때 시간 초과가 떠서 덱을 직접 구현하니 통과했다.
(배열에서 unshift, shift의 시간 복잡도가 O(N)이라 직접 구현하는 것이 빠르다.)
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
let idx = 0;
const N = +input[idx++];
const SIZE = 500;
const board = Array.from({length: SIZE+1}, () => Array(SIZE+1).fill(0));
class Deque{
constructor(){
this.storage = {};
this.front = 0;
this.rear = 0;
}
size(){
return this.rear - this.front
}
addRear(val){
this.storage[this.rear++] = val;
}
deleteRear(){
if(this.size() === 0) return undefined;
this.rear--
const val = this.storage[this.rear]
delete this.storage[this.rear]
return val;
}
addFront(val){
this.front--
this.storage[this.front] = val
}
deleteFront(){
if(this.size() === 0) return undefined;
const val = this.storage[this.front]
delete this.storage[this.front++]
return val;
}
}
if(N === 0 && +input[1] === 0){
console.log(0);
return;
}
for (let i = 0; i < N; i++) {
const [X1,Y1,X2,Y2] = input[idx++].split(' ').map(Number); // 위험 구역
let sx = X1 < X2 ? X1 : X2;
let lx = X1 > X2 ? X1 : X2;
let sy = Y1 < Y2 ? Y1 : Y2;
let ly = Y1 > Y2 ? Y1 : Y2;
for(let j=sx;j<=lx;j++){
for(let k=sy;k<=ly;k++){
board[j][k] = 1;
}
}
}
const M = +input[idx++];
for (let i = 0; i < M; i++) {
const [X1,Y1,X2,Y2] = input[idx++].split(' ').map(Number); // 죽음 구역
let sx = X1 < X2 ? X1 : X2;
let lx = X1 > X2 ? X1 : X2;
let sy = Y1 < Y2 ? Y1 : Y2;
let ly = Y1 > Y2 ? Y1 : Y2;
for(let j=sx;j<=lx;j++){
for(let k=sy;k<=ly;k++){
board[j][k] = -1;
}
}
}
const move = [[0,1], [0,-1], [-1,0], [1,0]];
const dist = Array.from({length: SIZE+1}, () => Array(SIZE+1).fill(Infinity));
const deque = new Deque();
deque.addRear([0,0]);
dist[0][0] = 0;
while(deque.size() > 0){
const [x,y] = deque.deleteFront();
for(let [dx,dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx < 0 || nx >= SIZE+1 || ny < 0 || ny >= SIZE+1) continue;
if(board[nx][ny] === -1) continue;
const cost = dist[x][y] + board[nx][ny];
if(cost < dist[nx][ny]){
dist[nx][ny] = cost;
if(board[nx][ny] === 0){
deque.addFront([nx,ny])
}else{
deque.addRear([nx,ny])
}
}
}
}
console.log(dist[SIZE][SIZE] === Infinity ? -1 : dist[SIZE][SIZE]);
여기서 한 가지 잘못 판단한 부분이 현재 칸과 상관없이 다음 칸이 벽에 인접한 칸이면 비용없이 이동 가능하다고 생각했는데, 현재 칸도 벽에 인접하고 다음 칸도 벽에 인접해야 비용없이 이동 가능한 것이었다. 이 부분만 신경쓰면 된다!
const fs = require('fs');
const input = fs.readFileSync(process.platform === 'linux' ? '/dev/stdin' : './test.txt').toString().trim().split('\n');
const [H, W] = input.shift().split(' ').map(Number);
const board = input.map(i => i.split(''));
const dist = Array.from({length: H}, () => Array(W).fill(Infinity));
const move = [[0,1], [0,-1], [1,0], [-1,0]]
class Deque{
constructor(){
this.storage = {};
this.front = 0;
this.rear = 0;
}
size(){
return this.rear - this.front
}
addFront(val){
this.front--;
this.storage[this.front] = val;
}
deleteFront(){
const val = this.storage[this.front];
delete this.storage[this.front++];
return val;
}
addRear(val){
this.storage[this.rear++] = val;
}
deleteRear(){
this.rear--;
const val = this.storage[this.rear];
delete this.storage[this.rear];
return val;
}
}
const start = [];
const end = [];
for(let i=0;i<H;i++){
if(start.length === 2 && end.length === 2) break;
for(let j=0;j<W;j++){
if(board[i][j] === "S"){
start[0] = i
start[1] = j
}else if(board[i][j] === "E"){
end[0] = i
end[1] = j
}
}
}
function isAdjacent(x,y){
for(let [dx,dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx < 0 || nx >= H || ny < 0 || ny >= W) continue;
if(board[nx][ny] === "#") return true;
}
return false;
}
const dq = new Deque();
dq.addRear(start);
dist[start[0]][start[1]] = 0;
while(dq.size() > 0){
const [x,y] = dq.deleteFront();
for(let [dx,dy] of move){
const nx = x+dx;
const ny = y+dy;
if(nx < 0 || nx >= H || ny < 0 || ny >= W) continue;
if(board[nx][ny] === "#") continue;
const nextCost = isAdjacent(x,y) && isAdjacent(nx,ny) ? 0 : 1;
const cost = dist[x][y] + nextCost;
if(cost < dist[nx][ny]){
dist[nx][ny] = cost;
if(nextCost === 0){
dq.addFront([nx,ny])
}else{
dq.addRear([nx,ny])
}
}
}
}
console.log(dist[end[0]][end[1]]);
추가로 최적화를 위해 매번 벽에 인접한지를 판단하는 대신 아래처럼 미리 인접 여부를 배열에 저장해두고 쓰면 된다.
const nearWall = Array.from({ length: H }, () => Array(W).fill(false));
for (let i = 0; i < H; i++) {
for (let j = 0; j < W; j++) {
if (board[i][j] === '#') continue;
for (const [dx, dy] of move) {
const ni = i + dx;
const nj = j + dy;
if (ni < 0 || ni >= H || nj < 0 || nj >= W) continue;
if (board[ni][nj] === '#') {
nearWall[i][j] = true;
break;
}
}
}
}
const nextCost = (nearWall[x][y] && nearWall[nx][ny]) ? 0 : 1;
문제 조건
BFS로 모든 육지에서 이동 가능한 지점까지 걸리는 시간 구하기
이때 걸리는 가장 큰 시간이 정답
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, M] = input[0].split(" ").map(Number);
const MAP = input.slice(1).map((r) => r.split(""));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let answer = -1;
function BFS(i, j) {
const visited = Array.from({ length: N }, () => Array(M).fill(false));
const queue = [];
queue.push([i, j, 0]);
visited[i][j] = true;
while (queue.length) {
const [x, y, d] = queue.shift();
if (answer < d) answer = d;
for (let [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
if (visited[nx][ny]) continue;
if (MAP[nx][ny] === "W") continue;
visited[nx][ny] = true;
queue.push([nx, ny, d + 1]);
}
}
}
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (MAP[i][j] === "L") {
BFS(i, j);
}
}
}
console.log(answer);
1년에 인접한 0의 개수만큼 높이가 줄어듦. 높이는 0보다 줄어들 수 없음.
빙산이 두 덩어리 이상으로 분리되는 최초의 시간 구하기
다 녹을 때까지 분리되지 않으면 0 출력
0보다 큰 지점(빙산) 상하좌우 인접한 0 개수 구하기 (배열에 저장, x,y,0개수)
한 번에 빙산 높이 줄이기 (0보다 작아지지 않도록 주의)
시간++
BFS로 빙산 개수 구하기 -> 2 이상이면 종료, 미만이면 계속 진행
59%에서 시간 초과 => 매번 맵을 모두 돌면서 빙산 개수와 녹여야 하는 개수를 구하니 시간 초가가 발생했다.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, M] = input[0].split(" ").map(Number);
const MAP = input.slice(1).map((r) => r.split(" ").map(Number));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let answer = 0;
function BFS(i, j, visited) {
const queue = [];
let idx = 0;
queue.push([i, j]);
visited[i][j] = true;
while (idx < queue.length) {
const [x, y] = queue[idx++];
for (let [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
if (MAP[nx][ny] === 0) continue;
if (visited[nx][ny]) continue;
visited[nx][ny] = true;
queue.push([nx, ny]);
}
}
}
while (true) {
// 녹여야 하는 빙산 위치 및 개수 저장
const melt = [];
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (MAP[i][j] > 0) {
let zero_cnt = 0;
for (let [dx, dy] of move) {
const nx = i + dx;
const ny = j + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
if (MAP[nx][ny] === 0) zero_cnt++;
}
if (zero_cnt > 0) melt.push([i, j, zero_cnt]);
}
}
}
// 빙산 녹이기 (높이가 0보다 작아지지 않도록 설정)
for (let [x, y, c] of melt) {
const height = MAP[x][y] - c;
MAP[x][y] = height < 0 ? 0 : height;
}
// 시간 증가
answer++;
const visited = Array.from({ length: N }, () => Array(M).fill(false));
let cnt = 0;
// 빙산 덩어리 개수 세기
for (let i = 0; i < N && cnt < 2; i++) {
for (let j = 0; j < M && cnt < 2; j++) {
if (MAP[i][j] > 0 && !visited[i][j]) {
BFS(i, j, visited);
cnt++;
}
}
}
// 덩어리가 2개 이상이면 종료
if (cnt > 1) break;
}
console.log(answer);
매번 맵을 모두 돌지 않고 초기에 빙산 위치를 구해둔 뒤 이를 바탕으로 다 녹은 빙산은 제거하며 구하면 시간 초과를 해결할 수 있다.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, M] = input[0].split(" ").map(Number);
const MAP = input.slice(1).map((r) => r.split(" ").map(Number));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let answer = 0;
// 초기 빙산 위치
let iceList = [];
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (MAP[i][j] > 0) iceList.push([i, j]);
}
}
function BFS(i, j, visited) {
const queue = [];
let idx = 0;
queue.push([i, j]);
visited[i][j] = true;
while (idx < queue.length) {
const [x, y] = queue[idx++];
for (let [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
if (MAP[nx][ny] === 0) continue;
if (visited[nx][ny]) continue;
visited[nx][ny] = true;
queue.push([nx, ny]);
}
}
}
while (true) {
const meltInfo = [];
// 녹는 양 계산
for (const [x, y] of iceList) {
let sea = 0;
for (const [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (MAP[nx][ny] === 0) sea++;
}
meltInfo.push([x, y, sea]);
}
// 빙산 녹이기 (높이가 0보다 작아지지 않도록 설정)
for (let [x, y, c] of meltInfo) {
const height = MAP[x][y] - c;
MAP[x][y] = height < 0 ? 0 : height;
}
// 시간 증가
answer++;
// 모두 녹은 것 제거
iceList = iceList.filter(([x, y]) => MAP[x][y] > 0);
if (iceList.length === 0) break;
const visited = Array.from({ length: N }, () => Array(M).fill(false));
let cnt = 0;
// 빙산 덩어리 개수 세기
for (const [x, y] of iceList) {
if (!visited[x][y]) {
BFS(x, y, visited);
cnt++;
if (cnt >= 2) {
console.log(answer);
return;
}
}
}
}
console.log(0);
걷기 : -1, +1
순간이동 하기 : *2
가장 빠른 시간과 가장 빠른 방법으로 찾는 방법 가짓수 출력
큰 수 -> 작은 수로 -1, +1, /2를 하며 가짓수를 세려고 했는데 25%에서 틀렸다.
특정 수가 되는 방법의 수가 여러개가 있을 수 있으므로 visited로 방문한 경우를 바로 막아버리면 안 된다.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, K] = input[0].split(" ").map(Number);
const B = Math.max(N, K);
const S = Math.min(N, K);
const visited = Array(B + 1).fill(false);
let time = Infinity;
let cnt = 0;
const queue = [];
queue.push([B, 0]);
visited[B] = true;
while (queue.length) {
const [l, t] = queue.shift();
if (l === S) {
time = t;
cnt++;
}
if (t > time) break;
if (l % 2 === 0 && !visited[l / 2]) {
visited[l / 2] = true;
queue.push([l / 2, t + 1]);
}
if (l > S && ((l - 1 !== S && !visited[l - 1]) || l - 1 === S)) {
visited[l - 1] = true;
queue.push([l - 1, t + 1]);
}
if ((l + 1 !== S && !visited[l + 1]) || l + 1 === S) {
visited[l + 1] = true;
queue.push([l + 1, t + 1]);
}
}
console.log(time);
console.log(cnt);
시간과 방법의 수를 각각 배열로 만들어둔다.
처음 방문한 경우 시간과 방법의 수를 세팅하고 큐에 추가한다.
이미 방문한 경우 방법의 수를 더해준다.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, K] = input[0].split(" ").map(Number);
const MAX = 100000; // N, K의 최대 범위
const dist = Array(MAX + 1).fill(-1); // 시간
const cnt = Array(MAX + 1).fill(0); // 방법의 수
const queue = [];
let head = 0;
queue.push(N);
dist[N] = 0;
cnt[N] = 1;
while (head < queue.length) {
const cur = queue[head++];
for (const next of [cur - 1, cur + 1, cur * 2]) {
if (next < 0 || next > MAX) continue;
// 처음 방문
if (dist[next] === -1) {
dist[next] = dist[cur] + 1;
cnt[next] = cnt[cur];
queue.push(next);
}
// 같은 최단 거리로 또 도달
else if (dist[next] === dist[cur] + 1) {
cnt[next] += cnt[cur];
}
}
}
console.log(dist[K]);
console.log(cnt[K]);
const fs = require("fs");
const { join } = require("path");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, M] = input[0].split(" ").map(Number);
const board = input.slice(1).map((r) => r.split(" ").map(Number));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let time = 0;
let cheese = [];
// 치즈 좌표 찾기
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (board[i][j] === 1) {
cheese.push([i, j]);
}
}
}
function BFS(i, j, visited) {
const queue = [[i, j]];
visited[i][j] = true;
while (queue.length) {
const [x, y] = queue.shift();
for (let [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
if (board[nx][ny] === 1) continue;
if (visited[nx][ny]) continue;
visited[nx][ny] = true;
queue.push([nx, ny]);
}
}
}
while (true) {
const visited = Array.from({ length: N }, () => Array(M).fill(false));
// 치즈 다 녹으면 끝
if (cheese.length === 0) break;
// 외부 표시
BFS(0, 0, visited);
// 외부와 두 면 이상 맞닿은 좌표 저장
const melt = [];
for (let [x, y] of cheese) {
let cnt = 0;
for (let [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
if (visited[nx][ny]) cnt++;
}
if (cnt > 1) melt.push([x, y]);
}
// 맞닿은 부분 제거
for (let [x, y] of melt) {
board[x][y] = 0;
}
cheese = cheese.filter(([x, y]) => board[x][y]);
// 시간 증가
time++;
}
console.log(time);
최적화 버전
치즈 위치를 저장하지 않고 외부만 탐색할 수 있는 방법도 있다.
외부를 BFS로 돌며 치즈 접촉 카운트를 저장하고 접촉 횟수가 2 이상인 치즈를 없애며 더 이상 녹일 치즈가 없으면 시간을 반환해도 된다.
이렇게 하면 치즈 배열을 매번 새로 갱신하지 않아도 되서 효율적이다.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [N, M] = input[0].split(" ").map(Number);
const board = input.slice(1).map(row => row.split(" ").map(Number));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let time = 0;
while (true) {
const visited = Array.from({ length: N }, () => Array(M).fill(false));
const contact = Array.from({ length: N }, () => Array(M).fill(0));
// 외부 공기 BFS
const queue = [[0, 0]];
let head = 0;
visited[0][0] = true;
while (head < queue.length) {
const [x, y] = queue[head++];
for (const [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) continue;
// 공기면 계속 BFS
if (board[nx][ny] === 0 && !visited[nx][ny]) {
visited[nx][ny] = true;
queue.push([nx, ny]);
}
// 치즈면 접촉 카운트
else if (board[nx][ny] === 1) {
contact[nx][ny]++;
}
}
}
// 녹일 치즈 확인
let melted = false;
for (let i = 0; i < N; i++) {
for (let j = 0; j < M; j++) {
if (board[i][j] === 1 && contact[i][j] >= 2) {
board[i][j] = 0;
melted = true;
}
}
}
if (!melted) break;
time++;
}
console.log(time);
난이도 : 골드 3
문제 조건
#: 벽 - 아무도 통과 못함
.: 지나갈 수 있는 공간
J: 지훈이의 미로에서의 초기위치 (지나갈 수 있는 공간)
F: 불이 난 공간
지훈은 상하좌우중 하나로 이동
불은 상하좌우로 동시에 확산
가장자리에서 탈출
벽은 통과 못함.
문제 설계
메모리 초과. -> 매번 불 BFS를 반복하며 이미 방문한 곳을 다시 방문하고 지훈 위치도 매번 복사해서 발생.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [R, C] = input[0].split(" ").map(Number);
const board = input.slice(1).map((r) => r.split(""));
const visited = Array.from({ length: R }, () => Array(C).fill(false));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
let minute = 0;
let J = []; // 지훈 위치
let fire = []; // 불 위치
for (let i = 0; i < R; i++) {
if (J.length > 0 && fire.length > 0) break;
for (let j = 0; j < C; j++) {
if (board[i][j] === "J") {
J = [i, j];
} else if (board[i][j] === "F") {
fire = [i, j];
}
}
}
let queue = [];
queue.push(J);
visited[J[0]][J[1]] = true;
while (true) {
// 탈출 불가능 - 더 이상 이동할 곳 없을 때
if (queue.length === 0) break;
// 지훈 이동 - 상하좌우 중 가능한 방향, 동시에 이동 해야 함!
const cur = [...queue];
queue = [];
while (cur.length) {
const [x, y] = cur.shift();
// 탈출 가능하면 멈추기 - 가장자리
if (x === 0 || x === R - 1 || y === 0 || y === C - 1) {
console.log(minute + 1);
return;
}
for (let [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= R || ny < 0 || ny >= C) continue;
if (visited[nx][ny]) continue;
if (board[nx][ny] === "#" || board[nx][ny] === "F") continue;
visited[nx][ny] = true;
queue.push([nx, ny]);
}
}
// 불 확산
const fires = [fire];
const fire_visited = Array.from({ length: R }, () => Array(C).fill(false));
fire_visited[fire[0]][fire[1]] = true;
while (fires.length) {
const [fx, fy] = fires.shift();
for (let [dx, dy] of move) {
const nx = fx + dx;
const ny = fy + dy;
if (nx < 0 || nx >= R || ny < 0 || ny >= C) continue;
if (fire_visited[nx][ny]) continue;
if (board[nx][ny] === "#") continue;
if (board[nx][ny] === "F") fires.push([nx, ny]);
board[nx][ny] = "F";
fire_visited[nx][ny] = true;
}
}
// 시간 증가
minute++;
}
console.log("IMPOSSIBLE");
최적화 풀이
불 BFS를 한 번만 돌아도 된다.
먼저 불이 맵의 각 위치에 도달하는 최단 시간을 모두 저장해두고, 지훈이 불보다 빠른 시간에 도달하는 경우 이동하며 가장자리에 도착하면 시간을 반환하면 된다.
불의 확산이 지훈이의 이동과 무관한 독립적인 사건이기 때문에 불 확산을 먼저 따로 처리해도 된다. 이후 조건에 따라 지훈이 이동하면 동시에 이동하는 것과 동일한 결과가 나온다.
const fs = require("fs");
const input = fs
.readFileSync(process.platform === "linux" ? "/dev/stdin" : "./test.txt")
.toString()
.trim()
.split("\n");
const [R, C] = input[0].split(" ").map(Number);
const board = input.slice(1).map((r) => r.split(""));
const move = [
[0, 1],
[0, -1],
[1, 0],
[-1, 0],
];
const fireTime = Array.from({ length: R }, () => Array(C).fill(Infinity));
const visited = Array.from({ length: R }, () => Array(C).fill(false));
let jq = [];
let fq = [];
// 초기 위치 수집
for (let i = 0; i < R; i++) {
for (let j = 0; j < C; j++) {
if (board[i][j] === "J") {
jq.push([i, j, 0]);
visited[i][j] = true;
}
if (board[i][j] === "F") {
fq.push([i, j, 0]);
fireTime[i][j] = 0;
}
}
}
// 불 BFS
while (fq.length) {
const [x, y, t] = fq.shift();
for (const [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= R || ny < 0 || ny >= C) continue;
if (board[nx][ny] === "#") continue;
if (fireTime[nx][ny] <= t + 1) continue;
fireTime[nx][ny] = t + 1;
fq.push([nx, ny, t + 1]);
}
}
// 지훈 BFS
while (jq.length) {
const [x, y, t] = jq.shift();
// 가장자리면 탈출
if (x === 0 || x === R - 1 || y === 0 || y === C - 1) {
console.log(t + 1);
return;
}
for (const [dx, dy] of move) {
const nx = x + dx;
const ny = y + dy;
if (nx < 0 || nx >= R || ny < 0 || ny >= C) continue;
if (visited[nx][ny]) continue;
if (board[nx][ny] === "#") continue;
// 불보다 먼저 도착해야 함
if (t + 1 >= fireTime[nx][ny]) continue;
visited[nx][ny] = true;
jq.push([nx, ny, t + 1]);
}
}
console.log("IMPOSSIBLE");