Построение минимального остовного дерева алгоритмом Борувки. Программная реализация
Гаврилов А. И.1, Тишин В. В.2
Гаврилов Александр Иванович / Gavrilov Alexandr Ivanovich - студент;
2Тишин Владимир Викторович / Tishin Vladimir Viktorovich - доцент, научный руководитель,
кафедра прикладной математики,
Самарский государственный аэрокосмический университет имени академика С. П. Королева, г. Самара
Аннотация: в статье рассматривается написание алгоритма поиска минимального остовного дерева на языке программирования высокого уровня. Применение этого алгоритма на практике, оценка его скорости вычисления.
Ключевые слова: графы, минимальное остовное дерево, компоненты связности, алгоритм Борувки, программирование алгоритма минимального остовного дерева, графы в практическом применении.
Введение
В настоящее время с развитием различного рода сетей: нейронных, сети Интернет, транспортных, сотовых, электрических и т.п. возникает задача создания таких сетей с минимальными затратами, а также их эффективное использование. Различные компании тратят огромные деньги на развитие логистики и решение подобных задач. А все данные задачи сводятся к одной задаче теории графов -построения минимального остовного дерева. Для примера можно взять карту России, в которой вершами будут являться населенные пункты, а ребра - это дороги между этими населенными пунктами, получился связный граф. Одна ветка какого-либо метрополитена является тоже графом (деревом).
Основные определения
Дадим основные определения терминам, которые будем использовать [4, с. 4].
Граф, или неориентированный граф G — это упорядоченная пара G = = (V,E), где V — это непустое множество вершин или узлов, а Е — множество пар (в случае неориентированного графа — неупорядоченных) вершин, называемых рёбрами.
Минимальное остовное дерево - это остовное дерево этого графа, имеющее минимальный возможный вес, где под весом дерева понимается сумма весов входящих в него рёбер.
Остовное дерево — ациклический связный подграф данного связного неориентированного графа, в который входят все его вершины V, содержащий ровно Е = V — 1 ребер.
Компонента связности [5, с. 151] - некоторое множество вершин графа, такое, что для любых двух вершин из этого множества существует путь из одной в другую, и не существует пути из вершины этого множества в вершину не из этого множества.
A
A
D
Рис. 1. Неориент. граф и его остов
D
Рис. 1. Неориентированный граф и его остовное дерево
Алгоритм Борувки построения остовного минимального дерева
Для решения подобных задач также существуют алгоритмы Краскала и Прима, но мы решили остановиться на алгоритме Борувки, так как программных реализаций на каком-либо языке программирования этого алгоритма в сети почти не встречается [1, с. 482]. А для Краскала и Прима они в достатке, да и алгоритм более интересен на наш взгляд. Алгоритм был впервые опубликован в 1926 году Отакаром Борувкой в качестве метода нахождения оптимальной электрической сети. Сам алгоритм состоит из следующих шагов:
1. Построим граф T. Изначально T содержит все вершины из G и не содержит ребер (каждая вершина в графе T — отдельная компонента связности).
2. Будем добавлять в граф T ребра следующим образом, пока T не является деревом:
a. для каждой компоненты связности находим минимальное по весу ребро, которое связывает эту компоненту с другой;
b. добавляем в T все найденные рёбра минимального веса;
c. повторяем шаг 2 до тех пор, пока не будут связаны все компоненты связности.
3. Получившийся граф T является минимальным остовным деревом графа G.
На первый взгляд ничего сложного в алгоритме нет, но вся трудность сводится к программной реализации. Для этого перейдем к псевдокоду для более наглядного представления, как это будет выглядеть в программном коде [2, с. 5]:
// G - исходный граф
// w - весовая функция function boruvkaMST()
{
while (T.size < n - 1)
{
for k £ Component // Component - множество компонент связности в Т
{
w (minEdge[k]) = оо; // для каждой компоненты связности вес минимального ребра равен бесконечности }
find Comp(T); // разбиваем граф T на компоненты связности обычным dfs-ом for u.comp ! = v.comp if (w(minEdge [u. comp]) < w(u, v))
{
minEdge [u. comp] < (u, v);
}
if (w (minEdge[v. comp])) < w(u, v))
{
minEdge [v. comp] = (u, v);
}
for k Component
T.add Edge (minEdge[k]); // добавляем ребро, если его не было в T return T;
}
}
Далее можно приступать к программной реализации. Код был написан на языке высокого уровня C++, компилятор Visual Studio. Для более конкретных действий рассмотрим следующую практическую задачу, которая реально встречается среди нефтяных компаний, занимающихся транспортировкой нефти. Заказчик дает исполнителю задачу, таблицу с расстояниями между заводами (табл.1) и топологию местности (рис. 2). Будем ее решать по нашему алгоритму.
Задача: Необходимо построить систему нефтепроводов, которые должны соединять семь нефтеочистительных заводов (Н1, Н2, Н3, Н4, Н5, Н6, Н7), принадлежащих компании «Нефтедобыча», с портом (П), куда поступает импортируемая сырая нефть. Стоимость прокладки нефтепровода между любыми двумя пунктами составляет 5000 долларов в расчете на одну милю. Расстояния между всеми парами вершин задаются в следующей таблице [6, с. 40]:
Таблица 1. Матрица расстояний
П Н1 Н2 Н3 Н4 Н5 Н6 Н7
П 0 5 6 8 2 6 9 10
Н1 0 4 10 5 8 6 10
Н2 0 11 8 4 9 10
Н3 0 10 3 6 7
Н4 0 2 5 9
Н5 0 10 5
Н6 0 8
Н7 0
Рис. 2. Топология заводов
Для начала по нашей исходной карте (рис. 2) рисуется неориентированный граф (рис. 3).
Рис. 3. Граф (V - завод, E - нефтепровод)
Идем по алгоритму. Шаг 1. Строится минимальный покрывающий лес (рис. 4), состоящий только из вершин, каждая вершина -компонента связности.
Рис. 4
Шаг 2. Для каждой компоненты (вершины) находим ребра минимального веса (рис. 5).
Рис. 5
Шаг 3. Добавляем найденные ребра к минимальному остовному дереву. Получилось 2 компоненты связности (рис. 6).
Рис. 6
Шаг 4. Для получившихся компонент снова находим ребра минимального веса (рис. 7).
Рис. 7
Шаг 5. Добавляем найденные ребра к минимальному остовному дереву. На этом шаге минимальное дерево уже построено, так как получилась одна компонента связности (рис. 8).
Рис. 8. Минимальное остовное дерево
Осталось подсчитать минимальный остовной вес. Ребра на рисунках не подписывались, для того чтобы их не загромождать. Смотрим таблицу расстояний и в итоге получаем расчеты: минимальный остовной вес: < Н 1 , Н2 > + < Н2 , Н5 > + < Н3 , Н5 > + < Н5 , Н4 > + < Н 5 , Н 7>+ <Н4, Н 6 >+< Н 4, П > = 2 5 , где < Н 1 ,Н 2> — в ес р е бр а Н 1 , Н2 . Стоимость нефтепровода составила 25*5000 = 125000 долларов.
В конечном итоге передаем заказчику топологию, соответствующую поставленной задаче (рис. 9).
Рис. 9. Топология нефтепроводов
Программная часть.
Основную работу программы выполняет следующий шаблон класса [3, с. 1]:
template < class Graph, class Edge > class MST { const Graph &G; vector<Edge *> a, b, mst;
UF uf; public:
MST (const Graph &G): G (G), uf (G.V()), mst (G.V()+1)
{ a = edges < Graph, Edge > (G); int N, k = 1;
for (int E = a.size(); E != 0; E = N)
{ int h, i, j;
b.assign (G.V(), 0);
for (h = 0, N = 0; h < E; h++)
{ Edge *e = a [h];
i = uf.find (e->v()), j = uf.find (e->w()); if (i == j) continue;
if (!b[i] || e- > wt() < b[i]->wt()) b[i] = e; if (!b[j] || e->wt() < b[j]->wt()) b[j] = e; a[N++] = e;
}
for (h = 0; h < G.V(); h++) if (b [h])
if (!uf.find(i = b[h]->v(), j = b[h]->w()))
{ uf.unite (i, j); mst [k++] = b [h]; }
}
}
};
Хранение ребер происходит в виде массива векторов, а вершин как обычный массив. Функция find связывает индексы с MST (минимальное остовное дерево)-поддеревьями, по мере их построения. На каждом этапе проверяются все оставшиеся ребра и те, которые соединяют отдельные поддеревья, сохраняются до следующего этапа. Массив а содержит еще ребра, которые не успели отбросить и не включили в MST-поддеревья. Индекс N используется для хранения ребер, отложенных до следующего этапа (в конце каждого этапа E = N). Индекс h используется для доступа к следующему проверяемому ребру (Edge *e = a[h]). Хранение ближайших соседей каждой компоненты хранится в массиве b с find номерами компонентов в качестве индексов. В конце каждого этапа каждый компонент соединяется с ближайшим соседним, а ребра ближайшей соседней вершины добавляются в дерево MST [2, с. 5].
Рис. 10. Пример работы программы
Время вычисления
Поскольку на каждом этапе количество деревьев в лесе уменьшается по крайней мере наполовину (худший случай - все компоненты разбиваются на пары), то количество этапов не превышает значения О (1 о g (7) ) . Для выполнения каждого этапа основного цикла требуется О(Е) времени. Общее время работы алгоритма получается равным О (Еlog(7) ) [2 , с. 1 5 ] .
Литература
1. Кормен Т. Х., Лейзерсон Ч. И., Ривест Р. Л., Штайн К. Алгоритмы: построение и анализ, 2-е изд. — М.: Вильямс, 2005. — 1296 с.
2. Chazelle B. A Minimum Spanning Tree Algorithm with Inverse-Ackermann Type Complexity. Journal of the ACM, 47 (2000), 1028-1047 с.
3. СеджвикР. Алгоритмы на С++, 1-е изд. - Вильямс, 2011г. - лекция 20.
4. Додонова Н. Л. Теория конечных графов и ее применение - конспект лекций, специальность ИБАС, 2014 г.
5. СвамиМ., Тхуласираман К. Графы, сети и алгоритмы. М.: Мир, 1984, — 454 с.
6. Майника Э. Алгоритмы оптимизации на сетях и графах, изд. - Мир, 1981, - 326 с.