토마토 백준 7569번
11/13/2024
문제
링크
이해
3차원 상자에 담긴 토마토들이 익어가는 과정을 시뮬레이션 하는 것.
- 익은 토마토는 하루가 지나면 주위의 익지 않은 토마토를 익게 만든다.
- 주위(인접한)의 토마토는 위, 아래, 왼쪽, 오른쪽, 앞, 뒤 여섯 방향을 의미한다.
- 모든 토마토가 익는데 걸리는 최소 일수를 구해야 한다.
해결
전체 코드
const BAEAKJOONFILE = "/dev/stdin";
const VSCODEFILE = "./coding/example.txt";
const fs = require('fs');
const input = fs.readFileSync(VSCODEFILE).toString().trim().split('\n');
const [M, N, H] = input[0].split(' ').map(Number);
const box = [];
const queue = [];
let unripeTomatoes = 0;
for (let h = 0; h < H; h++) {
const floor = [];
for (let n = 0; n < N; n++) {
const row = input[1 + h * N + n].split(' ').map(Number);
floor.push(row);
for (let m = 0; m < M; m++) {
if (row[m] === 1) queue.push([h, n, m, 0]);
else if (row[m] === 0) unripeTomatoes++;
}
}
box.push(floor);
}
const directions = [
[0, 0, 1], [0, 0, -1], [0, 1, 0], [0, -1, 0], [1, 0, 0], [-1, 0, 0]
];
let maxDays = 0;
let queueIndex = 0;
while (queueIndex < queue.length && unripeTomatoes > 0) {
const [h, n, m, days] = queue[queueIndex++];
for (let i = 0; i < 6; i++) {
const nh = h + directions[i][0];
const nn = n + directions[i][1];
const nm = m + directions[i][2];
if (nh >= 0 && nh < H && nn >= 0 && nn < N && nm >= 0 && nm < M && box[nh][nn][nm] === 0) {
box[nh][nn][nm] = 1;
queue.push([nh, nn, nm, days + 1]);
unripeTomatoes--;
maxDays = days + 1;
}
}
}
console.log(unripeTomatoes === 0 ? maxDays : -1);
부분 설명
입력
const fs = require('fs');
const input = fs.readFileSync(VSCODEFILE).toString().trim().split('\n');
const [M, N, H] = input[0].split(' ').map(Number);
- 파일에서 입력을 읽어 온다. 첫 줄에서 상자의 가로(M), 세로(N), 높이(H)를 가져온다.
토마토 상태 파악하기
const box = [];
const queue = [];
let unripeTomatoes = 0;
for (let h = 0; h < H; h++) {
const floor = [];
for (let n = 0; n < N; n++) {
const row = input[1 + h * N + n].split(' ').map(Number);
floor.push(row);
for (let m = 0; m < M; m++) {
if (row[m] === 1) queue.push([h, n, m, 0]);
else if (row[m] === 0) unripeTomatoes++;
}
}
box.push(floor);
}
- 문자열로 된 배열을 숫자 형태로 변환하면서 3차원 배열로 생성
- 만약 토마토가 익었다면(1이라면) 큐에 넣기, 큐의 마지막은 날짜(days)가 됨.
- 아니고 만약 토마토가 안 익었다면(0이라면) 안 익은 토마토 변수의 값을 1 증가
토마토 익히기
const directions = [
[0, 0, 1], [0, 0, -1], [0, 1, 0], [0, -1, 0], [1, 0, 0], [-1, 0, 0]
];
let maxDays = 0;
let queueIndex = 0;
while (queueIndex < queue.length && unripeTomatoes > 0) {
const [h, n, m, days] = queue[queueIndex++];
for (let i = 0; i < 6; i++) {
const nh = h + directions[i][0];
const nn = n + directions[i][1];
const nm = m + directions[i][2];
if (nh >= 0 && nh < H && nn >= 0 && nn < N && nm >= 0 && nm < M && box[nh][nn][nm] === 0) {
box[nh][nn][nm] = 1;
queue.push([nh, nn, nm, days + 1]);
unripeTomatoes--;
maxDays = days + 1;
}
}
}
BFS(너비 우선 탐색)를 사용해 토마토가 익어가는 과정을 시뮬레이션
- 큐에서 익은 토마토를 하나씩 꺼내서 주변 6방향 확인
- 안 익은 토마토를 발견하면 익힌 후 큐에 넣기
- 위 과정 반복하며 최대 일수 갱신
특이사항
시간 초과
현재 코드말고 이전 코드로 했을 때 시간 초과가 났었다.
토마토의 익힘 정도
토마토상태파악하기 부분의 코드를 전에는 3차원 배열을 만드는 로직과 토마토의 익힘 정도 찾는 로직을 따로 for문을 사용해 처리 했다. 아마 시간초과에 유의미한 차이가 있을 것 같아 한 번에 배열과 토마토의 익힘 정도를 구분하는 로직을 모두 처리하기로 한다. 이를 통해 불필요한 반복은 줄이고 실행 시간은 단축시킨다.
큐 구현
const [h, n, m, days] = queue[queueIndex++]; // 이 부분을
const [h, n, m, days] = queue.shift(); // 이렇게 했었다.
이전의 코드와 비교하면 다음과 같이 차이가 난다고 한다.
shift연산의 시간 복잡도는 O(n)인덱스를 사용하여 큐를 구현하면 O(1) 이라고 한다.
Shift와 인덱스의 시간 차이
- shift 연산(Array.prototype.shift):
- shift 연산은 배열의 첫 번째 요소를 제거하고 반환한다.
- 이 연산은 O(n) 시간 복잡도를 가진다, 이유는:
- 첫 번째 요소를 제거한 후 나머지 모든 요소를 한 칸씩 앞으로 이동시키기 때문이다.
- 따라서 배열의 크기가 클수록 이 연산은 더 많은 시간이 걸리게 된다.
- 인덱스를 사용한 큐 구현:
- 배열을 그대로 두고 인덱스만 이동시킨다.
- 큐에서 요소를 꺼낼 때마다 인덱스를 1 증가시키기만 하면 된다.
- 이 연산은 O(1) 시간 복잡도를 가진다. 단순히 인덱스 값만 변경하기 때문이다.
결론적으로, 맨 앞의 값을 꺼내오는 것은 동일하지만, 그 과정에서 발생하는 내부 연산의 복잡도 차이로 인해 성능 차이가 발생한다. 따라서 큐를 구현할 때는 가능한 인덱스 기반의 접근 방식을 사용하는 것이 효율적이라 한다.
블로그 내 관련 문서
참고 자료
출처 :
- AI가 그렇다고 함