Документация Engee
Notebook

Поиск кратчайшего пути на графе

В этом примере мы воспользуемся функциями библиотеки Graphs.jl чтобы создать граф и найти на нем кратчайший маршрут между двумя точками.

Работа с графами

Графы часто используются как элемент моделей в задачах логистики, транспортных, информационных или социальных сетей и других связанных систем. Они являются абстрактными математическими объектами, состоящими из вершин (vertex, vertices) и рёбер (edges). Обычно каждая вершина имеет свой уникальный номер, а ребра обозначаются двумя числами – номерами вершин, которые они соединяют.

К узлам и ребрам может быть привязана какая-то информация (например, координаты или пропускная способность). В библиотеке Graphs.jl предполагается, что дополнительная информация хранится во внешней таблице или словаре (Dict). Записи могут быть привязаны к вершинам (через их номера) или ребрам (через кортеж из чисел, идентифицирующих ребро). Для экономии можно использовать разреженные матрицы (SparseArrays.jl).

In [ ]:
# Если библиотека у вас установлена, закомментируйте следующую строчку:
import Pkg; Pkg.add( ["Graphs", "GraphRecipes"] )
In [ ]:
using Graphs, GraphRecipes

Пакет GraphsRecipes.jl отвечает за отрисовку графов.

Создадим простой направленный граф, содержащий три узла и несколько ребер.

In [ ]:
g = DiGraph()
add_vertices!( g, 3 );

Сейчас в нашем графе три вершины, соединим их ребрами.

In [ ]:
add_edge!( g, 1, 2 );
add_edge!( g, 1, 3 );
add_edge!( g, 2, 3 );

И выведем этот граф:

In [ ]:
gr() # Упрощенная отрисовка
graphplot( g, names=1:nv(g) )
Out[0]:

Обратите внимание, что имена узлов names взяты из "внешнего" массива 1:nv(g), где хранятся номера узлов от 1 до nv (number of vertices, количество вершин).

Создадим карту, на которой будем искать путь

В качестве поля для поиска кратчайшего пути мы возьмём случайную матрицу и зададимся целью пройти по ней вдоль главной диагонали. Связанность этой "карты" и возможность достичь точки Б из точки А, увы, не обеспечена ничем, кроме выбора "правильного" стартового значения генератора псевдослучайных чисел Random.seed!().

In [ ]:
using Random;
Random.seed!( 4 )

карта = rand( 30, 30 ) .> 0.3;
heatmap( карта )

# Точка старта и финиша
точка_А = CartesianIndex(1,1)
точка_Б = CartesianIndex(30,30)

# Добавим на график токи старта и финиша
scatter!( [точка_А[1]], [точка_А[2]], c=:red )
scatter!( [точка_Б[1]], [точка_Б[2]], c=:green, leg=false )
Out[0]:

Наш граф будет ненаправленным, то есть вдоль каждого ребра можно будет пройти как туда, так и обратно.

In [ ]:
g = Graph();
индексы_доступных_точек = LinearIndices( карта )[ карта .== 1 ]
println( "Количество площадок, куда можно наступать: ", count(индексы_доступных_точек .> 0 ))
Количество площадок, куда можно наступать: 634

Вспомогательные объекты (перевод координат)

Сохраним матрицу, где будут храниться 2D координаты всех точек карты.

In [ ]:
координаты_всех_точек = CartesianIndices( size(карта) );

Создадим граф, который переводит номер вершины графа в координату:

In [ ]:
координаты_точек_графа = Dict(zip( 1:length(индексы_доступных_точек), координаты_всех_точек[индексы_доступных_точек] ))
first( sort(collect(координаты_точек_графа)), 5 )
Out[0]:
5-element Vector{Pair{Int64, CartesianIndex{2}}}:
 1 => CartesianIndex(1, 1)
 2 => CartesianIndex(2, 1)
 3 => CartesianIndex(3, 1)
 4 => CartesianIndex(4, 1)
 5 => CartesianIndex(5, 1)

Теперь – инвертируем этот словарь и получим возможность переводить координату в номер точки на графе.

