사각맵에서 가장 빨리 목적지로 도달하는 최단 경로 찾는 방법

1. 문제

 

https://programmers.co.kr/learn/courses/30/lessons/1844

 

코딩테스트 연습 - 게임 맵 최단거리

[[1,0,1,1,1],[1,0,1,0,1],[1,0,1,1,1],[1,1,1,0,1],[0,0,0,0,1]] 11 [[1,0,1,1,1],[1,0,1,0,1],[1,0,1,1,1],[1,1,1,0,0],[0,0,0,0,1]] -1

programmers.co.kr

 

ROR 게임은 두 팀으로 나누어서 진행하며, 상대 팀 진영을 먼저 파괴하면 이기는 게임입니다. 따라서, 각 팀은 상대 팀 진영에 최대한 빨리 도착하는 것이 유리합니다.

 

지금부터 당신은 한 팀의 팀원이 되어 게임을 진행하려고 합니다. 다음은 5*5 크기의 맵에, 당신의 캐릭터가 (행:1, 열:1) 위치에 있고, 상대 팀 진영은 (행: 5, 열: 5) 위치에 있는 경우의 예시입니다.

 

 

위 그림에서 검은색 부분은 벽으로 막혀있어 갈 수 없는 길이며, 흰색 부분은 갈 수 있는 길입니다. 캐릭터가 움직일 때는 동, 서, 남, 북 방향으로 한 칸씩 이동하며, 게임 맵을 벗어난 길은 갈 수 없습니다.

 

아래 예시는 캐릭터가 상대 팀 진영으로 가는 두 가지 방법을 나타내고 있습니다.

 

 

 

위 예시에서는 첫 번째 방법보다 더 빠르게 상대팀 진영에 도착하는 방법은 없으므로, 이 방법이 상대 팀 진영으로 가는 가장 빠른 방법입니다.

 

만약, 상대 팀이 자신의 팀 진영 주위에 벽을 세워두었다면 상대 팀 진영에 도착하지 못할 수도 있습니다. 예를 들어, 다음과 같은 경우에 당신의 캐릭터는 상대 팀 진영에 도착할 수 없습니다.

 

 

게임 맵의 상태 maps가 매개변수로 주어질 때, 캐릭터가 상대 팀 진영에 도착하기 위해서 지나가야 하는 칸의 개수의 최솟값을 return하도록 solution 함수를 완성하세요.

 

단 상대 팀 진영에 도착할 수 없을 때는 -1을 return해주세요.

 

2. 제한사항

 

maps는 n*m 크기의 게임 맵의 상태가 들어있는 2차원 배열로 n과 m은 각각 1 이상 100이하의 자연수

 

n과 m은 서로 같을 수도, 다를 수도 있지만, n과 m이 모두 1인 경우는 입력으로 주어지지 않습니다.

 

maps는 0과 1로만 이루어져 있으며, 0은 벽이 있는 자리, 1은 벽이 없는 자리를 나타냅니다.

 

처음에 캐릭터는 게임 맵의 좌측 상단인 (1,1) 위치에 있으며, 상대방 진영은 게임 맵의 우측 하단인 (n,m) 위치에 있습니다.

 

3. 예시

 

 

4. 나의 풀이

 

먼저 (n,m-1)과 (n-1,m)에 벽이 있으면 들어갈 수 없으므로 이런 경우는 바로 -1을 return할 수 있도록 한다.

 

def solution(maps):
    
    n = len(maps)
    
    m = len(maps[0])
    
    if (maps[n-2][-1] == 0) and (maps[n-1][-2] == 0):
         
         return -1
         
    else:

 

그런데 착각하면 안되는 것이 (n,m-1)과 (n-1,m)에 막혀있지 않더라도 절대로 못들어갈 수도 있다는 것이다

 

 

예를 들면 위와 같이 막혀도 절대로 못들어간다..

 

아무튼 상하좌우로 이동할 수 있으니까 상하좌우로 전부 이동시켜보면서 최솟값을 찾아나가는 BFS 방법을 사용하고자 한다.

 

말 그대로 현재 위치에서 가까운 노드를 모두 검사해보는 방법이다

 

