[Algorithm] 로봇 청소기 문제

yongkini ·2021년 10월 1일

Algorithm

목록 보기
34/55

문제

출처 : 알고리즘 잡스


문제 해석 및 해결책

# 로봇 청소기 문제(알고리즘 잡스 제공 문제)
# 문제 velog 링크 : 

#### 문제 파악 (1)
# 로봇 청소기가 있고,
# 그 로봇 청소기의 기능은 only 직진만 가능함(현재 보고있는 방향으로)
# 하지만 여기에 구조물 및 장애물이 있고, 그것에 따라 로봇 청소기의 직진 방향, 경로가 바뀜

# 구조물 및 장애물 종류
# 1) 1~5까지의 구조물은 마주치게되면 방향을 전환시킴
# 2) 6~10번까지의 구조물은 워프라고해서 만나면 같은 숫자로 곳으로 이동시키며(순간이동) 그곳부터 가던 방향대로 가게됨
# ** 이 때 워프를 한다음에 곧바로 다시 워프를 할 수 없음
# 3) 턱이 있는데(-1) 이 턱에 빠지면 운행 정지임

# 결과적으로 로봇을 특정 위치, 특정 방향으로 놓고 작동시킨 다음에 자유자재로 돌아다니게 하고
# 그 다음에 원래 있던 위치 및 방향으로 돌아올 때까지 몇칸을 밟고(청소하고) 돌아왔는지를 체크하며, 그 값의 최대값을 구하라는 문제
# 추가로, 턱에 위치하게 돼도 운행이 끝나므로, 그 때까지 밟은 칸도 최대값이면 정답이 될 수 있음

# 제약 조건 및 주의 사항
# 1) 구조물이 아닌 일반 벽에 부딪히면 직관적으로 뒤도는 방향으로 운행하게 된다
# 2) 워프로 이동한 뒤에 곧바로 워프를 쓸 수 없다. 예를 들어, 워프를 했는데 바로 벽이 있어서 다시 그 워프로 돌아오면 그 워프를 이용못하고 운행 중지가 되는듯!
# 3) 워프는 무조건 한쌍이기 때문에 만약에 7번이 있으면 7번은 하나 더있음(무조건)
# 4) 제자리로 돌아와서 운행이 종료되려면 반드시 그 좌표와 그 방향으로 끝나야한다 
# 5) 칸을 셀 때 구조물 1,2,3,4도 카운트를 하며 워프도 카운트를 각각 해준다(출발지, 목적지) 그러나, 중복해서 계산은 안한다.
# 예를 들어, 특정 칸을 지났다면 돌아올 때 또 밟더라도 중복 카운트 x
# 6) 출발은 구조물, 턱, 벽이 아닌 모든 곳이다. 표기상으로는 0에서만 출발할 수 있음 
# 7) -1인 낮은턱은 밟은 칸으로 카운트하면 안됨!
#### 문제 설계 및 해결책 

# 완전탐색
# 모든 0인 부분에 대해서 동서남북의 방향으로 로봇을 출발시키는 것이다.
# N*N 행렬인데 N은 최대 100이라 했으니 10,000개의 궤적이 있을 것이고,
# 그 만개의 궤적을 최대 만개의 0에 대해서 4방향으로 출발시킬꺼니까... 
# 10000 * 10000 * 4 = 400,000,000 4억개 이상의 시행을 하게 될 것 
# 제한시간이 7초니까 충분히 가능하다

# 만든 배열에 대해서 0인 곳에서만 완전탐색을 실행한다
# 4방향으로 모두 직진시켜보는 것 
# 하나의 dot(좌표)에 대해서 동서남북(for문)으로 직진시켜본다
# 직진 시키다가 장애물을 만나면(혹은 구조물) 그 구조물에 따른 처리를 해준다(워프도 있고)
# 처리를 그렇게 계속 알아서 돌게하다가 -1(턱)을 만나거나 다시 있던 자리로 돌아오면 그 때까지 거쳐온 칸의 수(중복허용x)를 여태까지 최대값과 비교
# 이 때 주의할 사항이
# - 무한 루프를 돌게하는 구간을 어떻게 탐지할 것인가(어떤 곳에 들어갔더니 양옆에 워프 혹은 벽이 있어서 왔다 갔다 빠져나갈 수 없다면)
# - 워프 직후 또 워프가 바로 나오는 것을 어떻게 탐지할지

