E038. Minimum spanning tree. Prim's algorithm

e-maxx algorithm original: C/C++ #algorithm #emaxx #graph #tree
Der Aufgabentext wird für die gewählte Sprache aus dem Russischen übersetzt. Code bleibt unverändert.

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

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

с

vertexми и

рёбрами. it is required find такое подBaum

этого Graphа, которое бы соединяло все его вершины, и при этом обладало наименьшим возможным весом (т.е. суммой весов рёбер). ПодBaum — это набор рёбер, соединяющих все вершины, причём из любой вершины можно добраться до любой другой ровно одним простым путём. Такое подBaum называется минимальным остовным Baumм или просто минимальным остовом.

Легко понять, что любой остов обязательно будет содержать

edge. В естественной постановке эта Aufgabe звучит следующим образом: есть

городов, и для каждой

пары известна стоимость соединения их дорогой (либо известно, что соединить их нельзя). it is required соединить все города так, чтобы можно было доехать из любого города в другой, а при этом стоимость прокладки дорог была бы минимальной.

Prim's algorithm

Этот Algorithmus назван в честь американского математика Роберта Прима (Robert Prim), который открыл этот Algorithmus в 1957 г. Впрочем, ещё в 1930 г. этот Algorithmus был открыт чешским математиком Войтеком Ярником (Vojtěch Jarník). Кроме того, Эдгар Дейкстра (Edsger Dijkstra) в 1959 г. также изобрёл этот Algorithmus, независимо от них.

Описание Algorithmusа

Сам Algorithmus имеет очень простой вид. Искомый minimum остов строится постепенно, добавлением в него рёбер по одному. Изначально остов полагается состоящим из единственной вершины (её можно выбрать произвольно). Затем выбирается edge минимального веса, исходящее из этой вершины, и добавляется в minimum остов. После этого остов содержит уже две вершины, и теперь ищется и добавляется edge минимального веса, имеющее один конец в одной из двух выбранных вершин, а другой — наоборот, во всех остальных, кроме этих двух. И так далее, т.е. всякий раз ищется минимальное по весу edge, один конец которого — уже взятая в остов vertex, а другой конец — ещё не взятая, и это edge добавляется в остов (если таких рёбер несколько, можно взять любое). Этот процесс повторяется до тех пор, пока остов не станет содержать все

вершины (или, что то же самое,

edge). В итоге будет построен остов, являющийся минимальным. Если Graph был изначально не связен, то остов найден не

будет (количество выбранных рёбер останется меньше

).

Beweis

Пусть Graph

был связным, т.е. ответ существует. Обозначим через

остов, найденный Algorithmusом Прима, а через

— minimum остов. Очевидно, что

действительно является остовом (т.е. подBaumм Graphа

). Покажем,

что веса

и

совпадают.

Рассмотрим первый момент времени, когда в

происходило добавление ребра, не Eingabeящего в оптимальный остов

. Обозначим это edge через

, концы его — через

и

, а множество Eingabeящих на тот момент в остов вершин —

через

(согласно Algorithmusу,

,

, либо наоборот). В оптимальном остове

вершины

и

соединяются каким-то путём

; найдём в этом пути любое edge

, один конец которого лежит в

, а другой —

нет. Поскольку Prim's algorithm выбрал edge

вместо ребра

, то это значит, что вес ребра

больше либо равен

весу ребра

.

Удалим теперь из

edge

, и добавим edge

. По только что сказанному, вес остова в результате не мог

увеличиться (уменьшиться он тоже не мог, поскольку

было оптимальным). Кроме того,

не перестало быть остовом

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

в цикл, и потом удалили из этого

цикла одно edge).

Итак, мы показали, что можно выбрать оптимальный остов

таким образом, что он будет включать edge

. Повторяя

эту процедуру необходимое number раз, мы получаем, что можно выбрать оптимальный остов

так, чтобы он совпадал

с

. Следовательно, вес построенного Algorithmusом Прима

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

Реализации

Laufzeit Algorithmusа существенно зависит от того, каким образом мы производим поиск очередного минимального ребра среди подходящих рёбер. Здесь могут быть разные подходы, приводящие к разным Asymptotic complexityм и разным Implementierungм.

Тривиальная Implementierung: Algorithmusы за

и

Если искать каждый раз edge простым просмотром среди всех возможных вариантов, то асимптотически

будет требоваться просмотр

рёбер, чтобы find среди всех допустимых edge с наименьшим весом.

Суммарная Asymptotic complexity Algorithmusа составит в таком случае

, что в худшем случае есть

, —

