두 동전

문제

언어

  • JavaScript

순서도

  1. N x M 크기의 보드에서 동전 두 개의 위치 찾기
  2. BFS 탐색을 진행하며, 버튼을 누른 횟수를 늘려가며 동전의 위치 변환
  3. 보드 밖으로 동전이 하나만 떨어졌을 때의 버튼을 누른 횟수 출력

문제 풀이 step 1

  • N x M 크기의 보드가 있을 때, 두 개의 동전을 버튼을 이용해서 상하좌우로 옮겨가며, 보드에서 동전이 한 개만 떨어졌을 때의 버튼을 누른 횟수를 구하는 문제입니다.
  • 우선, 주어진 보드에서 두 개의 동전의 위치를 찾습니다.
  • 그리고 BFS 탐색을 실시합니다.
    • 두 개의 좌표의 방문 처리를 해야하므로, 4 차원 배열을 선언합니다.
    • 그리고 Queue 를 2 개를 선언해서, 단계별로 탐색을 진행합니다.
      • Queue 를 2 개 사용하는 이유는 확실하게 단계를 나눠가며 버튼을 누르기 위해서입니다.
      • 우선 queue 에 들어있는 모든 좌표들을 탐색하며, 다음에 탐색할 좌표들을 nextQueue 에 넣습니다.
      • queue 가 텅 비게되면, nextQueue 에 있는 좌표들을 queue 로 옮겨와서 탐색을 진행합니다.
      • nextQueue 에서 queue 로 옮기는 순간은 하나의 단계가 끝났음을 의미하며, 이 때 버튼을 누른 횟수를 증가시킵니다.
    • 매 탐색마다 다음에 탐색할 좌표에서 동전이 몇 개 떨어지는지에 따라서 경우를 나눠서 탐색을 진행합니다.
      • 하나도 안 떨어진 경우, 다음 탐색을 위해 nextQueue 에 좌표를 넣습니다.
      • 한 개 떨어진 경우, 탐색을 종료하고 버튼을 누른 횟수를 출력합니다.
      • 두 개 떨어진 경우, 탐색을 할 수 없습니다.
    • 위 과정에 추가로 방문 처리까지 해가며 탐색을 진행합니다.
    • 탐색 도중 버튼을 누른 횟수가 10 을 초과하면 탐색을 종료하고 -1 을 출력합니다.
  • 최종적으로 버튼을 누른 횟수를 출력하면 정답입니다.
  • 추가 설명은 주석으로 작성하겠습니다.

후기

  • 꼭 다시 풀어볼 문제입니다.
  • 처음 BFS 탐색을 했을 때, 두 개의 좌표를 어떻게 방문처리를 해야할 지 생각을 못했습니다. 그래서 시간이 1600 ms 가 소요되었습니다.
    • 어떤 분의 블로그를 보고 4 차원 배열을 사용하는 방법도 있다는 것을 깨닫게 되었습니다.
    • 영감을 얻은 곳 : https://moonsbeen.tistory.com/265
    • 역시 방문 처리를 하니 소요 시간이 300 ms 까지 줄었습니다.
    • 4 차원 배열을 선언해본 것도 처음이고, 이런 방법이 있다는 것을 알게 되어서 상당히 기뻤습니다.
  • 조건을 제대로 파악하지 못해서, 상당히 많은 시행 착오를 겪었습니다.
    • 두 동전을 떨어뜨릴 수 없거나, 버튼을 10 번 보다 많이 눌러야 한다면 -1 을 출력하는 것인데
    • “앞의 두 동전을 떨어뜨릴 수 없거나” 이 부분을 못 읽어서 BFS 탐색상 로직을 잘못 짜서 불필요한 시행착오를 겪었습니다.
    • BFS 함수의 마지막에 return 하는 값을 -1 로 하지 않고, depth 로 해서 이 조건을 잡아내지 못했었습니다.
    • 문제를 정확히 읽어야 하고, 또 로직을 짤 때 어떤 조건이 성립이 되면 바로 return 하는 방식도 좋은 것 같습니다.

소스 코드 1

const input = require("fs")
	.readFileSync("/dev/stdin")
	.toString()
	.trim()
	.split("\n");

// 간단한 Queue 구현, 배열을 사용해도 무방
class Queue {
	constructor() {
		this.bucket = [];
		this.rear = -1;
		this.front = -1;
	}

	enqueue(data) {
		this.bucket[++this.rear] = data;
	}

	dequeue() {
		return this.bucket[++this.front];
	}

	isEmpty() {
		return this.front === this.rear;
	}
}

// 다음에 탐색할 좌표가 보드를 벗어나는지 검사하는 함수
const checkOut = (n, m, x, y) => 0 <= x && x < n && 0 <= y && y < m;

const BFS = (n, m, arr, start, visited) => {
	const nx = [0, 0, -1, 1];
	const ny = [-1, 1, 0, 0];

	// 두 개의 Queue 선언
	let queue = new Queue();
	queue.enqueue(start);
	let nextQueue = new Queue();

	let depth = 1;
	while (!queue.isEmpty()) {
		const [[ax, ay], [bx, by]] = queue.dequeue();

		// 버튼을 누른 횟수가 10 을 초과한다면 탐색 종료
		if (depth > 10) break;

		for (let i = 0; i < 4; i++) {
			let [cx, cy] = [ax + nx[i], ay + ny[i]];
			let [dx, dy] = [bx + nx[i], by + ny[i]];

			// 떨어졌는지 검사
			let cnt = 0;
			if (!checkOut(n, m, cx, cy)) cnt += 1;
			if (!checkOut(n, m, dx, dy)) cnt += 1;

			// 두 개의 동전이 떨어진 경우
			if (cnt === 2) continue;
			// 한 개의 동전만 떨어진 경우
			if (cnt === 1) return depth;

			// 벽이라면 이동할 수 없음
			if (arr[cx][cy] === "#") [cx, cy] = [ax, ay];
			if (arr[dx][dy] === "#") [dx, dy] = [bx, by];

			// 방문한 곳은 다시 탐색하지 않음
			if (visited[cx][cy][dx][dy] === true) continue;

			// 방문 처리 및 다음 탐색을 위해 nextQueue 에 좌표 넣기
			visited[cx][cy][dx][dy] = true;
			nextQueue.enqueue([
				[cx, cy],
				[dx, dy],
			]);
		}

		// 버튼을 누른 횟수를 갱신하는 순간
		// nextQueue 에 queue 에 있는 좌표 넣기
		if (queue.isEmpty()) {
			queue = nextQueue;
			nextQueue = new Queue();
			depth += 1;
		}
	}

	// 두 동전을 떨어뜨릴 수 없거나, 버튼을 10 번 보다 많이 눌러야 하는 경우
	return -1;
};

const solution = (input) => {
	const [n, m] = input[0].split(" ").map(Number);
	const arr = input.slice(1, n + 1).map((v) => v.split(""));

	// 두 동전의 위치 찾기
	let start = [];
	for (let i = 0; i < n; i++) {
		for (let j = 0; j < m; j++) {
			if (arr[i][j] !== "o") continue;
			start.push([i, j]);
		}
	}

	// 4 차원 배열 생성
	const visited = Array.from({length: n}, () =>
		Array.from({length: m}, () =>
			Array.from({length: n}, () => Array(m).fill(false))
		)
	);

	// 시작 위치에 대한 방문 처리
	const [[ax, ay], [bx, by]] = start;
	visited[ax][ay][bx][by] = true;

	const depth = BFS(n, m, arr, start, visited);

	// 버튼을 누른 횟수 반환
	return depth;
};

console.log(solution(input));