Skip to Content

Flujo máximo

HechoFuenteNombreDificultadTagsSolución
CSESDownload SpeedFácilMax FlowSolución

Solución

Explicación

El algoritmo de Edmonds-Karp  usa un enfoque voraz para resolver el problema de flujo máximo.

La velocidad de descarga (el flujo) se puede mejorar mientras podamos encontrar un camino aumentante (augmenting path) de capacidad no negativa desde la fuente (el servidor) hasta el sumidero (la computadora de Kotivalo). Usamos BFS para hallar ese camino.

Luego actualizamos los pesos de las aristas a lo largo de este camino aumentante. Cada arista uu, vv pierde un peso equivalente a su capacidad en el camino aumentante, mientras que cada arista inversa vv, uu gana ese peso.

Se puede demostrar  que el número total de aumentos de flujo (llamadas a BFS) es O(VE)\mathcal{O}(VE) y que cada llamada a BFS requiere O(E)\mathcal{O}(E) de tiempo.

Implementación

Complejidad temporal: O(VE2)\mathcal{O}(VE^2)

#include <bits/stdc++.h> using namespace std; long long max_flow(vector<vector<int>> adj, vector<vector<long long>> capacity, int source, int sink) { int n = adj.size(); vector<int> parent(n, -1); // Hallar un camino de la fuente al sumidero con capacidades no negativas auto reachable = [&]() -> bool { queue<int> q; q.push(source); while (!q.empty()) { int node = q.front(); q.pop(); for (int son : adj[node]) { long long w = capacity[node][son]; if (w <= 0 || parent[son] != -1) continue; parent[son] = node; q.push(son); } } return parent[sink] != -1; }; long long flow = 0; // Mientras exista un camino de la fuente al sumidero con capacidades no // negativas while (reachable()) { int node = sink; // La capacidad mínima en el camino de la fuente al sumidero long long curr_flow = LLONG_MAX; while (node != source) { curr_flow = min(curr_flow, capacity[parent[node]][node]); node = parent[node]; } node = sink; while (node != source) { // Restar la capacidad de las aristas de capacidad capacity[parent[node]][node] -= curr_flow; // Sumar el flujo actual a las aristas inversas capacity[node][parent[node]] += curr_flow; node = parent[node]; } flow += curr_flow; fill(parent.begin(), parent.end(), -1); } return flow; } int main() { int n, m; cin >> n >> m; vector<vector<long long>> capacity(n, vector<long long>(n)); vector<vector<int>> adj(n); for (int i = 0; i < m; i++) { int a, b, c; cin >> a >> b >> c; adj[--a].push_back(--b); adj[b].push_back(a); capacity[a][b] += c; } cout << max_flow(adj, capacity, 0, n - 1) << endl; }

Algoritmo Push-Relabel

El algoritmo Push-Relabel  es una solución alternativa para hallar el flujo máximo.

Para hallar el flujo máximo, manejaremos un preflujo (preflow). La única diferencia entre esto y un flujo normal es que el flujo entrante puede superar al flujo saliente.

Definamos el exceso de flujo del nodo uu como inuoutuin_u - out_u, donde inin es el flujo entrante y outout es el flujo saliente.

El algoritmo empieza con un preflujo inicial en el que la fuente tiene un exceso de flujo. En cada etapa, elige un nodo con exceso y empuja (push) el exceso hacia sus vecinos, si la capacidad lo permite. Para empujar exceso de flujo del nodo uu al nodo vv, definimos Δ=min(excessu,capacityu,vflowu,v)\Delta = min(excess_u, capacity_{u,v} - flow_{u,v}), donde Δ\Delta es el flujo máximo que soporta la arista (u,v)(u, v). El proceso se detiene cuando ya no quedan nodos con exceso en la red de flujo.

Otra característica importante del algoritmo es la función de etiquetado hh, también conocida como función de altura, que asigna a cada nodo un entero. Un etiquetado es válido si cumple las siguientes condiciones: hsource=Vh_{source} = |V|, hsink=0h_{sink} = 0, y huhv+1h_u \le h_v + 1. Si hay una arista del nodo uu al nodo vv con capacidad positiva, es decir, si soporta más flujo. La función de altura nos dice hacia dónde enviar el exceso de flujo, y dónde se necesita. Es como el agua: solo puede fluir de arriba hacia abajo.