слишком медленный Algorithmus. Этот Algorithmus можно улучшить, если просматривать каждый раз не все рёбра, а только по одному ребру из каждой уже выбранной вершины. Для этого, наBeispiel, можно отсортировать рёбра из каждой вершины в порядке возрастания весов, и хранить указатель на первое допустимое edge (напомним, допустимы только те рёбра, которые ведут в множество ещё не выбранных вершин). Тогда, если пересчитывать эти указатели при каждом

добавлении ребра в остов, суммарная Asymptotic complexity Algorithmusа будет

, но предварительно

поit is required выполнить сортировку всех рёбер за

, что в худшем случае (для плотных Graphов)

даёт асимптотику

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

Случай плотных Graphов: Algorithmus за

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

у каждой не выбранной ещё вершины — Asymptotic complexity составит

. Но теперь при добавлении в остов очередного ребра и вершины эти указатели надо пересчитывать. Заметим, что эти указатели могут только уменьшаться, т.е. у каждой не просмотренной ещё вершины надо либо оставить её указатель без изменения, либо присвоить ему вес ребра в только что добавленную вершину. Следовательно, эту

фазу можно сделать также за

. Таким образом, мы получили вариант Algorithmusа Прима с асимптотикой . В частности, такая Implementierung особенно удобна для решения так называемой евклидовой задачи

о минимальном остове: когда given

точек на плоскости, расстояние между которыми измеряется

по стандартной евклидовой метрике, и it is required find остов минимального веса, соединяющий их все (причём добавлять новые вершины где-либо в других местах запрещается). Эта Aufgabe решается описанным здесь Algorithmusом

за

времени и

памяти, чего не получится добиться Algorithmusом Крускала. Implementierung Algorithmusа Прима для Graphа, заданного матрицей смежности :

// Eingabeные данные

int n;

vector < vector<int> > g;

const int INF = 1000000000; // значение "бесконечность"

// Algorithmus

vector<bool> used (n);

vector<int> min_e (n, INF), sel_e (n, -1);

min_e[0] = 0;

for (int i=0; i<n; ++i) {
int v = -1;
for (int j=0; j<n; ++j)
if (!used[j] && (v == -1 || min_e[j] < min_e[v]))

v = j;

if (min_e[v] == INF) {

cout << "No MST!";

exit(0);

}

used[v] = true;

if (sel_e[v] != -1)

cout << v << " " << sel_e[v] << endl;

for (int to=0; to<n; ++to)
if (g[v][to] < min_e[to]) {

min_e[to] = g[v][to];

sel_e[to] = v;

} }

На Eingabe подаются number вершин

и матрица

размера

, в которой отмечены веса рёбер, и стоят

числа

, если соответствующее edge отсутствует. Algorithmus поддерживает три Arrayа: флаг

означает, что vertex

включена в остов, величина

хранит вес наименьшего

допустимого ребра из вершины

, а element

содержит конец этого наименьшего ребра (это нужно для

вывода рёбер в ответе). Algorithmus делает

шагов, на каждом из которых выбирает вершину

с наименьшей

меткой

, помечает её

, и затем просматривает все рёбра из этой вершины, пересчитывая их метки.

Случай разреженных Graphов: Algorithmus за

В описанном выше Algorithmusе можно увидеть стандартные операции нахождения минимума в множестве и изменение значений в этом множестве. Эти две операции являются классическими, и выполняются многими структурами данных, наBeispiel, реализованным в языке C++ красно-чёрным Baumм set. По смыслу Algorithmus остаётся точно таким же, однако теперь мы можем find минимальное edge за время .

С другой стороны, время на пересчёт

указателей теперь составит

, что хуже, чем в

вышеописанном Algorithmusе.

Если учесть, что всего будет

пересчётов указателей и

поисков минимального ребра, то

суммарная Asymptotic complexity составит

— для разреженных Graphов это лучше, чем оба

вышеописанных Algorithmusа, но на плотных Graphах этот Algorithmus будет медленнее предыдущего. Implementierung Algorithmusа Прима для Graphа, заданного списками смежности :

// Eingabeные данные

int n;

vector < vector < pair<int,int> > > g;

const int INF = 1000000000; // значение "бесконечность"

// Algorithmus

vector<int> min_e (n, INF), sel_e (n, -1);

min_e[0] = 0;

set < pair<int,int> > q;

q.insert (make_pair (0, 0));

for (int i=0; i<n; ++i) {
if (q.empty()) {

cout << "No MST!";

exit(0);

}

int v = q.begin()->second;

q.erase (q.begin());

if (sel_e[v] != -1)

cout << v << " " << sel_e[v] << endl;

for (size_t j=0; j<g[v].size(); ++j) {
int to = g[v][j].first,

cost = g[v][j].second;

if (cost < min_e[to]) {

q.erase (make_pair (min_e[to], to));

min_e[to] = cost;

sel_e[to] = v;

q.insert (make_pair (min_e[to], to));

} } }

На Eingabe подаются number вершин

