티스토리 뷰

반응형

백준 4485 녹색 옷 입은 애가 젤다지? Python


✅ 문제 소개


문제 내용

젤다의 전설 게임 속 주인공은 **링크**입니다. 그러나 많은 사람들이 “녹색 옷 입은 애가 젤다지?”라고 착각합니다.
스트레스를 받은 링크는 도둑루피로 가득한 동굴에 들어갑니다.
링크는 동굴을 통과하며 최대한 적은 루피만 잃고 출구까지 도달해야 합니다.

✅ 테스트케이스


입력 예시

3
5 5 4
3 9 1
3 2 7
0

출력 예시

Problem 1: 20

✅ 문제 설명


동굴 구조

입력 예시에서 주어진 3×3 동굴입니다.
각 칸의 숫자는 해당 칸을 지나갈 때 잃는 루피입니다.

5 5 4
3 9 1
3 2 7

링크는 왼쪽 위(5)에서 출발해 오른쪽 아래(7)로 가야 합니다.

잘못된 경로

**오른쪽 → 오른쪽 → 아래 → 아래** 로만 이동하는 경우입니다.
경로: (0,0) → (0,1) → (0,2) → (1,2) → (2,2)
손해 합계: **22**

**5 →** **5 →** **4 ↓**
**1 ↓**
**7**

더 좋은 경로

**아래 → 아래 → 오른쪽 → 오른쪽** 로 이동하는 경우입니다.
경로: (0,0) → (1,0) → (2,0) → (2,1) → (2,2)
손해 합계: **20**

**5 ↓**
**3 ↓**
**3 →** **2 →** **7**

🔷 문제 풀이 아이디어

이 문제는 링크가 도둑루피가 가득한 동굴을 지나 최소한의 손해로 출구까지 도달하는 것이 목표입니다.
단순히 오른쪽과 아래로만 가는 것이 아니라, 더 적은 손해를 위해 **되돌아가 위쪽으로 올라가거나 왼쪽으로 돌아가는 경로**까지 고려해야 합니다.
이 특징 때문에, 단순히 DP로는 풀 수 없고 다익스트라를 써야 합니다.

DP(동적 계획법)의 한계

❌ DP는 현재 위치에서 **오른쪽과 아래만 탐색하며 최소 손해를 계산**합니다.
하지만 이 문제처럼 상하좌우 어디든 갈 수 있는 상황에서는 최적 경로를 놓칩니다.

예를 들어, 아래는 DP가 계산한 경로입니다.
모든 칸에는 원래 숫자를 표시하고, 경로를 따라간 칸에는 화살표를 붙였습니다.

**3→** **7→** **2→** **0→** **1↓**
2 8 0 9 **1↓**
1 2 1 8 **1↓**
9 8 9 2 **0↓**
3 6 5 1 **5**

🧾 **DP 손해 합계: 20**

다익스트라의 강점

✅ 다익스트라는 이미 방문한 칸이라도 더 적은 손해로 도달할 수 있다면 값을 갱신하며 탐색합니다.
상하좌우를 모두 고려하며 최적 경로를 찾기 때문에, 반드시 다익스트라를 사용해야 합니다.

아래는 다익스트라가 계산한 최적 경로입니다.

**3↓** 7 **2→** **0→** **1↓**
**2↓** 8 **0↑** 9 **1↓**
**1→** **2→** **1↑** 8 **1↓**
9 8 9 2 **0↓**
3 6 5 1 **5**

🧾 **다익스트라 손해 합계: 19**

다익스트라는 무엇인가?

다익스트라는 **가장 적은 비용으로 목적지에 가는 길을 찾는 방법**입니다.
원리는 간단합니다.

“지금까지 찾은 길 중에서 제일 싼 길부터 하나씩 확인해 나가면 된다.”

이렇게 하면 큐에서 꺼내는 순간, 그 칸까지 가는 최소 비용이 이미 결정됩니다.
왜냐하면 더 싼 길이 있었으면 그 길이 큐의 맨 앞에 먼저 나왔을 것이기 때문입니다.
이것이 다익스트라가 최소 비용을 보장하는 이유입니다.

