네트워크 플로우 — 최대 유량 문제의 원리
서울에서 부산까지 고속도로 네트워크를 통해 한 시간에 최대 몇 대의 차량을 보낼 수 있을까요? 이것이 네트워크 플로우 문제입니다.
네트워크 플로우란
네트워크 플로우(Network Flow)는 소스(Source)에서 싱크(Sink)까지 보낼 수 있는 최대 유량 을 구하는 문제입니다.
기본 용어
- 소스(S): 유량이 시작되는 노드
- ** 싱크(T)**: 유량이 도착하는 노드
- ** 용량(Capacity)**: 각 간선이 처리할 수 있는 최대 유량
- ** 유량(Flow)**: 실제로 흐르는 양
- ** 잔여 용량(Residual Capacity)**: 용량 - 유량
유량의 조건
- ** 용량 제한 **: 각 간선의 유량 ≤ 용량
- ** 유량 보존 **: 소스와 싱크를 제외한 모든 노드에서 들어오는 유량 = 나가는 유량
- ** 반대칭 **: f(u,v) = -f(v,u)
왜 필요한가
- 네트워크 대역폭 계산
- 이분 매칭 (작업 배정)
- 최소 컷 (네트워크 취약점)
- 서버 용량 설계
Ford-Fulkerson 방법
핵심 아이디어
- 소스에서 싱크까지의 ** 증가 경로(Augmenting Path)**를 찾는다
- 그 경로를 따라 가능한 최대 유량을 보낸다
- ** 잔여 그래프(Residual Graph)**를 갱신한다
- 더 이상 증가 경로가 없을 때까지 반복한다
잔여 그래프
유량을 보낸 후 남은 용량을 나타내는 그래프입니다. 핵심은 ** 역방향 간선 **입니다.
원래: A →(10)→ B (용량 10)
유량 6을 보낸 후:
순방향: A →(4)→ B (잔여 용량 = 10-6 = 4)
역방향: B →(6)→ A (되돌릴 수 있는 유량 = 6)
역방향 간선은 "이미 보낸 유량을 취소"하는 것을 허용합니다. 이를 통해 유량을 재분배하여 전체 최대 유량을 달성합니다.
왜 역방향 간선이 필요한가
S →(1)→ A →(1)→ T
S →(1)→ B →(1)→ T
A →(1)→ B
경로 1: S→A→B→T (유량 1)
이제 S→A는 꽉 참. 하지만 S→B→... 도 가야 함.
역방향 간선 B→A를 통해:
경로 2: S→B→A→T (유량 1, B→A는 역방향으로 A→B의 유량 취소)
결과: 총 유량 2 (S→A→T: 1, S→B→T: 1)
Edmonds-Karp 알고리즘
Ford-Fulkerson에서 증가 경로를 BFS 로 찾는 구체적인 알고리즘입니다.
public class EdmondsKarp {
static int[][] capacity;
static int[][] flow;
static int n;
public static int maxFlow(int source, int sink) {
int totalFlow = 0;
while (true) {
// BFS로 증가 경로 찾기
int[] parent = bfs(source, sink);
if (parent == null) break; // 증가 경로 없음
// 경로를 따라 보낼 수 있는 최대 유량 계산
int pathFlow = Integer.MAX_VALUE;
for (int v = sink; v != source; v = parent[v]) {
int u = parent[v];
pathFlow = Math.min(pathFlow,
capacity[u][v] - flow[u][v]);
}
// 유량 갱신
for (int v = sink; v != source; v = parent[v]) {
int u = parent[v];
flow[u][v] += pathFlow;
flow[v][u] -= pathFlow; // 역방향
}
totalFlow += pathFlow;
}
return totalFlow;
}
private static int[] bfs(int source, int sink) {
int[] parent = new int[n];
Arrays.fill(parent, -1);
parent[source] = source;
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v = 0; v < n; v++) {
// 잔여 용량이 있고 아직 방문하지 않은 노드
if (parent[v] == -1 && capacity[u][v] - flow[u][v] > 0) {
parent[v] = u;
if (v == sink) return parent;
queue.offer(v);
}
}
}
return null; // 싱크에 도달 불가
}
}
시간 복잡도
- Ford-Fulkerson (DFS): O(E × maxFlow) — 유량이 큰 경우 비효율적
- Edmonds-Karp (BFS): O(VE²) — 항상 다항 시간 보장
최대 유량-최소 컷 정리
** 최대 유량 = 최소 컷의 용량**
** 컷(Cut)**: 소스와 싱크를 분리하는 간선 집합 ** 최소 컷 **: 용량의 합이 최소인 컷
S →(3)→ A →(2)→ T
S →(2)→ B →(3)→ T
최대 유량 = 5 (S→A→T: 2, S→B→T: 2, S→A→... 추가 1)
아니, 재계산:
S→A: 3, A→T: 2 → 병목 2
S→B: 2, B→T: 3 → 병목 2
최대 유량 = 4?
정확한 계산은 간선 구조에 따라 다르지만,
핵심: 최소 컷 = 네트워크에서 가장 취약한 지점의 총 용량
최소 컷 구하기
최대 유량 알고리즘 실행 후, 잔여 그래프에서 소스에서 도달 가능한 노드 집합 S와 나머지 T를 나누면 최소 컷입니다.
// 최소 컷: 잔여 그래프에서 소스에서 BFS 도달 가능한 노드 집합
public static Set<Integer> minCut(int source) {
Set<Integer> reachable = new HashSet<>();
Queue<Integer> queue = new LinkedList<>();
queue.offer(source);
reachable.add(source);
while (!queue.isEmpty()) {
int u = queue.poll();
for (int v = 0; v < n; v++) {
if (!reachable.contains(v) && capacity[u][v] - flow[u][v] > 0) {
reachable.add(v);
queue.offer(v);
}
}
}
return reachable;
// 최소 컷 간선: reachable에서 !reachable로 가는 포화된 간선
}
이분 매칭
이분 그래프에서 최대 매칭을 네트워크 플로우로 해결할 수 있습니다.
왼쪽 그룹: 사람 {A, B, C}
오른쪽 그룹: 업무 {1, 2, 3}
간선: 수행 가능한 조합
네트워크 구성:
S → A(1), S → B(1), S → C(1) // 소스에서 왼쪽으로 (용량 1)
A → 1(1), A → 2(1) // 원래 간선 (용량 1)
B → 2(1), B → 3(1)
C → 1(1), C → 3(1)
1 → T(1), 2 → T(1), 3 → T(1) // 오른쪽에서 싱크로 (용량 1)
최대 유량 = 최대 매칭
Hungarian 알고리즘
이분 매칭에 특화된 알고리즘으로, Hopcroft-Karp 알고리즘은 O(E√V)에 최대 매칭을 구합니다.
// 간단한 이분 매칭 (DFS 기반)
static int[] match; // match[v] = v와 매칭된 왼쪽 노드 (-1이면 미매칭)
public static int maxMatching(List<List<Integer>> adj, int leftSize, int rightSize) {
match = new int[rightSize];
Arrays.fill(match, -1);
int result = 0;
for (int u = 0; u < leftSize; u++) {
boolean[] visited = new boolean[rightSize];
if (dfsMatch(adj, u, visited)) {
result++;
}
}
return result;
}
private static boolean dfsMatch(List<List<Integer>> adj, int u, boolean[] visited) {
for (int v : adj.get(u)) {
if (!visited[v]) {
visited[v] = true;
// v가 미매칭이거나, v의 현재 매칭 상대를 다른 곳에 배정할 수 있으면
if (match[v] == -1 || dfsMatch(adj, match[v], visited)) {
match[v] = u;
return true;
}
}
}
return false;
}
Dinic 알고리즘
Edmonds-Karp보다 빠른 O(V²E) 알고리즘입니다. BFS로 레벨 그래프를 구성하고, DFS로 차단 유량(blocking flow)을 찾습니다.
// Dinic의 핵심 구조
// 1. BFS로 레벨 그래프 구성 (각 노드의 거리 레벨)
// 2. DFS로 차단 유량 찾기 (레벨이 증가하는 방향으로만 탐색)
// 3. 1-2를 반복하여 더 이상 증가 경로가 없을 때까지
단위 용량 그래프(모든 간선 용량 1)에서는 O(E√V)로 더 빠릅니다. 이분 매칭에 적합합니다.
백엔드/실무에서의 연결
서버 용량 설계
서비스 아키텍처를 네트워크 플로우로 모델링하면 시스템의 ** 최대 처리량(throughput)**을 계산할 수 있습니다.
클라이언트 → 로드밸런서(1000rps) → 서버A(300rps)
→ 서버B(500rps)
→ 서버C(400rps) → DB(800rps)
최대 처리량 = min(1000, 300+500+400, 800) = 800rps
→ DB가 병목 (최소 컷)
네트워크 대역폭
인터넷 백본 네트워크에서 두 지점 간 최대 전송 속도를 계산하는 것은 최대 유량 문제입니다.
CDN 트래픽 라우팅
CDN에서 사용자에게 콘텐츠를 전달하는 최적 경로를 결정할 때, 각 링크의 대역폭을 용량으로 모델링합니다.
프로젝트 할당
개발자를 프로젝트에 배정하는 문제는 이분 매칭입니다. 각 개발자가 수행 가능한 프로젝트 목록이 주어질 때, 최대한 많은 프로젝트를 수행할 수 있도록 배정합니다.
주의할 점
역방향 간선을 추가하지 않는 실수
Ford-Fulkerson에서 증가 경로를 통해 유량을 보낼 때, ** 역방향 간선에 유량을 더해야** 합니다. 역방향 간선이 없으면 유량을 되돌릴 수 없어 최대 유량을 찾지 못합니다.
BFS 대신 DFS를 사용하여 시간 초과 (Edmonds-Karp)
DFS 기반 Ford-Fulkerson은 비합리적인 경로를 반복 선택하여 수렴이 느릴 수 있습니다. BFS를 사용하는 Edmonds-Karp는 O(VE^2)을 보장합니다.
정리
| 알고리즘 | 시간 복잡도 | 특징 |
|---|---|---|
| Ford-Fulkerson | O(E × maxFlow) | 유량이 작을 때 적합 |
| Edmonds-Karp | O(VE²) | BFS 기반, 항상 다항 시간 |
| Dinic | O(V²E) | 레벨 그래프 + 차단 유량 |
| Hopcroft-Karp | O(E√V) | 이분 매칭 특화 |
기억할 포인트:
- 네트워크 플로우의 핵심은 ** 잔여 그래프와 역방향 간선 **입니다.
- ** 최대 유량 = 최소 컷 **은 네트워크의 병목을 찾는 강력한 도구입니다.
- 이분 매칭, 작업 배정, 서버 용량 설계 등 다양한 문제가 네트워크 플로우로 모델링됩니다.
- 실무에서 시스템의 처리량 병목을 분석할 때, 최소 컷 개념이 직접적으로 활용됩니다.