https://www.acmicpc.net/problem/14939
코드설명
비트마스킹(BitMasking) + 그리디(Greedy) 를 활용합니다.
처음에 문제를 접했을때, 완전탐색으로 하는 것을 생각했습니다.
하지만, 100개의 전구를 껏다 키면 총 2^100 개의 가짓수가 나오게 됩니다.
그렇기에, 그리디 알고리즘을 사용하여 최대한 시간초과를 줄여야 합니다. 그 과정에서 비트마스킹을 활용하여 불필요한 메모리 공간을 줄일 수 있습니다. 비트마스킹을 활용하지 않고, 따로 VISITED 배열을 활용하여서 작업해도 되지만, 비트마스킹을 활용한다면 더 효과적입니다.
문제를 풀기 위해서는 아래의 성질을 이해합니다.
어떤 순서로 칸들을 클릭하든 상관이 없습니다. 각 칸의 상태는 자신과 인접한 칸들이 몇번 클릭되었는지에 따라 정해집니다. 따라서 어느 순서대로 클릭을 하건 같은 칸을 같은 횟수로 클릭한다면, 격자의 최종 상태는 변함이 없습니다.
한칸을 두 번 이상 클릭할 필요가 없습니다. 같은 칸을 두 번 클릭하는 것은 결국 한번도 클릭하지 않은 것과 같습니다.
앞에서 칸들을 클릭하는 순서는 상관이 없다는 것을 알았기 때문에, 같은 칸을 두번 이상 클릭하는 것은 아무 소용이 없습니다.
맨 위쪽 맨 왼쪽에서부터 클릭한다고 가정합니다. 즉, 맨 위 칸에서 모든 경우의 수를 만들어가면서 진행합니다.
이렇게 할경우 자연스레, 맨 아래줄칸 외에는 모든 칸이 불을 끈다는 점을 알 수 있습니다.
최적화하여 진행할경우,
시간복잡도는 O(2^10) + O(90) 이 됩니다.
기존에 O(2^25)에 비하면, 엄청나게 줄어듭니다.
코드를 작성하면서 헷갈렸던 부분들입니다.
첫번쨰는, 비트마스킹에 대한 이해입니다.
맨 첫번쨰 칸을 비트마스킹으로 진행했는데, 이 부분은 완탐으로 구현해도 됩니다. 하지만, 그렇게할경우 다른 함수를 만들어서 진행해야하기에 좋지 않습니다. 그러므로 비트마스킹을 활용하는 것을 추천합니다.
비트마스킹 구현 중에 처음에 == 1 조건을 넣었습니다. 코드에서 if( (i & (1 << j)) == 1 ) 조건이 == 1이 아닌 > 0이어야 하는 이유는 비트 연산의 결과를 올바르게 판별하기 위해서입니다. 비트 연산에서 i & (1 << j)는 i의 j번째 비트가 1인지 아닌지를 검사합니다. 이때, == 1로 비교하면 j가 0 일때만, 10진수로 변환되어 1이 나옵니다. 그러나 i의 j번째 비트가 1일 때는 j번째 비트가 1이기만 하면 되므로 > 0으로 검사해야 합니다.
다시 말해, i & (1 << j)의 결과는 i의 j번째 비트가 1인 경우 2의 j승 값이 되며, 0이 아닌 어떤 값이라도 됩니다. 그러므로 정확한 조건 검사는 > 0이어야 합니다. 이를 통해 j번째 비트가 1일 경우를 정확히 확인할 수 있습니다.
즉, 2진수로 변환하더라도 결국 비트연산자 & 로 반환되는 값은 10진수라는 것 입니다. 그렇기에 조건을 > 0 으로 해야만 합니다.
결국 우리는 위의 로직을 가지고 맨 위의 칸을 아무것도 안누르는 방식부터 (i = 0) ~ 모든 칸을 누르는 방식(i=1023)까지 숫자를 통해 모두 불을 킬 수 있습니다.
for(int i=0; i<(1<<10); i++) { //첫째줄을 먼저 결정시킵니다. 가장 맨 위줄부터 완성시킵니다. for(int j=0; j<10;j++) { if( (i & (1 << j)) > 0 ) { ret += turnLight(0, j); } } ... ...
두번쨰는, 모든 불이 꺼졌는지 확인합니다. 처음에 모든 경우를 다 확인했지만, 사실 마지막 줄만 확인해도 됩니다.
for(int col=0;col<10;col++) { if( map[9][col] == 'O') { isAllOff = false; break; } }
코드
비트마스킹과 그리디 알고리즘을 사용하여 시간을 단축시킨 코드입니다.
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.StringTokenizer; public class Main { public static int N, M; public static int answer = Integer.MAX_VALUE; public static char[][] map; public static int[] dx = {0,-1,1,0,0}; public static int[] dy = {0,0, 0,-1,1}; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // StringTokenizer st = new StringTokenizer(br.readLine()); map = new char[10][10]; for(int i=0;i<10;i++) { StringTokenizer st = new StringTokenizer(br.readLine()); String str = st.nextToken(); map[i] = str.toCharArray(); } // System.out.println(1 << 1); 1 << 1 : 2 이다. 비트를 왼쪽으로 1번 이동시킨다. 0001 에서 0010으로. // System.out.println(Math.pow(2, 10)); simulate(); // System.out.println(answer); } public static void simulate() { int switchCnt = 0; char[][] storeMap = new char[10][10]; for(int i=0;i<10;i++) { for(int j=0;j<10;j++) { storeMap[i][j] = map[i][j]; } } //for(int i=0; i<Math.pow(2, 10); i++) { //첫번째 행의 모든 가능한 상태를 확정하고, 맨 윗행부터 맨 아랫행까지 차례대로 전구를 꺼진상태로 변경합니다. //이때 배열을 통해서도 똑같이 구현가능하지만, 비트마스킹을 활용할경우 코드가 훨씬 간단해집니다. for(int i=0; i< (1 << 10) ; i++) { switchCnt = 0; for(int j=0;j<10;j++) { for(int k=0;k<10;k++) { map[j][k] = storeMap[j][k]; } } //첫번쨰 행의 전구를 비트마스킹에 따라 변화시키는 과정입니다. for(int c=0; c<10; c++) { if( (i & (1 << c)) > 0 ) { changeStatus(0, c); switchCnt += 1; } } for(int r=0; r< 9; r++) { for(int c=0; c<10; c++) { if(map[r][c] == 'O') { changeStatus(r + 1, c); switchCnt += 1; } } } if(isAllLightDown() == true) { answer = Math.min(answer, switchCnt); } } if(answer == Integer.MAX_VALUE) { System.out.println(-1); }else { System.out.println(answer); } } //기본적으로 clone()은 얕은복사.(참조 복사) //예외로, array에서 clone은 deep copy, public static void changeStatus(int r, int c) { for(int dir = 0; dir < 5; dir++) { int nr = r + dx[dir]; int nc = c + dy[dir]; if(nr < 0 || nr >= 10 || nc < 0 || nc >= 10) continue; map[nr][nc] = (map[nr][nc] == '#') ? 'O' : '#'; } } public static boolean isAllLightDown() { for(int c=0;c<10;c++) { if(map[9][c] == 'O') { return false; } } return true; } }
조금 다르게 풀어본 코드(위의 코드와 거의 동일)
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.StringTokenizer; public class Main { public static int C, N, M; public static int answer = Integer.MAX_VALUE; public static char[][] map; public static int[] dr = {0, -1, 1, 0, 0}; public static int[] dc = {0, 0, 0, -1, 1}; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // StringTokenizer st = new StringTokenizer(br.readLine()); map = new char[10][10]; for(int i=0;i<10;i++) { StringTokenizer st = new StringTokenizer(br.readLine()); map[i] = st.nextToken().toCharArray(); } System.out.println(countSwitchToLightDown()); } //어떤 순서로 칸들을 클릭하든 상관이 없습니다. 각 칸의 상태는 자신과 인접한 칸들이 몇번 클릭되었는지에 따라 정해집니다. //따라서 어느 순서대로 클릭을 하건 같은 칸을 같은 횟수로 클릭한다면, 격자의 최종 상태는 변함이 없습니다. //한칸을 두 번 이상 클릭할 필요가 없습니다. 같은 칸을 두 번 클릭하는 것은 결국 한번도 클릭하지 않은 것과 같습니다. //앞에서 칸들을 클릭하는 순서는 상관이 없다는 것을 알았기 때문에, 같은 칸을 두번 이상 클릭하는 것은 아무 소용이 없습니다. //맨 위쪽 맨 왼쪽에서부터 클릭한다고 가정합니다. 즉, 맨 위 칸에서 모든 경우의 수를 만들어가면서 진행합니다. public static int countSwitchToLightDown() { char[][] storeMap = new char[10][10]; for(int j=0; j<10; j++) { for(int k=0; k<10;k++) { storeMap[j][k] = map[j][k]; } } int ret = 0; int minRet = Integer.MAX_VALUE; //첫번째 칸을 먼저 완전탐색으로 결정시킵니다. ( 10칸 밖에 없기 떄문에 2^10 개의 연산으로 처리할 수 있습니다. ) //비트마스킹을 활용하지 않고, Combination 조합으로 진행해도 됩니다. //하지만, 비트마스킹을 활용할경우 더 깔끔한 코드가 완성되기에 비트마스킹을 사용합니다. for(int i=0; i<(1<<10); i++) { ret = 0; for(int j=0; j<10; j++) { for(int k=0; k<10;k++) { map[j][k] = storeMap[j][k]; } } //첫째줄을 먼저 결정시킵니다. 가장 맨 위줄부터 완성시킵니다. for(int j=0; j<10;j++) { if( (i & (1 << j)) > 0 ) { ret += turnLight(0, j); } } //나머지 줄들을 결정시킬 것 입니다. for(int row = 1; row <= 9; row++) { for(int col = 0; col <10; col++) { if(map[row-1][col] == 'O') { ret += turnLight(row, col); } } } boolean isAllOff = true; //모든 불이 꺼졌는지 확인합니다. 처음에 모든 경우를 다 확인했지만, 사실 마지막 줄만 확인해도 됩니다. // for(int row=0;row <10;row++) { // for(int col=0;col<10;col++) { // if( map[row][col] == 'O') { // isAllOff = false; // break; // } // } // if(isAllOff == false) break; // } for(int col=0;col<10;col++) { if( map[9][col] == 'O') { isAllOff = false; break; } } if(isAllOff == true) minRet = Math.min(minRet, ret); } return minRet; } public static int turnLight(int r, int c) { for(int dir = 0; dir < 5; dir++) { int nr = r + dr[dir]; int nc = c + dc[dir]; if(nr < 0 || nr >= 10 || nc < 0 || nc >= 10 ) continue; if( map[nr][nc] == '#') map[nr][nc] = 'O'; else map[nr][nc] = '#'; } return 1; } }
완전탐색코드(시간초과)
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.StringTokenizer; public class Main { public static int N, M; public static int answer = Integer.MAX_VALUE; public static char[][] map; public static int[] dx = {0,-1,1,0,0}; public static int[] dy = {0,0, 0,-1,1}; public static void main(String[] args) throws IOException{ BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // StringTokenizer st = new StringTokenizer(br.readLine()); map = new char[10][10]; for(int i=0;i<10;i++) { StringTokenizer st = new StringTokenizer(br.readLine()); String str = st.nextToken(); map[i] = str.toCharArray(); // for(int j=0;j<10;j++) { // System.out.print(map[i][j]); // } // System.out.println(); } simulate(0, 0, 0, 0); System.out.println(answer); } public static void simulate(int level, int r, int c, int switchCnt) { // System.out.println("=============="); // for(int i=0;i<10;i++) { // for(int j=0;j<10;j++) { // System.out.print(map[i][j]); // } // System.out.println(); // } if(level == 100) { if(isAllDown() == true) { answer = Math.min(answer, switchCnt); } return ; } if( r >= 10 ) { r = 0; c = c + 1; } char[][] storeMap = new char[10][10]; for(int i=0;i<10;i++) { for(int j=0;j<10;j++) { storeMap[i][j] = map[i][j]; } } //전구의 색깔을 바꾸는 경우 changeStatus(r, c); simulate(level + 1, r, c + 1, switchCnt + 1); //전구의 색깔을 바꾸지 않는 경우 for(int i=0;i<10;i++) { for(int j=0;j<10;j++) { map[i][j] = storeMap[i][j]; } } simulate(level + 1, r, c + 1, switchCnt); } public static void changeStatus(int r, int c) { for(int dir = 0; dir < 5; dir++) { int nr = r + dx[dir]; int nc = c + dy[dir]; if(nr < 0 || nr >= 10 || nc < 0 || nc >= 10) continue; map[nr][nc] = (map[nr][nc] == '#') ? 'O' : '#'; } } public static boolean isAllDown() { for(int i=0;i<10;i++) { for(int j=0;j<10;j++) { if(map[i][j] == 'O') return false; } } return true; } } class Node{ int r; int c; }
'알고리즘 > 백준' 카테고리의 다른 글
[백준] 11051 이항 계수 2 - 재귀함수(Recursive Function) JAVA (0) | 2024.07.02 |
---|---|
[백준] 11050 이항 계수 1 - 재귀함수(Recursive Function) JAVA (0) | 2024.07.02 |
[백준] 10709 기상캐스터 - BFS(너비우선탐색) JAVA (0) | 2024.05.07 |
[백준] 2775 부녀회장이 될테야 - DP(Dynamic Programming) JAVA (0) | 2024.04.04 |
[백준] 17779 게리맨더링2 - 시뮬레이션(Simulation) + 브루트포스(brute force) JAVA (0) | 2024.01.17 |