다익스트라는 어떻게 움직이는가?

  • 1️⃣ 시작점에서 갈 수 있는 칸들의 비용을 계산해 큐에 넣습니다.
  • 2️⃣ 큐에서 가장 비용이 적은 칸을 꺼냅니다.
  • 3️⃣ 그 칸에서 갈 수 있는 새로운 칸들의 비용을 계산합니다.
  • 4️⃣ 더 싸게 갈 수 있으면 그 값을 새로 기록하고 큐에 넣습니다.
  • 5️⃣ 이 과정을 반복하면 모든 칸을 최소 비용으로 방문하게 됩니다.

🔷 단계별 진행


🔷 0단계 — 시작

**설명**
현재 위치는 `(0,0)`이고, 지금까지 든 비용은 `5`입니다.
갈 수 있는 후보는 오른쪽 `(0,1)=5`와 아래 `(1,0)=3`입니다.
비용 계산:

  • 오른쪽: `5+5=10`
  • 아래: `5+3=8`
둘 중 더 싼 `8`이 있으니, `(1,0)`을 다음 탐색 대상으로 선택합니다.

**고유비용**

5★ 5 4
3 9 1
3 2 7

**설명**
큐에는 `(1,0)=8`과 `(0,1)=10`이 들어 있습니다. 현재 큐의 가장 작은 값은 `8`입니다.
더 싼 길이 있었다면 큐에 먼저 나왔을 텐데 없으니, `(1,0)=8`이 최소임이 보장됩니다.

**합산비용**

5★

**설명**
현재 큐 상태: `←[ (1,0)=8 | (0,1)=10 ]←`
다음 탐색 예정: `(1,0)=8`

🔷 1단계 — (1,0)=8 선택

**설명**
현재 위치는 `(1,0)`이고, 지금까지 든 비용은 `8`입니다.
갈 수 있는 후보는 아래 `(2,0)=3`과 오른쪽 `(1,1)=9`입니다.
비용 계산:

  • 아래: `8+3=11`
  • 오른쪽: `8+9=17`
둘 중 더 싼 `11`이 있으니, `(2,0)`을 다음 탐색 대상으로 선택합니다.

**고유비용**

5 5 4
3★ 9 1
3 2 7

**설명**
큐에는 기존의 `(0,1)=10`, 그리고 새로 발견한 `(2,0)=11`, `(1,1)=17`이 있습니다.
가장 작은 값은 여전히 `10`입니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로 `10`이 최소입니다.

**합산비용**

5 10
8★ 17
11

**설명**
현재 큐 상태: `←[ (0,1)=10 | (2,0)=11 | (1,1)=17 ]←`
다음 탐색 예정: `(0,1)=10`

🔷 2단계 — (0,1)=10 선택

**설명**
현재 위치는 `(0,1)`이고, 지금까지 든 비용은 `10`입니다.
갈 수 있는 후보는 오른쪽 `(0,2)=4`와 아래 `(1,1)=9`입니다.
비용 계산:

  • 오른쪽: `10+4=14`
  • 아래: `10+9=19`
`14`가 더 작습니다. `(0,2)=14`를 큐에 넣습니다. `(1,1)=19`는 기존 `17`보다 크므로 무시합니다.

**고유비용**

5 5★ 4
3 9 1
3 2 7

**설명**
큐에는 기존의 `(2,0)=11`, `(1,1)=17`, 그리고 새로 발견한 `(0,2)=14`가 있습니다.
가장 작은 값은 `11`입니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로 `11`이 최소입니다.

**합산비용**

5 10★ 14
8 17
11

**설명**
현재 큐 상태: `←[ (2,0)=11 | (1,1)=17 | (0,2)=14 ]←`
다음 탐색 예정: `(2,0)=11`

🔷 3단계 — (2,0)=11 선택

**설명**
현재 위치는 `(2,0)`이고, 지금까지 든 비용은 `11`입니다.
갈 수 있는 후보는 오른쪽 `(2,1)=2`입니다.
비용 계산:

  • 오른쪽: `11+2=13`
더 이상 아래로는 갈 수 없으니, `(2,1)=13`을 다음 탐색 대상으로 선택합니다.

**고유비용**

5 5 4
3 9 1
3★ 2 7

