Skip to Content

Flujo de costo mínimo

En esta sección voy a recorrer brevemente mi comprensión del flujo de costo mínimo. Recomiendo mucho leer Min Cost Flow de CP-Algorithms  para entender primero la idea de la solución. Además, conviene revisar el tutorial de TopCoder  para una explicación más detallada.

Desigualdad triangular

En teoría de grafos, la desigualdad triangular (triangle inequality) afirma que si hay una arista uvu \rightarrow v de peso ww, y dkd_k es el camino más corto hasta el nodo kk (para alguna definición razonable de camino más corto), entonces dvduwd_v - d_u \leq w.

Recursos
FuenteRecursoNotas
WikipediaTriangle Inequality

Principalmente en geometría, pero transmite la idea.

Algoritmo de Johnson

La idea principal del algoritmo de Johnson es que si todos los pesos de arista son positivos, entonces ejecutar Dijkstra desde cada nodo daría un algoritmo O(VElogE)\mathcal{O}(VE \log E). Si hay aristas negativas, el algoritmo de Johnson define una función de potencial π\pi tal que para toda arista u,v,wu,v,w se cumple w>=π(u)π(v)w>=\pi(u)-\pi(v). Entonces cada peso de arista se puede transformar en ww+π(v)π(u)w \rightarrow w + \pi(v)-\pi(u), lo que resulta en peso positivo. Esta condición coincide con la desigualdad triangular, así que podemos elegir un nodo arbitrariamente y ejecutar un algoritmo de camino más corto O(VE)\mathcal{O}(VE) para determinar esta función.

Flujo de costo mínimo

La idea general del flujo de costo mínimo es empujar flujo repetidamente a lo largo del camino más corto. Como los grafos de flujo tienen aristas negativas, cada paso de forma naive tomaría O(VE)\mathcal{O}(VE) de tiempo. Para acelerarlo, podemos usar la misma función de potencial del algoritmo de Johnson para emplear Dijkstra en este proceso. En este caso debemos usar la distancia desde SS, el nodo fuente, como función π\pi. En cada paso, ejecutamos Dijkstra usando la función π\pi, actualizamos π\pi para que coincida con las distancias actuales, y luego empujamos flujo a lo largo del camino más corto, invirtiendo aristas según haga falta. Cuando se alcanza el flujo pedido o el sumidero es inalcanzable, terminamos.

Recursos
FuenteRecursoNotas
CP-AlgorithmsMinimum-cost flow

Nota: no usa la solución óptima, pero explica bien el concepto.

TopCoderMinimum Cost Flow Algorithms

Implementación

Dicho esto, aquí está mi implementación.

template <int MN, int MM> struct MCF // MN = nodes, MM = edges [assume edges one-directional] { public: int N, M, S, T; int flow[MM * 2], cap[MM * 2], hd[MN], nx[MM * 2], to[MM * 2], cost[MM * 2]; int pi[MN], p[MN], d[MN]; int vis[MN]; void init(int n, int s, int t) { N = n, S = s, T = t; memset(hd, -1, sizeof hd); } void adde1(int a, int b, int f, int c) { nx[M] = hd[a], hd[a] = M; to[M] = b, cost[M] = c, cap[M] = f; M++; } void adde(int a, int b, int f, int c) { adde1(a, b, f, c); adde1(b, a, 0, -c); } void setpi() { std::queue<int> q; memset(pi, 0x3e, sizeof pi); memset(vis, 0, sizeof vis); q.push(S); pi[S] = 0; for (int n; !q.empty();) { n = q.front(); q.pop(); for (int id = hd[n], x; ~id; id = nx[id]) { if (cap[id] - flow[id] <= 0) continue; x = to[id]; if (ckmin(pi[x], pi[n] + cost[id])) assert(++vis[x] <= N), q.push(x); } } } struct state { public: int n, d; bool operator>(state o) const { return d > o.d; } }; void dijk() { std::priority_queue<state, std::vector<state>, std::greater<state>> q; memset(p, -1, N * sizeof p[0]); memset(vis, 0, N * sizeof vis[0]); memset(d, 0x3e, N * sizeof d[0]); d[S] = 0; q.push({S, 0}); for (int n; !q.empty();) { n = q.top().n; q.pop(); if (vis[n]) continue; vis[n] = 1; for (int id = hd[n], x, w; ~id; id = nx[id]) { if (cap[id] - flow[id] <= 0) continue; x = to[id]; w = cost[id] + pi[n] - pi[x]; if (ckmin(d[x], w + d[n])) p[x] = id, q.push({x, d[x]}); } } } int mincost(int F) { setpi(); int C = 0; while (F > 0) { dijk(); if (d[T] == INF) return INF; int c = d[T] + pi[T] - pi[S], f = F; for (int x = T; x != S; x = to[p[x] ^ 1]) ckmin(f, cap[p[x]] - flow[p[x]]); C += c * f; for (int x = T; x != S; x = to[p[x] ^ 1]) { flow[p[x]] += f; flow[p[x] ^ 1] -= f; } F -= f; for (int i = 0; i < N; ++i) pi[i] += d[i]; } return C; } };

