포드-풀커슨 알고리즘

포드-풀커슨 알고리즘(Ford-Fulkerson algorithm) 은 그래프의 최대 유량을 찾는 알고리즘이다. 유량이 0인 상태에서 알고리즘을 시작하고, 단계마다 소스에서 싱크로 가는 경로 중 유량을 늘릴 수 있는 경로(증강 경로, augmenting path)를 찾는다. 더는 유량을 늘릴 수 없을 때의 값이 최대 유량이 된다.

이 알고리즘에서는 간선마다 반대 방향의 간선도 추가한 형태의 특별한 그래프 표현을 사용한다. 각 간선의 가중치는 유량을 얼마나 늘릴 수 있는지를 나타낸다. 알고리즘의 시작 단계에서는 원래 간선의 가중치를 간선의 용량과 동일하게 두고, 반대 방향 간선의 가중치를 0으로 둔다. 그림 20에 예제 그래프에 대한 표현이 나와 있다.

  • 반대 간선은 이미 보낸 유량을 나중에 취소하거나 재조정하기 위해 잔여 그래프에 추가하는 가상 간선

포드-풀커슨 알고리즘은 여러 라운드로 구성되어 있다. 라운드마다 소스에서 싱크로 가는 경로 중 모든 간선의 가중치가 양수인 경로를 찾는다. 그러한 경로가 여러 가지라면 어떤 것을 선택해도 된다. 선택한 경로에 포함된 간선의 가중치 최솟값이 라면 유량을 만큼 증가시킬 수 있다. 이를 위해 경로 상의 각 간선에 대해 그 가중치를 만큼 감소시키고, 반대 방향 간선의 가중치를 만큼 증가시킨다.

  • 그림 20. 포드-풀커슨 알고리즘에서 사용하는 그래프 표현

이 알고리즘에서 사용된 아이디어는 간선의 유량이 증가하면 앞으로 추가할 수 있는 유량은 감소한다는 데 바탕을 두고 있다. 알고리즘을 진행하면서 다른 경로를 택하는 것이 더 유리한 상황임을 알게 되었다면, 역으로 반대 방향 간선을 이용하여 기존의 유량을 감소시키는 것도 가능하다. 소스에서 가중치가 양수인 간선을 이용하여 싱크로 가는 경로가 존재하는 동안 이 알고리즘을 계속 반복한다. 만일 그러한 경로가 존재하지 않는다면 최대 유량을 구한 것이며, 알고리즘은 종료한다.

그림 21에 포드-풀커슨 알고리즘을 이용하여 예제 그래프의 최대 유량을 찾는 과정이 나와 있다. 알고리즘은 네 번의 라운드로 진행된다. 첫 번째 라운드에서는 경로 을 선택한다. 이 경로상의 최소 가중치는 2이므로 유량이 2만큼 증가한다. 다음으로 경로를 세 번 더 선택한 뒤, 유량을 각각 3, 1, 1만큼 증가시킨다. 그 다음에는 가중치가 양수인 간선으로 구성된 경로가 없고, 최대 유량은 이 된다.

  • 그림 21. 포드-풀커슨 알고리즘

시간 복잡도

S ──M──> A ──M──> T
 \       │
  M      1
   \     ↓
    ───> B ──M──> T
  1. DFS가 S→A→B→T를 고르면 병목이 A→B의 1이라 유량이 +1만 증가.
  2. 이때 A→B는 0이 되고, 역방향 B→A가 1로 열린다.
  3. 다음 DFS가 S→B→A→T를 고르면 다시 병목이 1이라 +1.
  4. B→A를 사용했으므로 기존 A→B 유량이 취소되어 A→B가 다시 1로 살아난다.
  5. 이후 A→B, B→A를 번갈아 사용하면서 최대 유량이 계속 1씩 증가할 수 있다.
  6. 최대 유량이 F라면 최악에 증강을 O(F)번 수행한다.
  7. 증강 경로 탐색 한 번이 O(E)이므로 Ford-Fulkerson은 정수 용량에서 O(EF).

요약

  1. 시작점 S에서 도착점 T까지 잔여 용량이 있는 경로 를 하나 찾는다.
  2. 그 경로에서 가장 작은 잔여 용량만큼 유량을 흘린다.
  3. 사용한 만큼 정방향 용량을 줄이고, 역방향 간선의 잔여 용량을 늘린다.
  4. 더 이상 S → T 경로를 찾을 수 없을 때까지 반복한다.
  5. 경로 탐색 방식에 따라 성능이 달라지며, 정수 용량이면 보통 O(EF)로 본다.
    1. F = maxflow

cpp

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
 
using ll = long long;
 
const ll INF = 1e18;
 
struct Edge {
    int to;     // 도착점
    int rev;    // 역방향 간선 인덱스
    ll cap;     // 잔여 용량
};
 
class FordFulkerson {
private:
    int n;
    vector<vector<Edge>> graph;
 