# 먼저 앞에서부터 주의사항을 제외하고 구현 설계해보면, 

# 먼저 주어진 board를 배열로 만들어야한다.
# 이 때, 경계처리를 해주는게 좋을듯. 벽에 부딪히면 무조건 뒤로 도는 모션을 취할텐데 이를 위해..
# 차라리 경계처리를 5번으로 해주는 것도 좋을듯 5번 자체가 뒤로 돌리는 것이기에 똑같다.
# 경계처리는 길이는 각각1로 해서 5로 해주기
# 그러면 탐색은 (1,1) ~ (n,m)까지 하게 될 것
# + 그리고 여기서 배열을 만들면서 워프의 위치를 찾아서 딕셔너리에 정리하는게 좋을듯
# 예를 들어 6번을 찾았다하면 딕셔너리에
# { 6 : {(x,y):None}} 이렇게 먼저 저장을 하고,
# 그 다음에 또 6이 '분명히' 나올 것이기에 6이 나오면
# { 6: {(x,y):(z,a), (z,a):(x,y)}}이렇게 해놓으면
# 만약에 6을 마주치면 해당 좌표의 값을 이용해서 어디로 워프되는지 알 수 있다.
# board[워프 번호][좌표] => 워프로 넘어갈 좌표
# + 0의 위치를 큐에(배열) 담아두면 그 큐에 있는 좌표에서만 돌려보면 되므로 이것도 받아놓자
# 여기까지 queue에 0의 좌표를 받아뒀고, 워프의 위치 딕셔너리, 주어진 맵을 배열로 만들었다 (1)

# 그럼 하나의 좌표에 대해서 탐색을 하고, 마무리까지 하는 로직을 설계해본다.
# 0 4 0 0 0
# 0 0 3 6 0
# 0 2 0 0 5
# 0 0 -1 0 0
# 0 6 0 1 0

# 테케로 주어진 예시로 해보면
# 먼저 (1,1)에서 동서남북으로 전진해본다
# 1) 전진하는 로직
# 2) 전진하다 장애물을 만났을 때(구조물) 1,2,3,4,5 처리하는 로직
# 3) -1 을 만났을 때 멈추고 최대값과 비교해서 최대값 갱신하는 로직
# 4) 처음 출발한 좌표 및 방향을 만났을 때 멈추고 최대값과 비교해서 최대값 갱신하는 로직
# 5) 워프를 만났을 때 이동하는 좌표로 이동하고 거기서부터 또 직진하게 하는 로직
# 6) 칸을 카운팅하는데 중복되는 칸을 카운트에서 제외하는 로직 

# 1) 전진하는 로직
# ways = [위,오른쪽,아래,왼쪽] 이니까
# ways = [x축-1, y축+1, x축+1, y축-1]
# dx = [-1,0,1,-1]
# dy = [0,1,0,-1]
# 이렇게 해놓고
# board[현재 x좌표 + dx[현재 방향]][현재 y좌표 + dy[현재 방향]]
# 이렇게 하면 현재 방향에서 한칸 전진하는 좌표가 나옴 
# 동서남북 혹은 위아래오른쪽왼쪽인데 x축 +1, -1 y축 +1, -1 이런식으로 갈 것.
# 현재 좌표를 dot = (x,y)라할 때,
# 위쪽은 (x-1,y), 아래쪽은 (x+1,y) 왼쪽은 (x,y-1), 오른쪽은 (x,y+1)
# 이걸 계속해서 반복하니까 while문으로 해놓기

# 2) 전진하다 장애물을 만났을 때 처리하는 로직(1,2,3,4,5)
# 1을 만났을 때 
#   - 오른쪽으로 가는 중이었을 때 : 위쪽으로 방향 전환이 이뤄지고, 위쪽으로 가게됨
#   - 아래쪽으로 가는 중이었을 때 : 왼쪽으로 방향 전환이 이뤄지고, 왼쪽으로 가게됨
#   - 왼쪽으로 가는 중이었을 때 : 벽처럼 뒤돌게됨
#   - 위쪽으로 가는 중이었을 때 : 벽처럼 뒤돌게됨