También conviene ver la implementación de Benq  y la implementación de KACTL  (que son bastante mejores que la mía).

Problemas

HechoFuenteNombreDificultadTagsSolución
CFBuild StringNormalMCF
CFFour MelodiesNormalMCF
CFApril Fools' Problem (medium)Normal
CFTeam BuildingDifícilBitmasks, MCFSolución
CFFor the Emperor!Difícil
CFCow and ExerciseMuy difícil
CFOne Billion Shades of GreyMuy difícilSolución

Aplicaciones

Problema de asignación

HechoFuenteNombreDificultadTagsSolución
CSESTask AssignmentFácilen el módulo

El problema de asignación se entiende mejor como un problema de flujo máximo de costo mínimo. En nuestra formulación, asignaremos WW trabajadores a JJ trabajos, JWJ \ge W, y el costo de asignar el ii-ésimo trabajo al jj-ésimo trabajador es Ci,jC_{i,j}.

Crearemos una red de flujo con un nodo fuente SS, nodos de trabajo JiJ_i, nodos de trabajador WjW_j y un nodo sumidero EE.

Tendremos las siguientes aristas:

  • (S,Ji)(S, J_i) con (capacity,cost)=(1,0)(capacity, cost) = (1, 0)
  • (Ji,Wj)(J_i, W_j) con (1,Ci,j)(1, C_{i,j})
  • (Wj,E)(W_j, E) con (1,0)(1, 0)

La respuesta será el costo mínimo del flujo máximo del grafo. Un ejemplo:

Hungarian Algorithm Flows Diagram

Para resolverlo, usaremos el algoritmo de flujo máximo de costo mínimo detallado arriba. En cada iteración del algoritmo, asignaremos un trabajo a un trabajador. En cada iteración, primero asignamos el trabajo a un trabajador auxiliar y luego intentamos hallar un camino aumentante de costo mínimo. Para optimizar esto, solo ejecutaremos Dijkstra sobre los nodos de trabajadores. Para los detalles, consultar el código.

Como hay JJ iteraciones (una por cada trabajo) y cada iteración toma O(W2)O(W^2) de tiempo, la complejidad temporal total es O(JW2)O(J W^2).

Implementación

De Wikipedia (que a su vez copia de e-maxx):

#include <bits/stdc++.h> using namespace std; int ckmin(int &a, int b) { return a > b ? ((a = b), true) : false; } /** * @return the jobs of each worker in the optimal assignment, * or -1 if the worker is not assigned */ template <class T> vector<int> hungarian(const vector<vector<T>> &C) { int J = C.size(); int W = C[0].size(); assert(J <= W); // job[w] = trabajo asignado al w-ésimo trabajador, o -1 si no hay trabajo // nota: se agregó un W-ésimo trabajador por conveniencia vector<int> job(W + 1, -1); vector<T> h(W); // potenciales de Johnson const T inf = numeric_limits<T>::max(); // asignar el j_cur-ésimo trabajo usando Dijkstra con potenciales for (int j_cur = 0; j_cur < J; j_cur++) { int w_cur = W; // trabajador no visitado con distancia mínima job[w_cur] = j_cur; vector<T> dist(W + 1, inf); // distancias reducidas de Johnson dist[W] = 0; vector<bool> vis(W + 1); // si ya se visitó vector<int> prv(W + 1, -1); // trabajador anterior en el camino más corto while (job[w_cur] != -1) { // paso de Dijkstra: extraer el trabajador mín. T min_dist = inf; vis[w_cur] = true; int w_next = -1; // siguiente trabajador no visitado con dist. mínima // considerar extender el camino más corto por w_cur -> job[w_cur] -> w for (int w = 0; w < W; w++) { if (!vis[w]) { // suma de pesos reducidos w_cur -> job[w_cur] -> w T edge = C[job[w_cur]][w] - h[w]; if (w_cur != W) { edge -= C[job[w_cur]][w_cur] - h[w_cur]; assert(edge >= 0); } if (ckmin(dist[w], dist[w_cur] + edge)) { prv[w] = w_cur; } if (ckmin(min_dist, dist[w])) { w_next = w; } } } w_cur = w_next; } for (int w = 0; w < W; w++) { // actualizar potenciales ckmin(dist[w], dist[w_cur]); h[w] += dist[w]; } while (w_cur != W) { // actualizar la asignación de trabajos job[w_cur] = job[prv[w_cur]]; w_cur = prv[w_cur]; } } return job; } int main() { int n; cin >> n; vector<vector<int>> c(n, vector<int>(n)); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { cin >> c[i][j]; } } vector<int> mat = hungarian(c); int cost = 0; for (int i = 0; i < n; i++) { cost += c[mat[i]][i]; } cout << cost << endl; for (int i = 0; i < n; i++) { cout << mat[i] + 1 << ' ' << i + 1 << endl; } }
Recursos
FuenteRecursoNotas
WikipediaHungarian Algorithm
YouTube - Algorithms ThreadHungarian for non Hungarians
TopcoderAssignment Problem and Hungarian Algorithm
HechoFuenteNombreDificultadTagsSolución
CFVasya and Endless CreditsFácil
KattisCordon BleuFácil