[백준] 17141번 : 연구소 2 - 자바(Java)
Problem 🔒
문제
https://www.acmicpc.net/problem/17141
인체에 치명적인 바이러스를 연구하던 연구소에 승원이가 침입했고, 바이러스를 유출하려고 한다. 승원이는 연구소의 특정 위치에 바이러스 M개를 놓을 것이고, 승원이의 신호와 동시에 바이러스는 퍼지게 된다.
연구소는 크기가 N×N인 정사각형으로 나타낼 수 있으며, 정사각형은 1×1 크기의 정사각형으로 나누어져 있다. 연구소는 빈 칸, 벽으로 이루어져 있으며, 벽은 칸 하나를 가득 차지한다.
일부 빈 칸은 바이러스를 놓을 수 있는 칸이다. 바이러스는 상하좌우로 인접한 모든 빈 칸으로 동시에 복제되며, 1초가 걸린다.
예를 들어, 아래와 같이 연구소가 생긴 경우를 살펴보자. 0은 빈 칸, 1은 벽, 2는 바이러스를 놓을 수 있는 칸이다.
2 0 0 0 1 1 0
0 0 1 0 1 2 0
0 1 1 0 1 0 0
0 1 0 0 0 0 0
0 0 0 2 0 1 1
0 1 0 0 0 0 0
2 1 0 0 0 0 2
M = 3이고, 바이러스를 아래와 같이 놓은 경우 6초면 모든 칸에 바이러스를 퍼뜨릴 수 있다. 벽은 -, 바이러스를 놓은 위치는 0, 빈 칸은 바이러스가 퍼지는 시간으로 표시했다.
6 6 5 4 - - 2
5 6 - 3 - 0 1
4 - - 2 - 1 2
3 - 2 1 2 2 3
2 2 1 0 1 - -
1 - 2 1 2 3 4
0 - 3 2 3 4 5
시간이 최소가 되는 방법은 아래와 같고, 5초만에 모든 칸에 바이러스를 퍼뜨릴 수 있다.
0 1 2 3 - - 2
1 2 - 3 - 0 1
2 - - 2 - 1 2
3 - 2 1 2 2 3
3 2 1 0 1 - -
4 - 2 1 2 3 4
5 - 3 2 3 4 5
연구소의 상태가 주어졌을 때, 모든 빈 칸에 바이러스를 퍼뜨리는 최소 시간을 구해보자.
입력
첫째 줄에 연구소의 크기 N(5 ≤ N ≤ 50), 놓을 수 있는 바이러스의 개수 M(1 ≤ M ≤ 10)이 주어진다.
둘째 줄부터 N개의 줄에 연구소의 상태가 주어진다. 0은 빈 칸, 1은 벽, 2는 바이러스를 놓을 수 있는 칸이다. 2의 개수는 M보다 크거나 같고, 10보다 작거나 같은 자연수이다.
출력
연구소의 모든 빈 칸에 바이러스가 있게 되는 최소 시간을 출력한다. 바이러스를 어떻게 놓아도 모든 빈 칸에 바이러스를 퍼뜨릴 수 없는 경우에는 -1을 출력한다.
예제 입력 1
7 3
2 0 0 0 1 1 0
0 0 1 0 1 2 0
0 1 1 0 1 0 0
0 1 0 0 0 0 0
0 0 0 2 0 1 1
0 1 0 0 0 0 0
2 1 0 0 0 0 2
예제 출력 1
5
예제 입력 2
7 3
2 0 2 0 1 1 0
0 0 1 0 1 2 0
0 1 1 2 1 0 0
2 1 0 0 0 0 2
0 0 0 2 0 1 1
0 1 0 0 0 0 0
2 1 0 0 2 0 2
예제 출력 2
5
예제 입력 3
7 4
2 0 2 0 1 1 0
0 0 1 0 1 2 0
0 1 1 2 1 0 0
2 1 0 0 0 0 2
0 0 0 2 0 1 1
0 1 0 0 0 0 0
2 1 0 0 2 0 2
예제 출력 3
4
예제 입력 4
7 5
2 0 2 0 1 1 0
0 0 1 0 1 2 0
0 1 1 2 1 0 0
2 1 0 0 0 0 2
0 0 0 2 0 1 1
0 1 0 0 0 0 0
2 1 0 0 2 0 2
예제 출력 4
3
예제 입력 5
7 3
2 0 2 0 1 1 0
0 0 1 0 1 0 0
0 1 1 1 1 0 0
2 1 0 0 0 0 2
1 0 0 0 0 1 1
0 1 0 0 0 0 0
2 1 0 0 2 0 2
예제 출력 5
7
예제 입력 6
7 2
2 0 2 0 1 1 0
0 0 1 0 1 0 0
0 1 1 1 1 0 0
2 1 0 0 0 0 2
1 0 0 0 0 1 1
0 1 0 0 0 0 0
2 1 0 0 2 0 2
예제 출력 6
-1
예제 입력 7
5 1
2 2 2 1 1
2 1 1 1 1
2 1 1 1 1
2 1 1 1 1
2 2 2 1 1
예제 출력 7
4
Approach ❌
- 시간초과(1%)
- 조합을 생성하는 부분에서 다음 자리를 뽑을 때, Combi(depth+1, start+1)이 잘못되어 있어서 Combi(depth+1, i+1)로 수정했다.
- 초기 바이러스를 큐에 넣을 때, 방문처리가 되어있지 않아서 방문처리했다.
- 틀렸습니다(5%)
- 빈 칸 개수를 계산할 때, '0'의 개수만 세는 것이 아니라 '2'(바이러스를 놓을 수 있는 칸)의 개수에서 M(바이러스의 개수)을 뺀 값을 더해주었다.
- 조합을 생성할 때 M(바이러스 개수)만큼 for문을 반복하는 것이 아니라, '2'(바이러스를 놓을 수 있는 칸)의 개수만큼 for문을 반복해주었다.
- 틀렸습니다(82%)
- 답이 0초인 테스트 케이스에서 1초로 출력되어, 초기에 빈 칸 개수가 0개라면 시간이 증가되지 않고 바로 출력되도록 수정하였다.
- 바이러스를 모든 칸에 퍼트렸는지 확인하기 위해 static boolean 변수를 생성하였는데, 가능한 모든 조합을 돌면서 가장 마지막 조합의 결과가 false 처리되면, 바이러스를 모든 칸에 퍼트렸어도 static boolean 변수가 false여서 -1이 출력되는 문제가 있었다. boolean 변수를 삭제하고, 최종 최소 시간이 초기화 값과 같으면 -1을 출력하도록 수정하였다.
Approach ⭕
- 입력 받기 및 초기 처리
- 입력 받으면서 바이러스의 위치를 10*2 배열에 저장한다.
- 실질적인 빈 공간 개수 구하기
- 바이러스가 퍼질 때마다, 빈 공간의 개수를 감소시키기 위해서 미리 구한다.
- ‘2’라고 해도! 바이러스를 놓기로 결정된 위치가 아닌 곳은 빈공간이므로, 실질적인 빈공간의 개수는 ‘0’의 개수 + ’2’의 개수 - M(바이러스 수)이다.
- 실질적인 빈 공간 개수가 없으면 0초를 출력한다.
- 조합을 활용한 바이러스 배치(Combi 메서드)
- '2'(바이러스를 놓을 수 있는 칸)의 개수 중 M개를 조합으로 뽑는다.
- 크기가 M인 배열을 생성하고 배열이 가득 찼으면, 바이러스를 퍼트린다.
- depth는 크기가 M인 배열에 저장할 인덱스이며, 현재 선택한 바이러스의 개수를 나타낸다.
- start는 바이러스의 위치를 저장한 10*2 배열의 행 인덱스를 나타낸다.
- BFS를 활용한 바이러스 확산(Spread 메서드)
- 방문배열, 큐, 걸린 시간을 초기화한다.
- 큐에 초기 바이러스 위치를 추가하고, 방문 처리한다.
- 시간을 먼저 증가시킨후, 큐 크기만큼 바이러스를 퍼트린다. (큐 크기가 현재 시간에 퍼질 바이러스의 양을 뜻한다.)
- 경계를 벗어나지 않고, 벽이 아니며, 방문하지 않았으면 바이러스를 퍼트린다.
- 바이러스를 퍼트리는데 성공하였으니, 빈 공간 개수를 감소하고, 방문 처리하고, 큐에 다음 바이러스의 위치를 넣는다.
- 바이러스를 다 퍼트린 후, 빈 공간의 개수가 0개라면 빈 공간에 다 퍼트린 경우를 의미하므로, 최소 시간을 갱신해주고 반복문을 빠져나간다.
- 빈 공간의 개수가 남아있는데, 큐 크기가 0이라면 더이상 바이러스가 퍼질 자리가 없는 것이므로 반복문을 종료한다.
- 출력
- 모든 경우의 수를 검토한 후 최소 시간이 초기 값과 같다면, 모든 빈 칸에 바이러스를 퍼뜨릴 수 없는 경우이므로 -1을 출력한다.
Solution 💡
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
public class 연구소2 {
static int N, M, ans=54321, emp=0, vLo;
static int[][] map, virus;
static int[] pick;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt(); M = sc.nextInt();
map = new int[N][N];
vLo=0; // 바이러스를 놓을 수 있는 자리 개수
for(int i=0; i<N; i++){
for(int j=0; j<N; j++){
map[i][j] = sc.nextInt();
if(map[i][j]==2){vLo++;}
if(map[i][j]==0){emp++;} // 빈 칸 개수
}
}
virus = new int[vLo][2]; // 바이러스를 놓을 수 있는 위치를 저장
int v=0;
for(int i=0; i<N; i++){
for (int j=0; j<N; j++){
if(map[i][j]==2){virus[v][0]=i; virus[v][1]=j; v++;}
}
}
pick = new int[M];
if(emp+(vLo-M) == 0){
System.out.println(0);
return;
}
// 조합으로 virus 행 인덱스를 뽑아
Combi(0,0);
if (ans!=54321) System.out.println(ans);
else System.out.println(-1);
}
static void Combi(int depth, int start){
if(depth==M){
// 바이러스 놓을 자리 정했으면 퍼트려
Spread();
return;
}
for(int i=start; i<vLo; i++){
pick[depth] = i;
Combi(depth+1, i+1);
}
}
static void Spread(){
int sec=0, empCopy=emp+(vLo-M); // 걸린시간, 빈 칸 개수 초기화
int[] dr = {0,0,1,-1};
int[] dc = {1,-1,0,0};
int[][] visit = new int[N][N];
Queue<int[]> q = new LinkedList<>();
// 깊은 복사
int[][] copy = new int[N][N];
for(int i=0; i<N; i++){
for(int j=0; j<N; j++){
copy[i][j] = map[i][j];
}
}
// 바이러스를 퍼뜨릴 자리를 3으로
for(int i=0; i< pick.length; i++){
int r = virus[pick[i]][0];
int c = virus[pick[i]][1];
copy[r][c] = 3;
visit[r][c] = 1;
q.add(new int[]{r,c}); // 큐에 바이러스 위치 넣기
}
while (true){
sec++;
int qSize = q.size();
// 바이러스 퍼뜨림
for(int i=0; i<qSize; i++){
int[] get = q.poll();
for(int d=0; d<4; d++){
int nr = get[0] + dr[d];
int nc = get[1] + dc[d];
if(nr>=0 && nr<N && nc>=0 && nc<N && map[nr][nc]!=1 && visit[nr][nc]==0){
empCopy--; // 빈 칸 개수 감소
visit[nr][nc]=1;
q.add(new int[]{nr,nc});
}
}
}
// 빈 칸에 다 퍼뜨린 경우
if(empCopy==0){
if(sec<ans){ans=sec;}
break;
}
// 더이상 이동할 수 없는데 빈 칸이 남아있는 경우
if(qSize==0){
break;
}
}
}
}
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;
public class 연구소2 {
static int N, M, P, ans=54321, Z=0, T=0, pick[], emp; // Z:'0' 개수, T:'2'개수, pick:조합 결과 저장, emp: 빈 공간 개수
static int[][] map, visit, virus=new int[10][2];
static int[] dr = {0,0,1,-1}, dc = {1,-1,0,0};
static Queue<int[]> q = new LinkedList<>();
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
N = sc.nextInt(); M = sc.nextInt();
map = new int[N][N];
visit = new int[N][N];
pick = new int[M];
for(int i=0; i<N; i++){
for(int j=0; j<N; j++){
P = sc.nextInt();
map[i][j] = P;
if(P==2){virus[T][0]=i; virus[T++][1]=j;}
if(P==0){Z++;}
}
}
emp = Z+(T-M); // '0'의 개수 + '2'의 개수 - 바이러스 개수
if(emp == 0){System.out.println(0); return;} // 빈 공간이 없으면 0초 출력
Combi(0,0);
System.out.println(ans!=54321?ans:-1);
}
static void Combi(int depth, int start){
if(depth==M){
// 바이러스 놓을 자리 정했으면 퍼트려
Spread(emp);
return;
}
for(int i=start; i<T; i++){
pick[depth] = i;
Combi(depth+1, i+1);
}
}
static void Spread(int emp){
visit = new int[N][N];
q.clear();
int sec=0; // 걸린 시간
// 큐에 초기 바이러스 위치 추가
for(int i=0; i<M; i++){
int r = virus[pick[i]][0];
int c = virus[pick[i]][1];
visit[r][c] = 9; // 방문 처리
q.add(new int[]{r,c});
}
while (true){
int qSize = q.size();
sec++;
// 바이러스 퍼뜨림
for(int i=0; i<qSize; i++){
int[] get = q.poll();
for(int d=0; d<4; d++){
int nr = get[0] + dr[d];
int nc = get[1] + dc[d];
if(nr>=0 && nr<N && nc>=0 && nc<N && map[nr][nc]!=1 && visit[nr][nc]!=9){
emp--; // 빈 칸 개수 감소
visit[nr][nc]=9;
q.add(new int[]{nr,nc});
}
}
}
// 빈 칸에 다 퍼뜨린 경우
if(emp==0) {ans = Math.min(ans, sec); break;}
// 빈 칸에 다 퍼뜨리지 않았는데 더이상 퍼트릴 자리가 없는 경우
if(qSize==0) break;
}
}
}
불필요한 코드 삭제 ⚠️
- BFS 돌릴 때, 깊은 복사와 방문 배열 둘 다 사용했는데, 로직 상 둘 중 하나만 사용하면 되었다.
- 깊은 복사를 제거하는 것이 메모리가 가장 작아서 깊은 복사 배열을 삭제하였다.
- 바이러스 초기 시작점을 큐에 넣고 시작하기 때문에, 초기 시작점을 3으로 바꿀 필요가 없었다.
- BFS 돌릴 때, 큐를 초기화할 필요가 없었다.
- BFS는 큐가 빌 때까지 진행되기 때문에, 다음 BFS 진행 시 큐는 항상 비어있다.(초기화되어있다는 뜻)
- 변수를 삭제해 가독성 높이기
- emp를 static으로 올리고, BFS 메서드 인자로 emp를 넣어주어 empCopy 변수로의 불필요한 복제 작업을 줄였다.
- 반복문 삭제해 메모리 절약
- '2'(바이러스를 놓을 수 있는 자리)의 위치를 배열에 저장하기 위해 '2'(바이러스를 놓을 수 있는 자리)의 개수를 먼저 알아야 해서, '2'의 개수를 구한뒤 한 번 더 반복문을 사용해 r,c 위치를 배열에 저장하였다.
- 어차피 '2'의 개수만큼 조합을 뽑기 때문에, '2'의 최대 개수만큼 배열을 생성하였다. (최대개수=10)
for(int i=0; i<N; i++){ for(int j=0; j<N; j++){ map[i][j] = sc.nextInt(); if(map[i][j]==2){T++;} // '2'의 개수 구하기 if(map[i][j]==0){Z++;} // '0'의 개수 } } virus = new int[T][2]; // 바이러스를 놓을 수 있는 위치를 저장 int v=0; for(int i=0; i<N; i++){ for (int j=0; j<N; j++){ if(map[i][j]==2){virus[v][0]=i; virus[v][1]=j; v++;} } }
int[][] virus=new int[10][2]; for(int i=0; i<N; i++){ for(int j=0; j<N; j++){ P = sc.nextInt(); map[i][j] = P; if(P==2){virus[T][0]=i; virus[T++][1]=j;} // '2'의 개수 구하고, 배열에 바로 저장 if(P==0){Z++;} // '0'의 개수 } }
의문점 테스트 ⏱️
- 깊은 복사 vs 방문 배열
- 깊은 복사와 방문 배열 둘 다 사용했는데, 둘 중 하나만 사용하면 되었다. 그렇다면, 둘 중 어느걸 사용하는게 더 효율적일까?
- 메모리가 2MB 차이 났지만, 대부분의 경우 성능상 큰 영향을 주지 않는다고 한다.
- 변수 선언 미리 vs 그때그때
- Spread 함수를 시작할 때, 방향 벡터와 큐를 선언하여 함수를 실행할 때마다 변수가 생성되었다. 미리 변수를 선언하는 것과 그때그때 선언하는 것 중 뭐가 더 효율적일까?
- 마찬가지로 메모리가 1MB 차이가 나므로 큰 영향을 준다고 보긴 어려울것 같다.
- 하지만 메모리가 큰 변수(2차원 배열)들은 미리 선언해놓는것이 좋을듯 하다.