Theme:

알고리즘은 쉬운데 코드가 100줄이 넘어가면서 버그가 생깁니다. 어떻게 하면 길고 복잡한 구현을 깔끔하게 작성할 수 있을까요?

구현/시뮬레이션 문제란

구현 문제 는 특별한 알고리즘 지식보다 정확한 코드 작성 능력 을 요구합니다. 조건이 많고 코드가 길어지면서 실수하기 쉬운 것이 특징입니다.

시뮬레이션 문제 는 주어진 규칙대로 상태를 단계별로 변화시키는 문제입니다. 게임 진행, 로봇 이동, 물리 시뮬레이션 등이 여기 속합니다.

왜 어려운가

  • 알고리즘 자체는 단순하지만 조건 분기가 많습니다
  • 코드가 길어지면서 ** 오타, 인덱스 오류 **가 발생합니다
  • 경계 조건(가장자리, 빈 입력)을 놓치기 쉽습니다
  • 디버깅이 어렵습니다 — 어디서 잘못됐는지 찾기 힘듭니다

좌표계 통일

2차원 격자 문제에서 가장 먼저 할 일은 좌표계를 통일하는 것입니다.

JAVA
// 배열 기준 좌표계 (가장 일반적)
// grid[row][col] = grid[r][c]
// r이 증가하면 아래로, c가 증가하면 오른쪽으로

// 4방향 이동 배열 (상, 하, 좌, 우)
int[] dr = {-1, 1, 0, 0};
int[] dc = {0, 0, -1, 1};

// 8방향 이동 배열 (상, 하, 좌, 우 + 대각선 4개)
int[] dr8 = {-1, -1, -1, 0, 0, 1, 1, 1};
int[] dc8 = {-1, 0, 1, -1, 1, -1, 0, 1};

방향과 회전

JAVA
// 방향: 0=북, 1=동, 2=남, 3=서
int[] dr = {-1, 0, 1, 0};  // 북, 동, 남, 서 순서
int[] dc = {0, 1, 0, -1};

// 오른쪽 회전: dir = (dir + 1) % 4
// 왼쪽 회전:  dir = (dir + 3) % 4  (= dir - 1 + 4를 mod 4)
// 반대 방향:  dir = (dir + 2) % 4

int direction = 0; // 북쪽 시작

// 오른쪽으로 90도 회전
direction = (direction + 1) % 4;

// 현재 방향으로 한 칸 이동
int nr = r + dr[direction];
int nc = c + dc[direction];

경계 조건 처리

JAVA
// 범위 체크 — 함수로 분리
private boolean inBounds(int r, int c, int rows, int cols) {
    return r >= 0 && r < rows && c >= 0 && c < cols;
}

// 사용
int nr = r + dr[d];
int nc = c + dc[d];
if (inBounds(nr, nc, rows, cols) && grid[nr][nc] != WALL) {
    // 이동 가능
}

패딩(padding) 기법

경계 체크를 줄이기 위해 배열 주위에 벽을 둘러싸는 기법입니다.

JAVA
// 원래 크기가 N×M이면, (N+2)×(M+2)로 만들고 테두리를 벽으로
int[][] grid = new int[N + 2][M + 2];

// 테두리를 벽으로 초기화
for (int i = 0; i < N + 2; i++) {
    grid[i][0] = grid[i][M + 1] = WALL;
}
for (int j = 0; j < M + 2; j++) {
    grid[0][j] = grid[N + 1][j] = WALL;
}

// 실제 데이터는 (1, 1) ~ (N, M)에 저장
// → inBounds 체크 없이 바로 grid[nr][nc] != WALL만 확인하면 됨

현재 상태와 다음 상태 분리

시뮬레이션에서 가장 흔한 실수는 현재 턴의 변경이 같은 턴의 다른 계산에 영향을 주는 것입니다.

JAVA
// 나쁜 예 — 같은 배열을 즉시 수정
for (int r = 0; r < rows; r++) {
    for (int c = 0; c < cols; c++) {
        // grid[r][c]를 보고 결정하는데, 앞의 셀이 이미 변경됨!
        grid[r][c] = computeNext(grid, r, c);
    }
}

// 좋은 예 — 별도 배열에 기록 후 한꺼번에 반영
int[][] next = new int[rows][cols];
for (int r = 0; r < rows; r++) {
    for (int c = 0; c < cols; c++) {
        next[r][c] = computeNext(grid, r, c);
    }
}
// 한꺼번에 복사
for (int r = 0; r < rows; r++) {
    System.arraycopy(next[r], 0, grid[r], 0, cols);
}

코드 모듈화 전략

반복되는 로직을 함수로

JAVA
// 나쁜 예 — 같은 코드가 여러 곳에 복붙
if (direction == 0) {
    nr = r - 1; nc = c;
    if (nr >= 0 && grid[nr][nc] != 1) { r = nr; }
} else if (direction == 1) {
    nr = r; nc = c + 1;
    if (nc < cols && grid[nr][nc] != 1) { c = nc; }
}
// ... 반복

// 좋은 예 — 방향 배열 + 함수
private int[] move(int r, int c, int dir, int[][] grid) {
    int nr = r + dr[dir];
    int nc = c + dc[dir];
    if (inBounds(nr, nc, grid.length, grid[0].length) && grid[nr][nc] != WALL) {
        return new int[]{nr, nc};
    }
    return new int[]{r, c}; // 이동 불가 시 제자리
}

