← Static tasks

E058. Нахождение минимального разреза. Алгоритм Штор-Вагнера

emaxx algorithm

#algorithm#emaxx#flow#graph

Task

Источник: e-maxx.ru/algo, страница PDF 169.

Постановка задачи

Дан неориентированный взвешенный граф

с

вершинами и

рёбрами. Разрезом

называется

некоторое подмножество вершин (фактически, разрез — разбиение вершин на два множества: принадлежащие

и

все остальные). Весом разреза называется сумма весов рёбер, проходящих через разрез, т.е. таких рёбер, ровно

один конец которых принадлежит

:

где через

обозначено множество всех рёбер графа

, а через

— вес ребра

. Требуется найти разрез минимального веса. Иногда эту задачу называют "глобальным минимальным разрезом" — по контрасту с задачей, когда заданы вершины-

сток и исток, и требуется найти минимальный разрез

, содержащий сток и не содержащий исток. Глобальный минимальный разрез равен минимуму среди разрезов минимальной стоимости по всевозможным парам исток-сток. Хотя эту задачу можно решить с помощью алгоритма нахождения максимального потока (запуская его

раз

для всевозможных пар истока и стока), однако ниже описан гораздо более простой и быстрый алгоритм, предложенный Матильдой Штор (Mechthild Stoer) и Франком Вагнером (Frank Wagner) в 1994 г. В общем случае допускаются петли и кратные рёбра, хотя, понятно, петли абсолютно никак не влияют на результат, а все кратные рёбра можно заменить одним ребром с их суммарным весом. Поэтому мы для простоты будем считать, что во входном графе петли и кратные рёбра отсутствуют.

Описание алгоритма

Базовая идея алгоритма очень проста. Будем итеративно повторять следующий процесс: находить

минимальный разрез между какой-нибудь парой вершин

и

, а затем объединять эти две вершины в одну

(соединяя списки смежности). В конце концов, после

итерации, граф сожмётся в единственную вершину и

процесс остановится. После этого ответом будет являться минимальный среди всех

найденных

разрезов. Действительно, на каждой

-ой стадии найденный минимальный разрез

между вершинами

и

либо окажется искомым глобальным минимальным разрезом, либо же, напротив, вершины

и

невыгодно относить

к разным множествам, поэтому мы ничего не ухудшаем, объединяя эти две вершины в одну. Таким образом, мы свели задачу к следующей: для данного графа найти минимальный разрез

между какой-нибудь, произвольной, парой вершин

и

. Для решения этой задачи

был предложен следующий, тоже итеративный процесс. Вводим некоторое множество вершин

, которое

изначально содержит единственную произвольную вершину. На каждом шаге находится вершина,

наиболее сильно связанная с множеством

, т.е. вершина

, для которой следующая

величина максимальна:

(т.е. максимальна сумма весов рёбер, один конец которых

, а другой принадлежит

).

Опять же, этот процесс завершится через

итерацию, когда все вершины перейдут в множество

(кстати

говоря, этот процесс очень напоминает алгоритм Прима). Тогда, как утверждает теорема Штор-Вагнера,

если мы обозначим через

и

последние две добавленные в

вершины, то минимальный разрез между вершинами

и

будет состоять из единственной вершины —

. Доказательство этой теоремы будет приведено в следующем

разделе (как это часто бывает, само по себе оно никак не способствует пониманию алгоритма). Таким образом, общая схема алгоритма Штор-Вагнера такова. Алгоритм состоит из

фазы. На каждой

фазе множество

сначала полагается состоящим из какой-либо вершины; подсчитываются стартовые веса

вершин

. Затем происходит

итерация, на каждой из которых выбирается вершина

с

наибольшим значением

и добавляется в множество

, после чего пересчитываются значения

для оставшихся вершин (для чего, очевидно, надо пройтись по всем рёбрам списка смежности выбранной вершины

). После выполнения всех итераций мы запоминаем в

и

номера последних двух добавленных вершин, а в

качестве стоимости найденного минимального разреза между

и

можно взять значение

. Затем

надо сравнить найденный минимальный разрез с текущим ответом, если меньше, то обновить ответ. Перейти к следующей фазе. Если не использовать никаких сложных структур данных, то самой критичной частью будет нахождение вершины

с наибольшей величиной

. Если производить это за

, то, учитывая, что всего фаз

, и по

итерации

в каждой, итоговая асимптотика алгоритма получается

.

Если для нахождения вершины с наибольшей величиной

использовать Фибоначчиевы кучи

(которые позволяют увеличивать значение ключа за

в среднем и извлекать максимум за

в среднем),

то все связанные с множеством

операции на одной фазе выполнятся за

. Итоговая

асимптотика алгоритма в таком случае составит

.

Доказательство теоремы Штор-Вагнера

Напомним условие этой теоремы. Если добавить в множество

по очереди все вершины, каждый раз добавляя

вершину, наиболее сильно связанную с этим множеством, то обозначим предпоследнюю добавленную вершину через

,

а последнюю — через

. Тогда минимальный