# 2를 만났을 때 
#   - 오른쪽으로 가는 중이었을 때 : 뒤돌게됨
#   - 아래쪽으로 가는 중이었을 때 : 오른쪽으로 방향전환
#   - 왼쪽으로 가는 중 : 위쪽으로 방향 전환
#   - 위쪽으로 가는 중 : 뒤돌게됨

# 3을 만났을 때 
#   - 오른쪽 : 뒤돌게됨
#   - 아래쪽 : 뒤돌게됨
#   - 왼쪽 : 아래쪽으로 방향전환
#   - 위쪽 : 오른쪽으로 방향전환 

# 4를 만났을 때
#   - 오른쪽 : 아래쪽으로 방향전환
#   - 아래쪽 : 뒤돌게됨
#   - 왼쪽 : 뒤돌게됨
#   - 위쪽 : 왼쪽으로 방향 전환 

# 5 를 만났을 때(벽도 5번)
# 어떤 방향이라도 뒤돌게됨

# 모두다 똑같이 ~할 때가 붙음
# 오른쪽으로 가고 있었을 때 이렇게
# 그러면 현재 가고 있는 방향에 대한 변수가 필요함 heading_to 
# 만약 1을 만났는데 heading_to 가 오른쪽이면 => 위쪽으로 방향이 전환됨
# 이걸 효율적으로 나타내려면...
# 이걸 이렇게 딕셔너리로..?
# 뒤돌기
# {
#     왼쪽: 오른쪽,
#     위쪽: 아래쪽
# }
# 1번 
# {
#     왼쪽 :
# }
# 노가다 같지만 이렇게 해야겠다
# 순서대로 위 오른쪽 아래 왼쪽 = 0123
{ 
    1 : {
        0:2,
        1:0,
        2:3,
        3:1
    }
}

# 이렇게 해두면 만약에 
# 1번을 만나면
# ways[1][heading_to] => 이제 어디로 갈지 방향이 나옴
# 그러면 그 방향을 heading_to 로 갱신해주면 방향이 바뀜 

# 3) -1 을 만났을 때 멈추고 최대값과 비교해서 최대값 갱신하는 로직
# 그러면 여태까지 전진 밑 방향 전환 후 전진 등(구조체를 만났을 때 등)
# 그 칸을 카운트하는 로직을 해놨어야했는데
# 그 로직은 그냥...음 .. set을 이용해서 하자
# set에 튜플 단위의 좌표를 추가하면서 중복되는건 자동으로 없어질테고
# 시작점 좌표 그리고 0을 밟았을 떄, 그리고 구조체 1~4 워프 6~10을 밟았을 때 추가한다 (-1은 추가하지말랬으므로)
# + 5번도 카운트 x임 
# 그렇게 해서 path라는 배열(집합)에 튜플 조합을 모으면서 카운팅하다가
# -1을 만나면 여태까지 max값 (디폴트 -1로 해놓기)과 비교해서 max보다 크면(len(path)가) 갱신 안크면 그대로

# 4) 처음 출발한 좌표 및 방향을 만났을 때 멈추고 최대값과 비교해서 최대값 갱신하는 로직
# 이를 위해 처음 출발한 좌표 및 방향을 미리 저장해둬야함 비교하고 갱신하는 로직은 3번과 동일

# 5) 워프를 만났을 때 이동하는 좌표로 이동하고 거기서부터 또 직진하게 하는 로직
# { 6: {(x,y):(z,a), (z,a):(x,y)}}
# 아까 만들어놓은 이 딕셔너리를 이용하는데
# 만약에 6이나왔다 => warp_dict[6]하면
# {(x,y):(z,a), (z,a):(x,y)} 이렇게 나올 것임
# 그러면 현재 좌표를 b라할 때 warp_dict[6][b]하면
# 그 좌표에서 이어지는 워프 좌표가 나옴 c라하자 
# 그러면 현재 좌표 변수를 now_dot이라할 때 now_dot을 c로 갱신해준다
# 그러면 방향은 그대로 냅뒀으니 다음 시행에서 c부터 원래 향하던 방향으로 직진할 것이다.

