본문으로 건너뛰기
김신건의 로그

이분 매칭 (Bipartite Matching)

· 수정 · 📖 약 4분 · 1,438자/단어 #algorithm #graph #bipartite #matching #hopcroft-karp #flow
bipartite matching, 이분 매칭, 최대 매칭, maximum matching, Hopcroft-Karp, augmenting path

정의

이분 매칭 (Bipartite Matching)이분 그래프 G = (L ∪ R, E) 에서 간선 부분집합 M ⊆ E 을 선택하되, M 의 어떤 두 간선도 정점을 공유하지 않는 (disjoint endpoints) 최대 크기 M.

수학적: M 이 매칭 ↔ 모든 v ∈ V 에 대해 M 의 간선이 v 를 많아야 한 번 포함.

최대 매칭 (maximum matching): 가능한 M 중 |M| 이 최대.

알고리즘:

  • DFS augmenting path (기본): O(VE).
  • Hopcroft-Karp: O(E√V), 레벨 그래프 + multi-path BFS.
  • 최대 유량: 이분 매칭 = max-flow (L→R 용량 1). Dinic O(V^2 E) 또는 Push-Relabel O(V^3).

문제 상황과 동기

PS 전형: 직원-작업, 학생-프로젝트, 축사-소 배정.

  • naive: 모든 간선 부분집합 (2^E) 순회 → 불가능.
  • greedy: 임의 순서로 간선 추가 → 최대 보장 없음.
  • augmenting path (Ford-Fulkerson 아이디어): 매칭을 키울 수 있는 경로를 DFS 로 찾기. O(VE).
  • Hopcroft-Karp: BFS 로 여러 경로 한꺼번에 증강. O(E√V).

핵심 통찰: 증가 경로 (augmenting path) 존재 ↔ 매칭이 최대가 아님. Berge’s lemma.

실무: 추천 시스템, 작업 스케줄링, 네트워크 최적화.

시각화

핵심 아이디어

augmenting path

  • 경로 P: 시작/끝 모두 미매칭 정점 (unmatched), 간선이 매칭/비매칭 교대.
  • P 를 발견하면 매칭 간선 반전 (M ← M △ P) → |M| 증가 1.
  • Berge’s lemma: augmenting path 없으면 M 이 최대 매칭.

DFS augmenting path (기본)

L 의 미매칭 정점에서 DFS:

  • 비매칭 간선 (u, v) 로 R 진입.
  • v 가 미매칭이면 성공 (경로 완성).
  • v 가 이미 매칭되어 있으면 (v, w) 로 L 로 돌아가 재귀.
  • 최악 O(VE): V 번의 DFS, 각 O(E).

Hopcroft-Karp (O(E√V))

  1. BFS 레벨 그래프 구축: 미매칭 L 에서 시작, 매칭/비매칭 교대로 레벨.
  2. DFS 로 여러 augmenting path 동시 증강.
  3. 레벨 그래프 갱신 반복.
  4. √V phase 내에 완료 (증명: 각 phase 마다 최단 augmenting path 길이 증가).

König 정리

이분 그래프에서:

|최대 매칭| = |최소 정점 덮개|.

  • 정점 덮개 (vertex cover): 모든 간선이 적어도 한 끝점을 포함하는 정점 집합.
  • 응용: 간선 선택 문제 = 정점 선택 문제 dual.

증명: 최대 매칭 M 에서 M-alternating path 로 덮개 구성.

Hall’s marriage theorem

L 의 모든 부분집합 S 에 대해 |N(S)| ≥ |S| (N(S) = S 의 이웃 집합 in R) ↔ L 의 완전 매칭 존재.

알고리즘

DFS augmenting path (기본)

bipartite_matching_dfs(G):
    match_L[] = -1, match_R[] = -1
    for u in L:
        if match_L[u] < 0:
            visited[] = false
            dfs_augment(u)
    return count(match_L >= 0)

dfs_augment(u):
    for v in adj[u]:
        if visited[v]: continue
        visited[v] = true
        if match_R[v] < 0 or dfs_augment(match_R[v]):
            match_L[u] = v
            match_R[v] = u
            return true
    return false

Hopcroft-Karp (O(E√V))

hopcroft_karp(G):
    match_L[] = -1, match_R[] = -1
    while bfs_level() < ∞:
        for u in L:
            if match_L[u] < 0:
                dfs_augment_hk(u)
    return count(match_L >= 0)

bfs_level():
    queue = all unmatched u in L
    level[u] = 0 for u in queue
    min_level = ∞
    while queue not empty:
        u = queue.pop()
        for v in adj[u]:
            if match_R[v] < 0:
                min_level = min(min_level, level[u] + 1)
            elif level[match_R[v]] < 0:
                level[match_R[v]] = level[u] + 2
                queue.push(match_R[v])
    return min_level

dfs_augment_hk(u):
    for v in adj[u]:
        if match_R[v] < 0:
            match_L[u] = v
            match_R[v] = u
            return true
        elif level[match_R[v]] == level[u] + 2 and dfs_augment_hk(match_R[v]):
            match_L[u] = v
            match_R[v] = u
            return true
    level[u] = -1  # 이번 phase 실패
    return false

