백준의 루빅의 사각형(2549) 문제이다.
[ 문제 바로가기 ]
[ 문제풀이 ]
접근하는 방법에 대해서 굉장히 오랫동안 고민했던 문제이다.
가장 단순하게 완전탐색을 하는 경우를 생각해보자. 쉽게 생각하기 위해서 초기에 모든 사각형에 정확한 숫자가 들어있다고
가정하겠다.
지금 현재 위와 같은 상황이라고 가정해 보겠다.
이 상태에서 한 번 움직이게 되면 몇 개가 틀려지는지 알아보자. 여기서 한 번 움직인다라는 것은 "한 행을 오른쪽으로 1 ~ 3칸
중 원하는 칸 수만큼 움직이는 것" 혹은 "한 열을 아래쪽으로 1 ~ 3칸 중 원하는 칸 수 만큼 움직이는 것" 을 의미한다.
그럼 가장 쉽게, 가장 첫 번째 행을 1칸 오른쪽으로 움직인 경우를 생각해보자.
그럼 위와 같은 모양이 될 것이다. 물론, 2칸씩 움직인 경우도, 3칸씩 움직인 경우도 "한 번 움직인 것" 에 해당되게 된다.
즉, 한번의 움직임으로 우리는 최대 3개의 상태를 만들어 낼 수 가 있다.
위의 3개의 그림은 모두 원래의 정확한 맵에서 "한 번 움직였을 때" 나올 수 있는 3가지 상태들을 나타낸 것이다.
즉, 우리는 한번의 움직임으로 3개의 상태를 만들어 낼 수 있다. 그렇다면 이 경우를 완전탐색으로 탐색해보면 몇 가지
경우를 고려해주어야 하는지 생각해보자.
한 번의 움직임으로 3개의 상태를 만들어 낼 수 있고, 우리는 이 한 번의 움직임을 어떤 행을 선택할지 어떤 열을 선택할지
선택할 수 있다. 즉 우리에게는 총 8개의 선택지가 존재한다.(4개의 행 + 4개의 열이 존재하므로)
그럼, 우리는 각 행, 각 열을 한 번씩 움직인다고 가정했을 때, 고려해봐야 할 경우의 수는 8 x 3 = 24 개이다.
왜냐하면, 각 행, 각 열을 한 번씩 움직일 때 마다 총 3개의 상태가 나올 수 있고, 그 3개의 상태가 8개의 선택지에 해당되기
때문에 "한 번 움직일 때 마다 24가지의 경우를 고려" 하게 된다.
그럼 그 이 후에 이 24가지 고려하는 상황을 몇 번을 더 생각해 줘야할까 ? 정말 다행히도 문제에서는 주어진 모든 입력은
"최대 7번" 타일을 움직이게 되면 원래의 정확한 맵과 똑같이 만들 수 있다고 힌트를 주었다.
즉, 24 가지 상황을 최악의 경우 최대 7번 까지 계산을 해야 한다는 것이다.
즉, 24 ^ 7 이 되고, 이는 약 40억이 넘는 연산을 진행하게 된다. 따라서 단순한 완전탐색으로 문제에 접근하게 되면
시간초과를 받게 된다.
따라서 시간초과를 면하기 위해서는 모든 경우를 탐색하는 것이 아닌, 일부 정답이 절대로 될 수 없는 경우들을 탐색하기
전에 미리 걸러주는 과정이 필요하다. 그럼 지금부터 어떤 경우를 걸러낼 수 있을지에 대해서 알아보자.
( 지금부터 사용하는 '정답맵' 이라는 단어는 아래 맵을 의미합니다. )
[ 정답맵 ]
( 또한, '틀린 갯수' 라는 말은, 정답맵과 현재맵을 비교했을 때 제 위치에 있지 않은 원소의 갯수를 의미합니다. )
먼저, 주어진 맵에서 정답맵과 한칸 한칸 비교해서 틀린 갯수를 통해서 앞으로 맵을 몇 번 회전시킬 수 있는지에
대해서 생각을 해보자.
생각해보면 정답맵에서 '한 번'을 움직이게 되면 4개의 숫자가 정답맵과 달라지게 된다.
예를 들어서 정답맵에서 가장 위의 행을 오른쪽으로 한 칸 움직였다고 생각해보자.
이런 형태가 될 것이고, 정답맵과 비교했을 때 첫 번째 행 4개의 숫자가 다르다는 것을 알 수 있다.
즉, 한 번의 움직에 의해서 정답맵과 비교했을 때 숫자가 4개 단위로 달라지게 된다.
위의 맵에서 두 번째 행을 오른쪽으로 한칸 움직여보자.
그럼 위와 같은 형태가 될 것이고, 정답맵과 총 8개의 숫자가 달라지게 된다.
하지만 무조건 4개 단위로 달라지지는 않는다. 첫 번째 행을 오른쪽으로 한 칸 움직인 맵에서 두 번째 행을 움직이는 것이
아니라, 첫 번째 열을 아래쪽으로 한 칸 움직인 상태를 봐보자.
이런 형태가 될 것이다. 정답맵과 비교를 했을 때, 총 7개의 숫자가 다르다. 이런 경우에는 위에서 말한 것 처럼 2번을
움직였음에도, 8개가 아닌 7개가 다르게 된다.
즉, 이 문제는 각각 한 칸 한 칸 단위가 아닌, 한 행 혹은 한 열 단위로 맵이 바뀌기 때문에 절대로 정답맵과 비교했을 때
1개 , 2개 , 혹은 5개 이러한 값들이 나올 수가 없다. 가장 간단하게 생각한다면, 현재 맵과 정답맵을 비교했을 때
틀린 갯수가 4의 배수라면, 틀린 갯수를 4로 나눈 값을 얼추 정답으로 예측할 수 있다.
예를 들어서 현재 맵과 정답 맵의 틀린 갯수를 비교했더니 4개가 나왔다면, 이 '4'라는 숫자를 통해서 "어쩌면, 한 번만 더
움직이면 정답맵이 될 수도 있겠다" 라고 추측할 수 있는 것이다.
하지만 틀린 갯수는 항상 4의 배수로만 존재하지는 않는다고 위에서 말했다. 위의 예시처럼 '7개'와 같이 4의 배수가
아닌 값들이 나올 수가 있다. 왜냐하면, 항상 깔끔하게 행에 대해서만, 열에 대해서만 움직이는 연산이 일어나는 것이 아니라,
행을 움직이고, 열을 움직이는 경우가 존재하기 때문이다. 이런 경우에는 '4의 배수'가 아닌 틀린 갯수가 나오게 될 것이다.
'7개'가 다를 경우에는 아마도 2번 움직이면 정답맵이 될 수도 있습니다 라고 생각을 할 수 있는 것이다.
따라서, '틀린 갯수 % 4' 를 한 값이 0이 아닐 경우, 즉, 4의 배수개로 틀린갯수가 아닐 경우에는 예측 가능한 움직여야 할
횟수를 + 1을 해주는 것이다. 7개로 계산을 해보면 7 / 4 = 1 이라서 일단 한번은 움직여야 하고, 7 % 4 != 0 이므로 + 1 번을
더해서 "아마도 2번을 움직이면 정답맵이 될 수도 있습니다." 라고 계산을 해볼 수 있다.
그럼 지금까지 왜 이런 이야기를 했고 무엇을 하기 위해서 이런 이야기를 했는지 정리해보자.
[ 지금까지 위의 내용이 무엇을 위한 과정일까 ? ]
완전탐색으로 모든 경우를 탐색하기에는 너무 많은 경우가 존재하고, 그 많은 경우에서 절대로 정답이 될 수 없는 경우를
걸러주기 위해서 "현재 맵에서 틀린 갯수를 통해서 대략 앞으로 몇 번 더 움직이면 정답맵이 될 수 있는지" 를 파악하기
위해서 틀린갯수와 틀린갯수에 따른 움직이는 횟수를 이야기한 것이다.
[ 그래서 어떤 경우를 걸러줄 수 있을까 ? ]
현재 맵의 상태를 보고 틀린 갯수를 파악할 수 있고, 그 틀린 갯수만으로 "어쩌면 X번만 더 돌리면 정답맵이 될 수도 있겠다"
라는 것을 알아냈으니, "현재 맵을 움직인 횟수 + X번" 한 값이, 기존에 정답으로 채택되어 있는 "움직이는 횟수"보다
더 크다면 굳이 탐색을 할 필요가 없는 것이다.
예를 들어서 기존에 정답으로 채택되어 있는 움직이는 횟수가 '3회' 라고 생각해보자.
즉, 기존에 어떤 방향으로 어떻게 어떻게 하다보니 3번만에 정답맵에 도달한 상태이다.
그런데, 현재 탐색하고 있는 과정에서 보니, "현재 맵을 2번 움직였고, 지금 이 상태에서 틀린 갯수가 8개" 라고 생각해보면
틀린갯수가 8개 이므로 어쩌면 정답맵이 되기 위해서는 2번만 더 돌리면 된다 라는 판단을 할 수 있게된다.
즉, 현재까지 맵을 움직인 횟수 = 2회 + 현재 틀린 갯수를 통해서 추측한 움직여야 할 예상 횟수 = 2회
총 4회가 된다. 이는, 정말로 4회가 걸린다고 하더라도 더 이상 탐색할 필요가 없다. 왜 ? 기존에 이미 3번만에 정답맵에
도달한 경우가 존재하는데 4회 걸리는 경우를 굳이 탐색을 해야할까? 할 필요가 없는 것이다.
이런 경우를 걸러줄 수 있는 것이다.
[ 소스코드 ]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 | #include<iostream> #include<vector> #define endl "\n" #define MAX 4 using namespace std; int Answer_Cnt = 8; int Answer_MAP[MAX][MAX]; int MAP[MAX][MAX]; int Process[10][3]; int Answer_Process[10][3]; void Input() { int Num = 1; for (int i = 0; i < MAX; i++) { for (int j = 0; j < MAX; j++) { cin >> MAP[i][j]; Answer_MAP[i][j] = Num++; } } } int Compare_MAP() { int Cnt = 0; for (int i = 0; i < MAX; i++) { for (int j = 0; j < MAX; j++) { if (MAP[i][j] != Answer_MAP[i][j]) Cnt++; } } return Cnt; } void Make_State(int RC, int Idx, int N, bool T) { int Arr[4]; if (RC == 1) // Idx번 행을, N칸 움직이는 걸로 바꾸겠다. { if (T == true) { for (int i = 0; i < 4; i++) Arr[(i + N) % 4] = MAP[Idx][i]; for (int i = 0; i < 4; i++) MAP[Idx][i] = Arr[i]; } else { int NN = 4 - N; for (int i = 0; i < 4; i++) Arr[(i + NN) % 4] = MAP[Idx][i]; for (int i = 0; i < 4; i++) MAP[Idx][i] = Arr[i]; } } else { if (T == true) { for (int i = 0; i < 4; i++) Arr[(i + N) % 4] = MAP[i][Idx]; for (int i = 0; i < 4; i++) MAP[i][Idx] = Arr[i]; } else { int NN = 4 - N; for (int i = 0; i < 4; i++) Arr[(i + NN) % 4] = MAP[i][Idx]; for (int i = 0; i < 4; i++) MAP[i][Idx] = Arr[i]; } } } void DFS(int Cnt) { /* Compare_MAP() = '틀린 갯수' 확인하기. */ int Default_Count = Compare_MAP(); if (Default_Count == 0) { /* 틀린 갯수가 0개일 때. */ /* 정답맵과 현재 맵 상태가 동일해 진 경우를 의미. */ Answer_Cnt = Cnt; for (int i = 0; i < Cnt; i++) { int RC = Process[i][0]; int Idx = Process[i][1]; int M_Cnt = Process[i][2]; Answer_Process[i][0] = RC; Answer_Process[i][1] = Idx; Answer_Process[i][2] = M_Cnt; } return; } /* 틀린 갯수를 통해서 앞으로 움직여야 할 횟수를 예측. */ int Candidate_Change = Default_Count / 4; if (Default_Count % 4 != 0) Candidate_Change++; /* 틀린 갯수가 항상 4의 배수는 아니다. */ /* 그런 경우에는 돌려야 할 예측 횟수를 + 1*/ int Possible = Cnt + Candidate_Change; if (Possible >= Answer_Cnt) return; /* 앞으로 돌려야할 예측횟수가 기존의 정답보다 더 큰 값이라면 */ /* 해당 경우는 더 이상 탐색해 볼 필요가 없다. */ /* 그게 아니라면, 행과 열을 돌아가면서 모두 바꿔보기. */ for (int k = 1; k <= 2; k++) { for (int i = 0; i < 4; i++) { for (int j = 1; j <= 3; j++) { Process[Cnt][0] = k; Process[Cnt][1] = i + 1; Process[Cnt][2] = j; Make_State(k, i, j, true); DFS(Cnt + 1); Make_State(k, i, j, false); } } } } void Solution() { DFS(0); cout << Answer_Cnt << endl; for (int i = 0; i < Answer_Cnt; i++) { cout << Answer_Process[i][0] << " " << Answer_Process[i][1] << " " << Answer_Process[i][2] << endl; } } void Solve() { Input(); Solution(); } int main(void) { ios::sync_with_stdio(false); cin.tie(NULL); cout.tie(NULL); //freopen("Input.txt", "r", stdin); Solve(); return 0; } | cs |
'[ BOJ Code ] > # BOJ -' 카테고리의 다른 글
[ 백준 11729 ] 하노이 탑 이동순서 (C++) (0) | 2020.04.30 |
---|---|
[ 백준 10875 ] 뱀 (C++) (1) | 2020.04.25 |
[ 백준 1202 ] 보석 도둑 (C++) (0) | 2020.04.21 |
[ 백준 1715 ] 카드 정렬하기 (C++) (2) | 2020.04.19 |
[ 백준 1516 ] 게임개발 (C++) (1) | 2020.04.19 |