단계별 분리

JAVA
// 시뮬레이션의 각 턴을 단계별로 분리
public void simulate() {
    for (int turn = 0; turn < maxTurns; turn++) {
        moveObjects();      // 1단계: 이동
        checkCollisions();  // 2단계: 충돌 검사
        applyEffects();     // 3단계: 효과 적용
        removeDestroyed();  // 4단계: 제거
    }
}

상태 클래스 활용

JAVA
// 여러 값을 묶어서 관리
class Robot {
    int r, c, dir;
    boolean alive;

    Robot(int r, int c, int dir) {
        this.r = r; this.c = c; this.dir = dir;
        this.alive = true;
    }

    void turnRight() { dir = (dir + 1) % 4; }
    void turnLeft() { dir = (dir + 3) % 4; }

    void moveForward(int[][] grid) {
        int nr = r + dr[dir];
        int nc = c + dc[dir];
        if (inBounds(nr, nc, grid.length, grid[0].length)) {
            r = nr; c = nc;
        }
    }
}

디버깅 전략

격자 상태 출력

JAVA
private void printGrid(int[][] grid, int robotR, int robotC) {
    for (int r = 0; r < grid.length; r++) {
        for (int c = 0; c < grid[0].length; c++) {
            if (r == robotR && c == robotC) {
                System.out.print("R ");
            } else {
                System.out.print(grid[r][c] + " ");
            }
        }
        System.out.println();
    }
    System.out.println("---");
}

작은 예제부터 확인

JAVA
// 큰 입력을 바로 테스트하지 말고
// 2×2, 3×3 같은 작은 격자에서 먼저 확인
int[][] smallGrid = {
    {0, 0, 0},
    {0, 1, 0},
    {0, 0, 0}
};
// 예상 결과와 비교

자주 나오는 시뮬레이션 패턴

뱀 게임

JAVA
// 뱀의 몸체를 Deque로 관리
Deque<int[]> snake = new ArrayDeque<>();
snake.offerFirst(new int[]{0, 0}); // 머리
Set<String> body = new HashSet<>();
body.add("0,0");

// 이동: 머리를 추가하고, 사과가 없으면 꼬리 제거
int[] head = {snake.peekFirst()[0] + dr[dir], snake.peekFirst()[1] + dc[dir]};
snake.offerFirst(head);
body.add(head[0] + "," + head[1]);

if (!hasApple(head)) {
    int[] tail = snake.pollLast();
    body.remove(tail[0] + "," + tail[1]);
}

톱니바퀴

JAVA
// 톱니바퀴 회전 — 인접 톱니바퀴와 맞물림 체크
// 한 바퀴 회전 시 인접 톱니바퀴의 연쇄 회전 처리
void rotate(int[][] gears, int idx, int dir) {
    int[] rotations = new int[4];
    rotations[idx] = dir;

    // 왼쪽 전파
    for (int i = idx - 1; i >= 0; i--) {
        if (gears[i][2] != gears[i + 1][6]) { // 맞닿은 극이 다르면
            rotations[i] = -rotations[i + 1]; // 반대 방향 회전
        } else break;
    }

    // 오른쪽 전파
    for (int i = idx + 1; i < 4; i++) {
        if (gears[i][6] != gears[i - 1][2]) {
            rotations[i] = -rotations[i - 1];
        } else break;
    }

    // 실제 회전 적용
    for (int i = 0; i < 4; i++) {
        if (rotations[i] != 0) applyRotation(gears[i], rotations[i]);
    }
}

체크리스트

코드 제출 전에 확인해야 할 사항입니다.

PLAINTEXT
□ 좌표계가 일관되는가? (행/열, x/y 혼동 없는지)
□ 경계 조건 — 0, N-1, 빈 배열을 처리하는가?
□ 방향 회전 — (dir + 1) % 4가 맞는지, 방향 배열과 일치하는지?
□ 현재/다음 상태 — 같은 턴의 변경이 서로 영향을 주지 않는지?
□ 오프 바이 원 — for문의 시작/끝이 정확한지?
□ 자료형 오버플로 — int 범위를 넘지 않는지?

주의할 점

문제를 읽자마자 코딩을 시작하는 실수

구현 문제는 설계 없이 바로 코딩하면 중간에 구조가 꼬여 전체를 다시 작성하게 됩니다. 입출력 형식, 상태 표현, 이동/변환 규칙을 먼저 정리한 뒤 코딩을 시작해야 합니다.

방향 배열(dx, dy)에서 인덱스 실수

4방향/8방향 이동에서 dx, dy 배열의 순서를 잘못 설정하면 대각선으로 가야 할 곳을 직진합니다. 디버깅이 어려우므로 항상 테스트 케이스로 검증해야 합니다.


정리

  • ** 좌표계를 통일 **하고, 방향 배열 dr/dc를 사용하여 이동을 간결하게 처리합니다
  • ** 경계 체크를 함수로** 분리하거나, 패딩 기법으로 제거합니다
  • ** 현재 상태와 다음 상태를 분리 **하여, 같은 턴의 변경이 간섭하지 않게 합니다
  • 반복되는 로직은 ** 함수로 분리 하고, 상태는 ** 클래스로 묶어 관리합니다
  • ** 작은 예제 **부터 격자 상태를 출력하며 단계별로 검증합니다
  • 제출 전 좌표, 경계, 방향, 오프바이원, 오버플로를 반드시 점검합니다
댓글 로딩 중...