Dionysus

[알고리즘🧩] 그래프의 최소 비용 문제 본문

CS 및 알고리즘 공부/알고리즘

[알고리즘🧩] 그래프의 최소 비용 문제

Gogumiing 2024. 9. 11. 11:40

목차

💬 최소 비용 신장 트리 (Minimum Spanning Tree, MST)

💬 Prim 알고리즘

💬 Kruskal 알고리즘

💬 최단 경로 알고리즘 (Dijkstra)


💬 최소 비용 신장 트리 (Minimum Spanning Tree, MST)

더보기

무방향 가중치 그래프에서 신장 트리를 구성하는 간선들의 가중치 합이 최소인 신장 트리를 의미한다.

  • Prim 알고리즘 또는 Kruskal 알고리즘을 활용한다.
💡 신장 트리란?
n개의 정점으로 이루어진 무방향 그래프에서 n개의 정점과 n - 1개의 간선으로 이루어진 트리
📌 대표적인 그래프에서의 최소 비용 문제

- 모든 정점을 연결하는 간선들의 가중치 합이 최소가 되는 트리
- 두 정점 사이의 최소 비용 경로 찾기

 

✅ MST 표현 예시

 

 

💬 Prim 알고리즘

더보기

하나의 정점에서 연결된 간선들 중 하나씩 선택하여 MST를 만들어 가는 방식

  1. 임의의 정점을 선택한다.
  2. 선택한 정점과 인접하는 정점들 중(BFS + 최소 비용)의 최소 비용 간선이 존재하는 정점을 선택한다.
  3. 모든 정점이 선택될 때까지 1, 2번 과정을 반복한다.
from heapq import heappush, heappop

def prim(start):
    heap = list()           
    MST = [0] * (V)                 # visited라고 생각하면 됨

    # 최소 비용 합계
    sum_weight = 0

    # 힙에서 관리해야 할 데이터
    # 가중치, 정점 정보
    # 정점 번호를 기준으로 정렬되기 때문에 이렇게 하면 안됨
    # heappush(heap, (start, 0))    
    heappush(heap, (0, start))      # 시작점은 가중치가 0임     

    while heap:
        weight, v = heappop(heap)
        
        # 이미 방문한 지점이면 통과
        if MST[v]:
            continue

        # 방문 처리
        MST[v] = 1
        # 누적합 추가
        sum_weight += weight

        # 갈수 있는 노드를 보면서
        for next in range(V):
            # 갈 수 없는 지점이면 continue
            if graph[v][next] == 0:
                continue

            # 이미 방문한 지점이면 continue
            if MST[next]:
                continue

            heappush(heap, (graph[v][next], next))

    return sum_weight

# V는 정점의 개수, E는 간선의 개수
V, E = map(int, input().split())
# 현재는 인접 행렬로 구현되어 있는데, 이것을 인접 리스트로 변경하는 것을 과제로 내주심
graph = [[0] * (V) for _ in range(V)] 
for _ in range(E):
    u, v, w = map(int, input().split())
    
    graph[u][v] = w
    graph[v][u] = w  # 가중치가 있는 무방향 그래프

result = prim(0)
print(f'최소 비용 = {result}')        # 최소 비용 = 175

'''
# 입력
7 11
0 1 32
0 2 31
0 5 60
0 6 51
1 2 21
2 4 46
2 6 25
3 4 34
3 5 18
4 5 40
4 6 51
'''

 

 

예제

아래 그래프에 Prim 알고리즘을 적용시켜 보자.

  1. 0번 노드에서 출발하여 가장 낮은 가중치인 2번 노드로 나아가게 된다. (0 → 2)
  2. 2번 노드에서 가장 낮은 가중치인 1번 노드로 나아간다.(2 → 1)
    • 하지만 1번 노드 이후로 연결되는 다음 노드가 없으므로 가중치가 두 번째로 낮은 6번 노드로 나아간다. (→ 6)
    • 6번 노드도 역시 다음으로 연결되는 노드가 없으므로 가중치가 마지막으로 낮은 4번 노드로 나아간다.( → 4)
  3. 4번 노드에서 가장 낮은 가중치인 3번 노드로 나아간다. (4 → 3)
  4. 3번 노드에서 가장 낮은 가중치인 5번 노드로 나아간다. (3 → 5)
  5. 모든 노드를 방문했으므로 종료한다.
π: 현재 정점의 부모 정점 / key: 두 정점 간의 가중치

 

 

💬 Kruskal 알고리즘

더보기

간선을 하나씩 선택하여 MST를 찾는 알고리즘

  1. 최초의 모든 간선을 가중치에 따라 오름차순으로 정렬한다.
  2. 가중치가 가장 낮은 간선부터 선택하면서 트리를 증가시킨다.
    • 사이클이 존재하면 다음으로 가중치가 낮은 간선을 선택함
  3. n - 1개의 간선이 선택될 때까지 1, 2번 과정을 반복한다.
