문제
cctv 방향 조합했을 때, N*M방의 cctv 사각지대 최소 크기 구하기
1. cctv번호에 따라 감시 가능한 방향이 다르다.
2. 해당 방향에 있는 칸 전체 감시 가능
3. 벽 통과하여 감시 불과, cctv는 통과 가능
4. cctv는 90도 회전 가능
5. 0=빈칸 6=벽 1~5=cctv
풀이
- 알고리즘 선택 :
각 cctv의 모든 방향 조합을 고려해서 최솟값을 구해야 한다. -> 완전 탐색
입력 범위가 1~8로 작으므로 완전탐색이 가능할 것이라고 판단했다.
- 대략적인 아이디어 :
예를 들어 1번, 2번, 3번 cctv가 존재하는 경우:
1번에서 4가지 중 한 가지 방향을 정해서 감시 가능한 모든 좌표를 board에 표시
-> 2번에서 2가지 중 한 가지 방향을 정해서 감시 가능한 모든 좌표를 board에 표시
-> 3번에서 4가지 중 한 가지 방향을 정해서 감시 가능한 모든 좌표를 board에 표시
모든 cctv를 다 고려했으므로 완성된 board에서 사각지대 개수를 구해 min과 비교, min을 업데이트.
위와 같은 과정이 각 cctv의 모든 방향의 조합을 만족할 때까지 반복된다. -> 재귀로 구현
(위의 경우 4*2*3=24가지 경우를 모두 고려해야 한다.)
- 코드 설계 :
dir[][][]에 cctv별 회전 방향을 dx, dy로 표현하여 전역으로 정의해둔다.
board[][] 에 입력값을 받는 동시에 cctv 리스트에 Node(x, y, cctv종류)를 채운다.
재귀함수를 시행한다 :
cctv리스트에서 인자로 전달받은 k번째 cctv를 꺼내 node변수에 저장. (처음 호출할 때는 0번째 cctv여야 하므로 0 전달)
node에서 x, y, type을 알아내서 변수로 저장.
k번째 cctv의 첫 번째 방향 선택한다.
board에 감시 가능한 곳 모두 표시한다 :
무한 반복문으로 dx, dy를 현재 노드의 x, y에 계속 더한다. 더 이상 진행 불가하다면 반복문을 탈출한다.
k+1번째 cctv를 재귀호출한다.
코드
import java.io.*;
import java.util.*;
public class boj_15683 {
static int[][][] dir = {
{{0, 1}, {1, 0}, {0, -1}, {-1, 0}},
{{0, 1, 0, -1}, {1, 0, -1, 0}},
{{-1, 0, 0, 1}, {0, 1, 1, 0}, {0, -1, 1, 0}, {0, -1, -1, 0}},
{{-1, 0, 0, 1, 0, -1}, {1, 0, -1, 0, 0, 1}, {0, 1, 0, -1, 1, 0}, {0, -1, 1, 0, -1, 0}},
{{0, 1, 1, 0, 0, -1, -1, 0}}
};
static List<Node1> cctv;
static int[][] board;
static int N, M;
static int min = Integer.MAX_VALUE;
public static void main(String[] args) throws IOException{
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st;
st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
//board에 값을 받는 동시에 cctv 리스트 채우기
board = new int[N][M];
cctv = new ArrayList<>();
for(int i=0; i<N; i++){
st = new StringTokenizer(br.readLine());
for(int j=0; j<M; j++){
board[i][j] = Integer.parseInt(st.nextToken());
if(board[i][j]>=1 && board[i][j] <=5)
cctv.add(new Node1(i, j, board[i][j]));
}
}
recur(0);
bw.write(min+" ");
bw.close();
}
static void recur(int k){
//재귀호출 탈출 조건: cctv리스트를 끝까지 다 돌았다면 (k==cctv.size()) 사각지대 개수를 리턴
if(k == cctv.size()){
int count = 0;
for(int i=0; i<N; i++){
for(int j=0; j<M; j++){
if(board[i][j] != 0)
count++;
}
}
int ans = N*M - count;
if(ans < min)
min = ans;
return;
}
//cctv리스트에서 k번째 cctv를 꺼냄
//type을 통해 dx, dy를 찾음
//dir배열을 참고하여 모든 회전하는 경우를 진행
//감시 완료된 곳을 -1로 표시하여 board를 업데이트 -> monitor함수
//k+1번째 cctv를 재귀호출
Node1 node = cctv.get(k);
int type = node.type;
int[][] tmp = copy(board);
for(int i=0; i<dir[type-1].length; i++){ //k번째 cctv의 i번째 방향
for(int j=0; j<dir[type-1][i].length; j+=2){ //i번째 방향에 정의된 j번째 원소
int dx = dir[type-1][i][j];
int dy = dir[type-1][i][j+1];
monitor(node, dx, dy); //node기준으로 dx, dy방향으로 감시할 때 감시 가능한 곳 표시
}
recur(k+1); //k번째 cctv의 i번째 방향을 진행한 후 k+1번째 cctv의 i번째 방향을 진행하러 재귀
board = copy(tmp); //분기 후 원래 board 상태로 복원
}
}
static void monitor(Node1 node, int dx, int dy){
int newX = node.x;
int newY = node.y;
while(true){
newX += dx;
newY += dy;
//board범위 밖으로 나가는 경우
if(newX<0 || newX>=N || newY<0 || newY>=M)
break;
//벽을 만나는 경우
if(board[newX][newY] == 6)
break;
//cctv를 만나거나 이미 감시한 경우
if(board[newX][newY] != 0)
continue;
board[newX][newY] = -1;
}
}
static int[][] copy(int[][] arr){
//arr배열과 동일한 배열을 새로 생성하여 리턴
int[][] ret = new int[N][M];
for(int i=0; i<N; i++){
for(int j=0; j<M; j++){
ret[i][j] = arr[i][j];
}
}
return ret;
}
}
class Node1 {
int x, y, type;
public Node1(int x, int y, int type){
this.x = x;
this.y = y;
this.type = type;
}
}
Note
- 굉장히 오래 걸렸던 문제이다.
- 처음에 알고리즘을 선택할 때 완탐으로 해가 될 수 있는 모든 경우를 찾아야 하고, 백트래킹으로 조합을 구성해야 하는 것까지는 판단했다.
- 그러나 시간복잡도 측면에서 통과 가능할지 판단하기 어려웠다. 그래서 완전탐색의 시간복잡도에 관해 공부했다.
- 이전에 풀었던 브루트 포스 완탐 문제에서는 재귀함수 호출 시 인자로 현재 상태를 전달하면서 depth를 더해갔다.
- 그리고 마지막 depth에 다다르면 재귀를 탈출하도록 재귀함수 시작 부분에 조건을 생성했다.
- 이 문제도 완전탐색이므로 그 구조를 똑같이 적용하면 되겠다는 생각까지는 해냈다.
- 그러나 cctv의 모든 방향 조합을 어떻게 코드로 표현할 것인지는 혼자 생각해내지 못했다.
- 보통 행렬을 탐색할 때는 다차원 배열로 모든 경우를 일일이 정의해두고 갖다쓰는 방법을 쓰는 것 같다.
- 처음에는 int[] c의 cctv리스트와 같은 인덱스에 cctv 번호를 저장하고, wall 리스트도 생성하려고 했다.
- 그리고 cctv리스트를 2차원 리스트로 만들어서 각 cctv마다 회전방향 수대로 방문체크를 만들려고 했다.
- 정답 코드를 다 이해한 후 백지에 다시 짜고, 틀린 부분을 디버깅하며 하나 하나 고쳐나가니 내가 어떤 부분에서 헷갈려하는지 알 수 있었다.
- 3차원 배열 dir의 인덱스에 접근할 때 for문의 i, j가 dir의 어떤 요소에 접근하는지 헷갈려서 한참 고쳤다.
- 재귀 호출 후에 새로운 board를 생성하는 것도 아직까지 깔끔하게 이해되지 않았다. board를 재귀 인자로 전달해야 되는 거 아닐까 생각했는데, 재귀호출을 하면 dfs로 재귀 탈출 조건까지 만족하는 부분까지 찍고 그 다음 코드(board = copy(tmp))로 넘어가는 거라고 이해하니까 쉽다. 나중에 직접 디버깅하면서 맞는지 봐야겠다.
- 처음에 문제를 잘못 이해해서 나중에 이 부분이 이중for문의 바깥쪽이 아니라 안쪽에 위치하도록 고쳤다. cctv1의 모든 방향을 고려한 후 cctv2로 넘어가는 게 아니라, cctv1의 한 방향을 선택해서 감시동작을 시행한 후 cctv2로 넘어가야 한다.
- 사각지대 개수를 세어서 min으로 삽입하는 실수도 했다.
- 스터디 피드백 : 재귀호출하기 전에 백트래킹할 때, tmp에 board를 복사해놓은 후, 재귀호출을 통해 board를 사용하고 다시 tmp에 저장해뒀던 상태로 board를 돌려놓는다. 해당 부분이 이해가 가지 않아 얘기를 나누었다.
- static으로 정의해둔 board를 직접 사용하는 것이 아니라 tmp에 복사한 배열을 통째로 재귀로 넘기는 식으로 사용하면 더 좋을 것 같아보였다. (이후에 14502 연구소 풀 때 적용하였음)
완전탐색 시간복잡도
- 완탐은 가능한 모든 경우를 다 체크해서 정답을 찾는 것이므로, 무조건 문제 해결이 가능하지만 일반적으로 효율적이지 않은 방식이다.
- 완전탐색으로 풀기 위해 고려할 점?
★ 주의 : 입력 개수가 작을 때가 좋다. 보통 O(2^N) 또는 O(N!)이기 때문.
★ 팁 : 이럴 때 완탐을 쓰자! 1)답의 범위가 작음 2)임의의 답이 조건을 만족하는지 역추적할 수 있음
★ 아래 다섯가지 방법을 다 고려한다.
- Brute Force
- 반복/조건문으로 모두 테스트
- 순열(Permutation)
- n개의 원소 중 r개를 중복 허용 없이 나열하는 경우의 수
- 재귀호출
- 필요한 것: 탈출 조건, 현재 함수의 상태를 전달하는 Parameter
- 각 원소가 두 가지 상태만을 가질 때 사용하면 좋음.
- 포함 되면 해당 원소를 넣고, 함수 호출
- 포함 안 되면 안 넣고 함수 호출
- 비트마스크
- 이진수 표현기법 사용
- 각 원소가 두 가지 상태만을 가질 때 사용
- 예) 원소 n개인 집합의 모든 부분합을 구할 때, 각 원소가 포함되는지 아닌지를 1, 0으로 배열에 저장
- 각 원소가 두 가지 상태만을 가질 때 사용
- BFS와 DFS
- 그래프 자료구조에서 모든 정점을 탐색할 때.
- 한 요소를 방문하고, 그 요소에 인접한 모든 요소를 우선 방문하는 방식
- 주로 길찾기 문제에 사용
- 그래프 자료구조에서 모든 정점을 탐색할 때.
- 이진수 표현기법 사용
- Brute Force
DP vs 재귀호출
- DP도 Top-Down 사용 시 재귀호출을 수행한다!
기저 사례를 통해 탈출 조건을 만들고 현재 함수 상태를 Parameter로 전달한다.
return값을 정답 구하는 연산에 사용한다. - DP 재귀
- 작은 문제가 큰 문제와 동일한 구조
- 큰 문제 해결 시 작은 문제의 결과를 기억했다가 그대로 사용
- 완탐 재귀
- 크고 작은 문제의 구조가 다름
- 이전 결과를 반드시 기억하지 X
- 해결 가능한 방법을 모두 탐색하는 경우 사용
'BOJ' 카테고리의 다른 글
[백준 20922번 / java] 겹치는 건 싫어 (0) | 2024.07.09 |
---|---|
[백준 14502 / java] 연구소 (0) | 2024.07.09 |
[백준 15686번 / java] 치킨 배달 (1) | 2024.07.03 |
[백준 17291번 / java] 새끼치기 (0) | 2024.07.03 |
[백준 14499번 / java] 주사위 굴리기 (0) | 2024.06.30 |