알고리즘 공부

손승열(Son Seungyeol)

네트워크 플로우(Network Flow)

네트워크 플로우의 개념과 에드몬드-카프 알고리즘을 통해 최대 유량 문제를 해결해봅니다.

손승열(Son Seungyeol)
네트워크 플로우(Network Flow)

🔍 네트워크 플로우(Network Flow)

네트워크 플로우는 한 노드에서 다른 노드로 흘러갈 수 있는 최대 흐름량을 구하는 알고리즘으로 이러한 알고리즘이 적용되는 문제를 최대 유량 문제라고도 합니다.

위 알고리즘에서 다루는(입력으로 들어오는) 자료구조는 유향 가중치 그래프로, 이와 함께 source 노드sink 노드 정보를 알고 있어야 합니다. 두 노드를 조금 더 살펴보면 다음과 같습니다.

  • Source 노드: 진입 간선이 없는(진입 차수 = 0) 노드

  • Sink 노드: 진출 간선이 없는(진출 차수 = 0) 노드

예를 들어, 다음과 같은 그래프의 경우

          ┌───┐   6   ┌───┐
          │ 2 │ ────→ │ 3 │
     5    └───┘       └───┘    5
       ↗    ↑           │    ↘
┌───┐       │           │      ┌───┐
│ 1 │     3 │         8 │      │ 6 │
└───┘       │           │      └───┘
       ↘    │           ↓    ↗
     4    ┌───┐       ┌───┐    2
          │ 4 │ ────→ │ 5 │
          └───┘   1   └───┘

노드 1이 source 노드가 되고 노드 6이 sink 노드가 됩니다.

최대 유량 문제 조금 더 살펴보기

최대 유량 문제에서 우리의 일은 source 노드로부터 sink 노드로까지 최대한의 유량을 흘려 보내는 것입니다.

최대 유량 문제의 그래프에서 간선들의 가중치는 해당 간선에 최대로 흐를 수 있는 용량을 나타내며, 각 중간 노드로 들어오는 유량과 나가는 유량의 크기는 같아야 합니다.

위의 예시에서는 최대 유량의 크기는 7이며 아래는 흐름의 예시입니다.

          ┌───┐  6/6  ┌───┐
          │ 2 │ ────→ │ 3 │
   3/5    └───┘       └───┘    5/5
       ↗    ↑           │    ↘
┌───┐       │           │      ┌───┐
│ 1 │   3/3 │       1/8 │      │ 6 │
└───┘       │           │      └───┘
       ↘    │           ↓    ↗
   4/4    ┌───┐       ┌───┐    2/2
          │ 4 │ ────→ │ 5 │
          └───┘  1/1  └───┘

간선마다 표시된 v/k의 의미는 용량 k의 간선에 유량이 v만큼 존재한다는 의미입니다.

위의 예시를 통해 우리는 source 노드에서 흘려보낸 유량의 총합(3 +4)과 sink 노드로 흘러들어온 유량의 총합(5 + 2)은 같다는 사실을 알 수 있습니다.

이러한 최대 유량 문제를 해결하기 위한 알고리즘들은 다음과 같습니다.

  • 포드-풀커슨 알고리즘(Ford-Fulkerson Algorithm): 해결에 DFS를 활용

  • 에드몬드-카프 알고리즘(Edmonds-Karp Algorithm): 해결에 BFS를 활용

이 글에서는 일반적으로 효율이 더 좋다고 알려진 에드몬드-카프 알고리즘에 대해 살펴보겠습니다.

에드몬드-카프 알고리즘

에드몬드-카프 알고리즘은 빈 흐름에서 시작하여 단계를 거쳐나가며 source 노드에서 sink 노드로 흐르는 유량을 증가시키는 경로를 찾아나갑니다.

이때, 유량을 더 이상 증가시킬 수 없다면 최대 유량을 발견한 것입니다.

알고리즘이 진행되면 갱신되는 그래프 간선의 가중치는 해당 간선에 추가적으로 흐를 수 있는 유량을 의미합니다.
➡️ 초기 값은 해당 간선에 흐를 수 있는 흐름의 최대 용량