Para resumir: empezamos con un preflujo válido y un etiquetado válido. En cada paso, para cada nodo con exceso, intentamos empujar el exceso hacia los vecinos del nodo. Después de cada paso comprobamos si el flujo y el etiquetado siguen siendo válidos. Si lo son y ya no hay caminos entre la sourcesource y el sinksink, el flujo máximo se ha encontrado.

La diferencia entre Edmonds-Karp (o Ford-Fulkerson) y Push-Relabel es que el primero mantiene un flujo válido todo el tiempo y lo mejora mientras haya caminos aumentantes, mientras que en Push-Relabel no existe un camino aumentante en ningún momento, y se mejora el preflujo hasta alcanzar el flujo máximo.

Complejidad temporal: O(V2E)\mathcal{O}(V^2E)

#include <bits/stdc++.h> #define ll long long using namespace std; const int MAXN = 5000; const ll INF = 1e15; int n, m, source, sink; ll capacity[MAXN + 1][MAXN + 1]; ll flow[MAXN + 1][MAXN + 1]; vector<ll> excess; vector<ll> height; vector<ll> next_son; queue<int> excess_vertexes; void relabel(int u) { ll d = INF; for (int v = 1; v <= n; v++) { // Si el vecino soporta flujo. if (capacity[u][v] > flow[u][v]) { d = min(d, height[v]); } } if (d < INF) { height[u] = d + 1; } } // Empujar flujo del nodo u al nodo v void push(int u, int v) { // delta = cuánto flujo soporta la arista ll d = min(excess[u], capacity[u][v] - flow[u][v]); // Aumentar el flujo del nodo u a v flow[u][v] += d; flow[v][u] -= d; // Actualizar el exceso excess[u] -= d; excess[v] += d; // Si el exceso de v es igual a delta, es la primera vez. if (d && excess[v] == d) { excess_vertexes.push(v); } } void discharge(int u) { // Mientras haya exceso de flujo para empujar while (excess[u] > 0) { // La siguiente parte también se puede escribir con un for if (next_son[u] <= n) { int v = next_son[u]; // Si la arista soporta más flujo y se puede empujar hacia ella if (capacity[u][v] > flow[u][v] && height[u] > height[v]) { push(u, v); } else { next_son[u]++; } } else { relabel(u); next_son[u] = 1; } } } int main() { cin >> n >> m; source = 1; sink = n; excess.resize(n + 1); height.resize(n + 1); next_son.resize(n + 1); height[source] = n; excess[source] = INF; for (int i = 1; i <= m; i++) { int x, y, cap; cin >> x >> y >> cap; capacity[x][y] += cap; } for (int i = 1; i <= n; i++) { if (i == source) { continue; } push(source, i); } while (!excess_vertexes.empty()) { int node = excess_vertexes.front(); excess_vertexes.pop(); if (node != source && node != sink) { discharge(node); } } ll max_flow = 0; for (int node = 1; node <= n; node++) { max_flow += flow[node][sink]; } cout << max_flow << endl; }

Recursos

Recursos
FuenteRecursoNotas
CPC10 - Network Flow
CPH20 - Flows & Cuts
CP24.6, 4.7.4 - Max Flow, Bipartite Graphs

Flujos

HechoFuenteNombreDificultadTagsSolución
CSESDistinct RoutesFácilMax FlowSolución

Matching bipartito

HechoFuenteNombreDificultadTagsSolución
CSESSchool DanceFácilMax Flowen el módulo

Se recomienda resolver el primer problema — Download Speed — de la sección anterior antes de intentar este.

Solución 1

Un grafo bipartito no parece tener nada en común con una red de flujo, pero podemos cambiar el punto de vista simplemente agregando la fuente conectada a los nodos del conjunto UU y el sumidero conectado a los nodos del conjunto VV. Y eso es todo. Ahora tenemos una red de flujo en la que toda capacidad es igual a 1.