и

списков смежности:

— это список всех рёбер, исходящих из вершины

, в

виде пар (второй конец ребра, вес ребра). Algorithmus поддерживает два Arrayа: величина

хранит

вес наименьшего допустимого ребра из вершины

, а element

содержит конец этого наименьшего ребра

(это нужно для вывода рёбер в ответе). Кроме того, поддерживается очередь

из всех вершин в порядке увеличения

их меток

. Algorithmus делает

шагов, на каждом из которых выбирает вершину

с наименьшей меткой

(просто извлекая её из начала очереди), и затем просматривает все рёбра из этой вершины, пересчитывая их метки (при пересчёте мы удаляем из очереди старую величину, и затем кладём обратно новую).

Аналогия с Algorithmusом Дейкстры

В двух описанных только что Algorithmusах прослеживается вполне чёткая аналогия с Algorithmusом Дейкстры: он имеет

такую же структуру (

фаза, на каждой из которых сначала выбирается оптимальное edge, добавляется в ответ, а затем пересчитываются значения для всех не выбранных ещё вершин). Более того, Dijkstra's algorithm тоже имеет

два варианта реализации: за

и

(мы, конечно, здесь не учитываем возможность

использования сложных структур данных для достижения ещё меньших асимптотик). Если взглянуть на Algorithmusы Прима и Дейкстры более формально, то получается, что они вообще идентичны друг другу, за исключением весовой функции вершин: если в Algorithmusе Дейкстры у каждой вершины поддерживается длина кратчайшего пути (т.е. сумма весов некоторых рёбер), то в Algorithmusе Прима каждой вершине приписывается только вес минимального ребра, ведущего в множество уже взятых вершин. На уровне реализации это означает, что после добавления очередной вершины

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

когда мы начинаем просматривать все рёбра

из этой вершины, то в Algorithmusе Прима указатель

обновляется весом ребра

, а в Algorithmusе Дейкстры — метка расстояния

обновляется суммой метки

и веса ребра

. В остальном эти два Algorithmusа можно считать идентичными (хоть они и решают совсем разные задачи).

Свойства минимальных остовов

● maximum остов также можно искать Algorithmusом Прима (наBeispiel, заменив все веса рёбер

на противоположные: Algorithmus не требует неотрицательности весов рёбер).

● minimum остов единственен, если веса всех рёбер различны. В противном случае, может

существовать несколько минимальных остовов (какой именно будет выбран Algorithmusом Прима, зависит от порядка просмотра рёбер/вершин с одинаковыми весами/указателями)

● minimum остов также является остовом, минимальным по произведению всех

рёбер (предполагается, что все веса положительны). В самом деле, если мы заменим веса всех рёбер на их логарифмы, то легко заметить, что в работе Algorithmusа ничего не изменится, и будут найдены те же самые рёбра.

● minimum остов является остовом с минимальным весом самого тяжёлого ребра. Яснее всего

это утверждение понятно, если рассмотреть работу Algorithmusа Крускала.

● Критерий минимальности остова: остов является минимальным тогда и только тогда, когда для любого

ребра, не принадлежащего остову, цикл, образуемый этим edgeм при добавлении к остову, не содержит рёбер тяжелее этого ребра. В самом деле, если для какого-то ребра оказалось, что оно легче некоторых рёбер образуемого цикла, то можно получить остов с меньшим весом (добавив это edge в остов, и удалив самое тяжелое edge из цикла). Если же это Beschreibung не выполнилось ни для одного ребра, то все эти рёбра не улучшают вес остова при их добавлении.

C# Lösung

