Search
🏫

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

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

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. 제약 조건 및 응용

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