**설명**
큐에는 기존의 `(1,1)=17`, `(0,2)=14`가 있습니다.
새롭게 발견한 `(2,1)=13`이 추가되었습니다.
큐를 보면 `13`, `14`, `17`이 있고, 가장 작은 값은 `13`입니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로, `(2,1)=13`이 최소임이 보장됩니다.

**합산비용**

5 10 14
8 17
11★ 13

**설명**
현재 큐 상태: `←[ (2,1)=13 | (0,2)=14 | (1,1)=17 ]←`
다음 탐색 예정: `(2,1)=13`

🔷 4단계 — (2,1)=13 선택

**설명**
현재 위치는 `(2,1)`이고, 지금까지 든 비용은 `13`입니다.
갈 수 있는 후보는 오른쪽 `(2,2)=7`입니다.
비용 계산:

  • 오른쪽: `13+7=20`
다른 더 싼 후보가 없으니, `(2,2)=20`을 큐에 넣습니다.

**고유비용**

5 5 4
3 9 1
3 2★ 7

**설명**
큐에는 기존의 `(0,2)=14`, `(1,1)=17`이 있습니다.
새롭게 발견한 `(2,2)=20`이 추가되었습니다.
큐를 보면 `14`, `17`, `20`이 있고, 가장 작은 값은 `14`입니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로, `(0,2)=14`가 최소임이 보장됩니다.

**합산비용**

5 10 14
8 17
11 13★ 20

**설명**
현재 큐 상태: `←[ (0,2)=14 | (1,1)=17 | (2,2)=20 ]←`
다음 탐색 예정: `(0,2)=14`

🔷 다익스트라 — 단계별 진행 (완결)


🔷 5단계 — (0,2)=14 선택

**설명**
현재 위치는 `(0,2)`이고, 지금까지 든 비용은 `14`입니다.
갈 수 있는 후보는 아래 `(1,2)=1`입니다.
비용 계산:

  • 아래: `14+1=15`
다른 더 싼 후보가 없으니, `(1,2)=15`를 큐에 넣습니다.

**고유비용**

5 5 4★
3 9 1
3 2 7

**설명**
큐에는 기존의 `(1,1)=17`, `(2,2)=20`이 있습니다.
새롭게 발견한 `(1,2)=15`이 추가되었습니다.
큐를 보면 `15`, `17`, `20`이 있고, 가장 작은 값은 `15`입니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로, `(1,2)=15`가 최소임이 보장됩니다.

**합산비용**

5 10 14★
8 17 15
11 13 20

**설명**
현재 큐 상태: `←[ (1,2)=15 | (1,1)=17 | (2,2)=20 ]←`
다음 탐색 예정: `(1,2)=15`

🔷 6단계 — (1,2)=15 선택

**설명**
현재 위치는 `(1,2)`이고, 지금까지 든 비용은 `15`입니다.
갈 수 있는 후보는 아래 `(2,2)=7`입니다.
비용 계산:

  • 아래: `15+7=22`
하지만 큐에 이미 `(2,2)=20`으로 기록돼 있고, `20 < 22`이므로 무시합니다.

**고유비용**

5 5 4
3 9 1★
3 2 7

**설명**
큐에는 기존의 `(1,1)=17`, `(2,2)=20`이 있습니다.
가장 작은 값은 `17`입니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로, `(1,1)=17`이 최소임이 보장됩니다.

**합산비용**

5 10 14
8 17 15★
11 13 20

**설명**
현재 큐 상태: `←[ (1,1)=17 | (2,2)=20 ]←`
다음 탐색 예정: `(1,1)=17`

🔷 7단계 — (1,1)=17 선택

**설명**
현재 위치는 `(1,1)`이고, 지금까지 든 비용은 `17`입니다.
갈 수 있는 후보는 아래 `(2,1)=2`와 오른쪽 `(1,2)=1`입니다.
하지만 `(2,1)=13`과 `(1,2)=15`가 이미 더 작은 비용으로 기록돼 있으므로, 아무것도 갱신되지 않습니다.

**고유비용**

5 5 4
3 9★ 1
3 2 7

**설명**
큐에는 이제 `(2,2)=20`만 남아 있습니다.
더 싼 길이 있었다면 이미 큐에 있었어야 하므로, `(2,2)=20`이 최소임이 보장됩니다.

**합산비용**