Auto-Entwurf, vor dem Einreichen prüfen
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.
    // входные данные
    int n;
    vector < List<int> > g;
    const int INF = 1000000000; // значение "бесконечность"
    // алгоритм
    List<bool> used (n);
    List<int> min_e (n, INF), sel_e (n, -1);
    min_e[0] = 0;
    for (int i=0; i<n; ++i) {
            int v = -1;
            for (int j=0; j<n; ++j)
                    if (!used[j] && (v == -1 || min_e[j] < min_e[v]))
                            v = j;
            if (min_e[v] == INF) {
                    Console.WriteLine( "No MST!";
                    exit(0);
            }
            used[v] = true;
            if (sel_e[v] != -1)
                    Console.WriteLine( v << " " << sel_e[v] << endl;
            for (int to=0; to<n; ++to)
                    if (g[v][to] < min_e[to]) {
                            min_e[to] = g[v][to];
                            sel_e[to] = v;
                    }
    }
    // входные данные
    int n;
    vector < vector < pair<int,int> > > g;
    const int INF = 1000000000; // значение "бесконечность"
    // алгоритм
    List<int> min_e (n, INF), sel_e (n, -1);
    min_e[0] = 0;
    set < pair<int,int> > q;
    q.insert (make_pair (0, 0));
    for (int i=0; i<n; ++i) {
            if (q.empty()) {
                    Console.WriteLine( "No MST!";
                    exit(0);
            }
            int v = q.begin()->second;
            q.erase (q.begin());
            if (sel_e[v] != -1)
                    Console.WriteLine( v << " " << sel_e[v] << endl;
            for (size_t j=0; j<g[v].size(); ++j) {
                    int to = g[v][j].first,
                            cost = g[v][j].second;
                    if (cost < min_e[to]) {
                            q.erase (make_pair (min_e[to], to));
                            min_e[to] = cost;
                            sel_e[to] = v;
                            q.insert (make_pair (min_e[to], to));
                    }
            }
    }
}

C++ Lösung

zugeordnet/original
// входные данные
int n;
vector < vector<int> > g;
const int INF = 1000000000; // значение "бесконечность"
// алгоритм
vector<bool> used (n);
vector<int> min_e (n, INF), sel_e (n, -1);
min_e[0] = 0;
for (int i=0; i<n; ++i) {
        int v = -1;
        for (int j=0; j<n; ++j)
                if (!used[j] && (v == -1 || min_e[j] < min_e[v]))
                        v = j;
        if (min_e[v] == INF) {
                cout << "No MST!";
                exit(0);
        }
        used[v] = true;
        if (sel_e[v] != -1)
                cout << v << " " << sel_e[v] << endl;
        for (int to=0; to<n; ++to)
                if (g[v][to] < min_e[to]) {
                        min_e[to] = g[v][to];
                        sel_e[to] = v;
                }
}
// входные данные
int n;
vector < vector < pair<int,int> > > g;
const int INF = 1000000000; // значение "бесконечность"
// алгоритм
vector<int> min_e (n, INF), sel_e (n, -1);
min_e[0] = 0;
set < pair<int,int> > q;
q.insert (make_pair (0, 0));
for (int i=0; i<n; ++i) {
        if (q.empty()) {
                cout << "No MST!";
                exit(0);
        }
        int v = q.begin()->second;
        q.erase (q.begin());
        if (sel_e[v] != -1)
                cout << v << " " << sel_e[v] << endl;
        for (size_t j=0; j<g[v].size(); ++j) {
                int to = g[v][j].first,
                        cost = g[v][j].second;
                if (cost < min_e[to]) {
                        q.erase (make_pair (min_e[to], to));
                        min_e[to] = cost;
                        sel_e[to] = v;
                        q.insert (make_pair (min_e[to], to));
                }
        }
}

Java Lösung

Auto-Entwurf, vor dem Einreichen prüfen
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.
    // входные данные
    int n;
    vector < ArrayList<Integer> > g;
    const int INF = 1000000000; // значение "бесконечность"
    // алгоритм
    ArrayList<Boolean> used (n);
    ArrayList<Integer> min_e (n, INF), sel_e (n, -1);
    min_e[0] = 0;
    for (int i=0; i<n; ++i) {
            int v = -1;
            for (int j=0; j<n; ++j)
                    if (!used[j] && (v == -1 || min_e[j] < min_e[v]))
                            v = j;
            if (min_e[v] == INF) {
                    System.out.println( "No MST!";
                    exit(0);
            }
            used[v] = true;
            if (sel_e[v] != -1)
                    System.out.println( v << " " << sel_e[v] << endl;
            for (int to=0; to<n; ++to)
                    if (g[v][to] < min_e[to]) {
                            min_e[to] = g[v][to];
                            sel_e[to] = v;
                    }
    }
    // входные данные
    int n;
    vector < vector < pair<int,int> > > g;
    const int INF = 1000000000; // значение "бесконечность"
    // алгоритм
    ArrayList<Integer> min_e (n, INF), sel_e (n, -1);
    min_e[0] = 0;
    set < pair<int,int> > q;
    q.insert (make_pair (0, 0));
    for (int i=0; i<n; ++i) {
            if (q.empty()) {
                    System.out.println( "No MST!";
                    exit(0);
            }
            int v = q.begin()->second;
            q.erase (q.begin());
            if (sel_e[v] != -1)
                    System.out.println( v << " " << sel_e[v] << endl;
            for (size_t j=0; j<g[v].size(); ++j) {
                    int to = g[v][j].first,
                            cost = g[v][j].second;
                    if (cost < min_e[to]) {
                            q.erase (make_pair (min_e[to], to));
                            min_e[to] = cost;
                            sel_e[to] = v;
                            q.insert (make_pair (min_e[to], to));
                    }
            }
    }
}

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

Stellen zu dieser Aufgabe

aktive Stellen with overlapping task tags are angezeigt.

Alle Stellen
Es gibt noch keine aktiven Stellen.