Search
moon
sun
🏫

[알고리즘] 8. 그래프 (3) - 최단 경로/네트워크 플로

Tags
CS
Algorithm
Last edited time
2025/01/05 02:35
2 more properties
Search
🏫
[알고리즘] 14. 탐색 알고리즘 요약
CS
Algorithm
2025/01/12 5:37
🏫
[알고리즘] 14. 탐색 알고리즘 요약
CS
Algorithm
2025/01/12 5:37

1. 최단 경로

•
최단 경로 정의
◦
두 정점 u와 v 간의 최단 경로
◦
가중 그래프에서 두 정점 u에서 v를 연결하는 경로 중 간선의 가중치 합이 가장 작은 경로
•
최단 경로 문제 유형
◦
단일 출발점 최단 경로 문제
▪
다익스트라, 벨만 포드 알고리즘
◦
단일 도착점 최단 경로 문제
◦
단일 쌍 최단 경로 문제
◦
모든 쌍 최단 경로 문제
▪
플로이드 알고리즘

2. 데이크스트라 알고리즘

2.1. 개념

•
정의
◦
단일 출발점 최단 경로를 찾는 알고리즘.
◦
하나의 출발점 S에서 모든 정점으로의 최단 경로를 계산
◦
욕심쟁이 방법을 적용한 알고리즘으로, 음수 가중치가 없는 그래프에서만 동작
•
동작 원리
1.
초기화
•
출발점 s의 거리 d[s] = 0
•
나머지 모든 정점 v의 거리 d[v]=∞
•
선택된 정점의 집합 S = {}
2.
아직 선택되지 않은 정점 (V-S) 중에서 d[]값이 최소인 정점 u 를 선택
3.
정점 u의 인접 정점 v에 대해:
•
d[v]=min⁡(d[v],d[u]+w(u,v))
4.
모든 정점을 처리할 때까지 반복
•
예시 1
•
예시 2

2.2. 예시 코드

// 입력: G=(V,E), s : 시작 정점 // 출력: d[] : s로부터 다른 모든 정점으로의 최단 경로의 길이 // prev[] : 최단 경로를 만드는 선행 정점 Dijkstra (G, s) { S = { }; d[s] = 0; for ( 모든 정점 v ∈ V ) { d[v] = ∞; prev[v] = NULL; } while ( S != V ) { d[u]가 최소인 정점 u ∈ V-S를 선택; S = S ∪ { u }; for ( u에 인접한 모든 정점 v ) { if ( d[v] > d[u] + W(u, v) ) { d[v] = d[u] + W(u, v); prev[v] = u; } } } return ( d[], prev[] ); }
C
복사
import heapq def dijkstra(graph, start): """ graph: 딕셔너리 형태의 인접 리스트 예) graph[u] = [(가중치, 인접정점), (가중치, 인접정점), ... ] start: 시작 정점 return: d, prev d[v]: start에서 v까지의 최단 거리 prev[v]: start->...->v 최단 경로에서 v 직전에 방문한 정점 """ # 모든 정점에 대해 최단 거리 d[v] = 무한대로 초기화 d = {node: float('inf') for node in graph} prev = {node: None for node in graph} # 시작 정점의 거리만 0으로 설정 d[start] = 0 # 최소 힙(우선순위 큐)에 (거리, 정점) 형태로 삽입 pq = [] heapq.heappush(pq, (0, start)) # 아직 방문하지 않은 정점들을 관리 -> (pq가 빌 때까지) while pq: # 현재 거리가 최소인 정점을 꺼냄 current_dist, u = heapq.heappop(pq) # 이미 저장된 최단 거리보다 크면 무시(과거 정보) if current_dist > d[u]: continue # u와 인접한 각 정점 v에 대해 거리 갱신 시도 for w, v in graph[u]: # (가중치, 인접정점) if d[v] > d[u] + w: d[v] = d[u] + w prev[v] = u # 거리 갱신되면 우선순위 큐에 새로 삽입 heapq.heappush(pq, (d[v], v)) return d, prev if __name__ == "__main__": # 예시 그래프 (인접 리스트) # graph[u] = [(가중치, 인접정점), ...] graph = { 1: [(2, 2), (4, 3)], 2: [(2, 1), (3, 3), (1, 4)], 3: [(4, 1), (3, 2), (5, 4)], 4: [(1, 2), (5, 3)] } start_node = 1 dist, prev = dijkstra(graph, start_node) print(f"최단 거리 배열 (d[]) = {dist}") print(f"선행 정점 배열 (prev[]) = {prev}") # dist[v]를 통해 "start_node에서 v까지의 최단 거리" 확인 가능 # prev[v]를 이용하면 역추적하여 경로를 복원할 수 있음. (1) / \ 2 4 / \ (2)---3---(3) \ / 1 5 \ / (4)
Python
복사

