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 |