https://www.acmicpc.net/problem/15778
진짜 디버깅.. 너무 힘들었다.
내가 스스로 고안한 풀이 방법으로 풀 땐 죽어도 안풀리다가,,,,
다른 사람이 사용한 구현 아이디어를 활용하여 구현하니까 한번 틀렸는데,
다행히 틀린 부분이 예제 입력 2번으로 디버깅이 되서 바로 통과됐다.
(그래서 자존심에 매우 큰 스크래치를 입은 상태이다..ㅡㅡ)
https://swoon1.tistory.com/4?category=804646
나는 윷놀이 판을 7*7 정사각형 틀에 짜맞춰서 위치를 잡고, 해당 틀에서 이동을 시키다가
마지막에 한번에 해당 틀의 값을 문제 정답형태로 옮겨서 출력하려고 했었다.
그리고 왜인지 이렇게 하니까 계속 틀려서 다른 사람의 코드를 봤는데, 보통 문자배열을 직접 활용하길래 따라해봤다.
기본적인 판 설계는 swoon님의 아이디어를 빌렸다.
외곽을 도는 방향, 우하향하는 방향, 좌하향하는 방향
그리고 각 방향에 대해 시작점으로부터 몇칸을 이동했는지
이렇게 방향과, 시작점으로부터의 거리 2가지 정보를 가지고 위치를 잡았다.
(수학에서 극좌표와 같은 원리)
상단에 링크를 걸어둔 swoon님 블로그 글에 위 좌표계를 이미지로 잘 표현해주셨다.
이해가 안된다면 한번 방문해보길 권장한다.
그리고 윷놀이에 쓰이는 말을 클래스로 만들었다.
필드는 보다시피 현재 방향, 위치 2개이다.
디버깅용 말 출력을 위해 str 메소드를 정의해주었다.
tuple(int i, int j) getBoardPos() = 방향, 위치 좌표를 2차원 배열 좌표로 변환
(앞으로 방위좌표, 배열좌표로 부르겠다.)
def getBoardPos(self):
self.setDirection()
if self.direction == "last":
return 30, 30
if self.direction == "default":
if self.pos // 5 == 0: # default 방향에서 1 ~ 4 위치면 오른쪽 세로임.
return 30 - (self.pos % 5) * 6, 30
if self.pos // 5 == 1: # default 방향에서 6 ~ 9 위치면 위쪽임
return 0, 30 - (self.pos % 5) * 6
if self.pos // 5 == 2: # default 방향에서 11 ~ 14 위치면 왼쪽 세로임.
return 30 - (5 - (self.pos % 5)) * 6, 0
if self.pos // 5 == 3: # default 방향에서 15 ~ 19 위치면 아래임.
return 30, 30 - (5 - (self.pos % 5)) * 6
if self.direction == "bot_left":
return self.pos * 5, 30 - self.pos * 5
if self.direction == "bot_right":
return self.pos * 5, self.pos * 5
출력할 문자열을 문자 배열로 보고 배열좌표를 고민해보자.
배열좌표는 32 * 32 사이즈이다. 인덱스로는 0 ~ 31 까지이다.
이때 abcd 각각 위치가 다른데, bcd의 위치는 a를 기준으로 상대적으로 잡아주면 된다.
따라서 a좌표를 기준으로 생각하자.
그러면 인덱스 상으로 a의 좌표는 방향에 따라 0, 5, 10, ,,, 25, 30 이런식으로 5씩 증가하거나 0, 6, 12, ,,, 24, 30 이런식으로 6씩 증가한다.
bool isSamePos(Mal other_mal) = 현재 자신의 방위좌표와 다른 말의 방위좌표를 비교
def isSamePos(self, _mal):
other = MalDict[_mal]
other.setDirection()
return self.direction == other.direction and self.pos == other.pos
방위좌표를 비교하는 건 간단하다. 방향과 기준점으로부터 상대적인 위치가 같으면 방위좌표가 같은 것이다.
void setDirection() = 현재 방위좌표에 따라 앞으로 이동할 방향을 정의
# 움직이기 전, 현재 위치를 바탕으로 이동할 방향 결정하기
def setDirection(self):
if self.direction == "default":
if self.pos == 5:
self.direction = "bot_left"
self.pos = 0
elif self.pos == 10:
self.direction = "bot_right"
self.pos = 0
elif self.pos == 20:
self.direction = "last"
self.pos = 0
elif self.direction == "bot_left":
if self.pos == 3:
self.direction = "bot_right"
elif self.pos == 6:
self.direction = "default"
self.pos = 15
elif self.direction == "bot_right":
if self.pos == 6:
self.direction = "last"
self.pos = 0
else:
return
왼쪽위 모서리를 bot_right의 기준점, 오른쪽 위 모서리를 bot_left의 기준점, 오른쪽 아래 모서리를 default 의 기준점으로 잡았다.
만약 말이 이동하여 오른쪽아래 모서리로 온다면, 그 곳에서는 어떻게 이동하든 1번만 이동하면 말이 나가므로
last 라고 방향을 정의했다.
last좌표에서 나간 말을 구분하기 위해 finish 방향을 정의했다.
(사실 방향으로 변수명을 짓긴 했지만, 말의 상태를 나타내는 역할도 하게 되었다.
사실 이렇게 의미가 섞이는 코딩은 별로 좋지않지만, 변수를 여러개 분리해서 관리하면 개인적으로 그게 더 복잡하고 실수하기 쉬워서 이렇게 했다.)
void move(int move_count) = 현재 지정된 방향을 따라 이동
def move(self, count):
assert 0 < count <= 5
while count:
self.pos += 1
if self.direction == "bot_left":
if self.pos == 6:
self.direction = "default"
self.pos = 15
elif self.direction == "default":
if self.pos == 20:
self.direction = "last"
self.pos = 0
elif self.direction == "bot_right":
if self.pos == 6:
self.direction = "last"
self.pos = 0
elif self.direction == "last":
self.direction = "Finish"
self.pos = 0
break
else: # Finish 에서 이동을 시도한 경우
raise Exception("Wrong Move : " + self.direction)
count -= 1
디버깅을 위해 assert 문과 raise 구문을 활용했다. 제출시 Assertion 에러와 런타임 에러를 잡아내는 용도이다.
이동하면서 방향이 바뀔 수 있는 왼쪽 아래 모서리, 그리고 이동하면서 last, finish로 상태가 바뀔 수 있는 오른쪽 아래 모서리에 대해 케이스를 나눴다.
이렇게 정의한 Mal 클래스를 이용하여 윷놀이 판 위의 말을 표현했다.
다음은 기타 함수이다.
단순히 보드를 출력하는 printBoard()
윷가락 상태를 이동횟수로 봐꿔주는 getMoveCount()
두 말이 같은 팀인지 판별하는 isSameKind()
함수명으로 Team 대신 Kind 를 쓴 이유는 Team 이라고 지었을 때,
같은 팀이 현재 같은 그룹으로 묶여서 이동 중인 팀인지,
대소문자가 같은 팀인지 헷갈릴 여지가 있다고 생각했기 때문이다.
이렇게 작성한 클래스와 함수를 활용하여 아래와 같이 문제를 풀었다.
우선 최종 출력할 문자배열과,
현재 보드에 올라와있는 말을 담을 MalDict 딕셔너리를 준비했다.
이 문제는 말이 이동할 때마다 보드 상태를 바꿔줄 필요가 없다.
어차피 필요한건 마지막 최종 보드상태이니까 보드는 마지막에 변환해서 한번에 출력하면 된다.
그 전까지는 우리가 정해둔 좌표계 내에서 이동하고 업고, 잡으면 된다.
N = int(input())
for _ in range(N):
mal, yutState = input().split()
moveCount = getMoveCount(yutState)
# 보드에 없던 말이면 새로 생성
if mal not in MalDict.keys():
MalDict[mal] = Mal()
friend_list = []
# 같이 이동할 친구 찾기
for _mal in MalDict.keys():
# 친구를 방위좌표 기준으로 찾기때문에 미리 방향 세팅, 이거 한줄 안써서 54점맞음 ㅡㅡ
MalDict[_mal].setDirection()
if MalDict[_mal].isSamePos(mal) and isSameKind(mal, _mal):
friend_list.append(_mal)
# 친구들끼리 이동
for friend in friend_list:
MalDict[friend].setDirection() # 이동 전 확실하게 방향 설정 (isSamePos 로 비교 하면서 설정이 되긴 했음)
MalDict[friend].move(moveCount)
enemy_list = []
# 이동한 곳에 적이 있는지 찾기 (비어있거나 친구가 있으면, 다음 이동때 알아서 같이 움직이게 됨)
for _mal in MalDict.keys():
if MalDict[_mal].isSamePos(mal) and not isSameKind(mal, _mal):
enemy_list.append(_mal)
for enemy in enemy_list:
MalDict.pop(enemy) # 초기화 대신 보드에 나와있는 말 딕셔너리에서 객체 자체를 없애버림.
말을 미리 만들어두고, 만든 말의 상태를 관리하고, 죽으면 말을 초기화하는 것은
나에게 있어 너무 복잡하고 실수할 여지가 많은 방식이었다.
(실제로 초기화를 하다 말아서 서브테스크 통과여부가 갈리기도 했다.)
그래서 그냥 말이 죽으면 객체를 제거해버리고, 다시 판에 올라오면 다시 객체를 만들기로 했다.
이렇게 하니 코드가 매우 간결해지고 직관적으로 변했다.
이제 하라는대로 이동을 다 했으니 출력만 하면 된다.
# 이동이 끝나고 보드에 있는 말 딕셔너리에 대해 각각의 위치를 보드 좌표에 맞게 바꿔서 보드에 넣어주기
for _mal in MalDict.keys():
if MalDict[_mal].direction == "Finish":
continue
MalDict[_mal].setDirection()
_i, _j = MalDict[_mal].getBoardPos()
if _mal in 'bB':
_j += 1
elif _mal in 'cC':
_i += 1
elif _mal in 'dD':
_i += 1
_j += 1
assert board[_i][_j] == "."
board[_i][_j] = _mal
# print answer
printBoard()
매번 이동 명령이 끝날 때마다 보드에 옮겨서 넣어줄 필요가 없다.
모든 이동명령이 끝나면, 그 상태를 그대로 보드로 옮겨준다.
Finish 상태의 말은 출력할 필요가 없으니 건너뛴다.
보드로 옮기는 작업이 끝나면 printBoard 함수로 최종 보드를 출력하면 끝이다.
전체 코드
##### 보드에 직접 그리는 방식으로 구현해봄 #####
import sys
input = sys.stdin.readline
def printBoard():
for i in range(32):
print("".join(board[i]))
def getMoveCount(_yutState):
count = _yutState.count('F')
return 5 if count == 0 else count
def isSameKind(_mal1, _mal2):
return _mal1.islower() == _mal2.islower()
class Mal:
def __init__(self):
self.direction = "default"
self.pos = 0
def __str__(self):
return self.direction + " " + str(self.pos)
def getBoardPos(self):
self.setDirection()
if self.direction == "last":
return 30, 30
if self.direction == "default":
if self.pos // 5 == 0: # default 방향에서 1 ~ 4 위치면 오른쪽 세로임.
return 30 - (self.pos % 5) * 6, 30
if self.pos // 5 == 1: # default 방향에서 6 ~ 9 위치면 위쪽임
return 0, 30 - (self.pos % 5) * 6
if self.pos // 5 == 2: # default 방향에서 11 ~ 14 위치면 왼쪽 세로임.
return 30 - (5 - (self.pos % 5)) * 6, 0
if self.pos // 5 == 3: # default 방향에서 15 ~ 19 위치면 아래임.
return 30, 30 - (5 - (self.pos % 5)) * 6
if self.direction == "bot_left":
return self.pos * 5, 30 - self.pos * 5
if self.direction == "bot_right":
return self.pos * 5, self.pos * 5
def isSamePos(self, _mal):
other = MalDict[_mal]
other.setDirection()
return self.direction == other.direction and self.pos == other.pos
def setDirection(self): # 움직이기 전, 현재 위치를 바탕으로 이동할 방향 결정하기
if self.direction == "default":
if self.pos == 5:
self.direction = "bot_left"
self.pos = 0
elif self.pos == 10:
self.direction = "bot_right"
self.pos = 0
elif self.pos == 20:
self.direction = "last"
self.pos = 0
elif self.direction == "bot_left":
if self.pos == 3:
self.direction = "bot_right"
elif self.pos == 6:
self.direction = "default"
self.pos = 15
elif self.direction == "bot_right":
if self.pos == 6:
self.direction = "last"
self.pos = 0
else:
return
def move(self, count):
assert 0 < count <= 5
while count:
self.pos += 1
if self.direction == "bot_left":
if self.pos == 6:
self.direction = "default"
self.pos = 15
elif self.direction == "default":
if self.pos == 20:
self.direction = "last"
self.pos = 0
elif self.direction == "bot_right":
if self.pos == 6:
self.direction = "last"
self.pos = 0
elif self.direction == "last":
self.direction = "Finish"
self.pos = 0
break
else: # Finish 에서 이동을 시도한 경우
raise Exception("Wrong Move : " + self.direction)
count -= 1
board = [
list("..----..----..----..----..----.."),
list(".. .. .. .. .. .."),
list("| \ / |"),
list("| \ / |"),
list("| \ / |"),
list("| .. .. |"),
list(".. .. .. .."),
list(".. \ / .."),
list("| \ / |"),
list("| \ / |"),
list("| .. .. |"),
list("| .. .. |"),
list(".. \ / .."),
list(".. \ / .."),
list("| \ / |"),
list("| .. |"),
list("| .. |"),
list("| / \ |"),
list(".. / \ .."),
list(".. / \ .."),
list("| .. .. |"),
list("| .. .. |"),
list("| / \ |"),
list("| / \ |"),
list(".. / \ .."),
list(".. .. .. .."),
list("| .. .. |"),
list("| / \ |"),
list("| / \ |"),
list("| / \ |"),
list(".. .. .. .. .. .."),
list("..----..----..----..----..----..")]
MalDict = {}
N = int(input())
for _ in range(N):
mal, yutState = input().split()
moveCount = getMoveCount(yutState)
# 보드에 없던 말이면 새로 생성
if mal not in MalDict.keys():
MalDict[mal] = Mal()
friend_list = []
# 같이 이동할 친구 찾기
for _mal in MalDict.keys():
MalDict[_mal].setDirection()
if MalDict[_mal].isSamePos(mal) and isSameKind(mal, _mal):
friend_list.append(_mal)
# 친구들끼리 이동
for friend in friend_list:
MalDict[friend].setDirection() # 이동 전 확실하게 방향 설정 (isSamePos 로 비교 하면서 설정이 되긴 했음)
MalDict[friend].move(moveCount)
enemy_list = []
# 이동한 곳에 적이 있는지 찾기 (비어있거나 친구가 있으면, 다음 이동때 같이 움직이게 됨)
for _mal in MalDict.keys():
if MalDict[_mal].isSamePos(mal) and not isSameKind(mal, _mal):
enemy_list.append(_mal)
for enemy in enemy_list:
MalDict.pop(enemy) # 초기화 대신 보드에 나와있는 말 딕셔너리에서 객체 자체를 없애버림.
# 이동이 끝나고 보드에 있는 말 딕셔너리에 대해 각각의 위치를 보드 좌표에 맞게 바꿔서 보드에 넣어주기
for _mal in MalDict.keys():
if MalDict[_mal].direction == "Finish":
continue
MalDict[_mal].setDirection()
_i, _j = MalDict[_mal].getBoardPos()
if _mal in 'bB':
_j += 1
elif _mal in 'cC':
_i += 1
elif _mal in 'dD':
_i += 1
_j += 1
assert board[_i][_j] == "."
board[_i][_j] = _mal
# print answer
printBoard()
1년 전에 시도했다가 맞왜틀 외치고 도망친뒤,
어제부터 다시 풀어보자고 각잡고 도전해서 힘들게 풀었다.
<풀이 후기>
전에는 알고리즘 문제풀 때 왜 클래스를 쓰는지 이해가 안됐는데, 이제는 이해가 간다.
복잡한 코드가 정말 간결해지고, 무엇보다 코드가 직관적이라 이해가 너무 잘된다.
객체 지향적으로 코딩하는 이유를 다시금 깨닫게 되었다.
'알고리즘 (PS) > BOJ' 카테고리의 다른 글
[백준] 15683 - 감시 (G5) (0) | 2022.01.05 |
---|---|
[백준] 1038 - 감소하는 수(G5) : DP, BFS 풀이 (0) | 2022.01.03 |
[백준] 1644 - 소수의 연속합 (G3) (0) | 2021.12.23 |
[백준] 2098 - 외판원 순환 (G1) (0) | 2021.12.22 |
[백준] 1562 - 계단 수 (G1) (0) | 2021.12.21 |