가중치 기준 오름차순으로 간선 선택 (n-1개의 간선이 선택될 때까지)
# V: 마지막 정점. 따라서 정점의 개수는 (V+1)개이다.
# E: 간선의 개수
V, E = map(int, input().split())    
edge = []

# 간선 집합 생성
for _ in range(E):
    u, v, w = map(int, input().split())
    edge.append([u, v, w])
edge.sort(key=lambda x : x[2])          # 가중치 기준 오름차순 정렬
parents = [i for i in range(V)]         # 대표원소 배열


def find_set(x):
    if parents[x] == x:
        return x

    parents[x] = find_set(parents[x])
    return parents[x]


def union(x, y):
    x = find_set(x)
    y = find_set(y)

    if x == y:
        return

    # 더 작은 루트노트에 합침
    if x < y:
        parents[y] = x
    else:
        parents[x] = y


# MST의 간선수 N = 정점 수 - 1
cnt = 0     # 선택한 edge의 수
total = 0   # MST 가중치의 합
for u, v, w in edge:
    # 다른 집합이라면
    if find_set(u) != find_set(v):
        cnt += 1
        union(u, v)
        total += w
        if cnt == V:  # MST 구성이 끝나면
            break
print(f'최소 비용 = {total}')        # 최소 비용 = 175

'''
# 입력
7 11
0 1 32
0 2 31
0 5 60
0 6 51
1 2 21
2 4 46
2 6 25
3 4 34
3 5 18
4 5 40
4 6 51
'''

 

💬 최단 경로 알고리즘 (Dijkstra)

더보기

간선의 가중치가 있는 그래프에서 두 정점 사이의 경로들 중, 간선의 가중치 합이 최소인 경로를 찾는 알고리즘

  • 시작 정점(s)에서 거리가 최소인 정점(x)을 선택해 나가면서 최단 경로를 도출한다.
    • 시작 정점(s)에서 끝 정점(t)까지의 최단 경로에 정점 x가 존재할 때, 최단 경로는 s에서 x까지의 최단 경로와 x에서 t까지의 최단 경로로 구성된다.
  • 탐욕 기법을 이용한 알고리즘으로, MST의 Prim 알고리즘과 유사하다.
📌 최단 경로 알고리즘

하나의 시작 정점에서 끝 정점까지의 최단 경로
- Dijkstra 알고리즘: 음의 가중치를 허용하지 않음
- Bellman - Ford 알고리즘: 음의 가중치를 허용함

모든 정점들에 대한 최단 경로
- Floyd - Warshall (플로이드 - 워샬) 알고리즘

 

D: 누적 가중치 리스트 / U: 누적 가중치 기반 최종 경로

 

import heapq

INF = int(1e9)  # 무한을 의미하는 값으로 1억

# 노드의 개수, 간선의 개수를 입력받기
n, m = map(int, input().split())
# 시작 노드 번호 (문제에 따라 다름)
start = 0
# 인접리스트 만들기
graph = [[] for i in range(n)]
# 누적거리를 저장할 테이블 - INF 로 저장
distance = [INF] * n

# 간선 정보를 입력
for _ in range(m):
    a, b, w = map(int, input().split())
    graph[a].append([b, w])  # 단방향 그래프이다!


def dijkstra(start):
    pq = []
    # heapq 에 리스트로 저장할 때는 맨 앞의 데이터를 기준으로 정렬된다.
    heapq.heappush(pq, (0, start))
    distance[start] = 0  # 시작 노드 최단 거리는 0

    # 우선순위 큐가 빌 때 까지 반복
    while pq:
        # 가장 최단 거리인 노드에 대한 정보 꺼내기
        dist, now = heapq.heappop(pq)
        # 현재 노드가 이미 처리됐다면 skip
        # 예제 그림: c 위치 가중치 3, 4 로 도착가능 [참고]
        if distance[now] < dist:
            continue

        # 현재 노드와 연결된 다른 인접한 노드 확인
        for next in graph[now]:
            next_node = next[0]
            cost = next[1]  # 다음 노드의 가중치

            new_cost = dist + cost  # 누적값(현재까지의 누적값 + 다음 노드 가중치)

            # 다음 노드를 가는 데 더 많은 비용이 드는 경우
            if new_cost >= distance[next_node]:
                continue

            distance[next_node] = new_cost  # next_node 까지 가는데 비용은 new_cost
            heapq.heappush(pq, (new_cost, next_node))


# 다익스트라 알고리즘 실행
dijkstra(start)

# 모든 노드로 가기 위한 최단 거리 출력
for i in range(n):
    # 도달할 수 없는 경우, 무한 출력
    if distance[i] == INF:
        print("INF", end=' ')
    else:
        print(distance[i], end=' ')

# 출력: 0 2 3 9 6 10
# -> 0번 노드에서 갈 수 있는 다른 노드들까지의 최단거리들을 모두 구할 수 있다.
# - 다익스트라 한 번이면, 하나의 정점 -> 다른 정점들까지의 최단거리들을 모두 구한다.

'''
# 입력
6 8
0 1 2
0 2 4
1 2 1
1 3 7
2 4 3
3 4 2
3 5 1
4 5 5
'''