본문 바로가기

취업과 기본기 튼튼/코딩 테스트 노트

[BOJ] 13460. 구슬 탈출 2

문제

구슬 탈출 2 (https://www.acmicpc.net/problem/13460)

풀이

1. 초기 접근

  • 문제조건 해석
    • 판이 주어지고, 스텝에 따라 판 위에 구슬의 위치가 규칙적으로 달라진다.
    • 목표하는 위치에 도달하기 위한 "최소" 스텝을 구해야 한다.
      • BFS 문제다.
  • 알고리즘
    • 이번 문제는 조금 까다로웠는데, step by step 으로 하나씩 코드화 해보자.
1) 먼저 기본적인 입력과 BFS 들을 만든다.

메인 코드는 아래와 같다.

from collections import deque

dxs = [-1, 0, 1, 0]
dys = [0, 1, 0, -1]

# n, m을 입력받고, board에 판을 저장한다.
n, m = list(map(int, input().split()))
board = [list(input()) for _ in range(n)]

# 빨간 구슬과 파란 구슬의 위치를 저장해둔다.
red, blue = None, None
for i in range(n):
    for j in range(m):
        if board[i][j] == 'R':
            red = (i, j)
        elif board[i][j] == 'B':
            blue = (i, j)

# bfs 함수에 두 구슬의 위치를 건네주면, 우리가 원하는 최소 스텝을 반환받는다.
print(bfs(red, blue))

bfs() 함수의 코드의 기본 틀은 아래와 같다.

def bfs(red, blue):
    q = deque([(red, blue)])
    visited = set([(red, blue)])
    step = 0

    while q and step <= 10:
        size = len(q)
        for _ in range(size):
            red, blue = q.popleft()

            ###########################
            # 채워야 하는 부분 #
            ###########################
        step += 1
    return -1

일단 여기까지는 쉽게 짤 수 있다.
이후에 할 일은, 채워야할 부분으로 표시한 부분을 채우는 일이다.

2) 각 4가지의 방향에 따라, 각 구슬이 이동할 다음 좌표를 구한다.

먼저 크게, 4가지 방향으로 우리는 판을 기울일 수 있다.
그리고 한번 정한 방향은 빨간 구슬과 파란 구슬, 두 구슬 모두의 다음 방향이 된다.

자 이제 각 구슬의 이동을 트래킹해보자.
예를 들어, 오른쪽으로 기울인다고 해보자. 그럼 오른쪽으로 이동할 것이다.
현재가 (x, y) 였다면 (x, y+1) 로 이동할 것이다.

그럼 얼마나 이런 이동을 반복할 것인가?
다음 이동하려는 좌표가 빈 공간(.) 일 경우, 해당 방향으로 계속해서 이동해야 한다.
한편, 다음 이동하려는 좌표가 벽(#) 이거나 탈출구멍(O) 일 경우, 이동을 멈춰야 한다. 그리고 그 좌표가 최종 이동 좌표가 된다.

여기까지 생각하면, 다음과 같이 코딩할 수 있다.

def bfs(red, blue):
              ...
              for _ in range(size):
            red, blue = q.popleft()

            # 기울이는 방향은 총 4가지다.
            for dx, dy in zip(dxs, dys):

                # 빨간 구슬을 먼저 움직인다.
                x, y = red
                nx, ny = x + dx, y + dy
                while True:
                    if board[nx][ny] == '.':
                        x, y = nx, ny
                        nx, ny = nx + dx, ny + dy
                    elif board[nx][ny] == 'O':
                        x, y = nx, ny
                        break
                    else:
                        break
                next_red = (x, y)

                # 그 다음 파란 구슬을 움직인다.
                # 코드는 위에 빨간 구슬이 움직인 것과 같다.
                x, y = blue
                nx, ny = x + dx, y + dy
                while True:
                    if board[nx][ny] == '.':
                        x, y = nx, ny
                        nx, ny = nx + dx, ny + dy
                    elif board[nx][ny] == 'O':
                        x, y = nx, ny
                        break
                    else:
                        break
                next_blue = (x, y)

        step += 1
    return -1
3) 불가능한 경우, 가능한 경우를 생각하여 BFS 큐에다가 넣어주자

먼저, 위 움직임에서 불가능한 경우는 어떤 경우일까?
파란색 구슬을 움직이는 중에, 다음 좌표가 탈출 구멍(O) 인 경우, 파란 구슬도 빠져버리는 경우이니, 이렇게 기울이는 경우는 제외해야 한다. 따라서, 별도의 is_possible 이라는 변수를 두어, 이렇게 빠져버리는 경우인지 아닌지 확인해야 한다.

한편, 우리는 "최소 횟수" 를 구하고 있으므로, 방문한 케이스를 또 방문할 필요가 없다. 이를 visited 로 해결하자.