그렇기 때문에 상하좌우로 이동하는 함수를 먼저 만든다

 

def solution(maps):
    
    from collections import deque
    
    def move_south(x,y,dir_len):
        
        lock = False
        
        x += 1
        
        if x > n-1:
            
            x -= 1
            
            lock = True
        
        else:
            
            if maps[x][y] == 1:
                
                dir_len += 1
            
            else:
                
                x -= 1
                
                lock = True
                
        return x,y,lock,dir_len
    
    n = len(maps)
    
    m = len(maps[0])
    
    if (maps[n-2][-1] == 0) and (maps[n-1][-2] == 0):
         
         return -1
         
    else:

 

현재 좌표가 (x,y)라고 하고 이동한 경로의 최소길이가 dir_len이라고 하여 이들을 변수로 받는 함수 move_south

 

lock는 막혀서 진행불가라는 뜻의 지시변수

 

남쪽으로 1칸 이동하므로 행이 1이 더해지니까 x에 1을 더한다

 

왜냐하면 위에서부터 0행이라하고 맨 아래가 n행이니까

 

아무튼 1칸 이동했더니 x가 n-1보다 크다면 최대행이 n-1이니까 더 이상 이동할 수 없다는 뜻이므로 x에 1을 빼서 원래대로 되돌리고

 

lock = True로 하여 못간다고 알려준다

 

그렇지않다면 maps에서 현재 (x,y)의 상태를 알아본다

 

현재 (x,y)의 상태 maps[x][y]가 1이면 이동할 수 있다는 뜻이므로 경로의 길이 dir_len에 1을 더해주고

 

0이면 이동할 수 없는 곳에 이동했다는 뜻이므로 x에 1을 빼주고 lock을 True로 하여 x,y,lock,dir_len을 return

 

이런 방식으로 move_north, move_east, move_west를 모두 만들어준다

 

def solution(maps):
    
    from collections import deque
    
    def move_south(x,y,dir_len):
        
        lock = False
        
        x += 1
        
        if x > n-1:
            
            x -= 1
            
            lock = True
        
        else:
            
            if maps[x][y] == 1:
                
                dir_len += 1
            
            else:
                
                x -= 1
                
                lock = True
                
        return x,y,lock,dir_len
    
    def move_north(x,y,dir_len):
        
        lock = False
        
        x -= 1
        
        if x < 0:
            
            x += 1
            
            lock = True
        
        else:
            
            if maps[x][y] == 1:
                
                dir_len += 1
            
            else:
                
                x += 1
                
                lock = True
        
        return x,y,lock,dir_len
        
    def move_east(x,y,dir_len):
        
        lock = False
        
        y += 1
        
        if y > m-1:
            
            y -= 1
            
            lock = True
        
        else:
        
            if maps[x][y] == 1:
                
                dir_len += 1
            
            else:
                
                y -= 1
                
                lock = True
        
        return x,y,lock,dir_len
    
    def move_west(x,y,dir_len):
        
        lock = False
        
        y -= 1
        
        if y < 0:
            
            y += 1
            
            lock = True
        
        else:
            
            if maps[x][y] == 1:
                
                dir_len += 1
                
            else:
                
                y += 1
                
                lock = True
                
        return x,y,lock,dir_len
        
    n = len(maps)
    
    m = len(maps[0])
    
    if (maps[n-2][-1] == 0) and (maps[n-1][-2] == 0):
         
         return -1
         
    else:

 

그러면 이제 현재 위치 x=0,y=0으로 두고 칸 수를 셀건데 현재 위치 (0,0)도 1칸으로 세니까 dir_len = 1로 초기화

 

모든 검사하는 노드 경로를 담을 total_dir을 deque로 만들어준다

 

그리고 검사가 끝난 노드를 담을 집합 all_loc도 세팅을 한다

 

else:
    
    x = 0
    
    y = 0
    
    num = 0
    
    dir_len = 1
    
    total_dir = deque([(x,y,dir_len,num)])
    
    all_loc = set([(0,0)])

 

그러면 이제 total_dir에서 하나씩 빼서 상하좌우로 움직여서 검사를 해보고 이동할 수 있다면 total_dir에 넣고

 

