백준 1600 - 말이 되고픈 원숭이

Updated:

Java

1600 번 - 말이 되고픈 원숭이

문제

말은 격자판에서 체스의 나이트와 같은 이동방식을 가진다. 참고로 말은 장애물을 뛰어넘을 수 있다.
원숭이는 능력이 부족해서 총 K번만 말(나이트)와 같이 움직일 수 있고, 그 외에는 그냥 인접한 칸으로만 움직일 수 있다. 대각선 방향은 인접한 칸에 포함되지 않는다.

이제 원숭이는 머나먼 여행길을 떠난다. 격자판의 맨 왼쪽 위에서 시작해서 맨 오른쪽 아래까지 가야한다.
인접한 네 방향으로 한 번 움직이는 것, 말의 움직임으로 한 번 움직이는 것, 모두 한 번의 동작으로 친다.
격자판이 주어졌을 때, 원숭이가 최소한의 동작으로 시작지점에서 도착지점까지 갈 수 있는 방법을 알아내는 프로그램을 작성하시오.

접근 방법

0,0에서 n-1,m-1까지 이동하여, 그 이동 횟수를 탐색하는 BFS 문제이다.
여기서 k번의 다른 이동 패턴이 주어졌다.
원숭이가 점프를 하였는지 하지 않았는지의 상태를 나타내기 위해, 1차원을 더하여 총 3차원을 배열로 나타내었다.

해당 좌표까지 원숭이가 몇번 이동하여 도착하였는가를 나타내는 3차원 배열로 이동횟수[남은 점프 횟수][y좌표][x좌표]로 두었다.
각 점에서 점프를 하지 않을 경우인 상,하,좌,우 탐색하고, k번 점프 횟수가 0보다 크면, 말의 이동 패턴에 따라 갈 수 있는 지점으로 이동한다.
각 좌표에 먼저 도착한 이동 횟수를 기록하고, 다른 queue에서 이동하는 경우를 막기 위해 visited[][][]배열을 선언하였다.

뒤늦게 도착한 경우, 이동 횟수는 처음 도착했을 때 보다 더 늦기 때문에, dp를 사용하지 않아도 된다.

코드

import java.util.*;
import java.io.*;

public class Main {
	static int n, m, result = Integer.MAX_VALUE, k;
	static int[][] board;
	static int[][][] move;
	static boolean visited[][][];
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    	StringTokenizer stk;
    	k = stoi(br.readLine());
    	
    	stk = new StringTokenizer(br.readLine());
    	m = stoi(stk.nextToken());
    	n = stoi(stk.nextToken());
    	board = new int[n][m];
    	move = new int[k + 1][n][m];
    	visited = new boolean[k+1][n][m];
    	
    	for(int i = 0; i < n; i++) {
    		stk = new StringTokenizer(br.readLine());
    		for(int j = 0; j < m; j++) {
    			board[i][j] = stoi(stk.nextToken());
    		}
    	}
    	for(int i = 0; i < k + 1; i++) {
    		visited[i][0][0] = true;
    	}
    	
    	bfs();
    	
    	for(int i = 0; i < k + 1; i++) {
    		if(visited[i][n-1][m-1]) {
    			result = Math.min(result, move[i][n-1][m-1]);
    		}
    	}
    	
    	if(result != Integer.MAX_VALUE)
    		System.out.println(result);
    	else
    		System.out.println(-1);
    	
    	br.close();
	}
	// 우 하 상 좌
	static int[] dy = {0, 1, -1, 0};
	static int[] dx = {1, 0, 0, -1};
	
	// 말이 점프하는 방향, 숫자는 점프 위치가 시계 방향으로 나타내었을 때, 1~8
	// 3 4 1 2 5 6 7 8
	static int[] hy = {1, 2, -2, -1, 2, 1, -1, -2};
	static int[] hx = {2, 1, 1, 2, -1, -2, -2, -1};
	
	static void bfs() {
		Queue<Point> q = new LinkedList<>();
		q.add(new Point(k, 0, 0));
		// lv은 몇 번째 움직임인지 나타냄
		int y, x, z, ny, nx, find = 1, cnt = 0, lv = 1;
		while(!q.isEmpty()) {
			// 몇 번째의 움직임인지 나타내기 위한 반복문
			while(find-- != 0) {
				Point p = q.poll();
				y = p.y;
				x = p.x;
				z = p.z;
				for(int i = 0; i < 4; i++) {
					ny = y + dy[i];
					nx = x + dx[i];
					if(isIn(ny,nx) && board[ny][nx] != 1 && !visited[z][ny][nx]) {
						move[z][ny][nx] = lv;		
						visited[z][ny][nx] = true;
						q.add(new Point(z, ny, nx));
						cnt++;
					}
				}
				// 말 점프
				if(z > 0) {
					for(int i = 0; i < 8; i++) {
						ny = y + hy[i];
						nx = x + hx[i];
						if(isIn(ny,nx) && board[ny][nx] != 1 && !visited[z-1][ny][nx]) {
							move[z - 1][ny][nx] = lv;
							visited[z - 1][ny][nx] = true;
							q.add(new Point(z - 1, ny, nx));
							cnt++;
						}
					}
				}
			}
			lv++;	// 다음 움직임
			find = cnt;
			cnt = 0;
		}
		
	}
	// 주어진 범위 안에 있는가
    public static boolean isIn(int y, int x){
        if(y < 0 || y >= n || x < 0 || x >= m)
            return false;
        return true;
    }
    
	static class Point{
		int z,y,x;
		public Point(int z, int y, int x) {
			super();
			this.z = z;
			this.y = y;
			this.x = x;
		}
	}
	
	static int stoi(String str) {
    	return Integer.parseInt(str);
    }
}

총평

후기

벽 부수고 이동하기와 비슷한 문제였다.
각 상태를 표현하는 차원을 고려하는 법을 생각해야 한다.

개선할 점