-

разрез состоит из единственной вершины —

.

Для доказательства рассмотрим произвольный

-

разрез

и покажем, что его вес не может быть меньше веса

разреза, состоящего из единственной вершины

:

Для этого докажем следующий факт. Пусть

— состояние множества

непосредственно перед добавлением

вершины

. Пусть

— разрез множества

, индуцированный разрезом

(проще говоря,

равно пересечению этих двух множеств вершин). Далее, вершина

называется активной (по отношению к разрезу

), если вершина

и предыдущая добавленная в

вершина принадлежат разным частям разреза

.

Тогда, утверждается, для любой активной вершины

выполняется неравенство:

В частности,

является активной вершиной (т.к. перед ним добавлялась вершина

), и при

это

неравенство превращается в утверждение теоремы: Итак, будем доказывать неравенство, для чего воспользуемся методом математической индукции.

Для первой активной вершины

это неравенство верно (более того, оно обращается в равенство) — поскольку

все вершины

принадлежат одной части разреза, а

— другой. Пусть теперь это неравенство выполнено для всех активных вершин вплоть до некоторой вершины

, докажем его

для следующей активной вершины

. Для этого преобразуем левую часть: Во-первых, заметим, что:

— это следует из того, что когда множество

было равно

, в него была добавлена именно вершина

, а не

,

значит, она имела наибольшее значение

.

Далее, поскольку

по предположению индукции, то получаем: откуда имеем:

Теперь заметим, что вершина

и все вершины

находятся в разных частях разреза

, поэтому эта

величина

обозначает сумму весов рёбер, которые учтены в

, но ещё не были учтены

в

, откуда получаем: что и требовалось доказать.

Мы доказали соотношение

, а из него, как уже говорилось выше, следует и вся теорема.

Реализация

Для наиболее простой и ясной реализации (с асимптотикой

) было выбрано представление графа в виде

матрицы смежности. Ответ хранится в переменных

и

(искомые стоимость минимального

разреза и сами вершины, содержащиеся в нём).

Для каждой вершины в массиве

хранится, существует ли она, или она была объединена с какой-то

другой вершиной. В списке

для каждой сжатой вершины

хранятся номера исходных вершин, которые были сжаты

в эту вершину

.

Алгоритм состоит из

фазы (цикл по переменной

). На каждой фазе сначала все вершины находятся

вне множества

, для чего массив

заполняется нулями, и связности

всех вершин нулевые. На каждой

из

итерации находится вершина

с наибольшей величиной

. Если это итерация последняя, то ответ,

если надо, обновляется, а предпоследняя

и последняя

выбранные вершины объединяются в одну.

Если итерация не последняя, то

добавляется в множество

, после чего пересчитываются веса всех

остальных вершин. Следует заметить, что алгоритм в ходе своей работы "портит" граф

, поэтому, если он ещё понадобится позже,

надо сохранять его копию перед вызовом функции.

const int MAXN = 500;

int n, g[MAXN][MAXN];
int best_cost = 1000000000;

vector<int> best_cut;

void mincut() {

vector<int> v[MAXN];

for (int i=0; i<n; ++i)

v[i].assign (1, i);

int 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;

} } } }

Литература

● Mechthild Stoer, Frank Wagner. A Simple Min-Cut Algorithm [1997]

● Kurt Mehlhorn, Christian Uhrig. The minimum cut algorithm of Stoer and Wagner [1995]

C# solution

auto-draft, review before submit
using System;
using System.Collections.Generic;
using System.Linq;

public static class AlgorithmDraft
{
    // Auto-generated C# draft from the original e-maxx C/C++ listing. Review before production use.
    const int MAXN = 500;
    int n, g[MAXN][MAXN];
    int best_cost = 1000000000;
    List<int> best_cut;
    void mincut() {
            List<int> v[MAXN];
            for (int i=0; i<n; ++i)
                    v[i].assign (1, i);
            int w[MAXN];
            bool[] exist = new bool[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;
                            }
                    }
            }
    }
}

C++ solution

matched/original
const int MAXN = 500;
int n, g[MAXN][MAXN];
int best_cost = 1000000000;
vector<int> best_cut;
void mincut() {
        vector<int> v[MAXN];
        for (int i=0; i<n; ++i)
                v[i].assign (1, i);
        int 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;
                        }
                }
        }
}

Java solution

auto-draft, review before submit
import java.util.*;
import java.math.*;

public class AlgorithmDraft {
    // Auto-generated Java draft from the original e-maxx C/C++ listing. Review before production use.
    const int MAXN = 500;
    int n, g[MAXN][MAXN];
    int best_cost = 1000000000;
    ArrayList<Integer> best_cut;
    void mincut() {
            ArrayList<Integer> v[MAXN];
            for (int i=0; i<n; ++i)
                    v[i].assign (1, i);
            int w[MAXN];
            boolean 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;
                            }
                    }
            }
    }
}

Explanation

Материал разбит как алгоритмическая задача: изучить постановку, понять асимптотику и реализовать алгоритм на выбранном языке.