검사가 끝난 노드는 중복 검사를 방지하기 위해 all_loc에 넣는다

 

이동할 수 없다면 total_dir에 넣을 필요가 없다

 

else:
    
    x = 0
    
    y = 0
    
    num = 0
    
    dir_len = 1
    
    total_dir = deque([(x,y,dir_len,num)])
    
    all_loc = set([(0,0)])
    
    while total_dir != deque():
        
        x,y,dir_len,num = total_dir.popleft()
 
        
        a,b,lock,new_dir = move_north(x,y,dir_len)
        
        c,d,lock,new_dir = move_south(x,y,dir_len)
        
        e,f,lock,new_dir = move_east(x,y,dir_len)
        
        g,h,lock,new_dir = move_west(x,y,dir_len)

여기서 num은 몇번째 경로를 의미하는지 나타내는 변수이다

 

현재 첫번째 (0,0) 위치는 0번째 경로라는 의미로 마지막에 num=0을 두는데

 

상하좌우로 이동하다보면 모두 이동가능하다면 결국 총 4가지 경우의 수로 될거잖어

 

아무튼 동서남북으로 이동하면서 만약 lock=False이면 이동가능하다는 뜻이므로

 

total_dir에 넣는다.

 

else:
    
    x = 0
    
    y = 0
    
    num = 0
    
    dir_len = 1
    
    total_dir = deque([(x,y,dir_len,num)])
    
    all_loc = set([(0,0)])
    
    while total_dir != deque():
        
        x,y,dir_len,num = total_dir.popleft()
 
        
        a,b,lock,new_dir = move_north(x,y,dir_len)
        
        if lock == False:
            
            total_dir.append((a,b,new_dir,num))
            
           
        c,d,lock,new_dir = move_south(x,y,dir_len)
        
        if lock == False:
        
            total_dir.append((c,d,new_dir,num))
        
        e,f,lock,new_dir = move_east(x,y,dir_len)
        
        if lock == False:
            
            total_dir.append((e,f,new_dir,num))
            
        
        g,h,lock,new_dir = move_west(x,y,dir_len)
        
        if lock == False:
        
            total_dir.append((g,h,new_dir,num))

 

그런데 이제 이 상태에서 검사가 끝난 (a,b), (c,d), (e,f), (g,h)는 다시 검사하지 않도록 all_loc에 넣어줘야함

 

all_loc에 넣지 않으면 중복 검사하기 때문에 비효율적이 되니까

 

while total_dir != deque():
        
    x,y,dir_len,num = total_dir.popleft()


    a,b,lock,new_dir = move_north(x,y,dir_len)

    if lock == False:

        total_dir.append((a,b,new_dir,num))
        
        all_loc.add((a,b))


    c,d,lock,new_dir = move_south(x,y,dir_len)

    if lock == False:

        total_dir.append((c,d,new_dir,num))
        
        all_loc.add((c,d))

    e,f,lock,new_dir = move_east(x,y,dir_len)

    if lock == False:

        total_dir.append((e,f,new_dir,num))
        
        all_loc.add((e,f))


    g,h,lock,new_dir = move_west(x,y,dir_len)

    if lock == False:

        total_dir.append((g,h,new_dir,num))
        
        all_loc.add((g,h))

 

all_loc에 넣더라도 문제는 move하면서 all_loc에 넣은 좌표가 또 나오면 total_dir에 넣지말아야하는데

 

이러한 조건을 추가해줘야할 것이다

 

while total_dir != deque():
        
    x,y,dir_len,num = total_dir.popleft()


    a,b,lock,new_dir = move_north(x,y,dir_len)

    if lock == False and not((a,b) in all_loc):

        total_dir.append((a,b,new_dir,num))
        
        all_loc.add((a,b))


    c,d,lock,new_dir = move_south(x,y,dir_len)

    if lock == False and not((c,d) in all_loc):

        total_dir.append((c,d,new_dir,num))
        
        all_loc.add((c,d))

    e,f,lock,new_dir = move_east(x,y,dir_len)

    if lock == False and not((e,f) in all_loc):

        total_dir.append((e,f,new_dir,num))
        
        all_loc.add((e,f))


    g,h,lock,new_dir = move_west(x,y,dir_len)

    if lock == False and not((g,h) in all_loc):

        total_dir.append((g,h,new_dir,num))
        
        all_loc.add((g,h))

 