에드몬드-카프 알고리즘은 입력받은 유향 가중치 그래프에 추가적으로 각각의 간선에 대한 반대 방향 간선을 추가하고 0으로 초기화합니다.
➡️ 나중에 이 역간선의 가중치 값만큼 흘려보낸 유량을 취소할 수 있음.
➡️ 간선을 통해 흘려보낸 유량의 크기를 역간선에 음의 값으로 표현하여 추후 가능한 모든 경로를 탐색하는데 활용

위의 예시와 함께 이를 도식화하면 다음과 같습니다.

                  6
          ┌───┐ ────→ ┌───┐
          │ 2 │       │ 3 │
          └───┘ ←──── └───┘    
    5 ↗↙ 0  ↑│    0     ↑│  0 ↖↘ 5
┌───┐       ││          ││      ┌───┐
│ 1 │     3 ││ 0      0 ││ 8    │ 6 │
└───┘       ││          ││      └───┘
    0 ↖↘ 4  │↓    1     │↓  2 ↗↙ 0
          ┌───┐ ────→ ┌───┐    
          │ 4 │       │ 5 │
          └───┘ ←──── └───┘
                  0

에드몬드-카프 알고리즘은 다음과 같은 과정을 반복합니다.

  1. BFS를 통해 source 노드에서 sink 노드로 이어지는 경로(경로 내 모든 간선의 잔여 용량 > 0)를 찾는다.

    1. 이러한 경로가 여러 개 존재하는 경우 아무거나 하나를 선택한다.

  2. 경로가 선택되었다면, 해당 경로를 구성하는 간선의 잔여 용량 중 최소 값 x만큼 유량이 증가한다.

    ➡️ 즉, 해당 경로의 간선들은 각각 x만큼 유량이 증가

  3. 방금 유량이 증가한 경로의 역간선(처음에 0으로 초기화한)들의 유량을 x만큼 감소시킨다.

  4. 방금의 경로는 용량이 가득 찬 간선(가중치 = 0)인 간선이 발생했으므로, 1로 돌아가 이러한 경로가 없을 때까지 과정을 반복한다.

⌨️ C++ 구현 코드

cpp
#include <bits/stdc++.h>
#define MAXN 100
#define INF 1000000000

using namespace std;

int N, M;
int source, sink;
int capacity[MAXN][MAXN], flow[MAXN][MAXN], before[MAXN];
vector< vector<int> > edges(MAXN);

int find_max_flow(){
    int ans = 0, min_capacity;
    queue<int> bfs;

    while(1){
        bfs.push(source);
        for(int i=1;i<=N;i++) before[i] = -1;

        while(!bfs.empty()){
            int node = bfs.front();
            for(int i=0;i<(int)edges[node].size();i++){
                int next = edges[node][i];
                if(capacity[node][next] - flow[node][next] > 0 && before[next] == -1){
                    bfs.push(next);
                    before[next] = node;
                    if(next == sink) break;
                }
            }
            bfs.pop();
        }
        while(bfs.size()) bfs.pop();
        if(before[sink] == -1) break;

        min_capacity = INF;
        for(int i=sink;i!=source;i=before[i]) { 
            min_capacity = min(min_capacity, capacity[before[i]][i] - flow[before[i]][i]);
        }
        for(int i=sink;i!=source;i=before[i]){
            flow[before[i]][i] += min_capacity;
            flow[i][before[i]] -= min_capacity;
        }
        ans += min_capacity;
    }

    return ans;
}

int main()
{
    int a, b, c;

    scanf("%d %d", &N, &M);
    for(int i=0;i<M;i++){
        scanf("%d %d %d", &a, &b, &c);

        edges[a].push_back(b);
        edges[b].push_back(a);
        capacity[a][b] = c;
    }
    scanf("%d %d", &source, &sink);

    printf("%d\n", find_max_flow());
}

📚 참고 문헌

Competitive Programmer’s Handbook ― Chapter 20: Flows and cuts


Made with React, Gatsby and DatoCMS by @smastrom

Contribute or star on GitHub

© 2022-2023 손승열 for 🅒🅞🅝🅣🅔🅝🅣🅢

모두 좋은 하루 보내세요! 😊