설명
N×N 크기의 공간에 물고기 M마리와 아기 상어 1마리가 있다. 공간은 1×1 크기의 정사각형 칸으로 나누어져 있다. 한 칸에는 물고기가 최대 1마리 존재한다.
아기 상어와 물고기는 모두 크기를 가지고 있고, 이 크기는 자연수이다. 가장 처음에 아기 상어의 크기는 2이고, 아기 상어는 1초에 상하좌우로 인접한 한 칸씩 이동한다.
아기 상어는 자신의 크기보다 큰 물고기가 있는 칸은 지나갈 수 없고, 나머지 칸은 모두 지나갈 수 있다. 아기 상어는 자신의 크기보다 작은 물고기만 먹을 수 있다. 따라서, 크기가 같은 물고기는 먹을 수 없지만, 그 물고기가 있는 칸은 지나갈 수 있다.
아기 상어가 어디로 이동할지 결정하는 방법은 아래와 같다.
아기 상어의 이동은 1초 걸리고, 물고기를 먹는데 걸리는 시간은 없다고 가정한다. 즉, 아기 상어가 먹을 수 있는 물고기가 있는 칸으로 이동했다면, 이동과 동시에 물고기를 먹는다. 물고기를 먹으면, 그 칸은 빈 칸이 된다.
아기 상어는 자신의 크기와 같은 수의 물고기를 먹을 때 마다 크기가 1 증가한다. 예를 들어, 크기가 2인 아기 상어는 물고기를 2마리 먹으면 크기가 3이 된다.
공간의 상태가 주어졌을 때, 아기 상어가 몇 초 동안 엄마 상어에게 도움을 요청하지 않고 물고기를 잡아먹을 수 있는지 구하는 프로그램을 작성하시오.
입력
첫째 줄에 공간의 크기 N(2 ≤ N ≤ 20)이 주어진다.
둘째 줄부터 N개의 줄에 공간의 상태가 주어진다. 공간의 상태는 0, 1, 2, 3, 4, 5, 6, 9로 이루어져 있고, 아래와 같은 의미를 가진다.
아기 상어는 공간에 한 마리 있다.
BFS를 이용한 시뮬레이션 문제입니다.
정리한 조건대로 아기 상어의 움직임을 그대로 구현합니다.
계획 1 - 상어와 먹을 물고기를 정의합니다.
int[] shark = {0, 0, 0, 2}; // 상어 -> 좌표, 크기가 커지기 전까지 먹은 물고기 수, 현재 크기
int[] eatFish = {N, N, N * N}; // 먹을 물고기 -> 좌표, 상어와의 거리
객체로 만들어 좀 더 명시적으로 표현할 수 있겠지만 간단하게 배열을 사용했습니다.
계획 2 - 초기 상어의 좌표를 초기화합니다.
for (int i = 0; i < N; i ++) {
StringTokenizer stk = new StringTokenizer(br.readLine());
for (int j = 0; j < N; j++) {
sea[i][j] = Integer.parseInt(stk.nextToken());
// 상어일 때 좌표를 저장합니다.
if (sea[i][j] == 9) {
shark[0] = i;
shark[1] = j;
sea[i][j] = 0;
}
}
}
이제 이 상어를 조건대로 이리저리 움직일 겁니다.
계획 3 - BFS를 이용해 상어를 이동시킵니다.
// 현재 아기 상어의 위치에서 탐색합니다.
q.add(new int[] {shark[0], shark[1], 0});
visit[shark[0]][shark[1]] = true;
while (!q.isEmpty()) {
int[] cur = q.poll();
int y = cur[0];
int x = cur[1];
int move = cur[2] + 1;
for (int dir = 0; dir < 4; dir++) {
int ny = y + my[dir];
int nx = x + mx[dir];
// 범위를 벗어날 때
if (ny < 0 || ny >= N || nx < 0 || nx >= N) continue;
// 이미 방문한 지점일 때
if (visit[ny][nx]) continue;
// 상어가 물고기보다 작을 때
if (shark[3] < sea[ny][nx]) continue;
// 이전의 먹을 수 있는 물고기보다 거리가 더 멀 때 (가지치기)
if (move > eatFish[2]) continue;
// 먹을 수 있는 물고기를 계속 탐색합니다.
q.add(new int[] {ny, nx, move});
visit[ny][nx] = true;
// 빈 칸일 때
if (sea[ny][nx] == 0) continue;
// 상어랑 물고기랑 크기가 같을 때
if (shark[3] == sea[ny][nx]) continue;
// 거리가 더 가깝거나 거리가 같고 이전 좌표보다 위에 있거나, 이전 좌표와 같은 위쪽 라인이라면 그 라인에서 왼쪽에 있을 때
if (move < eatFish[2] || (move == eatFish[2] && (ny < eatFish[0] || (ny == eatFish[0] && nx < eatFish[1])))) {
// 물고기 좌표와 거리 갱신
eatFish[0] = ny;
eatFish[1] = nx;
// 물고기와의 거리 갱신
eatFish[2] = move;
}
}
}
꽤 까다로운 구현부입니다.
먹을 수 있는 모든 물고기를 탐색해서 2-1에서 정리한 조건에 부합하도록
상어가 먹으러 갈 물고기의 좌표를 갱신해줘야 합니다.
여기서 눈 여겨볼 부분은
// 이전의 먹을 수 있는 물고기보다 거리가 더 멀 때 (가지치기)
if (move > eatFish[2]) continue;
이 부분의 코드인데요.
BFS는 최소 거리를 보장하기 때문에
이전의 먹을 수 있는 물고기까지의 거리보다
현재 탐색한 물고기까지의 거리가 더 먼 경우 continue
로 넘어갑니다.
이런 스킬을 가지치기라고 합니다.
가지치기를 하느냐 안 하느냐에 따라 시간복잡도 차이가 꽤 크게 납니다.
계획 4 - 더 이상 먹을 수 있는 물고기가 없을 때까지 반복합니다
// 더 이상 먹을 수 있는 물고기가 없을 때 엄마 상어를 찾으러 갑니다.
if (eatFish[0] == N) break;
더 이상 먹을 수 있는 물고기가 없는 조건은
물고기의 좌표가 처음에 초기화한 값인 N
일 때겠죠?
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Stack;
import java.util.StringTokenizer;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
public class Main {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
public static void main(String[] args) throws Exception {
Queue<int[]> q = new LinkedList<>();
int[] my = {-1, 0, 1, 0};
int[] mx = {0, 1, 0, -1};
int N = Integer.parseInt(br.readLine());
boolean[][] visit = new boolean[N][N];
int[][] sea = new int[N][N];
int[] shark = {0, 0, 0, 2}; // 상어 -> 좌표, 크기가 커지기 전까지 먹은 물고기 수, 현재 크기
int[] eatFish = {N, N, N * N}; // 먹을 물고기 -> 좌표, 상어와의 거리
for (int i = 0; i < N; i ++) {
StringTokenizer stk = new StringTokenizer(br.readLine());
for (int j = 0; j < N; j++) {
sea[i][j] = Integer.parseInt(stk.nextToken());
// 상어일 때 좌표를 저장합니다.
if (sea[i][j] == 9) {
shark[0] = i;
shark[1] = j;
sea[i][j] = 0;
}
}
}
int ans = 0;
while (true) {
// 현재 아기 상어의 위치에서 탐색합니다.
q.add(new int[] {shark[0], shark[1], 0});
visit[shark[0]][shark[1]] = true;
while (!q.isEmpty()) {
int[] cur = q.poll();
int y = cur[0];
int x = cur[1];
int move = cur[2] + 1;
for (int dir = 0; dir < 4; dir++) {
int ny = y + my[dir];
int nx = x + mx[dir];
// 범위를 벗어날 때
if (ny < 0 || ny >= N || nx < 0 || nx >= N) continue;
// 이미 방문한 지점일 때
if (visit[ny][nx]) continue;
// 상어가 물고기보다 작을 때
if (shark[3] < sea[ny][nx]) continue;
// 먹을 수 있는 물고기를 계속 탐색합니다.
q.add(new int[] {ny, nx, move});
visit[ny][nx] = true;
// 빈 칸일 때
if (sea[ny][nx] == 0) continue;
// 상어랑 물고기랑 크기가 같을 때
if (shark[3] == sea[ny][nx]) continue;
// 거리가 더 가깝거나 거리가 같고 이전 좌표보다 위에 있거나, 이전 좌표와 같은 위쪽 라인이라면 그 라인에서 왼쪽에 있을 때
if (move < eatFish[2] || (move == eatFish[2] && (ny < eatFish[0] || (ny == eatFish[0] && nx < eatFish[1])))) {
// 물고기 좌표와 거리 갱신
eatFish[0] = ny;
eatFish[1] = nx;
// 물고기와의 거리 갱신
eatFish[2] = move;
}
}
}
// 더 이상 먹을 수 있는 물고기가 없을 때 엄마 상어를 찾으러 갑니다.
if (eatFish[0] == N) break;
// 아기 상어의 좌표를 갱신합니다.
shark[0] = eatFish[0];
shark[1] = eatFish[1];
// 물고기를 먹은 다음, 자기 크기만큼 물고기를 먹었다면 크기를 키웁니다.
if (++shark[2] == shark[3]) {
shark[3]++;
shark[2] = 0;
}
// 먹었으면 해당 좌표 빈 칸 표시
sea[shark[0]][shark[1]] = 0;
// 이동 시간을 누적합니다.
ans += eatFish[2];
// visit 배열 초기화
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
visit[i][j] = false;
}
}
// 먹을 물고기 좌표 초기화
eatFish[0] = N;
eatFish[1] = N;
eatFish[2] = N * N;
}
bw.write(ans + "");
bw.close();
}
}