5 10 14
8 17★ 15
11 13 20

**설명**
현재 큐 상태: `←[ (2,2)=20 ]←`
다음 탐색 예정: `(2,2)=20`

🔷 8단계 — (2,2)=20 선택

**설명**
현재 위치는 `(2,2)`이고, 지금까지 든 비용은 `20`입니다.
목적지에 도달했으므로 탐색을 종료합니다.

**고유비용**

5 5 4
3 9 1
3 2 7★

**설명**
이제 큐가 비었고, 목적지까지 최소 비용 `20`으로 도달했습니다.
이 과정에서 매 단계마다 큐에서 가장 작은 값부터 탐색했고, 더 싼 길이 있었다면 그 순간 큐에 나타났을 것이므로 최소임이 증명됩니다.

**합산비용**

5 10 14
8 17 15
11 13 20★

**설명**
최종 최소 비용: `20`

전체코드

from queue import PriorityQueue

class Node:
    def __init__(self,x,y,cost):
        self.x=x
        self.y=y
        self.cost=cost

    def __lt__(self,other):
        return self.cost<other.cost

dx=[-1,1,0,0]
dy=[0,0,-1,1]

cnt =0
while True:
    N= int(input())
    if N==0:
        break
    cnt+=1
    arr = [[0 for _ in range(N)] for _ in range(N)]
    dis = [[21000000 for _ in range(N)] for _ in range(N)]
    for i in range(N):
        row=list(map(int,input().split()))
        for j in range(N):
            arr[i][j]=row[j]
    q=PriorityQueue()
    node=Node(0,0,arr[0][0])
    q.put(node)
    dis[0][0]=arr[0][0]

    while not q.empty():
        node = q.get()
        x = node.x
        y = node.y
        cost = node.cost

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

            if nx<0 or nx>=N or ny<0 or ny>=N:
                continue

            next_cost=cost+arr[nx][ny]

            if(next_cost<dis[nx][ny]):
                dis[nx][ny]=next_cost
                q.put(Node(nx,ny,next_cost))
    print(f'Problem {cnt}: {dis[N-1][N-1]}')

개별 코드 설명 및 보강


1. class Node

class Node:
    def __init__(self,x,y,cost):
        self.x=x
        self.y=y
        self.cost=cost

    def __lt__(self,other):
        return self.cost<other.cost

  • `Node` 클래스는 탐색 대상인 2차원 배열 내 위치를 나타내는 좌표 `(x, y)`와, 그 위치까지 도달하는 데 필요한 누적 비용 `cost`를 저장합니다.
  • 이 클래스를 우선순위 큐에 넣어 최소 비용부터 처리하기 위해 `__lt__` (less than) 매직 메서드를 오버로딩하였습니다.
  • 오버로딩된 `__lt__` 메서드는 `self.cost < other.cost` 형태로 구현하여, **비용이 작은 노드가 우선순위 큐에서 먼저 나오도록** 작동합니다.
  • 이렇게 하면 Python의 `PriorityQueue`가 기본적으로 최소힙(min-heap)으로 동작하여, 항상 가장 비용이 적은 노드를 먼저 처리할 수 있습니다.

2. 배열 선언 및 초기화

arr = [[0 for _ in range(N)] for _ in range(N)]
dis = [[21000000 for _ in range(N)] for _ in range(N)]

  • `arr` 배열은 각 칸마다 존재하는 개별 비용(도둑루피 크기)을 저장하는 2차원 리스트입니다.
  • 입력값으로 초기화되며, 탐색 대상 동굴의 구조와 비용을 나타냅니다.
  • `dis` 배열은 현재까지 발견한 해당 칸까지 도달하는 **최소 누적 비용**을 저장합니다. 초기값은 매우 큰 수(21000000)로 설정하여, 어떤 경로든 초기값보다 작게 되도록 합니다. 이는 무한대 값을 의미합니다.
  • 탐색 진행 중에 지속적으로 갱신되며, 최종적으로 출구까지 도달하는 최소 비용을 찾는 데 사용됩니다.

3. 초기 설정 및 탐색 시작

