
포드퍼커슨과 에드몬드카프 알고리즘은 네트워크 관련 문제에서 가장 많이 출제되며, 그래프 순회의 고급 기술로 쓰이는 알고리즘 입니다. 두개의 알고리즘을 적용하는 경우는 노드의 각 간선 간 데이터 흐름이나 특정 병목이 있을 때 출발 노드와 도착노드 사이의 간선에서 어떤 지점이 가장 많은 흐름을 가지는지를 파악하는 알고리즘인데요.
두 알고리즘은 비슷하지만 세부적으로 보면 다른점이 있는데요. 여기서 둘이 공통적으로 사용되는 키워드는 "Augmenting path" - "증강 경로" 입니다.
여기서 증강 경로를 각 순회마다 어떻게 선택하냐에 따라 포드퍼커슨과 에드몬드 카브 알고리즘이 나뉘는데요 더 자세히 알아보도록 하겠습니다.
Ford-Fulkerson 알고리즘
Ford-Fulkerson은 그리디(Greedy) 알고리즘에 기반한 방법입니다.
반복적으로 증강되는 경로를 찾아 capacity를 수정해 나아가 더 나은 경로를 찾는 방법입니다.
프로세스는 더이상 증강 될 수 있는 경로를 찾을 수 없을 때 까지 반복되는데 가장 단순하고 무식하게 순회하는 방식입니다.
그러나 이 알고리즘의 단점은 동일한 가중치로 여러가지의 옵션이 생길 때 어느 경로를 선택해야 하는지 제시하지를 못하는 단점이 있습니다. 만약 동일 가중치라 하더라고 반복 횟수가 더 많은 방법이 있을 수 도 있는 반면 더 적은 횟수의 반복으로 같은 가중치를 낼 수 있는 경로가 있지만 이 두개의 우위를 못가린다는 말입니다.
따라서 이 점을 해결하기 위해 Edmons-Karp 알고리즘이 나온겁니다.
복잡성(DFS) - O(fE). f - 최대 흐름, E - 간선
Edmons-Karp 알고리즘, 쉽게 이해하기 💧
네트워크에 물을 최대로 흘려보내는 문제를 푼다고 상상해 보세요. 각 파이프(간선)는 정해진 용량(최대 흐름)이 있고, 물을 보내는 시작점(Source)과 물을 받는 도착점(Sink)이 있습니다. Edmons-Karp 알고리즘은 이 네트워크에 물을 얼마나 최대로 보낼 수 있는지 계산하는 똑똑한 방법입니다.
핵심 아이디어: 가장 짧은 길부터 공략하기 (출처)
기존 방식(포드-풀커슨)은 깊이 우선 탐색(DFS)을 사용해서 물을 보낼 길을 찾습니다. 이건 마치 미로를 탐험할 때 한쪽 길로 무작정 깊이 들어가 보는 것과 같아요. 운이 나쁘면 아주 길고 비효율적인 경로를 선택할 수 있고, 이러면 전체 효율이 떨어집니다. [01:48]
반면, Edmons-Karp 알고리즘은 너비 우선 탐색(BFS)을 사용합니다. 이것은 시작점에서부터 한 칸, 두 칸... 이렇게 주변을 넓혀가며 가장 가까운, 즉 가장 적은 파이프를 거치는 최단 경로를 먼저 찾아냅니다. [03:09]
왜 최단 경로가 좋을까요? 길고 구불구불한 경로로 물을 먼저 보내면, 그 경로에 있는 파이프들이 일찍 채워져 버립니다. 그런데 그 파이프들이 나중에 다른 더 좋은 경로의 일부였다면 손해겠죠? 최단 경로를 우선으로 사용하면 자원을 더 효율적으로 배분하여 전체적으로 더 많은 물을 보낼 방법을 빨리 찾을 수 있습니다.
알고리즘 동작 방식 (수도 파이프 비유)
- 길 찾기 (BFS): 수도꼭지(시작점)에서부터 하수구(도착점)까지 물이 흐를 수 있는 가장 짧은 경로를 찾습니다. 이때, 아직 용량이 남아있는 파이프(흐름 > 0)로만 길을 찾습니다.
- 병목 찾기: 찾은 경로에서, 물을 가장 적게 흘려보낼 수 있는 '가장 좁은 파이프'의 용량, 즉 병목(bottleneck)을 확인합니다. [05:54]
- 물 흘려보내기:
- 찾아낸 병목 용량만큼 경로에 물을 흘려보냅니다.
- 경로에 포함된 모든 파이프의 용량에서 이 병목 용량을 빼줍니다 (정방향).
- 반대 방향으로는 같은 양을 더해줍니다 (역방향). 이 '역방향' 개념은 중요해요. 혹시 잘못된 경로로 물을 보냈을 때, 이 역방향 용량을 이용해 "물 흐름을 취소"하고 다른 길로 보낼 기회를 만들기 때문입니다.
- 반복: 더 이상 시작점에서 도착점까지 물을 보낼 수 있는 경로가 없을 때까지 1~3번 과정을 계속 반복합니다.
- 결과: 각 단계에서 흘려보낸 물의 양(병목 값들)을 모두 더하면, 그것이 바로 이 네트워크가 최대로 흘려보낼 수 있는 물의 양, 즉 최대 유량(Max Flow)이 됩니다. [08:03]
복잡성: O(V·E²)
- V (Vertex): 정점의 개수 (수도 시스템의 분기점)
- E (Edge): 간선의 개수 (파이프의 개수)
이 알고리즘은 BFS를 사용해 최단 경로를 찾기 때문에, 파이프 용량이 아무리 커도 일정한 계산 횟수 안에 답을 찾을 수 있습니다. 그래서 DFS를 사용할 때보다 훨씬 안정적이고 빠른 성능을 보장합니다. [02:31]
요약하자면, Edmons-Karp는 "무작정 아무 길이나 찾지 말고, 가장 짧은 길을 계속 찾아서 물을 보내자!" 라는 간단하고 효율적인 아이디어를 기반으로 한 알고리즘입니다.
코드:
import heapq
from collections import deque
from typing import List, Dict, Tuple, Optional
class Node:
def __init__(self, name : str):
self.name = name
self.adjacent_nodes : Dict['Node', int] = {}
def get_name(self) -> str:
return self.name
def get_adjacent_nodes(self) -> Dict['Node', int]:
return self.adjacent_nodes
def add_adjacent_nodes(self, node: 'Node', weight : int):
self.adjacent_nodes[node] = weight
#This determines the shortest path - 용량이 0보다 큰 간선만을 따라 증가 경로 찾기
def bfs(
s : Node, #시작
t : Node, #끝
parent : Dict[Node, Optional[Node]], #그래프
visited : Dict[Node, bool]) -> bool: #방문 그래프
queue = deque()
queue.append(s)
visited[s] = True
while queue:
current_node : Node = queue.popleft()
for neighbor, capacity in current_node.get_adjacent_nodes().items():
#And value is larger
if not visited[neighbor] and capacity > 0:
visited[neighbor] = True
parent[neighbor] = current_node
queue.append(neighbor)
if neighbor == t:
return True
return False
#Find a manximum flow betweeb source - sink
def edmonds_karp(graph: Dict[str, Dict[str, int]], source: str, sink: str) -> int:
#노드화
nodes = {name: Node(name) for name in graph.keys()}
#노드 연결, 잔여 그래프(Residual Graph) - 역방향 간선 추가. u - 현재노드, v - 이웃 노드
for u_name, neighbors in graph.items():
for v_name, capacity in neighbors.items():
u, v = nodes[u_name], nodes[v_name]
#정방향 추가
u.add_adjacent_nodes(v, capacity)
#역방향 추가 - 초기 0으로 세팅
if u not in v.get_adjacent_nodes(): #만약 역으로 가는 방향이 있다면
v.add_adjacent_nodes(u, 0)
#시작 노드 지정
source_node = nodes[source]
#끝나는 노드 지정
sink_node = nodes[sink]
#최대 유량
max_flow : int = 0
#증가 경로가 없을 때 까지 반복
while True:
#각 반복마다 parent와 visited 초기화
parent = {node : None for node in nodes.values()}
visited = {node : False for node in nodes.values()}
#BFS를 통해 s -> t 경로 탐색
if not bfs(source_node, sink_node, parent, visited):
break #경로가 없으면 종료
#경로 병목 용량 계산 - 끝점에서 시작지점까지 역 traverse
path_flow = float('inf')
v = sink_node
while v != source_node:
u = parent[v] #이전 간선
# u -> v 간선 중 최솟값 찾기
path_flow = min(path_flow, u.get_adjacent_nodes()[v])
v = u
#병목을 찾으면 용량만큼 전체 흐름(max_flow)에 추가
max_flow += path_flow
#잔여 그래프 업데이트
v = sink_node
while v != source_node:
u = parent[v]
u.get_adjacent_nodes()[v] -= path_flow
v.get_adjacent_nodes()[u] += path_flow
v = u
return max_flow
if __name__ == '__main__':
#Directed graph
first_graph = {
'S': {'A': 10, 'C': 10},
'A': {'B': 4, 'C': 2, 'D': 8},
'B': {'T': 10},
'C': {'D': 9},
'D': {'B': 6, 'T': 10},
'T': {}
}
print(edmonds_karp(first_graph, 'S', 'T'))
위의 문제의 답으로 최대 우량이 19가 나옵니다.
그러면 모든 그래프 순회에서 Edmons-Karp 알고리즘이 답인가?
아닙니다, 경로가 음수인 경우 적용을 못하는 단점이 있습니다. 경로의 가중치가 음수인 경우 "보상"이라고 보면 될 것입니다. 애초에 애드몬드 카프 알고리즘이 적용되는 문제는 각 경로가 다룰 수 있는 흐름의 최대 용량을 확인하는 것이기 때문에 음수의 경로를 가정하지 않습니다.
만약 방대한 네트워크 환경에서 속도가 중요한 경우의 경로 탐색은 Dinic's algorithm 이라고 대안점이 있습니다. 해당 알고리즘은 다음에 다루도록 하겠습니다.
'알고리즘' 카테고리의 다른 글
| Knapsack (Greedy, 0/1) 알고리즘 정리 (3) | 2025.08.29 |
|---|---|
| 최소 연결 그래프 - Minimum Spanning Tree (MST) (1) | 2025.08.19 |
| 음수를 가진 최적경로 탐색 - 벨만포드(Bellman ford) 알고리즘 정리 (5) | 2025.08.17 |
| 💡 개발자를 위한 꿀팁! 효율적인 데이터 관리의 핵심: Fenwick Tree (Binary Indexed Tree) 완벽 해부! (2) | 2025.06.14 |
| Topological Sort란? Kahn's Algorithm으로 DAG 정렬 쉽게 이해하기 (0) | 2025.06.01 |