# 6) 칸을 카운팅하는데 중복되는 칸을 카운트에서 제외하는 로직
# 집합으로 해결 

## 여기까지 주의사항을 제외하고 로직을 설계해봤고
# 마지막으로 주의사항 관련해서 로직설계
# 주의사항 리스트 

# 1) 구조물이 아닌 일반 벽에 부딪히면 직관적으로 뒤도는 방향으로 운행하게 된다
# 이건 5번과 같은 처리를 하기로 했으니 pass
# 2) 워프로 이동한 뒤에 곧바로 워프를 쓸 수 없다. 예를 들어, 워프를 했는데 바로 벽이 있어서 다시 그 워프로 돌아오면 그 워프를 이용못하고 운행 중지가 되는듯!
# - 무한 루프를 돌게하는 구간을 어떻게 탐지할 것인가(어떤 곳에 들어갔더니 양옆에 워프 혹은 벽이 있어서 왔다 갔다 빠져나갈 수 없다면)
# warp_count를 만들어서 워프를 밟은 즉시 +1 하고
# 이 후에 다른 칸(이 때, 카운트가 안되는 -1, 5는 제외)을 밟을 때 warp_count = 0으로 
# 하지만 곧바로 바로 워프칸을 밟으면 또 count++ 되는데, 그러면 warp_count가 2 이상이된다
# 그런 경우에는 그냥 break..탈수없다고 했으니 이부분은 없는 케이스로 쳐야할 것 같음? 아닌가..(여기 조금 애매함)
# 무한 루프 도는 것..  => 그럴일은 없다 
# 3) 워프는 무조건 한쌍이기 때문에 만약에 7번이 있으면 7번은 하나 더있음(무조건)
# 위의 설계에서 해결
# 4) 제자리로 돌아와서 운행이 종료되려면 반드시 그 좌표와 그 방향으로 끝나야한다 
# 위의 설계에서 해결
# 5) 칸을 셀 때 구조물 1,2,3,4도 카운트를 하며 워프도 카운트를 각각 해준다(출발지, 목적지) 그러나, 중복해서 계산은 안한다.
# 위의 설계에서 해결
# 예를 들어, 특정 칸을 지났다면 돌아올 때 또 밟더라도 중복 카운트 x
# 6) 출발은 구조물, 턱, 벽이 아닌 모든 곳이다. 표기상으로는 0에서만 출발할 수 있음 
# 위의 설계에서 해결
# 7) -1인 낮은턱은 밟은 칸으로 카운트하면 안됨!
# 위의 설계에서 해결


## 1차 구현 결과 에러
# - 문제 파악을 잘못함
# => 만약에 1번을 만났을 때 오른쪽으로 오고있었으면 그부분을 청소조차 하지 못하므로(뒤도는 케이스) 카운트하면 안됨
# 이에 대한 처리가 필요함
# 1~4번에 대해서 뒤돌게 되면 카운트 안하는 처리 필요
# 0~3번 방향이라 했을때 반대로 도는 경우
# 0=2 
# 1=3 
# 2=0
# 3=1
# 이렇게되는데 공통점은 둘의 차이의 절대값이 2이라는 것
# 이걸 이용해서 둘의 차가 절대값 2가 되면 카운트 안하는 로직이 필요할듯

구현 코드

: 구현 코드 깃헙 링크

마무리

: 전형적인 시뮬레이션 문제였던 것 같다. 이런 문제는 정말 한번에 풀어야하고, 한번에 못풀더라도 코드를 간소화해서 에러 핸들링에 유용하도록 해야한다. 잔실수가 일어날 수 있기 때문에 코드 간소화는 항상! 그리고 여기서 내가 썼던 테크닉들은 염두해두자.

profile
Web3.0에 관심이 많은 FE 개발자입니다. VPA와 캔들 차트 분석을 기반으로 정량적 트레이딩 시스템을 직접 개발하여 암호화폐를 트레이딩하고 있습니다.

0개의 댓글