2.3. 성능 및 특징

•
시간 복잡도
◦
인접 행렬 O(|V|^2)
◦
인접 리스트 + 힙 O((|V| + |E|) log |V|)
•
음의 가중치를 갖는 간선이 없어야 함

3. 벨만-포드 알고리즘

3.1. 개념

•
정의
◦
단일 출발점 최단 경로를 찾는 알고리즘
•
특징
◦
음의 가중치를 갖는 간선을 포함한 그래프에서도 적용 가능.
▪
음의 사이클이 없는 경우에 한함
◦
G = (V, E) 에서 |V| = n 일때 단계적으로 최단 경로를 구해나가는 방법
▪
단계적으로 간선 개수를 늘려가며 최단 경로를 계산
▪
최대 (n-1)개의 간선을 사용하는 최단 경로를 구함
•
동작 원리
◦
초기화:
▪
출발점 s의 거리를 0으로 설정 (d[s]=0).
▪
나머지 모든 정점의 거리는 ∞로 설정 (d[v]=∞).
1.
반복:
•
총 |V|-1번의 반복 과정에서 각 간선을 순회하며 거리값을 갱신
•
거리 갱신 식: d[v] = min(d[v], d[u] + w(u, v)).
2.
최종 거리 갱신
•
마지막 단계에서 거리값 조정이 발생한다면 음의 사이클 존재
•
예시 1
•
예시 2

3.2. 예시 코드

Bellman_Ford(G, s) { for ( 모든 정점 v ∈ V ) { d[v] = ∞; prev[v] = NULL; } d[s] = 0; for ( i = 1; i < n; i++ ) { for ( 모든 간선 (u, v) ∈ E ) if ( d[v] > d[u] + W(u, v) ) { d[v] = d[u] + W(u, v); prev[v] = u; } } return ( d[], prev[] ); }
C
복사
def bellman_ford(V, E, s): """ 벨만-포드 알고리즘 (단일 시작점 s 로부터의 최단 경로) :param V: 정점들의 리스트 혹은 집합 (ex. [1,2,3,4,...]) :param E: 간선들의 리스트 (ex. [(u, v, w), (u2, v2, w2), ...]) :param s: 시작 정점 :return: (d, prev) d[v] : s 로부터 v 까지의 최단 경로 거리 prev[v]: s -> ... -> v 최단 경로 상에서 v 바로 이전 정점 """ # 1. 거리 배열 d, 이전 정점 배열 prev 초기화 d = {v: float('inf') for v in V} prev = {v: None for v in V} # 시작 정점 s의 거리는 0으로 설정 d[s] = 0 # 2. 모든 간선에 대해 (V-1)번 반복하여 거리 완화(Relaxation) for _ in range(len(V) - 1): for (u, v, w) in E: # 현재 기록된 거리보다 u -> v를 거쳐 가는 경로가 더 짧다면 갱신 if d[u] != float('inf') and d[u] + w < d[v]: d[v] = d[u] + w prev[v] = u # 3. 음수 사이클(Negative Cycle) 검사 # (추가로 원하는 경우, 음수 사이클 존재 여부 확인 가능) for (u, v, w) in E: # 더 갱신될 수 있다면, 음수 사이클 존재 if d[u] != float('inf') and d[u] + w < d[v]: print("Error: 음수 사이클(Negative Cycle)이 존재합니다.") # 필요 시 여기서 별도로 처리(예: 예외 발생, 사이클에 속한 정점 추적 등) break return d, prev # 예시 실행 if __name__ == "__main__": # 예: 정점이 1~5, 간선이 아래와 같다고 가정 V = [1, 2, 3, 4, 5] E = [ (1, 2, 6), (1, 3, 7), (2, 4, 5), (2, 3, 8), (2, 5, -4), (3, 4, -3), (3, 5, 9), (4, 2, -2), (5, 1, 2), (5, 4, 7) ] s = 1 # 시작 정점 d, prev = bellman_ford(V, E, s) print("최단 거리 d:", d) print("이전 정점 prev:", prev)
Python
복사