구현

// 이분 매칭 DFS augmenting path O(VE)
#include <bits/stdc++.h>
using namespace std;

int n, m, e;
vector<int> adj[1005];  // L[0..n-1] → R
int match_L[1005], match_R[1005];
bool visited[1005];

bool dfs_augment(int u) {
  for (int v : adj[u]) {
      if (visited[v]) continue;
      visited[v] = true;
      if (match_R[v] < 0 || dfs_augment(match_R[v])) {
          match_L[u] = v;
          match_R[v] = u;
          return true;
      }
  }
  return false;
}

int main() {
  cin >> n >> m >> e;  // L 크기 n, R 크기 m, 간선 e
  fill(match_L, match_L + n, -1);
  fill(match_R, match_R + m, -1);
  for (int i = 0; i < e; i++) {
      int u, v; cin >> u >> v; u--; v--;  // 0-indexed
      adj[u].push_back(v);
  }
  int cnt = 0;
  for (int u = 0; u < n; u++) {
      if (match_L[u] < 0) {
          fill(visited, visited + m, false);
          if (dfs_augment(u)) cnt++;
      }
  }
  cout << "최대 매칭: " << cnt << "\n";
  for (int u = 0; u < n; u++) {
      if (match_L[u] >= 0) {
          cout << u + 1 << " - " << match_L[u] + 1 << "\n";
      }
  }
}
stdin
3 3 5
1 1
1 2
2 2
2 3
3 1
결과
최대 매칭: 3
1 - 2
2 - 3
3 - 1

복잡도

항목
DFS augmenting pathO(VE) 시간, O(V+E) 공간
Hopcroft-KarpO(E√V) 시간, O(V+E) 공간
최대 유량 (Dinic)O(V^2 E) 시간 (이분 그래프에서 O(E√V))
전처리없음 (그래프 입력만)

König 정리와 최소 정점 덮개

이분 그래프에서:

  • 최대 매칭 M: 간선 부분집합, disjoint endpoints.
  • 최소 정점 덮개 C: 모든 간선이 C 의 정점 하나 이상 포함.

König 정리: |M| = |C|.

구성: 최대 매칭 M 에서 시작, M-alternating path 로 도달 가능한 정점 집합 Z 구하기.

  • C = (L \ Z) ∪ (R ∩ Z).
  • 증명: C 가 모든 간선 커버, |C| = |M|.

응용: “모든 간선을 덮는 최소 정점 개수” = “최대 매칭 크기” (이분 그래프 한정).

변형

  1. 가중치 이분 매칭 (Hungarian algorithm): 간선 가중치 최대/최소화. O(V^3).
  2. 완전 매칭 (perfect matching): |M| = min(|L|, |R|). Hall’s theorem.
  3. 최소 간선 덮개 (edge cover): 모든 정점을 덮는 최소 간선 집합. Gallai’s theorem: |edge cover| + |matching| = V.
  4. 최대 독립 집합 (maximum independent set): 이분 그래프에서 V - |최소 정점 덮개|.

함정

1. DFS visited 초기화

매 augmenting path 시도마다 visited[] 초기화 필수. 전역 visited 재사용하면 경로 놓침.

2. 미매칭 정점 체크

L 의 미매칭 정점에서만 DFS 시작. 이미 매칭된 정점에서 시작하면 무의미.

3. 양방향 간선 입력

이분 그래프는 L → R 단방향. 무방향 입력 시 L, R 분리 필요.

4. Hopcroft-Karp 레벨 그래프

레벨 초기화 타이밍 주의. 매 phase 마다 레벨 재계산.

BOJ 연습 문제

번호제목정답률링크
BOJ 11375열혈강호-kokoa-lab
BOJ 11376열혈강호 2 (2배 용량)-kokoa-lab
BOJ 2188축사 배정-kokoa-lab
BOJ 1298노트북의 주인을 찾아서-kokoa-lab
BOJ 9577토렌트 (시간 슬롯 + 이분 매칭)-kokoa-lab

참고

이 글의 용어 (3개)
이분 그래프 (Bipartite Graph)algorithm
정의 이분 그래프 (Bipartite Graph) 는 정점 집합 V 를 두 개의 독립 집합 L, R 로 분할할 수 있고, 모든 간선이 L 과 R 을 잇는 그래프. 같은 집합 내 …
Maximum Flow: Ford-Fulkerson, Edmonds-Karp, Dinicalgorithm
정의 Maximum Flow (최대 유량) 문제는 방향 그래프 $G = (V, E)$ 와 소스 $s$, 싱크 $t$ 가 주어졌을 때, 각 간선의 용량 제약 아래에서 $s$ 에서 …
Minimum Vertex Cover: 최소 정점 덮개algorithm
정의 Vertex Cover 는 그래프의 모든 간선이 최소 한 정점을 포함하도록 하는 정점 집합. 최소 크기의 vertex cover 를 찾는 문제. - 일반 그래프: NP-ha…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기