Transformamos nuestro grafo bipartito en una red de flujo, de modo que el flujo máximo de la red es igual al matching máximo. ¡Ahora podemos aplicar nuestro algoritmo favorito de flujo máximo para resolver el problema!

Complejidad temporal: O(VE2)\mathcal{O}(VE^2)

Solución 2

Sin embargo, podemos hacerlo mejor. Primero definamos algunas propiedades de los algoritmos de matching.

Digamos que el conjunto MM contiene todas las aristas que forman el matching máximo.

Definimos un camino alternante (alternating path) como un camino cuyas aristas están en el matching MM y fuera del matching, de forma alternada. Un camino alternante empieza en un nodo no emparejado y termina cuando ya no se puede agregar otra arista manteniendo una secuencia alternante.

Un camino aumentante se construye sobre el camino alternante y nodos no emparejados en ambos extremos: los nodos no están incluidos en MM.

El matching máximo MM se puede mejorar si y solo si se encuentra un camino aumentante en MM; en caso contrario, MM es el matching máximo. Puede parecer difícil de entender, pero la idea principal es la siguiente:

El matching máximo se puede seguir mejorando mientras la secuencia alternante se pueda extender. Para una mejor comprensión, se puede imaginar los cordones de un zapato como un camino alternante en un grafo bipartito — o las zapatillas.

El algoritmo descrito arriba se llama algoritmo de Hopcroft-Karp .

Aquí hay una animación del algoritmo si todavía hay algo de confusión:

Complejidad temporal: O(EV)\mathcal{O}(E\sqrt{V})

#include <bits/stdc++.h> using namespace std; const int INF = 1e9; int n, m, k; // dist[node] = la posición de node en el camino alternante vector<int> match, dist; vector<vector<int>> g; bool bfs() { queue<int> q; // El camino alternante empieza con nodos no emparejados for (int node = 1; node <= n; node++) { if (!match[node]) { q.push(node); dist[node] = 0; } else { dist[node] = INF; } } dist[0] = INF; while (!q.empty()) { int node = q.front(); q.pop(); if (dist[node] >= dist[0]) { continue; } for (int son : g[node]) { // Si el match de son está emparejado if (dist[match[son]] == INF) { dist[match[son]] = dist[node] + 1; q.push(match[son]); } } } // Devuelve true si se encontró un camino alternante return dist[0] != INF; } // Devuelve true si se encontró un camino aumentante que empieza en el // vértice node bool dfs(int node) { if (node == 0) { return true; } for (int son : g[node]) { if (dist[match[son]] == dist[node] + 1 && dfs(match[son])) { match[node] = son; match[son] = node; return true; } } dist[node] = INF; return false; } int hopcroft_karp() { int cnt = 0; // Mientras haya un camino alternante while (bfs()) { for (int node = 1; node <= n; node++) { // Si node no está emparejado pero podemos emparejarlo con un // camino aumentante if (!match[node] && dfs(node)) { cnt++; } } } return cnt; } int main() { cin >> n >> m >> k; dist.resize(n + m + 1); match.resize(n + m + 1); g.resize(n + m + 1); for (int i = 1; i <= k; i++) { int x, y; cin >> x >> y; y += n; g[x].push_back(y); g[y].push_back(x); } cout << hopcroft_karp() << '\n'; for (int node = 1; node <= n; node++) { if (match[node]) { cout << node << ' ' << match[node] - n << '\n'; } } }

Algoritmo de Dinic

Video de YouTube (M6cm8UeeziI)

HechoFuenteNombreDificultadTagsSolución
SPOJFast FlowFácil
YSBipartite MatchingFácilSolución

¿Matching bipartito de Hopcroft-Karp?

Flujo más rápido

Existen algoritmos de flujo más rápidos, como Push-Relabel. Véase también la siguiente entrada de blog:

Sin embargo, la implementación estándar de Dinic es (casi) siempre suficientemente rápida en problemas razonables.

Implementación

Recursos
FuenteRecursoNotas
BenqDinic

Cuando el flujo se rompe

Cuando las restricciones son demasiado altas…