32. 네트워크 유량

32.1 도입

image-20240425165559095.png

  • 경로 이외에도 그래프의 용량이라는 중요한 개념이 있다.
  • 네트워크에서 전송받을 자료의 양이 많을 때 우리가 관심 있어 하는 부분은
    • 서버의 패킷이 몇 밀리초 망네 도착하느냐가 아니라, 1초에 몇 MB의 자료를 전송받을 수 있느냐 이다.
    • 네트워크를 구성하는 케이블들에는 일정한 대역폭이 있다.
    • 최단 경로로 1초에 1MB를 전송받는 것 보다,
    • 패킷을 여러 경로로 나누어 보내 그중 일부가 좀더 먼길을 돌아오더라도 초당 10MB 전송받는 게 이득이다.
  • 네트워크 유량(network flow) 문제
    • 각 간선이 용량을 갖는 그래프에서 두 정점 사이에 얼마나 많은 ‘흐름, 유량, flow’ 을 보낼 수 있는지를 계산하는 문제

32.1.1 유량 네트워크

  • 유량 네트워크(flow network): 각 간선에 용량(capacity) 이라는 추가 속성이 존재하는 방향 그래프
    • 각 간선은 유량을 흘려보낼 수 있는 파이프 역할
  • c(u, v): u -> v 가는 간선의 용량
  • f(u, v): u -> v 로 가는 실제 흐르는 유량
  • 네트워크 유량은 3가지 속성을 만족해야 한다.
    1. 용량 제한 속성: f(u, v) <= c(u, v)
    2. 유량의 대칭성: f(u, v) = -f(v, u)
    3. 유량의 보존: 모든 v에 대해, f(u, v)의 총합은 0 이다.
      • 들어오는 유량과 나가는 유량의 양은 정확히 같아야 한다.
  • 특수한 두 정점
    • 소스, source: start node
    • 싱크, sink: end node
    • 이 두 정점에서 유량의 보전 속성은 성립 않함
    • 소스에서 나온 모든 유량은 결국 싱크로 들어 가게 된다.

32.2 포드-풀커슨 알고리즘

image-20240425165612504.png

  • 유량 네트워크의 모든 간선의 유량을 0으로 두고 시작해, 소스에서 싱크로 유량을 더 보낼 수 있는 경로를 찾아 유량을 보내기를 반복
  • 증가 경로(augmenting path): 유량을 보내는 경로
    • 증가 경로를 통해 흘려 보낼 수 있는 유량의 최대량은, 포함된 간선의 잔여 용량가장 작은 값이다
  • 잔여 용량(residual capacity): 간선의 용량과 유량의 차이
    • r(u, v) = c(u, v) - f(u, v)

image-20240425165628485.png

  • 포드-풀커슨 알고리즘은
    • 증가 경로 가 더 이상 존재하지 않을 때까지 그래프 탐색 알고리즘으로 증가 경로를 찾고,
    • 보낼 수 있는 최대 유량을 해당 경로를 따라 보내는 작업을 반복

32.2.1 알고리즘의 구현 (python)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from collections import deque

# graph는 그래프. 간선의 비용 정보
# s는 시작 노드, t는 도착 노드, n은 노드의 개수
def fordFulkerson(graph, start, target, n):
    remain = [[0] * n for _ in range(n)]
    for i in range(n):
        for j in range(n):
            remain[i][j] = graph[i][j]

    maxFlow = 0  # 최대 유량
    while True:  # bfs
        visited = [False] * n
        visited[start] = True
        queue = deque([start])
        path = [-1] * n  # 경로. 부모 노드를 저장. 거슬러 올라가면 경로 구할 수 있음
        path[start] = -1  # 처음 노드는 부모 노드가 없음

        while queue:
            a = queue.popleft()
            for b in range(n):
                if not visited[b] and remain[a][b] > 0:  # 방문하지 않았고 갈 수 있다면
                    queue.append(b)
                    path[b] = a  # 부모 노드 저장
                    visited[b] = True

        if not visited[target]:  # 목적지로 가는 경로가 없다면
            break  # bfs 끝. 경로는 도착 노드인 t 부터 path를 보면서 부모 노드를 거슬러 올라가면 됨

        b = target  # 경로를 거슬러 올라가기 위한 변수
        min_remain = float('inf')  # 경로 간선들 중 가장 작은 용량을 구하기 위한 변수
        while b != start:  # 처음 노드에 도착할 때까지
            a = path[b]
            min_remain = min(min_remain, remain[a][b])
            b = a

        b = target
        while b != start:
            a = path[b]
            remain[a][b] -= min_remain
            remain[b][a] += min_remain
            b = a
        maxFlow += min_remain

    return maxFlow

32.8 이분 매칭

image-20240425165650492.png

  • 끝점을 공유하지 않는 간선의 집합를 그래프의 matching(매칭) 이라 한다.
  • 가장 큰 매칭을 찾아내는 문제를 최대 매칭 문제 라고 한다.
  • 프로그래밍 대회에서는 일반 그래프가 아니라, 이분 그래프 에서 최대 매칭을 찾는 문제가 나온다.

image-20240425164737721.png

32.8.1 이분 매칭의 중요성

  • 정점을 두 그룹으로 나눠서 모든 간선이 서로 다른 그룹의 정점들을 연결하도록 할 수 있는 그래프들을 이분 그래프 라고 한다.
  • 이분 그래프는 두 집합 간의 대응 관계를 표현하기 위해 흔히 사용된다.
  • 이분 매칭(bipartite matching): 이분 그래프에서 최대 매칭을 찾는 문제

image-20240425165701478.png

32.8.2 네트워크 유량으로 이분 매칭 풀기

  • 싱크와 소스를 이분 그래프에서 추가하면 네트워크 유량으로 이분 매칭을 쉽게 풀 수 있다.
  • 모든 간선의 용량은 1로 한다.
  • 최대 유량을 구한 뒤 유량이 흐르는 간선들을 모으면 최대 매칭이 된다.
    • 네트워크의 최대 유량: O(V)
    • BFS는 O(V)번만 하게 된다.
    • 포드-풀커슨 알고리즘의 시간 복잡도는 O(VE)

image-20240425165710250.png

32.8.3 이분 매칭 구현하기 (python)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from collections import deque

def odd_even(n):
    return 1 if n % 2 == 0 else -1

def bfs(current):
    level = 0
    queue = deque([(current, level + 1)])
    vi[current] = odd_even(level)  # 깊이에 따라, 서로 다른 색상을 칠해준다. (인접한 정점 끼리는 같은 색상)

    while queue:
        now = queue.popleft()
        curNode, level = now

        for nxt in graph[curNode]:
            if not vi[nxt]:  # 방문한 적이 없다면 두 색상 중 하나로 칠해준다.
                queue.append((nxt, level + 1))
                vi[nxt] = odd_even(level)
            elif vi[nxt] == odd_even(level):  # 방문한 적이 있고, 현재 칠하려는 색상과 같으면 pass
                continue
            elif vi[nxt] != odd_even(level):  # 방문한 적이 있고, 현재 칠하려는 색상과 다르다면, 인접리스트가 아니다.
                return False

    return True

result = []
vertex, edge = map(int, input().split())
graph = [[] for _ in range(vertex)]
vi = [0] * (vertex)
for _ in range(edge):
    u, v = map(int, input().split())
    graph[u].append(v)
    graph[v].append(u)

graph_list = []  # 그래프의 정점이 서로 이어지지 않았을 수 있음 (두개의 그래프 존재 가능성)
for i in range(vertex):
    if not vi[i]:
        graph_list.append(bfs(i))

print(*result)

Reference