Минимальное остовное дерева графа по алгоритму Краскала
Нахождение минимального остовного дерева во взвешенном неориентированном связном графе по алгоритму Краскала
Создание графа на примере транспортной сети. Нахождение минимального остовного дерева этой сети по алгоритму Краскала. Вывод схематического рисунка найденого результата на двухмерном графики. Рассмотрим пример транспортной сети, где данные считываются из Excel.
Алгоритм Краскала находит минимальное остовное дерево (MST) графа — подмножество рёбер, соединяющее все вершины без циклов и с минимальной суммой весов. Ссылки: https://evileg.com/ru/post/523/#algoritm-kraskala-vs-prima, https://habr.com/ru/articles/569444/.
Описание задачи
Рассматривается железнодорожная сеть Европейского и Приуральского Севера России как взвешенный неориентированный связный граф (см. рисунок). Неориентированный граф, т.е. по ребру можно перемещаться в прямом и обратном направлениях. Взвешенный граф — это такой граф, у которого с каждым из ребер связано некое сопоставимое значение, обычно числовое. В потенциальной транспортной сети вес ребер – это расстояния между узлами. Данные хранятся в Excel: таблица рёбер графа (начальный узел, конечный узел, расстояние) и координаты узлов (x, y) для визуализации .
Задача: найти минимальное остовное дерево (MST) по алгоритму Краскала и визуализировать исходный граф и MST на 2D-схеме с помощью Plots.jl (без привязки к карте).
В качестве исходных данных взята схема перспективной железнодорожной сети, соединяющей экономические центры и транспортные узлы Европейского и Приуральского Севера России.
Железнодорожная транспортная сеть Европейского и Приуральского Севера России на перспективу
Убираем с рисунка карту-подложку (она нам не нужна) - остается неориентированный граф нашей транспортной сети, для которого нам нужно определить минимальное остовное дерево.
Граф транспортной сети

