알파카컵 1회: D - 코딩 천재 알파카
의견: 0
알파카는 천재라서 Brainfuck으로 무엇이든 짤 수 있다. 하지만 자신이 짠 코드가 무엇을 출력하는지는 모른다. 주어진 Brainfuck 프로그램을 실행하면 무엇이 출력되는지 구해 보자.
명령어
- > : 포인터를 오른쪽으로 한 칸 이동
- < : 포인터를 왼쪽으로 한 칸 이동
- + : 현재 셀의 값을 \(1\) 증가
- - : 현재 셀의 값을 \(1\) 감소
- . : 현재 셀의 값을 아스키 문자로 출력
- [ : 현재 셀의 값이 \(0\)이면 짝이 맞는 ] 다음으로 점프
- ] : 현재 셀의 값이 \(0\)이 아니면 짝이 맞는 [로 점프
프로그램의 명령어 개수 \(\le 1{,}000{,}000\). 메모리는 \(30{,}000\)개의 셀로 이루어지며, 각 셀은 \(0\) 이상 \(255\) 이하의 정수를 담는다(\(255\)에서 \(1\)을 더하면 \(0\), \(0\)에서 \(1\)을 빼면 \(255\)로 wrap-around). 포인터는 \(0\)번 셀에서 시작하고 모든 셀은 \(0\)으로 초기화되어 있다.
하나의 Brainfuck 프로그램이 주어진다(여러 줄에 걸쳐 주어질 수 있다). 사용되는 명령어는 > < + - . [ ] 뿐이며, 입력 명령어 ,(콤마)는 주어지지 않는다. 명령어가 아닌 문자는 무시한다.
프로그램을 실행했을 때 출력되는 내용을 그대로 출력한다. 단, 다음 두 경우에는 아무것도 출력하지 않고 \(-1\)만 출력한다.
1. 프로그램에 문법 오류가 있어 실행할 수 없는 경우([와 ]의 짝이 맞지 않는 경우).
2. 프로그램이 무한 루프에 빠지는 경우. 프로그램이 명령을 \(10{,}000{,}000\)번 실행하고도 종료되지 않으면 무한 루프로 간주한다.
| 서브태스크 | 점수 | 설명 |
|---|---|---|
Subtask 1 | 65점 | 정상 동작 (출력 있음) |
Subtask 2 | 15점 | 정상 동작 (출력 없음) |
Subtask 3 | 10점 | 문법 오류 → -1 |
Subtask 4 | 10점 | 무한 루프 → -1 |
++++++++[>++++[>++>+++>+++>+<<<<-]>+>+>->>+[<]<-]>>.>---.+++++++..+++.>>.<-.<.+++.------.--------.>>+.>++.Hello World!
+++++++++[>+++++++<-]>++.A+++[>++<--1+[]-1해설
알파카컵 1회: D - 코딩 천재 알파카
알파카컵 1회: D - 코딩 천재 알파카
1. 문제 요약
하나의 Brainfuck 프로그램이 주어지면, 그 프로그램을 실행했을 때 출력되는 내용을 그대로 구하는 문제입니다. 사용되는 명령어는 >, <, +, -, ., [, ] 일곱 가지이며 입력 명령어 ,는 등장하지 않고, 명령어가 아닌 문자는 모두 무시합니다.
메모리는 \(30,000\)개의 셀로 이루어지며 각 셀은 \(0\)부터 \(255\)까지의 값을 가집니다(\(255\)에서 \(1\)을 더하면 \(0\), \(0\)에서 \(1\)을 빼면 \(255\)로 wrap-around). 포인터는 \(0\)번 셀에서 시작하고 모든 셀은 \(0\)으로 초기화되어 있습니다. 단, 다음 두 경우에는 출력 대신 \(-1\)만 출력해야 합니다.
- [와 ]의 짝이 맞지 않는 문법 오류가 있는 경우.
- 프로그램이 명령을 \(10,000,000\)번 실행하고도 종료되지 않는 무한 루프의 경우.
서브태스크는 입력의 크기가 아니라 프로그램이 어떤 거동을 보이는지(출력 유무, 문법 오류, 무한 루프)로 나뉩니다. 핵심은 이 네 가지 경우를 모두 하나의 견고한 인터프리터로 처리하는 것입니다. 따라서 아래 각 서브태스크에서는 동일한 인터프리터가 해당 경우를 어떻게 다루는지에 초점을 두어 설명합니다.
2. [서브태스크 1]: 정상 동작 (출력 있음)
가장 기본이 되는, 문법이 올바르고 정상적으로 종료되며 . 명령으로 무언가를 출력하는 경우입니다. 인터프리터의 실행 엔진 자체를 정확히 구현하는 것이 이 서브태스크의 목표입니다.
-
풀이: 먼저 입력에서 명령어 일곱 가지만 골라 명령어 스트림
code로 만듭니다(명령어가 아닌 문자는 이 단계에서 버립니다). 포인터ptr은 \(0\)에서 시작하고, 셀 배열mem은unsigned char로 잡아 자동으로 \(0\)~\(255\) wrap-around가 되게 합니다. 프로그램 카운터pc를 \(0\)부터 진행시키며 명령어를 하나씩 해석합니다.>/<는 포인터를 좌우로 옮기고,+/-는 현재 셀 값을 증감하며,.는 현재 셀 값을 아스키 문자로 출력 버퍼에 담습니다.[/]는 다음 서브태스크에서 설명할 점프 테이블을 이용해 분기합니다. 모든 명령이 끝나면 누적한 출력 버퍼를 그대로 출력합니다. -
시간 복잡도: 실제로 실행되는 명령 수를 \(E\)라 하면 인터프리터는 \(O(E)\)에 동작합니다. 실행 명령 수의 상한이 \(10^7\)이므로 충분히 빠릅니다.
#include <bits/stdc++.h>
#define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define m2(x) ((x) << 1)
#define d2(x) ((x) >> 1)
#define endl "\n"
#define sp " "
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
const ll LIMIT = 10000000;
const ll CELLS = 30000;
int main() {
fastio
// 입력 프로그램은 여러 줄에 걸쳐 주어질 수 있으므로 전부 읽어들인다.
string line, src = "";
while (getline(cin, line)) src += line;
// 명령어 일곱 가지만 추려 명령어 스트림을 만든다(나머지는 무시).
string code = "";
for (char ch : src) {
if (ch == '>' || ch == '<' || ch == '+' || ch == '-' ||
ch == '.' || ch == '[' || ch == ']') code += ch;
}
ll len = (ll)code.size();
// (이 서브태스크에서는 문법이 올바르므로 점프 테이블이 항상 정상적으로 만들어진다.)
vector<ll> jump(len);
stack<ll> st;
for (ll i = 0; i < len; i++) {
if (code[i] == '[') st.push(i);
else if (code[i] == ']') { ll j = st.top(); st.pop(); jump[i] = j; jump[j] = i; }
}
vector<unsigned char> mem(CELLS, 0); // unsigned char 로 0~255 wrap-around 자동 처리
ll ptr = 0, pc = 0;
string out = "";
while (pc < len) {
char c = code[pc];
if (c == '>') ptr++;
else if (c == '<') ptr--;
else if (c == '+') mem[ptr]++;
else if (c == '-') mem[ptr]--;
else if (c == '.') out += (char)mem[ptr];
else if (c == '[') { if (mem[ptr] == 0) pc = jump[pc]; }
else if (c == ']') { if (mem[ptr] != 0) pc = jump[pc]; }
pc++;
}
cout << out;
return 0;
}
3. [서브태스크 2]: 정상 동작 (출력 없음)
문법이 올바르고 정상적으로 종료되지만 . 명령이 단 한 번도 실행되지 않아 아무것도 출력하지 않는 경우입니다. 예를 들어 셀 값을 잔뜩 계산하기만 하고 출력하지 않는 프로그램이 여기에 해당합니다.
-
풀이: 서브태스크 1의 인터프리터를 그대로 사용하면 됩니다. 핵심은 출력 버퍼를 빈 문자열로 초기화해 두고,
.명령이 실행될 때만 문자를 덧붙이는 구조입니다..이 한 번도 등장하지 않거나 실행되지 않으면 버퍼는 끝까지 비어 있으므로, 마지막에 그대로 출력하면 자연스럽게 아무것도 출력하지 않게 됩니다. 즉 출력 유무를 위해 별도의 분기를 둘 필요가 전혀 없고, 정상 종료라면 누적 버퍼를 출력하기만 하면 됩니다.-1을 내보내야 하는 경우와 혼동하지 않도록, "출력이 없는 정상 종료"와 "오류로 인한 \(-1\)"을 명확히 구분하는 것이 중요합니다. -
시간 복잡도: 마찬가지로 실행 명령 수 \(E\)에 대해 \(O(E)\)입니다.
#include <bits/stdc++.h>
#define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define m2(x) ((x) << 1)
#define d2(x) ((x) >> 1)
#define endl "\n"
#define sp " "
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
const ll LIMIT = 10000000;
const ll CELLS = 30000;
int main() {
fastio
string line, src = "";
while (getline(cin, line)) src += line;
string code = "";
for (char ch : src) {
if (ch == '>' || ch == '<' || ch == '+' || ch == '-' ||
ch == '.' || ch == '[' || ch == ']') code += ch;
}
ll len = (ll)code.size();
vector<ll> jump(len);
stack<ll> st;
for (ll i = 0; i < len; i++) {
if (code[i] == '[') st.push(i);
else if (code[i] == ']') { ll j = st.top(); st.pop(); jump[i] = j; jump[j] = i; }
}
vector<unsigned char> mem(CELLS, 0);
ll ptr = 0, pc = 0;
string out = ""; // 빈 채로 시작 → '.' 이 안 실행되면 끝까지 비어 있다.
while (pc < len) {
char c = code[pc];
if (c == '>') ptr++;
else if (c == '<') ptr--;
else if (c == '+') mem[ptr]++;
else if (c == '-') mem[ptr]--;
else if (c == '.') out += (char)mem[ptr]; // 여기서만 출력이 쌓인다.
else if (c == '[') { if (mem[ptr] == 0) pc = jump[pc]; }
else if (c == ']') { if (mem[ptr] != 0) pc = jump[pc]; }
pc++;
}
cout << out; // 비어 있으면 아무것도 출력되지 않는다.
return 0;
}
4. [서브태스크 3]: 문법 오류 → -1
[와 ]의 짝이 맞지 않아 실행 자체가 불가능한 경우입니다. 예를 들어 +++[>++<- 처럼 ]가 부족하거나, 반대로 여는 괄호 없이 ]가 먼저 나오는 경우입니다. 이때는 프로그램을 실행하지 않고 곧바로 \(-1\)만 출력해야 합니다.
-
풀이: 인터프리터를 돌리기 전에 점프 테이블을 만드는 과정에서 짝 맞춤 여부를 검사합니다. 스택을 두고 명령어 스트림을 왼쪽부터 훑으며,
[를 만나면 그 위치를 스택에 넣고,]를 만나면 스택에서 하나를 꺼내 서로의 점프 대상으로 연결합니다. 이때 두 가지 불균형을 잡아내야 합니다. (1)]를 만났는데 스택이 비어 있으면 짝이 없는 닫는 괄호이므로 오류입니다. (2) 스트림을 모두 훑은 뒤에도 스택이 비어 있지 않으면 짝이 없는 여는 괄호가 남은 것이므로 오류입니다. 둘 중 하나라도 발생하면 메모리/포인터를 건드리지 않고 즉시-1을 출력하고 종료합니다. -
시간 복잡도: 명령어 개수 \(L\)에 대해 점프 테이블 구성과 검사는 한 번의 선형 스캔이므로 \(O(L)\)입니다.
#include <bits/stdc++.h>
#define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define m2(x) ((x) << 1)
#define d2(x) ((x) >> 1)
#define endl "\n"
#define sp " "
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
const ll LIMIT = 10000000;
const ll CELLS = 30000;
int main() {
fastio
string line, src = "";
while (getline(cin, line)) src += line;
string code = "";
for (char ch : src) {
if (ch == '>' || ch == '<' || ch == '+' || ch == '-' ||
ch == '.' || ch == '[' || ch == ']') code += ch;
}
ll len = (ll)code.size();
vector<ll> jump(len);
stack<ll> st;
bool ok = true;
for (ll i = 0; i < len; i++) {
if (code[i] == '[') st.push(i);
else if (code[i] == ']') {
if (st.empty()) { ok = false; break; } // 짝 없는 ']'
ll j = st.top(); st.pop();
jump[i] = j; jump[j] = i;
}
}
// 짝 없는 ']' 가 있었거나, 닫히지 않은 '[' 가 남았으면 문법 오류.
if (!ok || !st.empty()) {
cout << -1; // 실행하지 않고 즉시 종료
return 0;
}
vector<unsigned char> mem(CELLS, 0);
ll ptr = 0, pc = 0;
string out = "";
while (pc < len) {
char c = code[pc];
if (c == '>') ptr++;
else if (c == '<') ptr--;
else if (c == '+') mem[ptr]++;
else if (c == '-') mem[ptr]--;
else if (c == '.') out += (char)mem[ptr];
else if (c == '[') { if (mem[ptr] == 0) pc = jump[pc]; }
else if (c == ']') { if (mem[ptr] != 0) pc = jump[pc]; }
pc++;
}
cout << out;
return 0;
}
5. [서브태스크 4]: 무한 루프 → -1
문법은 올바르지만 영원히 종료되지 않는 경우입니다. 예를 들어 +[]는 현재 셀이 항상 \(1\)이라 루프를 빠져나갈 수 없습니다. 이런 프로그램은 무한히 실행되므로, 정지 여부를 미리 판별할 수는 없고(정지 문제) 대신 실행 횟수에 상한을 두어 처리합니다.
-
풀이: 실제로 실행되는 명령의 개수를 세는 카운터
steps를 두고, 명령을 하나 해석할 때마다 \(1\)씩 증가시킵니다. 이 값이 \(10,000,000\)을 초과하면 무한 루프로 간주하고 루프를 빠져나와-1만 출력합니다. 주의할 점은 무시되는 문자나 점프로 건너뛰는 명령이 아니라 실제로 처리되는 명령 하나하나를 세야 한다는 것입니다([/]의 분기 판정도 하나의 실행으로 셉니다). 이 한도 검사를 실행 루프 맨 앞에 두면, 정상 종료 프로그램은 한도에 닿기 전에 끝나서 정상 출력을 내고, 무한 루프 프로그램만 한도에서 걸려-1을 내게 됩니다. -
시간 복잡도: 실행 명령 수가 한도 \(L_{\max}=10^7\)로 제한되므로 최악의 경우에도 \(O(L_{\max})\)에 종료가 보장됩니다.
#include <bits/stdc++.h>
#define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define m2(x) ((x) << 1)
#define d2(x) ((x) >> 1)
#define endl "\n"
#define sp " "
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
const ll LIMIT = 10000000;
const ll CELLS = 30000;
int main() {
fastio
string line, src = "";
while (getline(cin, line)) src += line;
string code = "";
for (char ch : src) {
if (ch == '>' || ch == '<' || ch == '+' || ch == '-' ||
ch == '.' || ch == '[' || ch == ']') code += ch;
}
ll len = (ll)code.size();
vector<ll> jump(len);
stack<ll> st;
bool ok = true;
for (ll i = 0; i < len; i++) {
if (code[i] == '[') st.push(i);
else if (code[i] == ']') {
if (st.empty()) { ok = false; break; }
ll j = st.top(); st.pop();
jump[i] = j; jump[j] = i;
}
}
if (!ok || !st.empty()) { cout << -1; return 0; }
vector<unsigned char> mem(CELLS, 0);
ll ptr = 0, pc = 0, steps = 0;
string out = "";
bool infinite = false;
while (pc < len) {
if (steps > LIMIT) { infinite = true; break; } // 1e7 초과 → 무한 루프 간주
steps++; // 실제로 실행되는 명령만 센다.
char c = code[pc];
if (c == '>') ptr++;
else if (c == '<') ptr--;
else if (c == '+') mem[ptr]++;
else if (c == '-') mem[ptr]--;
else if (c == '.') out += (char)mem[ptr];
else if (c == '[') { if (mem[ptr] == 0) pc = jump[pc]; }
else if (c == ']') { if (mem[ptr] != 0) pc = jump[pc]; }
pc++;
}
if (infinite) { cout << -1; return 0; } // 누적 출력은 버리고 -1 만 출력
cout << out;
return 0;
}
6. 정해
네 가지 경우를 모두 처리하는 완전하고 견고한 Brainfuck 인터프리터입니다.
-
풀이: 동작 순서는 다음과 같습니다. ① 입력 프로그램을 여러 줄에 걸쳐 주어질 수 있음을 고려해 끝까지 읽은 뒤, 일곱 가지 명령어만 추려 명령어 스트림을 만듭니다(나머지 문자는 무시). ② 스택으로 한 번 스캔하며
[/]의 짝 맞춤을 검사하고, 동시에 서로를 가리키는 점프 테이블을 구성합니다. 이때 짝 없는](스택이 빔)나 닫히지 않은[(스캔 후 스택에 잔여)가 발견되면 곧바로-1을 출력하고 종료합니다. ③ 포인터를 \(0\), 셀 배열을 모두 \(0\)으로 초기화한 뒤 명령어 스트림을 해석합니다. 셀은unsigned char로 두어 \(0\)~\(255\) wrap-around가 자동으로 처리되게 합니다. ④ 실행되는 명령마다steps를 증가시켜 \(10^7\)을 초과하면 무한 루프로 보고 출력을 버린 채-1만 출력합니다. ⑤ 정상적으로 종료되면 누적한 출력 버퍼를 그대로 내보냅니다. 점프 테이블을 미리 구성해 두므로 루프 진입/탈출 시 괄호를 다시 찾아 헤맬 필요 없이 \(O(1)\)에 분기할 수 있습니다. -
시간 복잡도: 명령어 개수 \(L\)에 대한 전처리(스트림 구성·점프 테이블)가 \(O(L)\), 실행은 실제 실행 명령 수 \(E \le 10^7\)에 대해 \(O(E)\)입니다. 따라서 전체 시간 복잡도는 \(O(L + 10^7)\)로 제한 시간 내에 안정적으로 통과합니다.
#include <bits/stdc++.h>
#define fastio ios_base::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define m2(x) ((x) << 1)
#define d2(x) ((x) >> 1)
#define endl "\n"
#define sp " "
using namespace std;
using ll = long long;
using pll = pair<ll, ll>;
const ll LIMIT = 10000000; // 무한 루프 판정 기준
const ll CELLS = 30000; // 메모리 셀 개수
int main() {
fastio
// 1) 프로그램 전체 읽기 (여러 줄 가능)
string line, src = "";
while (getline(cin, line)) src += line;
// 2) 명령어가 아닌 문자는 무시하고 명령어 스트림 구성
string code = "";
for (char ch : src) {
if (ch == '>' || ch == '<' || ch == '+' || ch == '-' ||
ch == '.' || ch == '[' || ch == ']') code += ch;
}
ll len = (ll)code.size();
// 3) 점프 테이블 구성 + 괄호 짝 검사
vector<ll> jump(len);
stack<ll> st;
bool ok = true;
for (ll i = 0; i < len; i++) {
if (code[i] == '[') st.push(i);
else if (code[i] == ']') {
if (st.empty()) { ok = false; break; } // 짝 없는 ']'
ll j = st.top(); st.pop();
jump[i] = j; jump[j] = i;
}
}
if (!ok || !st.empty()) { // 짝 없는 ']' 또는 닫히지 않은 '['
cout << -1;
return 0;
}
// 4) 실행
vector<unsigned char> mem(CELLS, 0); // 0~255 wrap-around 자동
ll ptr = 0, pc = 0, steps = 0;
string out = "";
bool infinite = false;
while (pc < len) {
if (steps > LIMIT) { infinite = true; break; } // 1e7 초과 → 무한 루프
steps++;
char c = code[pc];
if (c == '>') ptr++;
else if (c == '<') ptr--;
else if (c == '+') mem[ptr]++;
else if (c == '-') mem[ptr]--;
else if (c == '.') out += (char)mem[ptr];
else if (c == '[') { if (mem[ptr] == 0) pc = jump[pc]; }
else if (c == ']') { if (mem[ptr] != 0) pc = jump[pc]; }
pc++;
}
if (infinite) { // 무한 루프면 출력 버리고 -1 만
cout << -1;
return 0;
}
// 5) 정상 종료 → 누적 출력 그대로 (출력 없으면 빈 문자열)
cout << out;
return 0;
}
평가 및 의견
알파카컵 1회: D - 코딩 천재 알파카
아직 의견이 없습니다. 자격이 된다면 위 양식에서 가장 먼저 평가해 보세요.
풀이 제출
알파카컵 1회: D - 코딩 천재 알파카