보드게임 설명서를 처음부터 끝까지 그대로 따라 하는 일과 같습니다. "주사위를 굴린다 → 그만큼 말을 옮긴다 → 도착한 칸의 효과를 적용한다"라는 순서가 정해져 있고, 이 중 하나라도 순서를 바꾸거나 빠뜨리면 결과가 달라집니다. 이기는 전략을 고민하는 게임이 아니라, 설명서를 틀리지 않고 읽어내는 게임입니다.
시뮬레이션(구현)은 문제가 정한 규칙을 그대로 코드로 옮겨 실행하는 유형입니다. 알고리즘적 통찰보다 꼼꼼함을 봅니다.
지문 한 문장 = 코드 한 단계
순서를 바꾸면 예시부터 틀린다
승부처 세 가지: 규칙의 순서, 동시성 처리, 경계 조건
3 × 3 격자의 (0, 0) 칸에서 북쪽을 보고 있는 로봇에게 명령 F, R, F, F를 내립니다. F는 보고 있는 방향으로 한 칸 전진, R은 제자리에서 오른쪽 90도 회전이고, 격자 밖으로 나가는 전진은 무시합니다. 방향은 0 = 북, 1 = 동, 2 = 남, 3 = 서로 번호를 붙입니다.
단계
명령
방향
위치 (r, c)
무슨 일이
시작
—
0 (북)
(0, 0)
1
F
0 (북)
(0, 0)
북쪽은 (-1, 0) — 격자 밖이라 무시
2
R
1 (동)
(0, 0)
(0 + 1) % 4 = 1
3
F
1 (동)
(0, 1)
동쪽으로 한 칸
4
F
1 (동)
(0, 2)
동쪽으로 한 칸
명령 네 개 중 실제로 움직인 것은 두 번뿐입니다. 1단계에서 경계를 검사하지 않았다면 grid[-1][0]을 읽으며 그 자리에서 틀렸을 것입니다. 시뮬레이션 문제의 대부분은 이런 한 칸짜리 경계에서 갈립니다.
상태 변수를 먼저 정합니다 — 위치, 방향, 격자처럼 "매 턴 변하는 것"의 목록입니다.
let r = 0, c = 0; // 현재 위치let dir = 0; // 0=북, 1=동, 2=남, 3=서
방향 배열을 놓습니다 — 방향 번호로 이동량을 바로 꺼내 쓰기 위해서입니다.
const dr = [-1, 0, 1, 0]; // 북, 동, 남, 서const dc = [0, 1, 0, -1];
회전은 모듈로 산술입니다 — 오른쪽은 +1, 왼쪽은 +3(= -1)입니다.
dir = (dir + 1) % 4; // 오른쪽 회전dir = (dir + 3) % 4; // 왼쪽 회전 — 음수를 피하려고 +3
이동 전에 반드시 경계를 검사합니다 — 검사를 함수로 빼두면 빠뜨리지 않습니다.
const inRange = (r: number, c: number) => r >= 0 && r < n && c >= 0 && c < m;if (inRange(r + dr[dir], c + dc[dir])) { r += dr[dir]; c += dc[dir]; }
전체 코드 — "명령 문자열을 실행하고 최종 위치 반환":
function solution(n: number, m: number, commands: string): [number, number] { // 1단계: 상태 변수 let r = 0, c = 0, dir = 0; // 2단계: 방향 배열 (북, 동, 남, 서) const dr = [-1, 0, 1, 0], dc = [0, 1, 0, -1]; const inRange = (nr: number, nc: number) => nr >= 0 && nr < n && nc >= 0 && nc < m; for (const cmd of commands) { if (cmd === "R") dir = (dir + 1) % 4; // 3단계: 회전 else if (cmd === "L") dir = (dir + 3) % 4; else { const nr = r + dr[dir], nc = c + dc[dir]; // 4단계: 경계 검사 후 이동 if (inRange(nr, nc)) { r = nr; c = nc; } } } return [r, c];}
지문이 길고 규칙이 번호나 문단으로 나열되어 있으면 시뮬레이션입니다. 삼성 등 한국 기업 코테의 최다 빈출 유형입니다.
"T초 동안", "매 턴마다", "다음 조건을 순서대로 수행한다" — 시간 루프 신호입니다.
"로봇이 이동한다", "컨베이어 벨트가 회전한다", "주사위를 굴린다", "물이 퍼진다" — 격자 위 상태 변화입니다.
입력 크기가 작고(격자 20 × 20, 턴 1000회 정도) 대신 규칙이 복잡하다면, 최적화가 아니라 정확한 구현을 요구하는 문제입니다.
특별한 알고리즘 이름이 떠오르지 않는데 지문만 긴 경우 — 대개 시키는 대로 하면 되는 문제입니다.
기본 장비
// 방향 배열 — 격자 이동의 표준 (북, 동, 남, 서)const dr = [-1, 0, 1, 0];const dc = [0, 1, 0, -1];// 오른쪽 회전: dir = (dir + 1) % 4, 왼쪽: dir = (dir + 3) % 4// 경계 검사 헬퍼 — 매번 인라인으로 쓰지 말고 함수로const inRange = (r: number, c: number) => r >= 0 && r < n && c >= 0 && c < m;
동시 갱신 — 최대 함정
"모든 개체가 동시에 움직인다"면, 순회하면서 그 자리에서 갱신하면 안 됩니다. 앞서 움직인 개체가 뒤 개체의 판단에 영향을 주기 때문입니다.
// 잘못된 방식: 순회하며 제자리 갱신 — 이동한 로봇을 같은 턴에 또 처리할 수 있음// 고친 방식: 스냅샷 기준 판단 → 새 상태에 기록 → 교체const next = grid.map((row) => [...row]);for (...) { /* grid(이전 상태)를 읽고 next에 쓴다 */ }grid = next;
1차원 벨트에서 "오른쪽으로 전부 한 칸"은 오른쪽 끝부터 역순 처리하면 스냅샷 없이 안전합니다 (부품 검사 라인 문제의 요령).
규칙 순서 = 명세
"이동 → 검사 → 투입"처럼 문제 지문이 정한 단계 순서를 함수로 분리하세요.
for (let t = 1; t <= T; t++) { move(); // 단계마다 함수 하나 — 지문과 1:1 대응 inspect(); spawn(t);}
지문 순서와 코드 순서가 다르면 예시부터 틀립니다. 예시 입력을 손으로 두 턴 따라가며 코드와 비교하는 것이 최고의 디버깅입니다.
회전 방향 혼동 (시계/반시계, L/R) — 방향 배열 순서를 주석으로 박아두기
const dr = [-1, 0, 1, 0]; // 잘못된 코드 — 순서를 적어두지 않으면 회전 부호가 뒤집힙니다const dr2 = [-1, 0, 1, 0]; // 고친 코드 — 북, 동, 남, 서 (시계 방향)라고 주석을 남깁니다
경계 밖 접근 — grid[r][c] 전에 항상 inRange
if (grid[nr][nc] === 1) ... // 잘못된 코드 — nr이 -1이면 런타임 에러if (inRange(nr, nc) && grid[nr][nc] === 1) ... // 고친 코드 — 검사가 앞
동시 이동을 순차 처리 (위 스냅샷 문제)
for (...) grid[nr][nc] = grid[r][c]; // 잘못된 코드 — 방금 옮긴 것을 또 옮깁니다const next = grid.map((row) => [...row]); // 고친 코드 — 이전 상태를 읽고 next에 씁니다
"턴 번호가 k의 배수일 때"류 조건의 시작점(1-기반? 0-기반?) 착오
for (let t = 0; t < T; t++) if (t % k === 0) ... // 잘못된 코드 — 0턴에 발동합니다for (let t = 1; t <= T; t++) if (t % k === 0) ... // 고친 코드 — 지문의 1-기반과 일치
상태 변수 초기화 누락 — 케이스마다 새로 시작해야 할 값이 이전 케이스를 기억
회전이 모듈로인 이유: 방향 배열을 시계 방향(북 → 동 → 남 → 서)으로 정렬해 두면, 오른쪽 90도 회전은 인덱스 +1, 180도는 +2, 왼쪽은 +3과 같습니다. 배열 순서를 반시계로 두면 부호가 통째로 뒤집히므로, 한 프로젝트 안에서는 순서를 고정하는 편이 안전합니다. 대각선까지 다루는 문제는 8방향 배열 dr = [-1,-1,0,1,1,1,0,-1], dc = [0,1,1,1,0,-1,-1,-1]을 같은 시계 순서로 씁니다.
격자 90도 회전: "판을 통째로 돌린다"는 규칙은 좌표 변환 한 줄로 처리합니다. 시계 방향은 next[c][n - 1 - r] = grid[r][c]이며, 반시계는 next[n - 1 - c][r] = grid[r][c]입니다.
역순 처리가 안전한 조건: 모든 개체가 같은 방향으로 한 칸씩 움직일 때만 성립합니다. 이동 방향이 섞여 있거나 개체마다 이동량이 다르면 스냅샷을 떠야 합니다.
큰 격자의 성능: 격자가 1000 × 1000이고 턴이 수천 번이면 매 턴 grid.map(row => [...row]) 복사가 병목이 됩니다. 이때는 버퍼 두 개를 만들어 두고 턴마다 참조만 맞바꾸거나(더블 버퍼링), Int32Array 같은 타입 배열에 1차원으로 펴서(r * m + c) 담습니다.
디버깅 습관: 규칙을 읽으며 단계 목록을 주석으로 먼저 적고, 각 단계를 함수 하나로 만든 뒤, 예시 입력의 첫 두 턴에 대해 매 단계 격자를 출력해 지문의 그림과 대조합니다. 틀린 단계가 어디인지 한 번에 좁혀집니다.
시각화로 명령 실행·경계 처리의 감을 잡은 뒤, 단일 개체 명령 실행 → 다개체 동시 갱신 순으로. 규칙을 읽으며 "단계 목록"을 먼저 주석으로 쓰고 코드를 채우는 습관을 들이세요.