Grafos Bipartidos e Emparelhamento
1. O Problema do Casamento Estável
Grafos Bipartidos modelam relacionamentos entre duas classes distintas de entidades: - Candidatos e Vagas de Emprego. - Alunos e Projetos. - Homens e Mulheres (no problema clássico do casamento).
Definição: Um grafo é Bipartido se podemos dividir os vértices em dois conjuntos \(U\) e \(V\) tal que todas as arestas conectam alguém de \(U\) a alguém de \(V\).
2. Emparelhamento Máximo (Maximum Bipartite Matching)
Um Emparelhamento (Matching) é um conjunto de arestas onde nenhum vértice aparece mais de uma vez. Objetivo: Encontrar o maior número possível de pares.
Exemplo: Temos 3 programadores e 3 vagas. - Dev A sabe Java e C++. - Dev B sabe Python. - Dev C sabe Java. - Vaga 1: Java. - Vaga 2: Python. - Vaga 3: C++.
Queremos alocar o maior número de DEVs possível.
3. Redução para Fluxo Máximo
Podemos resolver isso criando um “Super-Source” (\(S\)) e um “Super-Sink” (\(T\)). 1. Crie arestas de \(S\) para todos os nós de \(U\) com capacidade 1. 2. Crie arestas de todos os nós de \(V\) para \(T\) com capacidade 1. 3. Mantenha as arestas originais (\(U \to V\)) com capacidade 1 (ou infinita). 4. O Fluxo Máximo de \(S\) para \(T\) será igual ao tamanho do Emparelhamento Máximo.
4. Algoritmo de Kuhn (Caminhos Aumentantes)
Embora possamos usar Dinic ou Edmonds-Karp, o algoritmo de Kuhn (baseado em DFS) é muito mais simples para codar em competições.
A ideia é: para cada vértice de \(U\), tentamos encontrar um par livre em \(V\). Se o par já estiver ocupado, tentamos gentilmente pedir para que o ocupante atual mude para outro par, liberando a vaga (“Caminho Aumentante”).
Implementação Java
import java.util.*;
public class BipartiteMatching {
int n, m; // n = tamanho de U, m = tamanho de V
List<List<Integer>> adj;
int[] pairV; // pairV[v] armazena quem de U está casado com v
boolean[] visited; // Para evitar ciclos na DFS
public BipartiteMatching(int n, int m) {
this.n = n;
this.m = m;
adj = new ArrayList<>(n);
for(int i=0; i<n; i++) adj.add(new ArrayList<>());
pairV = new int[m];
Arrays.fill(pairV, -1);
visited = new boolean[n];
}
public void addEdge(int u, int v) {
adj.get(u).add(v); // u em [0..n-1], v em [0..m-1]
}
// Tenta encontrar um par para u
boolean dfs(int u) {
visited[u] = true;
for (int v : adj.get(u)) {
// Se v está livre OU se quem está com v pode mudar
if (pairV[v] < 0 || (!visited[pairV[v]] && dfs(pairV[v]))) {
pairV[v] = u; // Match!
return true;
}
}
return false;
}
// Retorna o tamanho do matching
public int maxMatching() {
int result = 0;
for (int u = 0; u < n; u++) {
Arrays.fill(visited, false); // Reseta visitados a cada tentativa
if (dfs(u)) {
result++;
}
}
return result;
}
public static void main(String[] args) {
BipartiteMatching bm = new BipartiteMatching(3, 3);
// Exemplo: Dev 0 sabe Vaga 0 e 2
bm.addEdge(0, 0);
bm.addEdge(0, 2);
// Dev 1 sabe Vaga 1
bm.addEdge(1, 1);
// Dev 2 sabe Vaga 0
bm.addEdge(2, 0);
System.out.println("Máximo de Vagas Preenchidas: " + bm.maxMatching());
}
}Complexidade
- No pior caso: \(O(E \times V)\).
- Na maioria dos casos práticos, é muito mais rápido, comportando-se quase como \(O(E)\).
- Para grafos muito densos, o algoritmo de Hopcroft-Karp atinge \(O(E \sqrt{V})\), mas é bem mais complexo de implementar.
5. Aplicações (Cobertura Mínima de Vértices)
Teorema de König: Em qualquer grafo bipartido, o número de arestas num emparelhamento máximo é igual ao número de vértices numa cobertura mínima de vértices (Minimum Vertex Cover). Isso resolve problemas como “Onde colocar câmeras de segurança para cobrir todas as ruas de Manhattan?” (Manhattan é uma grid, que é bipartida).