Algorithm/theory

[파이썬] 그래프, 트리 알고리즘 정리

takeU 2022. 7. 5. 09:11
반응형

파이썬 그래프, 트리 알고리즘 정리

알고리즘 선택 (최단거리)

  • 가중치가 없는 경우 - BFS
  • 가중치가 있는 경우
    • 음의 가중치가 있을 때 - 벨만-포드
    • 음의 가중치가 없을 때 - 다익스트라
  • 전체 쌍 최단 경로 - 플로이드-워셜 ( n ≤ 100 )

1. 벨만-포드 (Bellman-Ford Algorithm)

  • 시간 복잡도: O(V * E)
  • 모든 간선을 확인하며 최단 거리를 찾기 때문에 느리다.
  • 음수 간선이 있어도 최단 거리를 찾을 수 있다.
import sys
input = sys.stdin.readline
INF = int(1e9)

n, m = map(int, input().split())
edges = []
dist = [INF] * (n + 1)

for _ in range(m):
    u, v, w = map(int, input().split())
    edges.append((u, v, w))

def bf(start):
    dist[start] = 0
    for i in range(n):
        for j in range(m):
            cur_node, next_node, cost = edges[j]
            if dist[cur_node] != INF and dist[next_node] > dist[cur_node] + cost:
                dist[next_node] = dist[cur_node] + cost
                if i == n - 1:
                    return 1
    return 0

negative_cycle = bf(1)

if negative_cycle:
    print('-1')
else:
    for i in range(2, n+1):
        if dist[i] == INF:
            print('-1')
        else:
            print(dist[i])

2. 다익스트라 (Dijkstra’s Algorithm)

  • 시간 복잡도: O(E * log(E))
  • 방문하지 않은 노드 중에서 최단 거리가 가장 짧은 노드를 선택하여 한 단계씩 최단 거리를 구해나간다.
  • 우선순위 큐 (heap) 사용
import heapq
import sys
input = sys.stdin.readline
INF = int(1e9)

n, m = map(int, input().split())
start = int(input())
graph = [[] for i in range(n + 1)]
dist = [INF] * (n + 1)

for _ in range(m):
    a, b, c = map(int, input().split())
    graph[a].append((b, c))

def dijkstra(start):
    q = []
    heapq.heappush(q, (0, start))
    dist[start] = 0

    while q:
        weight, cur = heapq.heappop(q)
        if dist[cur] < weight:
            continue
        for g in graph[cur]:
            cost = weight + g[1]
            if cost < dist[g[0]]:
                dist[g[0]] = cost
                heapq.heappush(q, (cost, g[0]))

dijkstra(start)

for i in range(1, n + 1):
    if dist[i] == INF:
        print("INFINITY")
    else:
        print(dist[i])

3. 플로이드-워셜 (Floyd-Warshall Algorithm)

  • 시간 복잡도: O(V^3) - 3중 for문
  • 공간 복잡도: O(V^2) - 2차원 배열
n, m = map(int, input().split())
INF = int(1e9)
graph = [[0 if i == j else INF for i in range(n + 1)] for j in range(n + 1)]

for _ in range(m):
    a, b, c = map(int, input().split())
    graph[a][b] = c

for k in range(1, n + 1):
    for a in range(1, n + 1):
        for b in range(1, n + 1):
            graph[a][b] = min(graph[a][b], graph[a][k] + graph[k][b])

for a in range(1, n + 1):
    for b in range(1, n + 1):
        if graph[a][b] == INF:
            print("INFINITY", end=" ")
        else:
            print(graph[a][b], end=" ")
    print()

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

Spanning Tree란?

  • 그래프 내의 모든 정점을 포함하는 트리
  • 즉, 그래프에서 일부 간선을 선택한 트리

MST란?

  • Spanning Tree 중 사용된 간선의 가중치 합이 최소인 트리
  • n개의 정점을 가지는 그래프에 대해 n-1 개의 간선만을 사용해야 함
  • Cycle이 있으면 안됨

