플레이어 A와 플레이어 B가 서로 게임을 합니다. 당신은 이 게임이 끝날 때까지 양 플레이어가 캐릭터를 몇 번 움직이게 될지 예측하려고 합니다.
각 플레이어는 자신의 캐릭터 하나를 보드 위에 올려놓고 게임을 시작합니다. 게임 보드는 1x1 크기 정사각 격자로 이루어져 있으며, 보드 안에는 발판이 있는 부분과 없는 부분이 있습니다. 발판이 있는 곳에만 캐릭터가 서있을 수 있으며, 처음 캐릭터를 올려놓는 곳은 항상 발판이 있는 곳입니다. 캐릭터는 발판이 있는 곳으로만 이동할 수 있으며, 보드 밖으로 이동할 수 없습니다. 밟고 있던 발판은 그 위에 있던 캐릭터가 다른 곳으로 이동하여 다른 발판을 밞음과 동시에 사라집니다. 양 플레이어는 번갈아가며 자기 차례에 자신의 캐릭터를 상하좌우로 인접한 4개의 칸 중에서 발판이 있는 칸으로 옮겨야 합니다.
다음과 같은 2가지 상황에서 패자와 승자가 정해지며, 게임이 종료됩니다.
움직일 차례인데 캐릭터의 상하좌우 주변 4칸이 모두 발판이 없거나 보드 밖이라서 이동할 수 없는 경우, 해당 차례 플레이어는 패배합니다.
두 캐릭터가 같은 발판 위에 있을 때, 상대 플레이어의 캐릭터가 다른 발판으로 이동하여 자신의 캐릭터가 서있던 발판이 사라지게 되면 패배합니다.
게임은 항상 플레이어 A가 먼저 시작합니다. 양 플레이어는 최적의 플레이를 합니다. 즉, 이길 수 있는 플레이어는 최대한 빨리 승리하도록 플레이하고, 질 수밖에 없는 플레이어는 최대한 오래 버티도록 플레이합니다. '이길 수 있는 플레이어'는 실수만 하지 않는다면 항상 이기는 플레이어를 의미하며, '질 수밖에 없는 플레이어'는 최선을 다해도 상대가 실수하지 않으면 항상 질 수밖에 없는 플레이어를 의미합니다. 최대한 오래 버틴다는 것은 양 플레이어가 캐릭터를 움직이는 횟수를 최대화한다는 것을 의미합니다.
아래 그림은 초기 보드의 상태와 각 플레이어의 위치를 나타내는 예시입니다.
위와 같은 경우, 플레이어 A는 실수만 하지 않는다면 항상 이길 수 있습니다. 따라서 플레이어 A는 이길 수 있는 플레이어이며, B는 질 수밖에 없는 플레이어입니다. 다음은 A와 B가 최적의 플레이를 하는 과정을 나타냅니다.
위 예시에서 양 플레이어가 최적의 플레이를 했을 경우, 캐릭터의 이동 횟수 합은 5입니다. 최적의 플레이를 하는 방법은 여러 가지일 수 있으나, 이동한 횟수는 모두 5로 같습니다.
게임 보드의 초기 상태를 나타내는 2차원 정수 배열 board와 플레이어 A의 캐릭터 초기 위치를 나타내는 정수 배열 aloc, 플레이어 B의 캐릭터 초기 위치를 나타내는 정수 배열 bloc이 매개변수로 주어집니다. 양 플레이어가 최적의 플레이를 했을 때, 두 캐릭터가 움직인 횟수의 합을 return 하도록 solution 함수를 완성해주세요.
- 크게 보면 백트래킹이긴 하나 MinMax 알고리즘을 공부한 뒤 풀어보도록 하자.
이 문제를 처음 접했을 때 백트래킹으로 풀어야겠다는 생각까지는 했지만,
구현하면 할 수록 뭔가 머리가 복잡해지는 느낌이었다.
문제 조건에 있는 "최적의 플레이" 라는 조건 때문인 거 같다.
A와 B는 최대한 지능적으로 플레이해야한다는 조건이 기존 백트래킹 방식을
어떻게 구현해야할지 막막하게 만들었다.
결국 고민 끝에 검색을 해봤는데, 게임이론 중 하나인 MinMax알고리즘이라고 한다.
주로 바둑, 장기, TicTacToe? 에 쓰이는 알고리즘이라고 한다.
알고리즘 설명을 들어도 여기에 어떻게 대입을 해야할까라는 것이 계속 이해가 안갔지만
https://blog.encrypted.gg/1032
이 블로거분의 해석을 들으니 살짝 이해가 간듯하다.
지는 경우의 최대 이동 리턴, 이기는 경우의 최소 이동 리턴이라고 생각하면 된다.
우선 순위는 이기는 경우가 우선순위에 있고,
전부 패배하는 경우가 아니라면 이기는 경우 중 최솟값을 리턴하면 된다.
dy,dx = [-1,0,1,0],[0,1,0,-1]
def check_out_of_range(R,C,row,col,drow,dcol):
if 0 <= row+drow < R and 0 <= col+dcol < C:
return False
else:
return True
def search(board,aloc,bloc,now):
answer = 0
if now == "A": # A가 움직일 차례
row,col = aloc
if board[row][col] == 0: # 현재 있는 발판이 0일경우 패배
return 0
for drow,dcol in zip(dy,dx):
# 범위를 벗어나거나 이동할 수 없는 발판일 경우
if check_out_of_range(len(board),len(board[0]),row,col,drow,dcol):
continue
elif board[row+drow][col+dcol] == 0:
continue
board[row][col] = 0
result = search(board,[row+drow,col+dcol],bloc,"B")+1
board[row][col] = 1
# 현재 저장된 결과가 짝수라면 패배인 상태, 홀수라면 승리인 상태
if answer % 2 == 0 and result % 2 == 1:
# 현재 저장된 결과가 패배이고, 결과가 승리가 나온 상태라면 갱신
answer = result
elif answer % 2 == 1 and result % 2 == 1:
# 현재 저장된 결과가 승리이고, 결과가 승리가 나온 상태라면 최솟값 갱신
answer = min(answer,result)
elif answer % 2 == 0 and result % 2 == 0:
# 현재 저장된 결과가 패배이고, 결과가 패배가 나온 상태라면 최댓값 갱신
answer = max(answer,result)
else: # B가 움직일 차례
row,col = bloc
if board[row][col] == 0: # 현재 있는 발판이 0일경우 패배
return 0
for drow,dcol in zip(dy,dx):
# 범위를 벗어나거나 이동할 수 없는 발판일 경우
if check_out_of_range(len(board),len(board[0]),row,col,drow,dcol):
continue
elif board[row+drow][col+dcol] == 0:
continue
board[row][col] = 0
result = search(board,aloc,[row+drow,col+dcol],"A")+1
board[row][col] = 1
현재 저장된 결과가 짝수라면 패배인 상태, 홀수라면 승리인 상태
if answer % 2 == 0 and result % 2 == 1:
# 현재 저장된 결과가 패배이고, 결과가 승리가 나온 상태라면 갱신
answer = result
elif answer % 2 == 1 and result % 2 == 1:
# 현재 저장된 결과가 승리이고, 결과가 승리가 나온 상태라면 최솟값 갱신
answer = min(answer,result)
elif answer % 2 == 0 and result % 2 == 0:
# 현재 저장된 결과가 패배이고, 결과가 패배가 나온 상태라면 최댓값 갱신
answer = max(answer,result)
return answer
def solution(board,aloc,bloc):
return search(board,aloc,bloc,"A")
카카오 마지막 문제기 때문에 어렵다는 것은 알았지만,
게임이론이라는 내용을 이번에 처음 알게 되었다.
게임이론에 사용되는 알고리즘이 기업 코딩테스트에서 나오는 것을 보니
아직 배워야할 알고리즘이 많다는 것을 알게 되었다.