{}연습장
대시보드학습스킬 경로문제모의고사라이브 코테복습데이터
← 학습 목록로드맵에서 보기
Lv.2 중급 준비

시뮬레이션

지문의 규칙을 그대로 코드로 옮기는 구현력 싸움 — 삼성류 최다 빈출. 규칙 순서·동시 갱신·경계 처리가 승부처입니다.

지문 신호“규칙대로 T턴”“로봇·보드”“명령을 수행”
0/4 해결
1

눈으로 보기

재생을 누르고 단계별로 동작을 따라가세요
↑
명령
FFRFFLF
1/9명령 문자열 "FFRFFLF"을 한 글자씩 실행합니다. F = 전진, L/R = 회전. 시뮬레이션은 규칙을 코드로 옮기는 정확성 싸움입니다.
■ 검정 = 장애물■ 연앰버 = 방문한 칸화살표 = 로봇의 방향
2

개념 이해

핵심 패턴 코드는 손으로 따라 쳐 보는 것을 권장
3

문제로 확인

난이도 순서대로 4문제 — 막히면 개념으로 돌아왔다 다시
1온실 급수 로봇#108다음 풀 문제중간2킥보드 심야 요율 정산#110중간3부품 검사 라인#109어려움4물류창고 화물 리프트#111어려움
← 이전 토픽 · Lv.2힙 / 우선순위 큐다음 토픽 · Lv.3 →BFS / DFS
1 / 8

비유로 이해하기

보드게임 설명서를 처음부터 끝까지 그대로 따라 하는 일과 같습니다. "주사위를 굴린다 → 그만큼 말을 옮긴다 → 도착한 칸의 효과를 적용한다"라는 순서가 정해져 있고, 이 중 하나라도 순서를 바꾸거나 빠뜨리면 결과가 달라집니다. 이기는 전략을 고민하는 게임이 아니라, 설명서를 틀리지 않고 읽어내는 게임입니다.

시뮬레이션(구현)은 문제가 정한 규칙을 그대로 코드로 옮겨 실행하는 유형입니다. 알고리즘적 통찰보다 꼼꼼함을 봅니다.

  • 지문 한 문장 = 코드 한 단계
  • 순서를 바꾸면 예시부터 틀린다
  • 승부처 세 가지: 규칙의 순서, 동시성 처리, 경계 조건

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]을 읽으며 그 자리에서 틀렸을 것입니다. 시뮬레이션 문제의 대부분은 이런 한 칸짜리 경계에서 갈립니다.

  1. 상태 변수를 먼저 정합니다 — 위치, 방향, 격자처럼 "매 턴 변하는 것"의 목록입니다.
let r = 0, c = 0;   // 현재 위치
let dir = 0;        // 0=북, 1=동, 2=남, 3=서
  1. 방향 배열을 놓습니다 — 방향 번호로 이동량을 바로 꺼내 쓰기 위해서입니다.
const dr = [-1, 0, 1, 0]; // 북, 동, 남, 서
const dc = [0, 1, 0, -1];
  1. 회전은 모듈로 산술입니다 — 오른쪽은 +1, 왼쪽은 +3(= -1)입니다.
dir = (dir + 1) % 4;       // 오른쪽 회전
dir = (dir + 3) % 4;       // 왼쪽 회전 — 음수를 피하려고 +3
  1. 이동 전에 반드시 경계를 검사합니다 — 검사를 함수로 빼두면 빠뜨리지 않습니다.
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) 담습니다.
  • 디버깅 습관: 규칙을 읽으며 단계 목록을 주석으로 먼저 적고, 각 단계를 함수 하나로 만든 뒤, 예시 입력의 첫 두 턴에 대해 매 단계 격자를 출력해 지문의 그림과 대조합니다. 틀린 단계가 어디인지 한 번에 좁혀집니다.

시각화로 명령 실행·경계 처리의 감을 잡은 뒤, 단일 개체 명령 실행 → 다개체 동시 갱신 순으로. 규칙을 읽으며 "단계 목록"을 먼저 주석으로 쓰고 코드를 채우는 습관을 들이세요.