q=PriorityQueue()
node=Node(0,0,arr[0][0])
q.put(node)
dis[0][0]=arr[0][0]

  • `PriorityQueue` 객체를 생성하여 다익스트라 알고리즘에 사용할 우선순위 큐를 준비합니다.
  • 출발 지점인 `(0,0)` 위치의 비용 `arr[0][0]`을 포함하는 `Node` 객체를 생성합니다.
  • 이 `Node`를 큐에 넣어 탐색을 시작할 준비를 합니다.
  • `dis[0][0]`에도 출발 지점의 비용을 초기화하여, 이 값이 시작점의 최소 비용임을 명시합니다.

4. 큐가 빌 때까지 반복

while not q.empty():

  • 큐가 비어있다는 것은, 탐색해야 할 후보 위치가 더 이상 없음을 뜻합니다.
  • 즉, 모든 가능한 경로를 확인했고, 최소 비용 경로를 찾았다는 의미입니다.
  • 따라서 이 반복문은 아직 탐색할 노드가 남아 있는 동안 계속 실행됩니다.

5. 현재 최소 비용 노드 꺼내기

node = q.get()
x = node.x
y = node.y
cost = node.cost

  • 큐의 맨 위에 있는 노드를 꺼내 현재 위치 `(x, y)`와 그 위치까지 도달하는 **누적 비용 `cost`**를 가져옵니다.
  • 이 노드는 지금까지 발견한 경로 중 비용이 가장 적은 위치를 의미합니다.
  • 큐에서 꺼낸 후, 그 위치를 기준으로 다음 위치 탐색을 진행합니다.

6. 상하좌우 4방향 탐색 및 유효성 검사

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

    if nx<0 or nx>=N or ny<0 or ny>=N:
        continue

  • 현재 위치에서 상하좌우 네 방향을 모두 탐색하기 위해 반복문을 사용합니다. `dx`와 `dy` 리스트를 활용하여 각 방향으로 이동한 새 좌표 `(nx, ny)`를 계산합니다.
  • 이동한 위치가 배열(`N x N` 그리드)의 범위를 벗어나면, 유효하지 않은 좌표이므로 탐색하지 않고 건너뜁니다.
  • 이렇게 범위를 벗어나는 경우를 사전에 걸러내어, 불필요한 오류와 탐색을 막습니다.

7. 다음 위치의 누적 비용 계산

next_cost=cost+arr[nx][ny]

  • 다음 위치 `(nx, ny)`까지 도달하는 데 드는 누적 비용을 계산합니다.
  • 현재 위치까지의 누적 비용 `cost`에 다음 위치의 개별 비용 `arr[nx][ny]`를 더합니다.
  • 이 값은 다음 위치까지 가는 경로의 새로운 비용 후보가 됩니다.

8. 더 적은 비용으로 갱신 및 큐에 추가

if(next_cost<dis[nx][ny]):
    dis[nx][ny]=next_cost
    q.put(Node(nx,ny,next_cost))

  • 만약 계산한 `next_cost`가 현재 `dis` 배열에 기록된 `(nx, ny)`까지의 최소 비용(`dis[nx][ny]`)보다 작다면,
  • 해당 위치까지 도달하는 최소 비용을 `next_cost`로 갱신합니다.
  • 그리고 이 `(nx, ny)` 위치를 포함하는 새로운 `Node`를 우선순위 큐에 추가하여, 추후에 이 위치를 기준으로 더 나은 경로를 탐색할 기회를 만듭니다.
  • 이 과정이 다익스트라 알고리즘의 핵심으로, 이미 방문한 위치라도 더 좋은 비용으로 도달 가능하다면 갱신하고 재탐색하는 방식입니다.

결론

  • 이 알고리즘은 단순히 한 번 방문한 칸을 다시 방문하지 않는 것이 아니라,
  • 더 낮은 비용으로 해당 칸에 도달할 수 있으면, 비용을 갱신하고 다시 큐에 넣어 더 나은 경로를 찾아가는 **유연한 탐색 방식**입니다.
  • 우선순위 큐가 최소 비용 노드를 먼저 꺼내 처리하기 때문에, 항상 비용이 작은 경로부터 확정해 나가면서 효율적으로 최소 비용 경로를 찾습니다.
  • 이 점이 다익스트라 알고리즘의 본질이며, 그리디한 접근임에도 전체 경로 최적해를 보장하는 이유입니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/11   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함
반응형