In [ ]:
индексы_точек_по_координатам = Dict(value => key for (key, value) in координаты_точек_графа)
first( sort(collect(индексы_точек_по_координатам)), 5 )
Out[0]:
5-element Vector{Pair{CartesianIndex{2}, Int64}}:
 CartesianIndex(1, 1) => 1
 CartesianIndex(2, 1) => 2
 CartesianIndex(3, 1) => 3
 CartesianIndex(4, 1) => 4
 CartesianIndex(5, 1) => 5

Сложим координаты точек в массивы xx и yy – их мы будем передавать в процедуру отрисовки графа.

In [ ]:
xy = Tuple.(first.(sort(collect(индексы_точек_по_координатам))));
xx = first.(xy);
yy = last.(xy);

Наполнение графа

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

In [ ]:
g = Graph()
add_vertices!( g, length(координаты_всех_точек[ индексы_доступных_точек ][:]) )
Out[0]:
634

Пройдемся по всем точкам и соединим их с соседними точками, если между ними есть проход.

In [ ]:
MAX_X,MAX_Y = size( карта )

for вершина in 1:nv(g)
    x,y = Tuple(координаты_точек_графа[вершина])
    точка_справа = CartesianIndex( x + 1, y )
    точка_снизу = CartesianIndex( x, y + 1 )
    
    # Соединим текущую точку с точкой справа, если есть связанность согласно карте пройденных точек 
    if x < MAX_X && карта[точка_справа] == 1 add_edge!(g, вершина, индексы_точек_по_координатам[точка_справа]); end;
    # То же самое для точки снизу
    if y < MAX_Y && карта[точка_снизу] == 1 add_edge!(g, вершина, индексы_точек_по_координатам[точка_снизу]); end;
end

Посмотрим, добавились ли в наш граф ребра, и сколько их:

In [ ]:
g
Out[0]:
{634, 871} undirected simple Int64 graph

Судя по выводу, к 634 точкам добавилось 840 ребер. Выведем график:

In [ ]:
heatmap( карта', title="Карта", titlefont=font(9), leg=:false )
graphplot!( g, x = xx, y = yy, nodeshape=:circle, nodecolor=:red, nodestrokecolor=:red, nodesize=1.5, edgecolor=:red, curves=true, size=(500,500) )
Out[0]:

Нам не пришлось "раскладывать" граф, поскольку мы уже знаем координаты каждого узла. Остальные параметры отрисовки можно изучить здесь.

Поиск кратчайшего пути

Мы используем алгоритм A-star, осуществляющий поиск кратчайшего пути с использованием эвристик. На выходе мы получим набор ребер.

In [ ]:
# Нахождение кратчайшего пути
path = a_star( g, индексы_точек_по_координатам[точка_А], индексы_точек_по_координатам[точка_Б] );
first( path, 5 )
Out[0]:
5-element Vector{Graphs.SimpleGraphs.SimpleEdge{Int64}}:
 Edge 1 => 2
 Edge 2 => 27
 Edge 27 => 51
 Edge 51 => 52
 Edge 52 => 79

Составим массив из точек графа, которые нужно посетить, чтобы пройти от точки А к точке Б.

In [ ]:
points = Tuple.( vcat([точка_А], [ координаты_точек_графа[dst(v)] for v in path ]) );
first( points, 5 )
Out[0]:
5-element Vector{Tuple{Int64, Int64}}:
 (1, 1)
 (2, 1)
 (2, 2)
 (2, 3)
 (3, 3)
In [ ]:
# Вывод карты (со смещенными значениями для нужной яркости графика)
heatmap( карта' .* 0.3 .+ 0.7, title="Кратчайший путь", leg=false, c=:grays, aspect_ratio=:equal )

# Вывод графа допустимых перемещений по карте
#graphplot!( g, x = xx, y = yy, curves=true, nodeshape=:circle, size=(400,400) )

# Вывод кратчайшего пути
plot!( first.(points), last.(points), lw=3, linez=range(0,1,length(points)), c=:viridis, yflip=true )
Out[0]:

Заключение

Графы – очень мощные математические объекты, которые позволяют находить структуру в разрозненных данных – циклы, деревья, кратчайшие или наиболее длинные пути и связанные области. Возможность работать с ними из среды моделирования динамических систем дает инженерам очень интересные возможности.