몬스터 헌터
1. 문제 접근 방법 및 순서
이 문제는 최단 경로 탐색과 순열, 그리고 조건부 방문이 결합된 시뮬레이션 및 최적화 문제입니다. 외판원 순회 문제의 변형이라고 할 수 있습니다.
- 데이터 구조화
- 맵을 탐색하면서 몬스터(양수)와 고객(음수)의 위치 정보를 저장합니다.
- 이때, 몬스터 k와 고객 -k를 쉽게 매칭할 수 있도록 인덱싱합니다.
- 핵심 조건 파악
- 몬스터를 잡기 전에는 해당 고객을 방문해도 의미가 없다는 것이 가장 중요합니다.
- 방문 순서에 선행 관계가 존재합니다.
- 거리 행렬 계산
- 시작점, 모든 몬스터, 모든 고객 간의 거리를 미리 계산해두면 매번 계산할 필요가 없어 효율적입니다.
- 거리 계산은 맨해튼 거리를 사용합니다.
- 모든 가능한 경로 탐색
- 몬스터와 고객의 총합은 최대 8입니다.
- 이들의 방문 순서를 결정하는 모든 경우의 수를 탐색하되, 조건을 만족하는지 확인합니다.
- 최솟값 갱신
- 모든 임무를 완수한 시점의 총 이동 시간 중 가장 작은 값을 찾습니다.
2. 문제 해결에 필요한 개념 및 알고리즘
- 완전 탐색
- 지점 방문 순서에 따라 결과가 판이하게 달라집니다.
- 지점 수가 8!이므로 충분히 제한 시간 내에 풀 수 있습니다.
- 맨해튼 거리
- 격자판에서 상하좌우로만 이동하며, 1칸 이동에 1시간이 걸린다고 명시되어 있습니다.
- dist = Math.abs(x1-x2)+Math.abs(y1-y2)
- 상태 관리
- 어떤 몬스터를 잡았는지, 어떤 고객을 방문했는지 체크해야 중복 방문을 막고 선행 조건을 확인할 수 있습니다.
3. 어려웠던 점
- 몬스터와 고객의 선행 관계를 코드로 어떻게 구현하는지 방법을 몰랐습니다.
- 고객을 방문하려고 할 때, 해당 번호의 몬스터를 이미 방문했는가?를 체크하는 로직이 필요합니다.
4. 코드 풀이
4.1 데이터 구조화
4.1.1 Pos 클래스
static class Pos {
int r, c, num;
Pos(int r, int c, int num) {
super();
this.r = r;
this.c = c;
this.num = num;
}
}
static List<Pos> list; // 고객, 몬스터 리스트
- 고객, 몬스터 위치, 정보를 저장하는 클래스를 선언합니다.
- 고객, 몬스터 정보를 담는 리스트를 선언합니다.
4.1.2 몬스터 포획 체크
static boolean[] checkMonster; // 몬스터 포획 체크
// 맵의 정보 입력
for (int i = 1; i <= N; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
for (int j = 1; j <= N; j++) {
int val = Integer.parseInt(st.nextToken());
// 고객이나 몬스터가 아니면 다음으로 넘어간다.
if (val == 0)
continue;
// 고객 또는 몬스터를 리스트에 추가
list.add(new Pos(i, j, val));
// 몬스터의 수 측정
if (val > 0)
Mcnt++;
}
}
// 몬스터 포획 체크 초기화
checkMonster = new boolean[Mcnt + 1];
- 몬스터와 고객의 선행 관계를 체크하는데 사용합니다.
- 몬스터 포획 체크 배열을 초기화합니다.
- 맵에서 입력된 몬스터의 개수로 체크합니다.
- 1 ~ 모든 몬스터 수 만큼 초기화합니다.
4.2 맨해튼 거리 계산
// 헌터와 방문 장소의 거리 계산
int dist = Math.abs(r - pos.r) + Math.abs(c - pos.c);
- 헌터와 방문 장소의 거리를 계산합니다.
4.3 최소값 갱신
// 종료 조건
if (level == list.size()) {
result = Math.min(result, curdist);
return;
}
- 모든 임무를 완수한 시점의 총 이동 시간 중 가장 작은 값을 갱신합니다.
4.4 DFS 함수 코드
static void dfs(int r, int c, int curdist, int level) {
// 종료 조건
if (level == list.size()) {
result = Math.min(result, curdist);
return;
}
// 경우의 수 수행
for (int i = 0; i < list.size(); i++) {
// 이미 방문한 곳은 다음으로 넘긴다.
if (visited[i])
continue;
// 현재 선택된 원소
Pos pos = list.get(i);
// 고객인지 경우
if (pos.num < 0) {
// 고객에 해당하는 몬스터 번호
int monidx = Math.abs(pos.num);
// 고객의 몬스터를 포획하지 못하면 다음으로 넘어간다.
if (checkMonster[monidx] == false)
continue;
}
// 몬스터인 경우 포획 체크
if (pos.num > 0) {
checkMonster[pos.num] = true;
}
// 현재 방문 체크
visited[i] = true;
// 헌터와 방문 장소의 거리 계산
int dist = Math.abs(r - pos.r) + Math.abs(c - pos.c);
// 다음 방문 이동
dfs(pos.r, pos.c, curdist + dist, level + 1);
// 포획 체크 해제
if (pos.num > 0) {
checkMonster[pos.num] = false;
}
// 현재 방문 체크 해제
visited[i] = false;
}
} // dfs 종료
4.5 전체 코드
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
public class Solution {
static int N; // 맵의 크기
static boolean[] checkMonster; // 몬스터 포획 체크
static List<Pos> list; // 고객, 몬스터 리스트
static boolean[] visited; // 방문 체크 배열
static int result; // 모든 몬스터를 사냥하고 확인받는데 걸리는 가장 빠른 시간
public static void main(String[] args) throws NumberFormatException, IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
int T = Integer.parseInt(br.readLine().trim());
// tc 수행
for (int tc = 1; tc <= T; tc++) {
// 맵의 크기 입력
N = Integer.parseInt(br.readLine().trim());
list = new ArrayList<>();
result = Integer.MAX_VALUE;
int Mcnt = 0; // 몬스터의 수
// 맵의 정보 입력
for (int i = 1; i <= N; i++) {
StringTokenizer st = new StringTokenizer(br.readLine());
for (int j = 1; j <= N; j++) {
int val = Integer.parseInt(st.nextToken());
// 고객이나 몬스터가 아니면 다음으로 넘어간다.
if (val == 0)
continue;
// 고객 또는 몬스터를 리스트에 추가
list.add(new Pos(i, j, val));
// 몬스터의 수 측정
if (val > 0)
Mcnt++;
}
}
// 몬스터 포획 체크 초기화
checkMonster = new boolean[Mcnt + 1];
// 방문 체크 배열 초기화
visited = new boolean[list.size()];
// 처리
dfs(1, 1, 0, 0);
// 출력 내용 추가
sb.append("#" + tc + " " + result + "\n");
} // tc 종료
// 출력
System.out.println(sb);
}
// 헌터의 사냥 및 보고 기능 수행 함수
// 현재 헌터의 좌표, 현재 거리, 다음 깊이
static void dfs(int r, int c, int curdist, int level) {
// 종료 조건
if (level == list.size()) {
result = Math.min(result, curdist);
return;
}
// 경우의 수 수행
for (int i = 0; i < list.size(); i++) {
// 이미 방문한 곳은 다음으로 넘긴다.
if (visited[i])
continue;
// 현재 선택된 원소
Pos pos = list.get(i);
// 고객인지 경우
if (pos.num < 0) {
// 고객에 해당하는 몬스터 번호
int monidx = Math.abs(pos.num);
// 고객의 몬스터를 포획하지 못하면 다음으로 넘어간다.
if (checkMonster[monidx] == false)
continue;
}
// 몬스터인 경우 포획 체크
if (pos.num > 0) {
checkMonster[pos.num] = true;
}
// 현재 방문 체크
visited[i] = true;
// 헌터와 방문 장소의 거리 계산
int dist = Math.abs(r - pos.r) + Math.abs(c - pos.c);
// 다음 방문 이동
dfs(pos.r, pos.c, curdist + dist, level + 1);
// 포획 체크 해제
if (pos.num > 0) {
checkMonster[pos.num] = false;
}
// 현재 방문 체크 해제
visited[i] = false;
}
} // dfs 종료
static class Pos {
int r, c, num;
public Pos(int r, int c, int num) {
super();
this.r = r;
this.c = c;
this.num = num;
}
}
}