    // DFS로 하나의 증강 경로 찾기
    ll dfs(int u, int sink, ll flow, vector<bool>& visited) {
        if (u == sink)
            return flow;
 
        visited[u] = true;
 
        for (auto& edge : graph[u]) {
            int v = edge.to;
            int rev = edge.rev;
            ll cap = edge.cap;
 
            // 잔여 용량이 없거나 이미 방문한 정점이면 무시
            if (cap <= 0 || visited[v])
                continue;
 
            // 현재 경로에서 보낼 수 있는 최대 유량
            ll amount = dfs(
                v,
                sink,
                min(flow, cap),
                visited
            );
 
            // sink까지 도달했다면 유량 갱신
            if (amount > 0) {
                // 정방향 잔여 용량 감소
                edge.cap -= amount;
 
                // 역방향 잔여 용량 증가
                graph[v][rev].cap += amount;
 
                return amount;
            }
        }
 
        return 0;
    }
 
public:
    FordFulkerson(int n)
        : n(n), graph(n) {}
 
    void add_edge(int u, int v, ll cap) {
        Edge forward = {
            v,
            (int)graph[v].size(),
            cap
        };
 
        Edge backward = {
            u,
            (int)graph[u].size(),
            0
        };
 
        graph[u].push_back(forward);
        graph[v].push_back(backward);
    }
 
    ll max_flow(int source, int sink) {
        ll total_flow = 0;
 
        while (true) {
            vector<bool> visited(n, false);
 
            // DFS로 하나의 증강 경로 탐색
            ll amount = dfs(
                source,
                sink,
                INF,
                visited
            );
 
            // 더 이상 증강 경로가 없음
            if (amount == 0)
                break;
 
            total_flow += amount;
        }
 
        return total_flow;
    }
};
 
 
int main() {
    // --------------------------------------------------
    // 테스트 그래프
    // --------------------------------------------------
 
    int n = 6;
 
    struct InputEdge {
        int u, v;
        ll cap;
    };
 
    vector<InputEdge> edges = {
        {0, 1, 12},  // 1 -> 2
        {0, 3, 11},  // 1 -> 4
        {1, 2, 6},   // 2 -> 3
        {1, 3, 3},   // 2 -> 4
        {1, 4, 5},   // 2 -> 5
        {1, 5, 9},   // 2 -> 6
        {2, 5, 8},   // 3 -> 6
        {3, 4, 9},   // 4 -> 5
        {4, 2, 3},   // 5 -> 3
        {4, 5, 4},   // 5 -> 6
    };
 
    int source = 0;
    int sink = 5;
 
    FordFulkerson ff(n);
 
    for (auto [u, v, cap] : edges) {
        ff.add_edge(u, v, cap);
    }
 
    cout << ff.max_flow(source, sink) << '\n';
}

python

import sys
 
sys.setrecursionlimit(10**6)
 
INF = 10**18
 
 
class FordFulkerson:
    def __init__(self, n):
        self.n = n
        self.graph = [[] for _ in range(n)]
 
    def add_edge(self, u, v, cap):
        # edge = [도착점, 역방향 간선 인덱스, 잔여 용량]
        forward = [v, len(self.graph[v]), cap]
        backward = [u, len(self.graph[u]), 0]
 
        self.graph[u].append(forward)
        self.graph[v].append(backward)
 
    # DFS로 하나의 증강 경로 찾기
    def dfs(self, u, sink, flow, visited):
        if u == sink:
            return flow
 
        visited[u] = True
 
        for edge in self.graph[u]:
            v, rev, cap = edge
 
            # 잔여 용량이 없거나 이미 방문한 정점이면 무시
            if cap <= 0 or visited[v]:
                continue
 
            # 현재 경로에서 보낼 수 있는 최대 유량
            amount = self.dfs(
                v,
                sink,
                min(flow, cap),
                visited
            )
 
            # sink까지 도달했다면 유량 갱신
            if amount > 0:
                # 정방향 잔여 용량 감소
                edge[2] -= amount
 
                # 역방향 잔여 용량 증가
                self.graph[v][rev][2] += amount
 
                return amount
 
        return 0
 
    def max_flow(self, source, sink):
        total_flow = 0
 
        while True:
            visited = [False] * self.n
 
            # DFS로 하나의 증강 경로 탐색
            amount = self.dfs(
                source,
                sink,
                INF,
                visited
            )
 
            # 더 이상 증강 경로가 없음
            if amount == 0:
                break
 
            total_flow += amount
 
        return total_flow
 
 
# --------------------------------------------------
# 테스트 그래프
# --------------------------------------------------
 
n = 6
 
edges = [
    (0, 1, 12),  # 1 -> 2
    (0, 3, 11),  # 1 -> 4
    (1, 2, 6),   # 2 -> 3
    (1, 3, 3),   # 2 -> 4
    (1, 4, 5),   # 2 -> 5
    (1, 5, 9),   # 2 -> 6
    (2, 5, 8),   # 3 -> 6
    (3, 4, 9),   # 4 -> 5
    (4, 2, 3),   # 5 -> 3
    (4, 5, 4),   # 5 -> 6
]
 
source = 0
sink = 5
 
ff = FordFulkerson(n)
 
for u, v, cap in edges:
    ff.add_edge(u, v, cap)
 
print(ff.max_flow(source, sink))