def bfs(red, blue):
        ...
              for _ in range(size):
            ...                    
            for dx, dy in zip(dxs, dys):
                is_possible = True

                # 빨간 구슬을 먼저 움직인다.
                ...
                next_red = (x, y)

                # 그 다음 파란 구슬을 움직인다.
                ...
                while True:
                    ...
                    elif board[nx][ny] == 'O':
                        is_possible = False
                        break
                    ...
                next_blue = (x, y)

                # 현재 경우가 가능한지, 방문한 적은 없는지 체크하고 BFS 큐에 다음 좌표를 넣어준다.
                if is_possible and (next_red, next_blue) not in visited:
                    q.append((next_red, next_blue))
                    visited.add((next_red, next_blue))
        step += 1
    return -1

하는 김에, 지금 탈출 구문도 넣어주자. BFS queue 에서 꺼냈을 때, 빨간공의 좌표가 탈출 구멍 좌표라면 탈출에 성공한 것이다.

def bfs(red, blue):
    ...
    while q:
        for _ in range(size):
                        # 큐에서 끄낸 빨간공의 좌표에 탈출 구멍이 있는 경우, step을 반환하고 끝낸다.
            if board[red[0]][red[1]] == 'O':
                return step
            ...
5) 예외 케이스들을 생각해본다.

이 예외케이스들을 생각하며 해야하기 때문에 조금 골치가 아픈데, 첫 번째로 다음과 같은 케이스다.

생각해봐야 하는 케이스 1

3 10
##########
#.O....RB#
##########

위 코드로 이 케이스를 수행하면, 빨간 구슬이 이동할 때에는 문제가 없다.
그런데, 파란 구슬이 이동할 때에 문제가 있다. 왼쪽으로 기울인다고 하자. 이 때 파란구슬은 다음 이동 좌표로 R 을 만나게 되는데, 위 코드를 보면 우리는 이 경우에 대해 처리해준적이 없다. 심지어, 빨간구슬은 이미 이동했기 때문에 저 위치에 있지 않다. 하지만 우리는 판(board)를 수정한적이 없다. 따라서 BFS가 아무리 진행되도 판(board)은 초기 상태 그대로인 셈이다.

이 경우를 어떻게 해결하면 좋을까 생각하다가, 애초에 우리는 빨간 구슬과 파란 구슬의 이동하는 좌표만 트래킹하며 BFS queue(q) 에 넣고 있다는 사실을 발견했다. 그런데 지금 문제는 판(board) 자체에 있는 RB 가 문제니, 이를 아예 판에서 없애버리면 된다. 즉, RB를 발견하면 . 로 바꿔준다.

따라서 위 코드에서 해당부분을 다음과 같이 수정한다.

red, blue = None, None
for i in range(n):
    for j in range(m):
        if board[i][j] == 'R':
            red = (i, j)
            board[i][j] = '.'
        elif board[i][j] == 'B':
            blue = (i, j)
            board[i][j] = '.'

생각해봐야 하는 케이스 2

5 7
#######
####O##
####.##
#R...B#
#######

두 번쨰로 생각해봐야 하는 케이스는 위 경우인데, 오른쪽으로 기울인다고 생각해보자.
먼저, 빨간 구슬이 오른쪽으로 쭈우욱 움직이는데, 우리는 판(board) 에서 B. 로 바꿨으니 파란 구슬이 있는 위치까지 움직여 결과적으로 파란 구슬과 겹치게 된다. 이는 구슬이 겹치게 되는 이상한 결과다.

이 케이스를 고려하여, 각 구슬이 얼마나 움직이는지 red_step, blue_step 을 두어, 후에 두 구슬의 다음 움직일 위치가 겹치는 경우, 더 적게 움직이는 쪽을 우선하여 위치를 고려해줘야 한다.

이를 코드화하면 다음과 같다.

            ...
            for dx, dy in zip(dxs, dys):
                ...
                while True:
                    if board[nx][ny] == '.':
                        ...
                                  # 빨간 구슬이 얼마나 움직였는지 체크해논다.
                        red_step += 1
                          ...
                ...       

                while True:
                    if board[nx][ny] == '.':
                        ...
                        # 파란 구슬이 얼마나 움직였는지 체크해논다.
                        blue_step += 1
                    ...
                ...

                # 두 구슬의 다음 이동 좌표가 같은 경우, 좌표를 조정해준다.
                if next_red == next_blue:
                    if red_step < blue_step:
                        a, b = next_blue
                        next_blue = (a-dx, b-dy)
                    else:
                        a, b = next_red
                        next_red = (a-dx, b-dy)
            ...

코드

전체적인 코드는 다음과 같다.