Поиск минимального остова на взвешенном неориентированном графе.
Шаг 1. Подготовка данных
В файле Excel занесем все информацию о исходном графе (см. рисунок).
- левая таблица: все узлы сети, пронумерованные в алфавитном порядка, для удобства обозначения; для каждого узла прописаны координаты (x, y) для отображения графа на двумерном графике. Координаты узла прописываем вручную примерно, как бы они располагались на двухмерной координатной сетке.
- правая таблица: ребра (начальный узел, конечный узел, расстояние) и соответствующие координаты вершин.Эта таблица и будет исходными данными по которым будут производиться расчеты на языке программирования Julia.
Данные загружаются с Excel-файла в таблицу DataFrame.
Исходные данные в Excel-файле
Подключаем нужные нам библиотеки
using DataFrames, Plots
using Graphs, SimpleWeightedGraphs
using XLSX
Если какая-то из библиотек отсутствует, то устанавливаем ее.
]status
# Pkg.add(["XLSX", "Graphs", "SimpleWeightedGraphs"])
Шаг 2. Построение графа и поиск MST
2.1. Считываем данные для расчетов c Excel-файла в DataFrame.
# Считывает данные с Excel-файла три столбца, оборачиваем в таблицу с шапочкой для наглядности.
dano = DataFrame(XLSX.readdata("ZhD_rebra.xlsx", "жд_ВсеПроекты_км", "I2:Q48"), ["УзелН", "УзелК", "УзелНачала", "УзелКонец", "Расстояние", "X1_i", "Y1_i", "X2_i", "Y2_i"])
2.2. Строим граф .В этом примере воспользуемся функциями библиотеки SimpleWeightedGraphs.jl чтобы создать граф. SimpleWeightedGraphs.jl- этот пакет определяет два типа графиков: SimpleWeightedGraph и SimpleWeightedDiGraph.
Для построения графа необходимо определить количество узлов и ребер сети. Найдем их:
# Число ребер (дуг) r = количеству строк в таблице
r = nrow(dano)
# Число узлов (вершин)
n = max(maximum(dano[:,:УзелНачала]), maximum(dano[:,:УзелКонец]))
println( "Количество вершин = ", n)
println("Количество ребер = ", r )
Строим граф:
# Строим граф
GraphZhd=SimpleWeightedGraph(n) # используйте "Простой взвешенный Орграф" для неориентированных графов
#добавляем ребра с их расстояниями (весами) из таблицы "dano"
for i=1:r
add_edge!(GraphZhd, dano[i,:УзелНачала], dano[i,:УзелКонец], dano[i,:Расстояние])
end
# Если вместо расстояния поставить 1, то получим невзвешанный, неориентированный граф
GraphZhd
# проверяем кол-во вершин и ребер графа на построенном нами графе
vershiny = nv(GraphZhd) # Количество вершин
rebra = ne(GraphZhd) # Количество ребер
println( "Количество вершин = ", vershiny)
println("Количество ребер = ", rebra )
2.3. Вычисляем минимальный остов графа с помощью алгоритма Краскала. Используем функцию kruskal_mst() из Graphs.jl для взвешенного графа.
Функция kruskal_mst() из библиотеки Graphs.jl реализует алгоритм Краскала для поиска минимального (по умолчанию) остовного дерева в связном неориентированном графе [https://juliagraphs.org/Graphs.jl/dev/algorithms/spanningtrees/].
#= Возвращает вектор ребер, представляющий минимальное (по умолчанию) остовное дерево связного неориентированного графа GraphZhd
с матрицей расстояний, используя алгоритм Крускала .=#
mintree = kruskal_mst(GraphZhd; minimize=true)
# Справочная информация для вывода показателей из вектора, вычисленного по алгоритму Краскала.
mintree[1].src # номер вершины начала_ребра 1
mintree[1].dst # номер вершины конец_ребра 1
mintree[1].weight # вес ребра 1
vershiny_1 = Int64[mintree[i].src for i in 1:length(mintree)] # номера вершин начало ребер минимального остова
vershiny_2 = Int64[mintree[i].dst for i in 1:length(mintree)] # номера вершин конца ребер минимального остова
dist = Float64[mintree[i].weight for i=1:length(mintree)]# вес ребра
# Количество ребер в минимальном остове стало
length(mintree)
# Число вершин в минимальном остове должны остаться все
n_min = max(maximum(vershiny_1), maximum(vershiny_2) )
Результаты вычисления MST по алгоритму Краскала занесем в таблицу:
# Таблица по результатам вычисления (узлы и ребра минимального остова графа)
min_result = DataFrame([vershiny_1 vershiny_2], ["УзелСтарт", "УзелФиниш"])
Шаг 3. Визуализация
Исходный граф: чёрные линии; MST: розовые прозрачные линии поверх исходного графа.
Из исходной таблицы Excel-файла добавляем соответсвующие узлам старта и финиша их координаты (X1, Y1, X2, Y2) в таблицу с ребрами минимального остова (min_result). Они нужны для построения графика (рисовка схемы графа).
# считывает исходные данные вершин графа
дано = DataFrame(XLSX.readdata("ZhD_rebra.xlsx", "жд_ВсеПроекты_км", "B2:F43"), ["НазваниеУзла", "НомерНаКарте", "Номер_пп", "X", "Y"])
# Создаём словарь: Номер_пп → X-координата
dict_X = Dict(дано.Номер_пп .=> дано.X)
# Добавляем столбец с X-координатой, подставляя 0 при отсутствии
#Это в точности имитирует работу ВПР: поиск значения в первом столбце диапазона и возврат значения из заданного столбца.
min_result.X1 = [get(dict_X, Номер_пп, 0) for Номер_пп in min_result.УзелСтарт]
min_result.X2 = [get(dict_X, Номер_пп, 0) for Номер_пп in min_result.УзелФиниш]
# Создаём словарь: Номер_пп → Y-координата
dict_Y = Dict(дано.Номер_пп .=> дано.Y)
# Добавляем столбец с X-координатой, подставляя 0 при отсутствии
#Это в точности имитирует работу ВПР: поиск значения в первом столбце диапазона и возврат значения из заданного столбца.
min_result.Y1 = [get(dict_Y, Номер_пп, 0) for Номер_пп in min_result.УзелСтарт]
min_result.Y2 = [get(dict_Y, Номер_пп, 0) for Номер_пп in min_result.УзелФиниш]
# Выводим таблицу, дополненную координатами узлов
min_result
Строим ребра исходного графа на графики (черная штриховая линия)
# Стоим ребра опорной железнодорожной сети ЕиПСР
p = plot(size=(1000, 800), legend=false) # отключение легенды (опционально)
for row in eachrow(dano)
plot!([row.X1_i, row.X2_i], [row.Y1_i, row.Y2_i],
color=:black, # чёрный цвет
linewidth=3, # толщина (опционально)
linestyle = :dash, # штриховая линия
label="",
xaxis = false, # убрать ось X
yaxis = false, # убрать ось Y
background_color = :transparent) # прозрачный фон графика
end
p
По верху предыдущего графика строим ребра минимального состова графа (розовая прозрачная линия)
# Стоим ребра железнодорожной сети ЕиПСР минимального остова (по алгоритму Краскаля), розовый прозрачный
for row in eachrow(min_result)
plot!([row.X1, row.X2], [row.Y1, row.Y2],
color=:red, alpha=0.4, linewidth=9, label="") # alpha=прозрачность 0.4
end
p
Наносим на график вершины (узлы) графа
# Собираем все узлы
all_nodes = vcat(dano.УзелНачала, dano.УзелКонец)
all_x = vcat(dano.X1_i, dano.X2_i)
all_y = vcat(dano.Y1_i, dano.Y2_i)
# Уникальные узлы с их координатами
unique_nodes = Dict{Int, Tuple{Float64, Float64}}()
for (node, x, y) in zip(all_nodes, all_x, all_y)
if !haskey(unique_nodes, node)
unique_nodes[node] = (x, y)
end
end
# Рисуем узлы сети, которые являются ж/д станциями
for (node, (x, y)) in unique_nodes
scatter!([x], [y],
markersize=15,
color=:white,
markerstrokewidth=2,
markerstrokecolor=:grey,
label="")
annotate!(x, y, text("$node", 10, :black, :center))
end
# Можно добавить заголовок и сохранить рисунок (в данном примере фон прозрачный)
title!("Минимальный остов сети, расчитанный по алгоритму Краскала", titlefontsize = 10)
savefig(p, "жд_ВсеПроекты.png")
p
Заключение:
В данном примере нахождения минимального остовного дерева во взвешенном неориентированном связном графе по алгоритму Краскала продемонстрированы возможности: считывание данных с Excel-файла, используя библиотеку XLSX.jl; создание графа и нахождение минимального остова, используя библиотеку Graphs.jl; построение схемы (рисунка) графа, используя библиотеку Plots.jl (визуализировать транспортную сеть без привлечения картографических инструментов). При большом числе вершин ручной ввод координат может быть трудоёмким — зависит от ваших целей. Подход удобен для учебных задач без картографических библиотек.
