Corte mínimo - algoritmo de Stoer-Wagner
Enunciado del problema
Se da un grafo no dirigido ponderado con vértices y aristas. Un corte es un subconjunto propio no vacío de los vértices (en efecto, un corte es una partición de los vértices en dos conjuntos no vacíos: los que pertenecen a y todos los demás). El peso de un corte es la suma de los pesos de las aristas que cruzan el corte, es decir, las aristas que tienen exactamente un extremo en :
donde denota el conjunto de todas las aristas del grafo , y es el peso de la arista .
La tarea es encontrar un corte de peso mínimo.
A veces este problema se llama el “corte mínimo global”, en contraste con el problema en el que se dan un vértice fuente y un vértice sumidero, y hay que encontrar un corte mínimo que contenga el sumidero pero no la fuente. El corte mínimo global es igual al mínimo entre los cortes de costo mínimo sobre todos los pares posibles de fuente y sumidero.
Aunque este problema se puede resolver con un algoritmo de flujo máximo (ejecutándolo veces para todos los pares posibles de fuente y sumidero), a continuación describimos un algoritmo mucho más simple y rápido, propuesto por Mechthild Stoer y Frank Wagner en 1994.
En general se permiten bucles y aristas múltiples, aunque los bucles obviamente no influyen en el resultado de ninguna forma, y las aristas múltiples siempre se pueden reemplazar por una sola arista con su peso combinado. Por tanto, por simplicidad, asumiremos que el grafo de entrada no contiene bucles ni aristas múltiples.
Descripción del algoritmo
La idea básica del algoritmo es muy simple. Repetimos de forma iterativa el siguiente proceso: encontrar el corte mínimo entre algún par de vértices y , y luego fusionar estos dos vértices en uno (conectando sus listas de adyacencia). Eventualmente, después de iteraciones, el grafo se comprimirá en un solo vértice y el proceso se detiene. Después de eso, la respuesta es el mínimo entre todos los cortes encontrados. En efecto, en cada -ésima etapa, el corte mínimo encontrado entre los vértices y o bien resulta ser el corte mínimo global deseado, o, por el contrario, es desventajoso poner y en conjuntos distintos, así que no empeoramos nada al fusionar estos dos vértices en uno.
Así redujimos el problema al siguiente: para un grafo dado, encontrar el corte mínimo entre algún par arbitrario de vértices y . Para resolver este problema se propuso el siguiente proceso, también iterativo. Introducimos un conjunto de vértices , que inicialmente contiene un solo vértice arbitrario. En cada paso, encontramos el vértice más fuertemente conectado al conjunto , es decir, el vértice para el cual la siguiente cantidad es máxima:
(es decir, la suma de los pesos de las aristas con un extremo en y el otro en es máxima).
Otra vez, este proceso termina después de iteraciones, cuando todos los vértices se han movido al conjunto (de paso, este proceso se parece mucho al algoritmo de Prim). Entonces, como afirma el teorema de Stoer-Wagner, si denotamos por y los últimos dos vértices agregados a , el corte mínimo entre los vértices y consiste en un solo vértice: . La demostración de este teorema se dará en la siguiente sección (como suele ocurrir, por sí misma no contribuye de ninguna forma a la comprensión del algoritmo).
Así, el esquema general del algoritmo de Stoer-Wagner es el siguiente. El algoritmo consiste en fases. En cada fase, el conjunto se inicializa para que contenga algún vértice, y se calculan los pesos iniciales de los vértices. Luego siguen iteraciones, en cada una de las cuales se elige el vértice con el mayor valor y se agrega al conjunto , después de lo cual se recalculan los valores de para los vértices restantes (para lo cual, obviamente, hay que recorrer todas las aristas de la lista de adyacencia del vértice seleccionado ). Después de realizar todas las iteraciones, registramos en y los últimos dos vértices agregados, y el valor se puede tomar como el costo del corte mínimo encontrado entre y . Luego comparamos el corte mínimo encontrado con la respuesta actual, y si es menor, actualizamos la respuesta, y pasamos a la siguiente fase.
Si no usamos estructuras de datos sofisticadas, la parte más crítica es encontrar el vértice con el mayor valor de . Si lo hacemos en , entonces, dado que hay fases con iteraciones cada una, la complejidad resultante del algoritmo es .
Si usamos heaps de Fibonacci para encontrar el vértice con el mayor valor de (que permiten aumentar el valor de una clave en amortizado y extraer el máximo en amortizado), entonces todas las operaciones relacionadas con el conjunto en una fase se realizan en . La complejidad resultante del algoritmo en este caso es .
Demostración del teorema de Stoer-Wagner
Recordemos el enunciado de este teorema. Si agregamos todos los vértices al conjunto uno por uno, cada vez agregando el vértice más fuertemente conectado a este conjunto, entonces denotamos el penúltimo vértice agregado por y el último por . Entonces el corte - mínimo consiste en un solo vértice: .
Para demostrarlo, consideramos un corte - arbitrario y mostramos que su peso no puede ser menor que el peso del corte que consiste en el único vértice :
Para ello, demostramos el siguiente hecho. Sea el estado del conjunto inmediatamente antes de agregar el vértice . Sea el corte del conjunto inducido por el corte (dicho de forma simple, es igual a la intersección de estos dos conjuntos de vértices). Además, un vértice se llama activo (respecto del corte ) si el vértice y el vértice agregado previamente pertenecen a partes distintas del corte . Entonces, afirmamos, para cualquier vértice activo se cumple la siguiente desigualdad:
En particular, es un vértice activo (ya que el vértice agregado antes de él fue ), y para esta desigualdad se convierte en el enunciado del teorema:
Así, demostraremos esta desigualdad usando inducción matemática.
Para el primer vértice activo la desigualdad se cumple (más aún, se convierte en una igualdad), ya que todos los vértices de pertenecen a una parte del corte, y pertenece a la otra.
Ahora supongamos que esta desigualdad se cumple para todos los vértices activos hasta algún vértice ; demostrémosla para el siguiente vértice activo . Para ello, transformamos el lado izquierdo:
Primero, nótese que:
lo cual se sigue del hecho de que cuando el conjunto era igual a , el vértice agregado a él fue precisamente y no , lo que significa que tenía el mayor valor de .
Además, como por la hipótesis de inducción, obtenemos:
de lo cual tenemos:
Ahora nótese que el vértice y todos los vértices de están en partes distintas del corte , así que la cantidad denota la suma de los pesos de las aristas que se cuentan en pero que aún no se habían contado en , de lo cual obtenemos:
como se requería.
Hemos demostrado la relación , y, como se mencionó arriba, de ella se sigue todo el teorema.
Implementación
Para la implementación más simple y clara (con complejidad ), el grafo se representa como una matriz de adyacencia. La respuesta se guarda en las variables best_cost y best_cut (el costo del corte mínimo y los vértices contenidos en él).
Para cada vértice, el arreglo exist guarda si todavía existe, o si se fusionó con algún otro vértice. La lista v[i] para cada vértice comprimido guarda los números de los vértices originales que se comprimieron en este vértice .
El algoritmo consiste en fases (el bucle sobre la variable ph). En cada fase, todos los vértices están inicialmente fuera del conjunto , así que el arreglo in_a se llena con ceros, y las conectividades de todos los vértices son cero. En cada una de las iteraciones, se encuentra el vértice sel con el mayor valor de . Si esta es la última iteración, se actualiza la respuesta si es necesario, y se fusionan en uno el penúltimo prev y el último sel vértices seleccionados. Si la iteración no es la última, entonces sel se agrega al conjunto , después de lo cual se recalculan los pesos de todos los vértices restantes.
Nótese que el algoritmo “estropea” el grafo g durante su trabajo, así que si todavía se necesita después, hay que guardar una copia de él antes de llamar a la función.
const int MAXN = 500;
int n;
long long g[MAXN][MAXN];
long long best_cost = (1LL << 62);
vector<int> best_cut;
void mincut() {
vector<int> v[MAXN];
for (int i = 0; i < n; ++i)
v[i].assign(1, i);
long long w[MAXN];
bool exist[MAXN], in_a[MAXN];
memset(exist, true, sizeof exist);
for (int ph = 0; ph < n - 1; ++ph) {
memset(in_a, false, sizeof in_a);
memset(w, 0, sizeof w);
for (int it = 0, prev; it < n - ph; ++it) {
int sel = -1;
for (int i = 0; i < n; ++i)
if (exist[i] && !in_a[i] && (sel == -1 || w[i] > w[sel]))
sel = i;
if (it == n - ph - 1) {
if (w[sel] < best_cost) {
best_cost = w[sel];
best_cut = v[sel];
}
v[prev].insert(v[prev].end(), v[sel].begin(), v[sel].end());
for (int i = 0; i < n; ++i)
g[prev][i] = g[i][prev] += g[sel][i];
exist[sel] = false;
} else {
in_a[sel] = true;
for (int i = 0; i < n; ++i)
w[i] += g[sel][i];
prev = sel;
}
}
}
}