3.3. 성능 및 특징

•
시간 복잡도
◦
최악의 경우: O(|V||E|) (모든 간선을 반복적으로 검사).
•
장점
◦
음의 가중치를 갖는 간선이 있는 경우에도 최단 경로를 계산할 수 있음
◦
모든 간선을 매번 조사하는 대신, 이전 단계에서 거리값이 조정된 정점에 연결된 간선만 조사하여 효율 개선 가능
•
특징
◦
음의 가중치를 갖는 간선이 있는 경우
▪
데이스크트라 알고리즘 적용 불가
▪
벨만-포드 알고리즘 적용 가능
•
단 음의 사이클이 존재하면 적용 불가
◦
음이 가중치를 갖는 간선이 없는 경우
▪
데이스크트라 알고리즘 → 바람직
▪
벨만-포드 알고리즘

4. 플로이드 알고리즘

4.1. 개념

•
정의
◦
모든 정점 쌍 간의 최단 경로를 찾는 알고리즘
•
특징
◦
동적 프로그래밍 기반의 알고리즘
◦
중간에 거쳐 갈 수 있는 정점을 하나씩 추가하면서 경로를 계산
◦
음의 가중치를 포함할 수 있지만, 음의 사이클이 존재하면 안 됨
•
수행 과정
1.
초기화
•
그래프를 인접 행렬로 표현: 초기 행렬 D[0]은 각 간선의 가중치를 나타냄.
•
없는 간선은 ∞, 자기 자신으로 가는 경로는 0.
2.
중간 정점 추가
•
정점 k까지를 포함한 최단 경로 계산.
•
점화식: d[i][j] = min(d[i][j], d[i][k] + d[k][j]).
3.
최종 행렬 반환
•
최단 경로를 나타내는 최종 거리 행렬 D[n]
•
인접 행렬 표현 활용
•
예시

4.2. 예시 코드

