알고리즘 문제 해결 전략의 세 번째 문제, 생각보다 쉽게 풀었다! 한 3트만에 성공한 듯. 재귀에 대한 이해느는 것 같아 기쁘다 ㅎㅎ
- 독해: 문제를 읽고 이해한다.
- 재정의+추상화: 문제를 익숙한 용어로 재정의한다.
- 설계: 어떻게 해결할지 계획을 세운다.
- 검증: 계획을 검증한다.
- 구현: 프로그램을 구현한다.
- 회고: 어떻게 풀었는지 돌아보고, 개선할 방법이 있는지 찾아본다.
16개의 시계가 주어지고, 각 시계는 3
, 6
, 9
, 12
의 값을 가진다. 여기서 10개의 스위치가 주어지는데, 각 스위치는 몇 개의 시계와 연결되어 있다. 스위치를 누르면 연결된 몇 개의 시계가 3시간 씩 돌아간다. 스위치를 최소한으로 눌러 모든 시계를 12시로 맞출 때, 최소의 버튼 누름 횟수를 구하라는 문제이다.
각 시계는
3
,6
,9
,12
의 값을 가짐: 3을 더하는 함수, 3을 빼는 함수를 각각 구현해야겠다는 생각이 드는 대목이다. 특히 3에서 3을 빼면 12, 12에서 3을 더하면 3이 됨을 예외 케이스로 처리해야 할 것이다.
각 스위치는 몇 개의 시계와 연결됨: 무슨 자료구조로 해당 데이터를 저장하면 좋을까? 나는 벡터 배열을 떠올렸다. 우선 스위치가 10개로 고정되어 있으니 배열을 쓰고, 각 스위치가 연결되어 있는 시계는 순회되어 활용될 것이 자명하기 때문에 벡터로 표현하면 좋을 것 같다. 백터 배열은 보드판 덮기 문제 (BOARDCOVER)에서 배웠다.
최소의 버튼 누름 횟수를 구하라: 최적화 문제이다. 최적화 문제는 다음과 같은 방식으로 풀 수 있다.
1. 완전 탐색
2. 동적 계획법
3. 조합 탐색
4. 최적화 문제를 결정 문제로 바꾸기
이번 문제에서는 재귀를 이용한 완전 탐색을 통해 해결한다.
조금만 더 생각해보면, 어차피 한 스위치는 4번 이상 누르지 못한다. (정확히는, 한 스위치를 4번 이상 누르면 그것과 동일한 결과를 내는 최소 누름 횟수가 존재한다.) 왜냐하면 시계가 낼 수 있는 모양이 4종류 밖에 없고, 4번 누르면 제자리로 돌아오기 때문이다. 즉, 시계가 낼 수 있는 모양은 주기가 4인 주기 함수로 볼 수 있다.
따라서, 각 스위치 누름의 횟수는 '0
, 1
, 2
, 3
' 4개 중 하나이다. 즉 이 문제는 10개의 스위치 각각의 누름의 횟수를 '0
, 1
, 2
, 3
'로 대입시켜보며 모든 시계를 12시로 만드는 최소 버튼 누름 개수를 확인하면 된다.
수많은 재귀를 이용한 완전 탐색 문제를 풀어보았던 시점에서... 재귀를 이용하면 될 것이라는 생각은 든다. 하지만 어떻게 코드를 작성해야 할지는 아직 잘 모르겠다 ㅠㅠ
비슷한 문제를 풀어본 적이 있다. 바로 예제로 주어진 여행하는 외판원 문제이다. (책 165p) 이 문제는 도시와 도시들 간 거리가 주어질 때, 모든 도시들을 방문하는 경로들 중 가장 짧은 것의 길이를 구하는 문제이다.
예제 코드는 다음과 같다.
int n; // 도시의 수
double dist[MAX][MAX]; // 두 도시 간의 거리를 저장하는 배열
// path: 지금까지 만든 경로
// visited: 각 도시의 방문 여부
// currentLength: 지금까지 만든 경로의 길이
// 나머지 도시들을 방문하는 경로들 중 가장 짧은 것의 길이를 반환한다.
double shortestPath(vector<int>& path, vector<bool>& visited, double currentLength) {
// 기저 사례: 모둔 도시를 다 방문했을 때는 시작 도시로 돌아가고 종료한다.
if(path.size() == n)
return currentLength + dist[path[0]][path.back()];
double ret = INF; // 매우 큰 값으로 초기화
// 다음 방문할 도시들을 전부 시도해 본다.
for(int next = 0; next < n; ++next) {
if(visited[next]) continue;
int here = path.back();
path.push_back(next);
visited[next] = true;
// 나머지 경로를 재귀 호출을 통해 완성하고 가장 짧은 경로의 길이를 얻는다.
double cand = shortestPath(path, visited, currentLength + dist[here][next];
ret = min(ret, cand);
visited[next] = false;
path.pop_back();
}
return ret;
}
요런 느낌?
이렇게 풀면 되지 않을까?
사실, 여기까지 생각이 들었다면... 그냥 연립 방정식으로 풀 수도 있지 않을까? 굳이 재귀를 하지 말고, 다중 for문을 구현하여 모든 경우를 탐색하는 것이다. 코드를 작성한다면 아마 이런 느낌일 것이다.
int minCntForClockSync() {
int ret = 987654321;
for (int x0 = 0; x0 < 4; ++x0) {
for (int x1 = 0; x1 < 4; ++x1) {
for (int x2 = 0; x2 < 4; ++x2) {
for (int x3 = 0; x3 < 4; ++x3) {
for (int x4 = 0; x4 < 4; ++x4) {
for (int x5 = 0; x5 < 4; ++x5) {
for (int x6 = 0; x6 < 4; ++x6) {
for (int x7 = 0; x7 < 4; ++x7) {
for (int x8 = 0; x8 < 4; ++x8) {
for (int x9 = 0; x9 < 4; ++x9) {
resOfCalc(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9);
if (clocksAllNoon()) {
ret = min(ret, x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9);
}
}
}
}
}
}
}
}
}
}
}
return ret;
}
물론 코드가 더욱 더러워지기는 했지만... 시간복잡도의 측면에서도 크게 차이가 없어 보인다.
10개의 스위치가 있는데, 각 스위치가 (0
, 1
, 2
, 3
)의 값을 가질 수 있으므로 관찰하는 경우의 수는 4^10 = 2^20
이므로 1억이 안된다. 물론 각 경우에 대한 구현이 어떻게 되느냐에 따라 시간은 더 들 수 있겠지만, 쉽게 10초를 넘기지는 못할 것이다. 즉, 완전 탐색으로 풀어도 문제가 되지는 않을 것이다.
#include <bits/stdc++.h>
#define endl "\n"
#define all(x) (x).begin(), (x).end()
#define sz(x) (int)(x).size()
using namespace std;
using lld = long long;
using pii = pair<int, int>;
using pll = pair<lld, lld>;
// variable
vector<int> clocks(16);
vector<int> switchConnectTo[10] = {
{0, 1, 2},
{3, 7, 9, 11},
{4, 10, 14, 15},
{0, 4, 5, 6, 7},
{6, 7, 8, 10, 12},
{0, 2, 14, 15},
{3, 14, 15},
{4, 5, 7, 14, 15},
{1, 2, 3, 4, 5},
{3, 4, 5, 9, 13}
};
int switchPushed[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
void printClocks() {
for (int i = 0; i < 16; ++i) {
cout << clocks[i] << " ";
}
cout << endl;
}
void plusThreeInClocks(int idx) {
if (clocks[idx] == 12) clocks[idx] = 3;
else clocks[idx] += 3;
}
void minusThreeInClocks(int idx) {
if (clocks[idx] == 3) clocks[idx] = 12;
else clocks[idx] -= 3;
}
bool clocksAllNoon() {
for (int i = 0; i < 16; ++i) {
if (clocks[i] != 12) return false;
}
return true;
}
bool allSwitchPushedFewerThanFour() {
for (int i = 0; i < 10; ++i) {
if (switchPushed[i] >= 4) return false;
}
return true;
}
int minCntForClockSync(int cnt) {
// printClocks();
if (clocksAllNoon()) return cnt;
if (!allSwitchPushedFewerThanFour()) return 987654321;
int ret = 987654321;
for (int s = 0; s < 10; ++s) {
switchPushed[s]++;
for (int c : switchConnectTo[s]) {
plusThreeInClocks(c);
}
int cand = minCntForClockSync(cnt + 1);
ret = min(ret, cand);
switchPushed[s]--;
for (int c : switchConnectTo[s]) {
minusThreeInClocks(c);
}
}
return ret;
}
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int c;
cin >> c;
while(c--) {
for (int i = 0; i < 16; i++) {
cin >> clocks[i];
}
int ans = minCntForClockSync(0);
if (ans == 987654321) cout << -1 << endl;
else cout << ans << endl;
}
return 0;
}
cnt
를 반환한다.INF
값을 반환한다.ret
값을 INF
로 정의하고, 모든 스위치들에 대하여 스위치 누르고 -> 재귀하여 결과 값 받고 -> 스위치 안눌렀다 치고 를 반복하여 결과 값의 최솟값을 얻는다.ret
을 반환한다.결과는...
시간 초과! ㅠㅠ
allSwitchPushedFewerThanFour()
이 함수에서 시간을 많이 잡아먹는 것으로 추론했다. (사실 문제는 이게 아니었지만... 후술 예정) 어차피 switchPushed
배열로 스위치의 누름 횟수를 알 수 있으니 누름 횟수가 4 이상이라면 호출을 하지 않아 굳이 확인 함수를 만들지 않고도 호출 횟수를 줄일 수 있을 것이다! 수정한 코드는 다음과 같다.
`
#include <bits/stdc++.h>
#define endl "\n"
#define all(x) (x).begin(), (x).end()
#define sz(x) (int)(x).size()
using namespace std;
using lld = long long;
using pii = pair<int, int>;
using pll = pair<lld, lld>;
// variable
vector<int> clocks(16);
vector<int> switchConnectTo[10] = {
{0, 1, 2},
{3, 7, 9, 11},
{4, 10, 14, 15},
{0, 4, 5, 6, 7},
{6, 7, 8, 10, 12},
{0, 2, 14, 15},
{3, 14, 15},
{4, 5, 7, 14, 15},
{1, 2, 3, 4, 5},
{3, 4, 5, 9, 13}
};
int switchPushed[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
void printClocks() {
for (int i = 0; i < 16; ++i) {
cout << clocks[i] << " ";
}
cout << endl;
}
void plusThreeInClocks(int idx) {
if (clocks[idx] == 12) clocks[idx] = 3;
else clocks[idx] += 3;
}
void minusThreeInClocks(int idx) {
if (clocks[idx] == 3) clocks[idx] = 12;
else clocks[idx] -= 3;
}
bool clocksAllNoon() {
for (int i = 0; i < 16; ++i) {
if (clocks[i] != 12) return false;
}
return true;
}
int minCntForClockSync(int cnt) {
// printClocks();
if (clocksAllNoon()) return cnt;
int ret = 987654321;
for (int s = 0; s < 10; ++s) {
if (switchPushed[s] >= 3) continue;
switchPushed[s]++;
for (int c : switchConnectTo[s]) {
plusThreeInClocks(c);
}
int cand = minCntForClockSync(cnt + 1);
ret = min(ret, cand);
switchPushed[s]--;
for (int c : switchConnectTo[s]) {
minusThreeInClocks(c);
}
}
return ret;
}
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int c;
cin >> c;
while(c--) {
for (int i = 0; i < 16; i++) {
cin >> clocks[i];
}
int ans = minCntForClockSync(0);
if (ans == 987654321) cout << -1 << endl;
else cout << ans << endl;
}
return 0;
}
추가한 코드는 다음과 같다.
if (switchPushed[s] >= 3) continue;
for문 안에 다음 코드를 작성함으로써 만약 현재 스위치 누름 횟수가 3 이상이라면 누를 필요가 없으므로 (0
, 1
, 2
, 3
이 가능한 경우인데 가능한 경우를 모두 돎) continue
를 통해 for문을 건너 뛸 수 있다.
결과는?
또! 시간초과... 뭐가 문제인지 곰곰히 생각해보니...! 아!
switchPushed
를 출력해보니...
0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0
3 1 0 0 0 0 0 0 0 0
3 2 0 0 0 0 0 0 0 0
3 3 0 0 0 0 0 0 0 0
3 3 1 0 0 0 0 0 0 0
3 3 2 0 0 0 0 0 0 0
3 3 3 0 0 0 0 0 0 0
3 3 3 1 0 0 0 0 0 0
3 3 3 2 0 0 0 0 0 0
3 3 3 3 0 0 0 0 0 0
3 3 3 3 1 0 0 0 0 0
3 3 3 3 2 0 0 0 0 0
3 3 3 3 3 0 0 0 0 0
3 3 3 3 3 1 0 0 0 0
3 3 3 3 3 2 0 0 0 0
3 3 3 3 3 3 0 0 0 0
3 3 3 3 3 3 1 0 0 0
3 3 3 3 3 3 2 0 0 0
3 3 3 3 3 3 3 0 0 0
3 3 3 3 3 3 3 1 0 0
3 3 3 3 3 3 3 2 0 0
3 3 3 3 3 3 3 3 0 0
3 3 3 3 3 3 3 3 1 0
3 3 3 3 3 3 3 3 2 0
3 3 3 3 3 3 3 3 3 0
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 1
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 1
3 3 3 3 3 3 3 3 2 1
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 2
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 0 1
3 3 3 3 3 3 3 3 1 1
3 3 3 3 3 3 3 3 2 1
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 2
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 0 2
3 3 3 3 3 3 3 3 1 2
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 0 3
3 3 3 3 3 3 3 3 1 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 2 1 0
3 3 3 3 3 3 3 3 1 0
3 3 3 3 3 3 3 3 2 0
3 3 3 3 3 3 3 3 3 0
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 1
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 1 1
3 3 3 3 3 3 3 3 2 1
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
...
처음에는 경우의 수를 잘 세다가 뒤로 갈수록 이상해진다. 순회의 첫 부분을 알려줄 부분이 필요해보인다!
#include <bits/stdc++.h>
#define endl "\n"
#define all(x) (x).begin(), (x).end()
#define sz(x) (int)(x).size()
using namespace std;
using lld = long long;
using pii = pair<int, int>;
using pll = pair<lld, lld>;
// variable
vector<int> clocks(16);
vector<int> switchConnectTo[10] = {
{0, 1, 2},
{3, 7, 9, 11},
{4, 10, 14, 15},
{0, 4, 5, 6, 7},
{6, 7, 8, 10, 12},
{0, 2, 14, 15},
{3, 14, 15},
{4, 5, 7, 14, 15},
{1, 2, 3, 4, 5},
{3, 4, 5, 9, 13}
};
int switchPushed[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
void printClocks() {
for (int i = 0; i < 16; ++i) {
cout << clocks[i] << " ";
}
cout << endl;
}
void plusThreeInClocks(int idx) {
if (clocks[idx] == 12) clocks[idx] = 3;
else clocks[idx] += 3;
}
void minusThreeInClocks(int idx) {
if (clocks[idx] == 3) clocks[idx] = 12;
else clocks[idx] -= 3;
}
bool clocksAllNoon() {
for (int i = 0; i < 16; ++i) {
if (clocks[i] != 12) return false;
}
return true;
}
int minCntForClockSync(int cnt, int startIdx) {
// printClocks();
if (clocksAllNoon()) return cnt;
int ret = 987654321;
for (int s = startIdx; s < 10; ++s) {
if (switchPushed[s] >= 3) continue;
switchPushed[s]++;
for (int c : switchConnectTo[s]) {
plusThreeInClocks(c);
}
int cand = minCntForClockSync(cnt + 1, s);
ret = min(ret, cand);
switchPushed[s]--;
for (int c : switchConnectTo[s]) {
minusThreeInClocks(c);
}
}
return ret;
}
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int c;
cin >> c;
while(c--) {
for (int i = 0; i < 16; i++) {
cin >> clocks[i];
}
int ans = minCntForClockSync(0, 0);
if (ans == 987654321) cout << -1 << endl;
else cout << ans << endl;
}
return 0;
}
startIdx
를 추가하였다.
스위치 눌림 배열을 출력해보면?
0 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 0 0 0
2 0 0 0 0 0 0 0 0 0
3 0 0 0 0 0 0 0 0 0
3 1 0 0 0 0 0 0 0 0
3 2 0 0 0 0 0 0 0 0
3 3 0 0 0 0 0 0 0 0
3 3 1 0 0 0 0 0 0 0
3 3 2 0 0 0 0 0 0 0
3 3 3 0 0 0 0 0 0 0
3 3 3 1 0 0 0 0 0 0
3 3 3 2 0 0 0 0 0 0
3 3 3 3 0 0 0 0 0 0
3 3 3 3 1 0 0 0 0 0
3 3 3 3 2 0 0 0 0 0
3 3 3 3 3 0 0 0 0 0
3 3 3 3 3 1 0 0 0 0
3 3 3 3 3 2 0 0 0 0
3 3 3 3 3 3 0 0 0 0
3 3 3 3 3 3 1 0 0 0
3 3 3 3 3 3 2 0 0 0
3 3 3 3 3 3 3 0 0 0
3 3 3 3 3 3 3 1 0 0
3 3 3 3 3 3 3 2 0 0
3 3 3 3 3 3 3 3 0 0
3 3 3 3 3 3 3 3 1 0
3 3 3 3 3 3 3 3 2 0
3 3 3 3 3 3 3 3 3 0
3 3 3 3 3 3 3 3 3 1
3 3 3 3 3 3 3 3 3 2
3 3 3 3 3 3 3 3 3 3
3 3 3 3 3 3 3 3 2 1
3 3 3 3 3 3 3 3 2 2
3 3 3 3 3 3 3 3 2 3
3 3 3 3 3 3 3 3 1 1
3 3 3 3 3 3 3 3 1 2
3 3 3 3 3 3 3 3 1 3
3 3 3 3 3 3 3 3 0 1
3 3 3 3 3 3 3 3 0 2
3 3 3 3 3 3 3 3 0 3
3 3 3 3 3 3 3 2 1 0
3 3 3 3 3 3 3 2 2 0
3 3 3 3 3 3 3 2 3 0
3 3 3 3 3 3 3 2 3 1
3 3 3 3 3 3 3 2 3 2
3 3 3 3 3 3 3 2 3 3
3 3 3 3 3 3 3 2 2 1
3 3 3 3 3 3 3 2 2 2
3 3 3 3 3 3 3 2 2 3
3 3 3 3 3 3 3 2 1 1
3 3 3 3 3 3 3 2 1 2
3 3 3 3 3 3 3 2 1 3
3 3 3 3 3 3 3 2 0 1
3 3 3 3 3 3 3 2 0 2
3 3 3 3 3 3 3 2 0 3
3 3 3 3 3 3 3 1 1 0
3 3 3 3 3 3 3 1 2 0
3 3 3 3 3 3 3 1 3 0
3 3 3 3 3 3 3 1 3 1
3 3 3 3 3 3 3 1 3 2
3 3 3 3 3 3 3 1 3 3
3 3 3 3 3 3 3 1 2 1
3 3 3 3 3 3 3 1 2 2
3 3 3 3 3 3 3 1 2 3
3 3 3 3 3 3 3 1 1 1
3 3 3 3 3 3 3 1 1 2
3 3 3 3 3 3 3 1 1 3
3 3 3 3 3 3 3 1 0 1
3 3 3 3 3 3 3 1 0 2
3 3 3 3 3 3 3 1 0 3
3 3 3 3 3 3 3 0 1 0
3 3 3 3 3 3 3 0 2 0
3 3 3 3 3 3 3 0 3 0
3 3 3 3 3 3 3 0 3 1
3 3 3 3 3 3 3 0 3 2
3 3 3 3 3 3 3 0 3 3
3 3 3 3 3 3 3 0 2 1
3 3 3 3 3 3 3 0 2 2
3 3 3 3 3 3 3 0 2 3
3 3 3 3 3 3 3 0 1 1
3 3 3 3 3 3 3 0 1 2
3 3 3 3 3 3 3 0 1 3
3 3 3 3 3 3 3 0 0 1
3 3 3 3 3 3 3 0 0 2
3 3 3 3 3 3 3 0 0 3
3 3 3 3 3 3 2 1 0 0
3 3 3 3 3 3 2 2 0 0
3 3 3 3 3 3 2 3 0 0
3 3 3 3 3 3 2 3 1 0
3 3 3 3 3 3 2 3 2 0
3 3 3 3 3 3 2 3 3 0
3 3 3 3 3 3 2 3 3 1
3 3 3 3 3 3 2 3 3 2
3 3 3 3 3 3 2 3 3 3
3 3 3 3 3 3 2 3 2 1
3 3 3 3 3 3 2 3 2 2
3 3 3 3 3 3 2 3 2 3
3 3 3 3 3 3 2 3 1 1
3 3 3 3 3 3 2 3 1 2
3 3 3 3 3 3 2 3 1 3
3 3 3 3 3 3 2 3 0 1
3 3 3 3 3 3 2 3 0 2
3 3 3 3 3 3 2 3 0 3
3 3 3 3 3 3 2 2 1 0
3 3 3 3 3 3 2 2 2 0
3 3 3 3 3 3 2 2 3 0
3 3 3 3 3 3 2 2 3 1
3 3 3 3 3 3 2 2 3 2
3 3 3 3 3 3 2 2 3 3
3 3 3 3 3 3 2 2 2 1
3 3 3 3 3 3 2 2 2 2
3 3 3 3 3 3 2 2 2 3
3 3 3 3 3 3 2 2 1 1
3 3 3 3 3 3 2 2 1 2
3 3 3 3 3 3 2 2 1 3
3 3 3 3 3 3 2 2 0 1
3 3 3 3 3 3 2 2 0 2
3 3 3 3 3 3 2 2 0 3
3 3 3 3 3 3 2 1 1 0
3 3 3 3 3 3 2 1 2 0
3 3 3 3 3 3 2 1 3 0
3 3 3 3 3 3 2 1 3 1
3 3 3 3 3 3 2 1 3 2
3 3 3 3 3 3 2 1 3 3
3 3 3 3 3 3 2 1 2 1
3 3 3 3 3 3 2 1 2 2
3 3 3 3 3 3 2 1 2 3
3 3 3 3 3 3 2 1 1 1
3 3 3 3 3 3 2 1 1 2
3 3 3 3 3 3 2 1 1 3
3 3 3 3 3 3 2 1 0 1
3 3 3 3 3 3 2 1 0 2
3 3 3 3 3 3 2 1 0 3
3 3 3 3 3 3 2 0 1 0
3 3 3 3 3 3 2 0 2 0
3 3 3 3 3 3 2 0 3 0
3 3 3 3 3 3 2 0 3 1
3 3 3 3 3 3 2 0 3 2
3 3 3 3 3 3 2 0 3 3
3 3 3 3 3 3 2 0 2 1
3 3 3 3 3 3 2 0 2 2
3 3 3 3 3 3 2 0 2 3
3 3 3 3 3 3 2 0 1 1
3 3 3 3 3 3 2 0 1 2
3 3 3 3 3 3 2 0 1 3
3 3 3 3 3 3 2 0 0 1
3 3 3 3 3 3 2 0 0 2
3 3 3 3 3 3 2 0 0 3
3 3 3 3 3 3 1 1 0 0
3 3 3 3 3 3 1 2 0 0
3 3 3 3 3 3 1 3 0 0
3 3 3 3 3 3 1 3 1 0
3 3 3 3 3 3 1 3 2 0
3 3 3 3 3 3 1 3 3 0
3 3 3 3 3 3 1 3 3 1
3 3 3 3 3 3 1 3 3 2
3 3 3 3 3 3 1 3 3 3
3 3 3 3 3 3 1 3 2 1
3 3 3 3 3 3 1 3 2 2
3 3 3 3 3 3 1 3 2 3
3 3 3 3 3 3 1 3 1 1
3 3 3 3 3 3 1 3 1 2
3 3 3 3 3 3 1 3 1 3
3 3 3 3 3 3 1 3 0 1
3 3 3 3 3 3 1 3 0 2
3 3 3 3 3 3 1 3 0 3
3 3 3 3 3 3 1 2 1 0
3 3 3 3 3 3 1 2 2 0
3 3 3 3 3 3 1 2 3 0
3 3 3 3 3 3 1 2 3 1
3 3 3 3 3 3 1 2 3 2
3 3 3 3 3 3 1 2 3 3
3 3 3 3 3 3 1 2 2 1
3 3 3 3 3 3 1 2 2 2
3 3 3 3 3 3 1 2 2 3
3 3 3 3 3 3 1 2 1 1
3 3 3 3 3 3 1 2 1 2
3 3 3 3 3 3 1 2 1 3
3 3 3 3 3 3 1 2 0 1
3 3 3 3 3 3 1 2 0 2
3 3 3 3 3 3 1 2 0 3
3 3 3 3 3 3 1 1 1 0
3 3 3 3 3 3 1 1 2 0
3 3 3 3 3 3 1 1 3 0
3 3 3 3 3 3 1 1 3 1
3 3 3 3 3 3 1 1 3 2
3 3 3 3 3 3 1 1 3 3
3 3 3 3 3 3 1 1 2 1
3 3 3 3 3 3 1 1 2 2
3 3 3 3 3 3 1 1 2 3
3 3 3 3 3 3 1 1 1 1
3 3 3 3 3 3 1 1 1 2
3 3 3 3 3 3 1 1 1 3
3 3 3 3 3 3 1 1 0 1
3 3 3 3 3 3 1 1 0 2
3 3 3 3 3 3 1 1 0 3
3 3 3 3 3 3 1 0 1 0
3 3 3 3 3 3 1 0 2 0
3 3 3 3 3 3 1 0 3 0
3 3 3 3 3 3 1 0 3 1
3 3 3 3 3 3 1 0 3 2
3 3 3 3 3 3 1 0 3 3
3 3 3 3 3 3 1 0 2 1
3 3 3 3 3 3 1 0 2 2
3 3 3 3 3 3 1 0 2 3
3 3 3 3 3 3 1 0 1 1
3 3 3 3 3 3 1 0 1 2
3 3 3 3 3 3 1 0 1 3
3 3 3 3 3 3 1 0 0 1
3 3 3 3 3 3 1 0 0 2
3 3 3 3 3 3 1 0 0 3
3 3 3 3 3 3 0 1 0 0
3 3 3 3 3 3 0 2 0 0
3 3 3 3 3 3 0 3 0 0
3 3 3 3 3 3 0 3 1 0
3 3 3 3 3 3 0 3 2 0
3 3 3 3 3 3 0 3 3 0
3 3 3 3 3 3 0 3 3 1
3 3 3 3 3 3 0 3 3 2
3 3 3 3 3 3 0 3 3 3
3 3 3 3 3 3 0 3 2 1
3 3 3 3 3 3 0 3 2 2
3 3 3 3 3 3 0 3 2 3
3 3 3 3 3 3 0 3 1 1
3 3 3 3 3 3 0 3 1 2
3 3 3 3 3 3 0 3 1 3
3 3 3 3 3 3 0 3 0 1
3 3 3 3 3 3 0 3 0 2
3 3 3 3 3 3 0 3 0 3
3 3 3 3 3 3 0 2 1 0
3 3 3 3 3 3 0 2 2 0
3 3 3 3 3 3 0 2 3 0
3 3 3 3 3 3 0 2 3 1
3 3 3 3 3 3 0 2 3 2
3 3 3 3 3 3 0 2 3 3
3 3 3 3 3 3 0 2 2 1
3 3 3 3 3 3 0 2 2 2
3 3 3 3 3 3 0 2 2 3
3 3 3 3 3 3 0 2 1 1
3 3 3 3 3 3 0 2 1 2
3 3 3 3 3 3 0 2 1 3
3 3 3 3 3 3 0 2 0 1
3 3 3 3 3 3 0 2 0 2
3 3 3 3 3 3 0 2 0 3
3 3 3 3 3 3 0 1 1 0
3 3 3 3 3 3 0 1 2 0
3 3 3 3 3 3 0 1 3 0
3 3 3 3 3 3 0 1 3 1
3 3 3 3 3 3 0 1 3 2
3 3 3 3 3 3 0 1 3 3
3 3 3 3 3 3 0 1 2 1
3 3 3 3 3 3 0 1 2 2
3 3 3 3 3 3 0 1 2 3
3 3 3 3 3 3 0 1 1 1
3 3 3 3 3 3 0 1 1 2
3 3 3 3 3 3 0 1 1 3
3 3 3 3 3 3 0 1 0 1
3 3 3 3 3 3 0 1 0 2
3 3 3 3 3 3 0 1 0 3
3 3 3 3 3 3 0 0 1 0
3 3 3 3 3 3 0 0 2 0
3 3 3 3 3 3 0 0 3 0
3 3 3 3 3 3 0 0 3 1
3 3 3 3 3 3 0 0 3 2
3 3 3 3 3 3 0 0 3 3
3 3 3 3 3 3 0 0 2 1
3 3 3 3 3 3 0 0 2 2
3 3 3 3 3 3 0 0 2 3
3 3 3 3 3 3 0 0 1 1
3 3 3 3 3 3 0 0 1 2
3 3 3 3 3 3 0 0 1 3
3 3 3 3 3 3 0 0 0 1
3 3 3 3 3 3 0 0 0 2
3 3 3 3 3 3 0 0 0 3
...
배열이 경우의 수를 잘 세고 있음을 알 수 있다.
결과는?
성공!
clocks
, switchConnectedTo
, switchPushed
를 글로벌 변수로 작성했다.plusThreeInClocks()
, 3시간을 빼는 함수 minusThreeInClocks()
, 모든 시계가 12시를 가리키고 있는지 확인하는 함수 clocksAllNoon()
, 으로 함수를 쪼개어 적극적으로 재사용했다. 덕분에 정답 코드가 훨씬 간결하고, 이해하기 쉬워졌다.vector<int>
를 적극적으로 사용했다. 특히 switchConnectedTo
의 경우 vector<int>
를 원소로 가지는 배열로써, 후에 범위 기반 반복문 문법을 사용하여 코드의 간결성을 높일 수 있었다. (for (int c : switchConnectTo[s])
)clocks
, switchConnectedTo
, switchPushed
, plusThreeInClocks()
, minusThreeInClocks()
, clocksAllNoon()
등 모두 명료한 이름을 가진다.switchConnectTo[10]
가 대표적인 예이다. 각 스위치가 어느 시계와 연결되어 있는지에 대한 데이터인데, 코드와 분리됨으로서 코드의 양을 줄인다.그냥 10중 for문을 만들어서 코드를 작성해도 원리 상으로는 별 차이가 없지 않을까?
코드를 작성하기 전에는 만만해보였는데 막상 코드를 작성하려니 애먹었다;; 많이 작성해보지 않은 코드여서 그런듯
#include <bits/stdc++.h>
#define endl "\n"
#define all(x) (x).begin(), (x).end()
#define sz(x) (int)(x).size()
using namespace std;
using lld = long long;
using pii = pair<int, int>;
using pll = pair<lld, lld>;
// variable
vector<int> clocks(16);
vector<int> switchConnectTo[10] = {
{0, 1, 2},
{3, 7, 9, 11},
{4, 10, 14, 15},
{0, 4, 5, 6, 7},
{6, 7, 8, 10, 12},
{0, 2, 14, 15},
{3, 14, 15},
{4, 5, 7, 14, 15},
{1, 2, 3, 4, 5},
{3, 4, 5, 9, 13}
};
void plusThreeInClocks(vector<int>& _clocks, int idx) {
if (_clocks[idx] == 12) _clocks[idx] = 3;
else _clocks[idx] += 3;
}
bool clocksAllNoon(vector<int> _clocks) {
for (int i = 0; i < 16; ++i) {
if (_clocks[i] != 12) return false;
}
return true;
}
vector<int> resOfCalc(int v0, int v1, int v2, int v3, int v4, int v5, int v6, int v7, int v8, int v9) {
vector<int> resClocks = clocks;
while(v0--) {
for (int c : switchConnectTo[0]) {
plusThreeInClocks(resClocks, c);
}
}
while(v1--) {
for (int c : switchConnectTo[1]) {
plusThreeInClocks(resClocks, c);
}
}
while(v2--) {
for (int c : switchConnectTo[2]) {
plusThreeInClocks(resClocks, c);
}
}
while(v3--) {
for (int c : switchConnectTo[3]) {
plusThreeInClocks(resClocks, c);
}
}
while(v4--) {
for (int c : switchConnectTo[4]) {
plusThreeInClocks(resClocks, c);
}
}
while(v5--) {
for (int c : switchConnectTo[5]) {
plusThreeInClocks(resClocks, c);
}
}
while(v6--) {
for (int c : switchConnectTo[6]) {
plusThreeInClocks(resClocks, c);
}
}
while(v7--) {
for (int c : switchConnectTo[7]) {
plusThreeInClocks(resClocks, c);
}
}
while(v8--) {
for (int c : switchConnectTo[8]) {
plusThreeInClocks(resClocks, c);
}
}
while(v9--) {
for (int c : switchConnectTo[9]) {
plusThreeInClocks(resClocks, c);
}
}
return resClocks;
}
int minCntForClockSync() {
int ret = 987654321;
for (int x0 = 0; x0 < 4; ++x0) {
for (int x1 = 0; x1 < 4; ++x1) {
for (int x2 = 0; x2 < 4; ++x2) {
for (int x3 = 0; x3 < 4; ++x3) {
for (int x4 = 0; x4 < 4; ++x4) {
for (int x5 = 0; x5 < 4; ++x5) {
for (int x6 = 0; x6 < 4; ++x6) {
for (int x7 = 0; x7 < 4; ++x7) {
for (int x8 = 0; x8 < 4; ++x8) {
for (int x9 = 0; x9 < 4; ++x9) {
vector<int> newClocks(16);
newClocks = resOfCalc(x0, x1, x2, x3, x4, x5, x6, x7, x8, x9);
if (clocksAllNoon(newClocks)) {
ret = min(ret, x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9);
}
}
}
}
}
}
}
}
}
}
}
return ret;
}
int main()
{
ios_base::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int c;
cin >> c;
while(c--) {
for (int i = 0; i < 16; i++) {
cin >> clocks[i];
}
int ans = minCntForClockSync();
if (ans == 987654321) cout << -1 << endl;
else cout << ans << endl;
}
return 0;
}
clocks
에서 10개의 스위치를 주어진 순열만큼 만큼 누른 결과를 저장한다.ret
는 스위치 누른 총 횟수의 최솟값으로 바꾼다.결과는?
정답! 하지만 시간은 약 5초 정도 걸렸고, 이는 재귀를 사용했을 때보다 10배 정도 느린 수치이다.
반복 완전 탐색을 이용한 최적화 문제(최대 최소 문제)의 알짜 코드는 다음과 같다.
int minCnt(){
// 고를 수 있는 원소들의 집합 순회하며 최솟값 찾기
int ret = INF;
for(next = 원소 in 고를 수 있는 원소들의 집합) {
int cand = next를 처리한 결과;
ret = min(ret, cand);
}
return ret;
}
ret
를 INF
로 정의cand
정의 후 next
를 처리하여 cand
답 채우기ret = min(ret, cand);
최적화 문제 with 반복 완전 탐색 문제는 다음과 같은 과정을 거친다.
1. for문 밖에서ret
를INF
로 정의
2. 선택 가능한 모든 원소에 대하여
3. 반환 후보cand
정의 후next
를 이용하여cand
답 채우기
4.ret = min(ret, cand);
재귀 완전 탐색을 이용한 최적화 문제(최대 최소 문제)의 알짜 코드는 다음과 같다.
int minCnt(vector<int>& path, int cnt){
if (주어진 조건 충족) {
return cnt;
}
if (주어진 조건 미충족) {
return INF;
}
// 고를 수 있는 원소들의 집합 순회하며 재귀함수 호출
int ret = INF;
for(next = 원소 in 고를 수 있는 원소들의 집합) {
path.push_back(next);
int cand = minCnt(path, cnt+1);
ret = min(ret, cand);
path.pop_back();
}
return ret;
}
ret
를 INF
로 정의cand
정의 후 재귀, ret = min(ret, cand)
최적화 문제 with 재귀 완전 탐색 문제는 다음과 같은 과정을 거친다.
1. for문 밖에서ret
를INF
로 정의
2. 해당 단계에서 선택 가능한 모든 원소에 대하여 원소 고르기
3. 반환 후보cand
정의 후 재귀,ret = min(ret, cand)
4. 원소 뺀 후 다음 원소 선택 ... (2번으로 돌아감)
INF
는 무슨 값을 써야 좋을까?책 68p를 보면 987654321
을 사용하라고 나와 있다. 이유는 2^30
에 가까운 매우 큰 값이면서 서로 더해지더라도 32bit signed int에서 오버플로우를 내지 않고, 1000000000
보다 오타가 났는지 눈으로 확인하기도 용이하기 때문이다.
const int INF = 987654321
INF
값은987654321
을 사용하자!
여담으로, 위에 코드를 작성할 때 한 가지 놓쳤던 것이, INF
변수를 사용하지 않고 987654321
을 그대로 정답 코드에 작성했다는 점이다. 이는 컴파일러가 잡아주지 못하는 상수 오타를 발생시킬 수 있으므로, 안 좋은 코드이다. (반성!)
reference
는 언제 써야할까?반복문을 이용한 풀이에서 보면 함수들에게 reference
로 벡터를 인자로 전달하는 경우가 있다.
void plusThreeInClocks(vector<int>& _clocks, int idx) {
if (_clocks[idx] == 12) _clocks[idx] = 3;
else _clocks[idx] += 3;
}
reference
는 어떤 local 변수를 함수 내부에서 수정하고 싶을 때 사용한다. 이때, type 뒤에 &
를 붙여주면 된다.
reference
는 어떤 local 변수를 함수 내부에서 수정하고 싶을 때 사용한다. 이때, type 뒤에&
를 붙여주면 된다.
여담으로, reference와 pointer에 대한 설명은 다음 링크를 참고한다.
문제에서 요구하는 대로 답이 없는 경우에 -1
을 출력하고자, minCntForClockSync()
에서 직접 -1
을 출력하려고 하면 어렵다. 왜냐하면 해당 함수는 답이 없는 경우에 INF
를 반환하기 때문이다. 따라서 함수 내에서 직접-1
을 반환하려고 하지 말고, 마지막에 답을 출력할 때 답이 INF
라면 -1
을 대신 출력해주면 된다.
답이 없는 경우에
-1
을 출력하라고 하면, 함수 내에서 직접-1
을 반환하려고 하지 말고, 마지막에 답을 출력할 때 답이 매우 크다면-1
을 대신 출력해주면 된다.
처음 깨달아야 하는 것은, 예제 입출력 설명이 유도하는 방향과는 다르게 스위치를 누르는 순서는 결과에 전혀 중요하지 않다는 점이다. 두 스위치를 누르는 순서를 바꾼다고 해서 그 결과는 바뀌지 않는다. 따라서 우리가 계산해야 할 것은 각 스위치를 몇 번이나 누를 것이냐 뿐이다.
두번째로 깨달아야 하는 것은, 어떤 스위치건 간에 누를 수 있는 최대 횟수는 3번이라는 것이다. 왜냐하면 어떤 스위치를 네 번 누르면 연결된 시계들은 모두 12시간씩 앞으로 이동하니 하나도 누르지 않은 것과 다름이 없기 때문이다. 따라서 각 스위치를 누르는 횟수는 0에서 3 사이의 정수이다.
10개의 스위치가 있으니 전체 경우의 수는 4^10 = 2^20
, 일반적으로 시간 안에 모든 경우를 세어 볼 수 있다. 따라서 완전 탐색으로 무난하게 풀 수 있을 것이다.
const int INF = 9999, SWITCHES = 10, CLOCKS = 16;
const char linked[SWITCHES][CLOCKS + 1] = {
"xxx.............",
"...x...x.x.x....",
"....x.....x...xx",
"x...xxxx........",
"......xxx.x.x...",
"x.x...........xx",
"...x..........xx",
"....xx.x......xx",
".xxxxx..........",
"...xxx...x...x.."
};
// 모든 시계가 12시를 가리키는지 확인
bool areAligned(const vector<int>& clocks) {
for (int i = 0; i < 16;i++) {
if (clocks[i] != 12) {
return false;
}
}
return true;
}
// swtch번 스위치를 누름
void push(vector<int>& clocks, int swtch) {
for (int clock = 0; clock < CLOCKS; ++clock) {
if (linked[swtch][clock] == 'x') {
clocks[clock] += 3;
if (clocks[clock] == 15)
clocks[clock] = 3;
}
}
}
// swtch: 이번에 누를 스위치의 번호 및 남은 스위치들의 시작 번호
// 가 주어질 때, 남은 스위치들을 눌러서 clocks를 12로 맞출 수 있는 최소 횟수를 반환한다.
// 만약 불가능하다면 INF 이상의 큰 수를 반환한다.
int solve(vector<int>& clocks, int swtch) {
if (swtch == SWITCHES) {
return areAligned(clocks) ? 0 : INF;
}
int ret = INF;
for (int cnt = 0; cnt < 4; ++cnt) {
ret = min(ret, cnt + solve(clocks, swtch + 1));
push(clocks, swtch);
}
return ret;
}
나는 선택 가능한 원소의 기준을 다음에 누를 스위치로 보았고, 종만 씨는 선택 가능한 원소의 기준을 스위치를 누를 횟수로 보았다.
결과적으로, 스위치를 누를 횟수로 재귀하는 것이 맞았다. 왜냐하면 앞서 언급했듯, 스위치를 누르는 순서는 중요하지 않기 때문이다. 내가 세운 기준은 다음에 누를 스위치로 순서 정보가 포함되어 있다. 따라서 순서를 고려하지 않기 위해 startIdx
가 필요했던 것이다.
문제 풀이를 설계하는 과정이 얼마나 중요한지.. 알게 되었다. 또 선택 가능한 원소들의 기준을 고를 때 신중을 기해야겠다는 생각이 들었다.
선택 가능한 원소: 뽑는 순서인가 뽑힌 횟수인가?
솔직히 linked[SWITCHES][CLOCKS + 1]
보다는 switchConnectTo[10]
가 더 낫지 않나 싶다.
linked[SWITCHES][CLOCKS + 1]
const char linked[SWITCHES][CLOCKS + 1] = {
"xxx.............",
"...x...x.x.x....",
"....x.....x...xx",
"x...xxxx........",
"......xxx.x.x...",
"x.x...........xx",
"...x..........xx",
"....xx.x......xx",
".xxxxx..........",
"...xxx...x...x.."
};
switchConnectTo[10]
vector<int> switchConnectTo[10] = {
{0, 1, 2},
{3, 7, 9, 11},
{4, 10, 14, 15},
{0, 4, 5, 6, 7},
{6, 7, 8, 10, 12},
{0, 2, 14, 15},
{3, 14, 15},
{4, 5, 7, 14, 15},
{1, 2, 3, 4, 5},
{3, 4, 5, 9, 13}
};
후자가 더 나은 이유는, 내부 원소를 순회할 때 전자는 x
인지 아닌지를 판별해야하는 반면, 후자는 그냥 반복문으로 간단하게 처리할 수 있기 때문이다. 더욱이 vector<int>
이기 때문에, 범위 기반 반복문을 사용하면 코드 간결성은 배가 될 것이다.
linked[SWITCHES][CLOCKS + 1]
void push(vector<int>& clocks, int swtch) {
for (int clock = 0; clock < CLOCKS; ++clock) {
if (linked[swtch][clock] == 'x') {
clocks[clock] += 3;
if (clocks[clock] == 15)
clocks[clock] = 3;
}
}
}
switchConnectTo[10] by 도열
void plusThreeInClocks(int idx) {
if (clocks[idx] == 12) clocks[idx] = 3;
else clocks[idx] += 3;
}
...
for (int c : switchConnectTo[s]) {
plusThreeInClocks(c);
}
...
내가 적은 코드가 훨씬 간결 한 것 같다.
벡터 배열을 적극적으로 활용하여 코드의 간결성을 높히자!
종만: "한 번 푼 문제를 다시 본다고 해서 배우는 것이 있을까 생각할 수도 있지만, 오히려 문제를 한 번만 풀어서는 그 문제에서 배울 수 있는 것들을 다 배우지 못하는 경우가 더 많습니다."
또 볼게요... 근데 이제 ps에서 살짝 무게를 줄여야 할 것 같은 느낌이다. 개인 프로젝트를 미뤄놓고 ps만 붙잡고 있다 ㅠㅠ