하지만 이렇게 하더라도 문제는 경로 구분을 하지 못한다는 점이다. 그것을 위해 num이라는 변수를 줄 것이고

 

before이라는 지시변수를 이용해서 이전 move 함수가 통과되면 before = True라 두고 

 

before = True인 경우는 num에 1을 더해주면 경로 구분이 가능하다

 

while total_dir != deque():
        
    x,y,dir_len,num = total_dir.popleft()

    before = False

    a,b,lock,new_dir = move_north(x,y,dir_len)

    if lock == False and not((a,b) in all_loc):
    
        before = True

        total_dir.append((a,b,new_dir,num))
        
        all_loc.add((a,b))


    c,d,lock,new_dir = move_south(x,y,dir_len)

    if lock == False and not((c,d) in all_loc):
    
        if before:
            
            num += 1
        
        before = True

        total_dir.append((c,d,new_dir,num))
        
        all_loc.add((c,d))

    e,f,lock,new_dir = move_east(x,y,dir_len)

    if lock == False and not((e,f) in all_loc):
    
        if before:
            
            num += 1
        
        before = True

        total_dir.append((e,f,new_dir,num))
        
        all_loc.add((e,f))


    g,h,lock,new_dir = move_west(x,y,dir_len)

    if lock == False and not((g,h) in all_loc):
        
        if before:
            
            num += 1
        
        total_dir.append((g,h,new_dir,num))
        
        all_loc.add((g,h))

중간에 before = False로 넣어줘야하는거 아니야??

 

생각을 해보면 move_north가 통과되지않으면 num = 0인데 before = False이고..

 

2번이 통과되면 여전히 num = 0으로 두고 before = True라 두고

 

3번이 통과되면 현재 before = True니까 num = 1로 될테고 total_dir에 넣은다음에 before = False로 해야하나??

 

그럴 필요가 없는게

 

4번이 통과되면 before = True이면 num = 2로 구분지어 주는게 맞고

 

4번이 통과안된다면 어차피 넣지 않을테니까 num이 1이든 2이든 상관없어

 

마찬가지로 1번이 통과되면 num=0을 넣고 before = True

 

2번이 통과되지않으면 num=0이든 1이든 상관없이 어차피 total_dir에 안넣을거니까 필요없고

 

여기서 before = False해야하냐?? 그럴 필요 없는게

 

3번이 통과되면 현재 before = True니까 num=1로 구분지어 넣는게 맞고

 

통과되지 않더라도 num=0이든 1이든 상관없이 안넣을거니까 before이 뭐든 상관없다

 

아무튼 이제 만약에 4가지 move를 하다가 그 중 어떤 것이라도 최종 지점인 (n-1,m-1)에 도달했다면 그것이 최소경로니까 바로 return하면 된다

 

while total_dir != deque():
        
    x,y,dir_len,num = total_dir.popleft()

    before = False

    a,b,lock,new_dir = move_north(x,y,dir_len)

    if lock == False and not((a,b) in all_loc):
    
        before = True

        total_dir.append((a,b,new_dir,num))
        
        all_loc.add((a,b))
            
        if a == n-1 and b == m - 1:
            
            return new_dir


    c,d,lock,new_dir = move_south(x,y,dir_len)

    if lock == False and not((c,d) in all_loc):
    
        if before:
            
            num += 1
        
        before = True

        total_dir.append((c,d,new_dir,num))
        
        all_loc.add((c,d))
        
        if c == n-1 and d == m-1:
            
            return new_dir

    e,f,lock,new_dir = move_east(x,y,dir_len)

    if lock == False and not((e,f) in all_loc):
    
        if before:
            
            num += 1
        
        before = True

        total_dir.append((e,f,new_dir,num))
        
        all_loc.add((e,f))
        
        if e == n-1 and f == m-1:
            
            return new_dir


    g,h,lock,new_dir = move_west(x,y,dir_len)

    if lock == False and not((g,h) in all_loc):
        
        if before:
            
            num += 1
        
        total_dir.append((g,h,new_dir,num))
        
        all_loc.add((g,h))
        
        if g == n-1 and h == m-1:
            
            return new_dir

 

