오늘 풀어본 문제는 백준의 9019번 문제1이다. 문제 풀이에 사용한 언어는 C++ 이다.
이 문제의 내용과 조건은 다음과 같다.
네 개의 명령어 D, S, L, R 을 이용하는 간단한 계산기가 있다. 이 계산기에는 레지스터가 하나 있는데, 이 레지스터에는 $0$ 이상 $10,000$ 미만의 십진수를 저장할 수 있다. 각 명령어는 이 레지스터에 저장된 n을 다음과 같이 변환한다. $n$의 네 자릿수를 $d_1$, $d_2$, $d_3$, $d_4$라고 하자(즉 $n = ((d_1 \times 10 + d_2) \times 10 + d_3) \times 10 + d_4$라고 하자)
D: D 는 $n$을 두 배로 바꾼다. 결과 값이 $9999$ 보다 큰 경우에는 $10000$ 으로 나눈 나머지를 취한다. 그 결과 값$(2n \mod 10000)$을 레지스터에 저장한다.
S: S 는 $n$에서 $1$ 을 뺀 결과 $n-1$을 레지스터에 저장한다. $n$이 $0$ 이라면 $9999$ 가 대신 레지스터에 저장된다.
L: L 은 $n$의 각 자릿수를 왼편으로 회전시켜 그 결과를 레지스터에 저장한다. 이 연산이 끝나면 레지스터에 저장된 네 자릿수는 왼편부터 $d_2$, $d_3$, $d_4$, $d_1$이 된다.
R: R 은 $n$의 각 자릿수를 오른편으로 회전시켜 그 결과를 레지스터에 저장한다. 이 연산이 끝나면 레지스터에 저장된 네 자릿수는 왼편부터 $d_4$, $d_1$, $d_2$, $d_3$이 된다. 위에서 언급한 것처럼, L 과 R 명령어는 십진 자릿수를 가정하고 연산을 수행한다. 예를 들어서 $n = 1234$ 라면 여기에 L 을 적용하면 $2341$ 이 되고 R 을 적용하면 $4123$ 이 된다.
여러분이 작성할 프로그램은 주어진 서로 다른 두 정수 $A$와 $B$ $(A \neq B)$에 대하여 $A$를 $B$로 바꾸는 최소한의 명령어를 생성하는 프로그램이다. 예를 들어서 $A = 1234$, $B = 3412$ 라면 다음과 같이 두 개의 명령어를 적용하면 $A$를 $B$로 변환할 수 있다.
$1234 \to_L 2341 \to_L 3412$
$1234 \to_R 4123 \to_R 3412$
따라서 여러분의 프로그램은 이 경우에 LL 이나 RR 을 출력해야 한다.
$n$의 자릿수로 $0$ 이 포함된 경우에 주의해야 한다. 예를 들어서 $1000$ 에 L 을 적용하면 $0001$ 이 되므로 결과는 $1$ 이 된다. 그러나 R 을 적용하면 $0100$ 이 되므로 결과는 $100$ 이 된다.
프로그램 입력은 $T$ 개의 테스트 케이스로 구성된다. 테스트 케이스 개수 $T$ 는 입력의 첫 줄에 주어진다. 각 테스트 케이스로는 두 개의 정수 $A$와 $B$ $(A \neq B)$가 공백으로 분리되어 차례로 주어지는데 $A$는 레지스터의 초기 값을 나타내고 $B$는 최종 값을 나타낸다. $A$ 와 $B$ 는 모두 $0$ 이상 $10,000$ 미만이다.
$A$에서 $B$로 변환하기 위해 필요한 최소한의 명령어 나열을 출력한다. 가능한 명령어 나열이 여러가지면, 아무거나 출력한다.
처음 문제를 읽어봤을 때, 한 번에 명령어 나열을 알아내는 것은 힘들어 보였고, ‘최소’ 길이의 명령어 나열을 해야된다는 조건을 고려했을 때, BFS를 활용하여 문제를 풀 수 있을 것이라는 생각이 들었다. 현재 숫자와 거쳐온 명령어들을 함꼐 저장하도록 std::pair
을 이용하여 정보를 묶고, std::queue
를 이용하여 저장하도록 하였다.
코드는 다음과 같이 작성하였다.
#include <bits/stdc++.h>
using namespace std;
pair<int, string> D(pair<int, string> initialValue);
pair<int, string> S(pair<int, string> initialValue);
pair<int, string> L(pair<int, string> initialValue);
pair<int, string> R(pair<int, string> initialValue);
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int numberOfTestCases, initialValue, finalValue;
string result;
queue<pair<int, string>> converted;
cin >> numberOfTestCases;
while (numberOfTestCases--) {
cin >> initialValue >> finalValue;
converted.push({ initialValue, "" });
while (true) {
pair<int, string> currentStatus = converted.front();
converted.pop();
if (currentStatus.first == finalValue) {
result = currentStatus.second;
break;
}
else {
converted.push(D(currentStatus));
converted.push(S(currentStatus));
converted.push(L(currentStatus));
converted.push(R(currentStatus));
}
}
cout << result << "\n";
while (!converted.empty()) {
converted.pop();
}
}
return 0;
}
pair<int, string> D(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first * 2) % 10000;
converted.second = initialValue.second + "D";
return converted;
}
pair<int, string> S(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first + 9999) % 10000;
converted.second = initialValue.second + "S";
return converted;
}
pair<int, string> L(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 1000) * 10 + (initialValue.first / 1000);
converted.second = initialValue.second + "L";
return converted;
}
pair<int, string> R(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 10) * 1000 + (initialValue.first / 10);
converted.second = initialValue.second + "R";
return converted;
}
실행 결과는 메모리 초과였다.
메모리 초과가 발생했다는 것은, 말 그대로 사용할 수 있는 저장 공간보다 많은 공간을 사용하려고 했다는 의미이다. 이를 해결하기 위해서는 중복된 정보를 제거하는 것이 필요하다고 생각했다.
중복된 정보가 뭐가 있을지를 생각해본 결과, 같은 숫자에 도달하는 다른 방식이 있을 수 있음을 떠올렸다. 예를 들어, $1010$ 에서 $0101$ 을 만드는 방법이 L 명령어를 사용하는 것도 가능하지만, R 명령어를 사용하는 것이 가능하다. 이러한 경우가 큐에 모두 들어가게 되면, 이후에 이미 확인한 수를 다시 확인하게 되는 경우가 늘어나게 된다.
visted
변수를 추가하여 각 숫자에 대하여 확인했는지 정보를 저장하도록 코드를 다음과 같이 수정하였다.
#include <bits/stdc++.h>
using namespace std;
pair<int, string> D(pair<int, string> initialValue);
pair<int, string> S(pair<int, string> initialValue);
pair<int, string> L(pair<int, string> initialValue);
pair<int, string> R(pair<int, string> initialValue);
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int numberOfTestCases, initialValue, finalValue;
string result;
bool visited[10001] = { 0, };
queue<pair<int, string>> converted;
cin >> numberOfTestCases;
while (numberOfTestCases--) {
cin >> initialValue >> finalValue;
converted.push({ initialValue, "" });
while (true) {
pair<int, string> currentStatus = converted.front();
converted.pop();
if (visited[currentStatus.first]) {
break;
}
else {
visited[currentStatus.first] = true;
if (currentStatus.first == finalValue) {
result = currentStatus.second;
break;
}
else {
converted.push(D(currentStatus));
converted.push(S(currentStatus));
converted.push(L(currentStatus));
converted.push(R(currentStatus));
}
}
}
cout << result << "\n";
while (!converted.empty()) {
converted.pop();
}
}
return 0;
}
pair<int, string> D(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first * 2) % 10000;
converted.second = initialValue.second + "D";
return converted;
}
pair<int, string> S(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first + 9999) % 10000;
converted.second = initialValue.second + "S";
return converted;
}
pair<int, string> L(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 1000) * 10 + (initialValue.first / 1000);
converted.second = initialValue.second + "L";
return converted;
}
pair<int, string> R(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 10) * 1000 + (initialValue.first / 10);
converted.second = initialValue.second + "R";
return converted;
}
실행 결과 실패하고 말았다.
실패의 원인을 한참 찾아본 결과, 테스트 케이스마다 visted
변수를 제대로 초기화해주지 않아 문제가 발생한 것이 원인이었다. 이를 수정한 코드는 다음과 같다.
#include <bits/stdc++.h>
using namespace std;
pair<int, string> D(pair<int, string> initialValue);
pair<int, string> S(pair<int, string> initialValue);
pair<int, string> L(pair<int, string> initialValue);
pair<int, string> R(pair<int, string> initialValue);
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int numberOfTestCases, initialValue, finalValue;
string result = "";
bool visited[10000] = { false, };
queue<pair<int, string>> converted;
cin >> numberOfTestCases;
while (numberOfTestCases--) {
cin >> initialValue >> finalValue;
converted.push({ initialValue, "" });
while (true) {
pair<int, string> currentStatus = converted.front();
converted.pop();
if (visited[currentStatus.first] == true) {
continue;
}
else {
visited[currentStatus.first] = true;
if (currentStatus.first == finalValue) {
result = currentStatus.second;
break;
}
else {
converted.push(D(currentStatus));
converted.push(S(currentStatus));
converted.push(L(currentStatus));
converted.push(R(currentStatus));
}
}
}
cout << result << "\n";
while (!converted.empty()) {
converted.pop();
}
for (int i = 0; i < 10000; i++) {
visited[i] = false;
}
}
return 0;
}
pair<int, string> D(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first * 2) % 10000;
converted.second = initialValue.second + "D";
return converted;
}
pair<int, string> S(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first + 9999) % 10000;
converted.second = initialValue.second + "S";
return converted;
}
pair<int, string> L(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 1000) * 10 + (initialValue.first / 1000);
converted.second = initialValue.second + "L";
return converted;
}
pair<int, string> R(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 10) * 1000 + (initialValue.first / 10);
converted.second = initialValue.second + "R";
return converted;
}
실행 결과, 시간 초과가 발생하였고, 이후의 제출에서도 계속 시간 초과가 발생하였다.
시간 초과의 원인을 계속 찾아 헤멘 결과, std::string
을 더해서 저장하는 과정에서 시간이 너무 오래걸리는 것으로 판명되었다.
처음 숫자에서 어떤 숫자로 오기까지의 전체 명령어를 저장하는 대신, 지난 번 숫자에서 넘어오기 위한 명령어 한 개만 저장하고, history
라는 변수를 만들어 거꾸로 숫자를 추적해 올라갈 수 있도록 저장해두었다.
마지막으로 출력하는 과정에서는 history
변수를 활용하여 결과 문자열을 만들고, 이를 뒤집어 출력하도록 하였다.
코드는 다음과 같이 작성하였다.
#include <bits/stdc++.h>
using namespace std;
void pushIntoQueue(queue<pair<int, string>>& queue, const pair<int, string>& currentStatus, const pair<int, string>& converted, bool* visited, pair<int, string>* history);
pair<int, string> D(pair<int, string> initialValue);
pair<int, string> S(pair<int, string> initialValue);
pair<int, string> L(pair<int, string> initialValue);
pair<int, string> R(pair<int, string> initialValue);
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
int numberOfTestCases, initialValue, finalValue;
string result = "";
bool visited[10000] = { false, };
pair<int, string> history[10000];
queue<pair<int, string>> converted;
cin >> numberOfTestCases;
while (numberOfTestCases--) {
cin >> initialValue >> finalValue;
converted.push({ initialValue, "" });
visited[initialValue] = true;
while (true) {
pair<int, string> currentStatus = converted.front();
converted.pop();
if (currentStatus.first == finalValue) {
result += currentStatus.second;
for (int i = finalValue; i != initialValue; i = history[i].first) {
result += history[i].second;
}
reverse(result.begin(), result.end());
cout << result << "\n";
break;
}
else {
pushIntoQueue(converted, currentStatus, D(currentStatus), visited, history);
pushIntoQueue(converted, currentStatus, S(currentStatus), visited, history);
pushIntoQueue(converted, currentStatus, L(currentStatus), visited, history);
pushIntoQueue(converted, currentStatus, R(currentStatus), visited, history);
}
}
result = "";
while (!converted.empty()) {
converted.pop();
}
for (int i = 0; i < 10000; i++) {
visited[i] = false;
}
}
return 0;
}
void pushIntoQueue(queue<pair<int, string>>& queue, const pair<int, string>& currentStatus, const pair<int, string>& converted, bool* visited, pair<int, string>* history) {
if (!visited[converted.first]) {
queue.push(converted);
visited[converted.first] = true;
history[converted.first] = currentStatus;
}
}
pair<int, string> D(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first * 2) % 10000;
converted.second = "D";
return converted;
}
pair<int, string> S(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first + 9999) % 10000;
converted.second = "S";
return converted;
}
pair<int, string> L(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 1000) * 10 + (initialValue.first / 1000);
converted.second = "L";
return converted;
}
pair<int, string> R(pair<int, string> initialValue) {
pair<int, string> converted;
converted.first = (initialValue.first % 10) * 1000 + (initialValue.first / 10);
converted.second = "R";
return converted;
}
그러자 모든 테스트 케이스를 통과하고 정답이 나오는 것을 확인할 수 있었다.
처음에 메모리 초과, 틀렸습니다, 시간 초과가 모두 발생했을 때 어이가 없었다. 시간 초과와 틀렸습니다는 많이 봐왔고, 메모리 초과도 종종 본 적은 있지만, 세 개가 한 문제에 모두 나왔던 경우는 처음이었기 때문이다.
그래도 3가지 문제점을 모두 경험했기 때문에, PS에서 자주 당하는 위험 요소를 상기할 수 있게 되었다. 중복 미고려, 변수 미초기화, 비싼 std::string
연산 이 3가지는 다시는 안 까먹도록 머리속에 새겨 두어야 겠다.
오늘의 PS는 여기까지!
1: https://www.acmicpc.net/problem/9019