알고리즘 선택

  • 간선이 적은 그래프 (희소 그래프, Sparse graph) - 크루스칼 (간선 위주)
  • 간선이 많은 그래프(밀집 그래프, Dense Graph) - 프림 (정점 위주)

1. 크루스칼 (Kruskal Algorithm)

  • 시간 복잡도: O(V^2)
  • 간선을 가중치 기준 오름차순으로 정렬 후 추가
  • 간선을 기준으로 추가하기 때문에, 사이클 여부를 확인해야 함
  • 유니온-파인드 사용
v, e = map(int, input().split())
parent = list(range(v + 1))

def find_parent(parent, x):
    if parent[x] != x:
        parent[x] = find_parent(parent, parent[x])
    return parent[x]

def union_parent(parent, a, b):
    a = find_parent(parent, a)
    b = find_parent(parent, b)
    if a < b:
        parent[b] = a
    else:
        parent[a] = b

edges = []
total_cost = 0

for _ in range(e):
    a, b, cost = map(int, input().split())
    edges.append((cost, a, b))
edges.sort()

for i in range(e):
    cost, a, b = edges[i]
    if find_parent(parent, a) != find_parent(parent, b):
        union_parent(parent, a, b)
        total_cost += cost

print(total_cost)

참고 - 유니온-파인드 (union-find)

def find_parent(parent, x):
    if parent[x] != x:
        return find_parent(parent, parent[x])
    return x

def union_parent(parent, a, b):
    a = find_parent(parent, a)
    b = find_parent(parent, b)
    if a < b:
        parent[b] = a
    else:
        parent[a] = b

v, e = map(int, input().split())
parent = list(range(v + 1))

for i in range(e):
    a, b = map(int, input().split())
    union_parent(parent, a, b)

2. 프림 (Prim Algorithm)

  • 시간 복잡도: O(E * log(V))
  • 시작 정점을 기준으로 연결된 간선 중 가중치가 가장 작은 간선과 연결된 정점을 추가 후 반복
  • 우선순위 큐 (heap) 사용
import heapq
import collections
import sys
sys.setrecursionlimit(10 ** 6)
input = sys.stdin.readline

n, m = map(int,input().split())
graph = collections.defaultdict(list)
visited = [0] * (n + 1)

for i in range(m):
    u, v, cost = map(int,input().split())
    graph[u].append([cost, u, v])
    graph[v].append([cost, v, u])

# 프림 알고리즘
def prim(graph, start_node):
    visited[start_node] = 1
    candidate = graph[start_node]
    heapq.heapify(candidate)
    mst = []
    total_cost = 0

    while candidate:
        cost, u, v = heapq.heappop(candidate)
        if visited[v] == 0:
            visited[v] = 1
            mst.append((u,v))
            total_cost += cost

            for edge in graph[v]:
                if visited[edge[2]] == 0:
                    heapq.heappush(candidate, edge)

    return total_cost

print(prim(graph,1))

위상정렬 (Topological Sort)

  • 시간 복잡도: O(V + E)
  • 유향 그래프
  • 그래프 내부에 Cycle이 없어야 함
  • 아래 코드는 bfs
from collections import deque

v, e = map(int, input().split())
indegree = [0] * (v + 1)
graph = [[] for _ in range(v + 1)]
for _ in range(e):
    a, b = map(int, input().split())
    graph[a].append(b)
    indegree[b] += 1

def topological_sort():
    result = []
    q = deque()
    for i in range(1, v + 1):
        if indegree[i] == 0:
            q.append(i)

    while q:
        cur = q.popleft()
        result.append(cur)

        for g in graph[cur]:
            indegree[g] -= 1
            if indegree[g] == 0:
                q.append(g)
    for i in result:
        print(i, end=' ')

topological_sort()