// 입력: G=(V,E), 인접 행렬 A[1..n][1..n] // 출력: D[][] : 모든 정점 쌍 간의 최단 경로의 길이 Floyd (G) { // D^(0) = ( d_ij^(0) = w_ij ) for (i=1; i<=n; i++) for (j=1; j<=n; j++) D[i][j] = A[i][j]; // D^(k): 경유하는 정점 k for (k=1; k<=n; k++) for (i=1; i<=n; i++) // D_ij for (j=1; j<=n; j++) if (D[i][j] > D[i][k] + D[k][j]) // 경유하는 경우 D[i][j] = D[i][k] + D[k][j]; return (D[][]); }
C
복사
def floyd_warshall(matrix): """ 플로이드-워셜 알고리즘을 이용해 모든 정점 쌍 최단 경로를 구한다. :param matrix: 그래프의 인접 행렬 (list of lists), matrix[i][j]는 정점 i -> j 로의 가중치 :return: dist (list of lists), dist[i][j]는 i에서 j로 가는 최단 거리 """ # 정점 개수 n n = len(matrix) # dist 배열을 초기 행렬(matrix)로 복사 dist = [row[:] for row in matrix] # 모든 정점 k(경유지)에 대해 for k in range(n): # 모든 출발점 i에 대해 for i in range(n): # 모든 도착점 j에 대해 # 경유지 k를 거치는 경로가 기존보다 더 짧다면 갱신 if dist[i][j] > dist[i][k] + dist[k][j]: dist[i][j] = dist[i][k] + dist[k][j] return dist # 예시 사용 if __name__ == "__main__": import math # 예시: 4개의 정점(0,1,2,3)이 있다고 가정 # 초기 인접 행렬 (가중치) # ∞는 math.inf 로 표현 matrix = [ [0, 5, math.inf, 10], [math.inf, 0, 3, math.inf], [math.inf, math.inf, 0, 1], [math.inf, math.inf, math.inf, 0] ] dist_result = floyd_warshall(matrix) print("플로이드 알고리즘 결과 (모든 쌍 최단 거리):") for row in dist_result: print(row)
Python
복사

4.3. 성능 및 특징

•
시간복잡도
◦
최악의 경우 O(n^3)
•
특징
◦
동적 프로그래밍 방법을 적용한 알고리즘
◦
데이크스트라 알고리즘으로 모든 쌍 최단 경로를 구할 수있음
▪
각 정점에 대해서 반복적으로 적용해서 해결 가능
▪
플로이드 알고리즘이 더 간단하므로 빠르게 수행
◦
P[1..n][1..n]을 활용하면 최단 경로 자체를 구할 수 있음

3. 포드-풀커슨 알고리즘

3.1. 네트워크 플로 문제

•
주어진 네트워크에 대해서 플로를 최대로 하는 값을 찾는 문제
◦
소스에서 싱크로 보낼 수 있는 플로 값을 최대로 하는 문제 → ”최대 플로 문제”
•
네트워크 N = (V, E, s, t, c)
◦
방향 그래프 G=(V, E)
◦
s → 소스 (시작점, 진입차수가 0인 정점)
◦
t → 싱크 (도착점, 진출차수가 0인 정점)
◦
c → 간선의 가중치 → 간선의 용량 (capacity)
▪
c(u, v) → 간선 <u, v>를 통해 보낼 수 있는 최대의 양/값

3.2. 포드-풀커슨 알고리즘 개요

•
목적: 네트워크 플로 문제에서 최대 플로 값을 찾는 방법
•
특징:
◦
증가 경로(augmenting path)를 반복적으로 찾아 플로를 증가시킴
◦
순방향 간선과 역방향 간선을 고려
◦
간선의 용량이 정수일 경우에는 종료 보장이 됨

3.3. 수행 과정

1.
초기화
•
모든 간선의 플로를 0으로 설정
2.
증가 경로 탐색
•
소스에서 싱크까지의 증가 경로를 탐색
•
증가 경로의 잔여 용량 계산: ∆ = min(r(u, v) for 모든 간선)
3.
플로 갱신
•
순방향 간선: f(u, v) += ∆
•
역방향 간선: f(u, v) -= ∆
4.
종료 조건
•
더 이상 증가 경로가 존재하지 않을 때 알고리즘 종료

3.4. 잔여 용량과 증가 경로

•
잔여 용량: r(u, v) = c(u, v) - f(u, v) (순방향 간선)
•
역방향 간선: r(v, u) = f(u, v) (간선 용량 감소 가능)

3.5. 성능

•
시간 복잡도: O(|E|F), 여기서 F는 최대 플로 값
•
개선 알고리즘: 에드몬즈-카프 알고리즘(BFS 사용)은 O(|V||E|²)

3.6. 제약 조건 및 응용

•
간선 용량이 무리수일 경우 종료 보장 없음
•
응용 사례:
◦
물류 네트워크 최적화
◦
전송 네트워크에서 최대 데이터 전송량 계산