from collections import deque

dxs = [-1, 0, 1, 0]
dys = [0, 1, 0, -1]

def bfs(red, blue):
    q = deque([(red, blue)])
    visited = set([(red, blue)])
    step = 0

    while q and step <= 10:
        size = len(q)
        for _ in range(size):
            red, blue = q.popleft()

            if board[red[0]][red[1]] == 'O':
                return step

            for dx, dy in zip(dxs, dys):
                is_possible = True

                x, y = red
                nx, ny = x + dx, y + dy
                red_step = 0
                while True:
                    if board[nx][ny] == '.':
                        x, y = nx, ny
                        nx, ny = nx + dx, ny + dy
                        red_step += 1
                        continue
                    elif board[nx][ny] == 'O':
                        x, y = nx, ny
                        red_step += 1
                        break
                    else:
                        break
                next_red = (x, y)

                x, y = blue
                nx, ny = x + dx, y + dy
                blue_step = 0
                while True:
                    if board[nx][ny] == '.':
                        x, y = nx, ny
                        nx, ny = nx + dx, ny + dy
                        blue_step += 1
                    elif board[nx][ny] == 'O':
                        is_possible = False
                        blue_step += 1
                        break
                    else:
                        break
                next_blue = (x, y)

                if next_red == next_blue:
                    if red_step < blue_step:
                        a, b = next_blue
                        next_blue = (a-dx, b-dy)
                    else:
                        a, b = next_red
                        next_red = (a-dx, b-dy)

                if is_possible and (next_red, next_blue) not in visited:
                    q.append((next_red, next_blue))
                    visited.add((next_red, next_blue))
        step += 1
    return -1


n, m = list(map(int, input().split()))
board = [list(input()) for _ in range(n)]

red, blue = None, None
for i in range(n):
    for j in range(m):
        if board[i][j] == 'R':
            red = (i, j)
            board[i][j] = '.'
        elif board[i][j] == 'B':
            blue = (i, j)
            board[i][j] = '.'

print(bfs(red, blue))

제출하면, 맞았습니다가 나온다.

그런데 위 코드에서 중복되는 부분이 있어, 이 부분을 좀 더 리팩토링하여 보았다.

중복을 제거하여 리팩토링한 코드

from collections import deque

dxs = [-1, 0, 1, 0]
dys = [0, 1, 0, -1]

def bfs(red, blue):
    q = deque([(red, blue)])
    visited = set([(red, blue)])
    step = 0

    while q and step <= 10:
        size = len(q)
        for _ in range(size):
            red, blue = q.popleft()

            if board[red[0]][red[1]] == 'O':
                return step

            for dx, dy in zip(dxs, dys):
                is_possible = True

                next_ball = [-1, -1]
                ball_step = [0, 0]
                for ball_idx, ball in enumerate([red, blue]):

                    x, y = ball
                    nx, ny = x + dx, y + dy
                    while True:
                        if board[nx][ny] == '.':
                            x, y = nx, ny
                            nx, ny = nx + dx, ny + dy
                            ball_step[ball_idx] += 1
                            continue
                        elif board[nx][ny] == 'O':
                            if ball == blue:
                                is_possible = False
                            else:
                                x, y = nx, ny
                                ball_step[ball_idx] += 1
                            break
                        else:
                            break
                    next_ball[ball_idx] = (x, y)

                if next_ball[0] == next_ball[1]:
                    if ball_step[0] < ball_step[1]:
                        a, b = next_ball[1]
                        next_ball[1] = (a-dx, b-dy)
                    else:
                        a, b = next_ball[0]
                        next_ball[0] = (a-dx, b-dy)
                next_ball = tuple(next_ball)
                if is_possible and next_ball not in visited:
                    q.append(next_ball)
                    visited.add(next_ball)
        step += 1
    return -1


n, m = list(map(int, input().split()))
board = [list(input()) for _ in range(n)]

red, blue = None, None
for i in range(n):
    for j in range(m):
        if board[i][j] == 'R':
            red = (i, j)
            board[i][j] = '.'
        elif board[i][j] == 'B':
            blue = (i, j)
            board[i][j] = '.'

print(bfs(red, blue))

그렇게 깔끔한 코드는 아니지만, 이전보다는 중복된 코드가 덜 보인다.

'취업과 기본기 튼튼 > 코딩 테스트 노트' 카테고리의 다른 글

[BOJ] 16235. 나무 재테크  (2) 2019.04.10
[BOJ] 16236. 아기 상어  (1) 2019.04.10
[BOJ] 1964. 소수 경로  (0) 2019.04.08
[BOJ] 2636. 치즈  (0) 2019.04.08
[BOJ] 13913. 숨바꼭질 4  (0) 2019.04.07