그러면 이제 모든 total_dir을 다 검사했어.. lock이 되거나 node를 전부 지나보면 total_dir에 안들어가잖아

 

그런 뜻은? 최종 지점에 도달하지 못한다는 뜻

 

그러니까 반복문을 빠져나온다는 뜻은 결국 모든 total_dir을 검사했는데도 막혀있다는 뜻으로

 

최종지점에 도달하지 못한다는 뜻이다

 

 

while total_dir != deque():
        
    x,y,dir_len,num = total_dir.popleft()

    before = False

    a,b,lock,new_dir = move_north(x,y,dir_len)

    if lock == False and not((a,b) in all_loc):
    
        before = True

        total_dir.append((a,b,new_dir,num))
        
        all_loc.add((a,b))
            
        if a == n-1 and b == m - 1:
            
            return new_dir


    c,d,lock,new_dir = move_south(x,y,dir_len)

    if lock == False and not((c,d) in all_loc):
    
        if before:
            
            num += 1
        
        before = True

        total_dir.append((c,d,new_dir,num))
        
        all_loc.add((c,d))
        
        if c == n-1 and d == m-1:
            
            return new_dir

    e,f,lock,new_dir = move_east(x,y,dir_len)

    if lock == False and not((e,f) in all_loc):
    
        if before:
            
            num += 1
        
        before = True

        total_dir.append((e,f,new_dir,num))
        
        all_loc.add((e,f))
        
        if e == n-1 and f == m-1:
            
            return new_dir


    g,h,lock,new_dir = move_west(x,y,dir_len)

    if lock == False and not((g,h) in all_loc):
        
        if before:
            
            num += 1
        
        total_dir.append((g,h,new_dir,num))
        
        all_loc.add((g,h))
        
        if g == n-1 and h == m-1:
            
            return new_dir
            
            
    return -1

 

 

5. 다른 풀이

 

좋아요를 가장 많이 받은 풀이 분석

 

from collections import deque

def solution(maps):
    x_move = [1, 0, -1, 0]
    y_move = [0, 1, 0, -1]

    x_h, y_h = (len(maps[0]), len(maps))
    queue = deque([(0, 0, 1)])

    while queue:
        x, y, d = queue.popleft()

        for i in range(4):
            nx = x + x_move[i]
            ny = y + y_move[i]

            if nx > -1 and ny > -1 and nx < x_h and ny < y_h:
                if maps[ny][nx] == 1 or maps[ny][nx] > d + 1:
                    maps[ny][nx] = d + 1
                    if nx == x_h - 1 and ny == y_h - 1:
                        return d + 1

                    queue.append((nx, ny, d + 1))

    return -1

 

먼저 x_move, y_move는 상하좌우로 이동하기 위해 만든 리스트일테고

 

중간에 for문 보면 nx = x + x_move[i], ny = y + y_move[i]로 한거 보면

 

서로 위치가 대응하는 관계끼리 묶어서

 

(1,0)을 이동, (0,1)을 이동, (-1,0)을 이동, (0,-1)을 이동시킬거라는 것을 알 수 있단 말이지

 

x_h와 y_h는 x축 길이와 y축 길이인듯

 

나랑은 좀 반대로 생각한 것 같고.. 나는 x를 행의 이동 len(maps)로 구했고 y를 열의 이동 len(maps[0])로 구했잖아

 

from collections import deque

def solution(maps):
    x_move = [1, 0, -1, 0]
    y_move = [0, 1, 0, -1]

    x_h, y_h = (len(maps[0]), len(maps))
    queue = deque([(0, 0, 1)])

 

queue에 deque([(0,0,1)])은 while문 보면 x,y,d로 뺐는데 x,y는 현재 위치이고 d는 경로의 길이를 의미하는듯

 

다음 queue에서 빼서 for문을 돌건에 이동은 상하좌우 4가지니까 range(4)로 검사를 하는데

 

nx와 ny는 move를 하고 나서 얻은 좌표이고

 

nx가 -1보다 크고 x_h보다 작고, ny가 -1보다 크고 y_h보다 작다는 것은 맵 안에 존재한다는 뜻으로 유효한 이동이라는 뜻

 

만약 유효한 이동이라면 이제 실제로 벽에 이동한건지 그렇지 않은지 검사를 해보기 위해 maps에 넣을건데

 

ny는 y축 이동으로 행이동이고 nx는 x축 이동으로 열이동이니까 maps[ny][nx]를 구했다

 

---------------------------------------------------------------------------------------------------------------------

 

그리고 여기가 지리네

 

maps[ny][nx] == 1이라면 진짜로 이동이 가능한 지역이니까 maps[ny][nx]를 현재 경로의 길이 d에 1을 더한 d+1로 바꿔준다

 

그리고 maps[ny][nx]가 d+1보다 클 수도 있는데 그런 경우도 maps[ny][nx] = d+1로 둔다.

 

이걸 왜 해야하느냐?? 그렇게 생각할 수 있는데

 

이것의 의미를 한번 생각해보면

 

 

현재 deque에 위 그림같이 2가지 경우 (nx1, ny1,8), (nx2,ny2,8)이 들어가 있을 것이란 말이야

 

계속 이동을 해보면

 

 

1번 경로로 가면 8 9 10 11 12까지 일단 채우고

 

2번 경로로 가면 8 9 10 11로 채울텐데

 

문제는 1번 경로에서 12까지 채운 상태에서 다음 경로로 검사를 할 때 이미 2번 경로로 이동한 것에 의해서

 

다음 타일이 9로 채워져 있단 말이지

 

그러니까 현재 d = 12여서 d+1=13인데

 

다음으로 이동해야할 칸 maps[ny][nx] = 9여서

 

maps[ny][nx]가 1도 아니고 maps[ny][nx] >13 도 아니다

 

실제로 1번 경로로는 이동할 필요가 없다

 

문제는 maps[ny][nx] > d+1이어야하는 이유도 알겠지?

 

maps[ny][nx] > d+1이라는 것은 이전에 d+1보다 큰 값으로 채워진 경로는 최소 경로가 아니라는 뜻이고

 

현재 지나는 경로가 최소니까 그래서 maps[ny][nx] = d+1로 갱신하라는 의미다.

 

---------------------------------------------------------------------------------------------------------------------

 

    while queue:
        x, y, d = queue.popleft()

        for i in range(4):
            nx = x + x_move[i]
            ny = y + y_move[i]

            if nx > -1 and ny > -1 and nx < x_h and ny < y_h:
                if maps[ny][nx] == 1 or maps[ny][nx] > d + 1:
                    maps[ny][nx] = d + 1
                    if nx == x_h - 1 and ny == y_h - 1:
                        return d + 1

                    queue.append((nx, ny, d + 1))

    return -1

 

그리고 이동하다가 nx가 x_h-1이고 ny가 y_h-1이면 최종 지점에 도달했다는 의미이므로 바로 d+1을 return하면 끝

 

모든 queue를 검사했는데도 return하지 못하면 최종지점에 도달하지 못했다는 뜻이므로 반복문을 탈출하면 

 

-1을 return하면 된다

 

 

6. 되돌아보기

 

결국에 나름대로 구현을 잘 하긴 했다

 

그런데 bfs와 dfs에 대해서 제대로 공부해서 정리해놓을 필요가 있겠다

 

인접한 경로를 모두 검사해나가는지?(bfs) 일단 한 경로를 정해서 쭈욱 검사해나가는지?(dfs)

 

그리고 검사하면 검사한 노드는 검사가 완료됐다는 뜻으로 따로 리스트에 저장해놔야한다는 점을 기억해야함

 

그리고 다른 풀이에서 if maps[ny][nx] == 1 and maps[ny][nx] > d+1 : maps[ny][nx] = d+1이 파고 들어보니까 지리네

 

 

 

TAGS.

Comments