Кратчайший путь
Одна из наиболее распространённых (после просто поиска наличия пути) задач на графах — поиск кратчайшего пути. Предполагается, что каждое ребро снабжено положительной длиной (бывают разновидности алгоритмов, позволяющие обрабатывать нулевые и отрицательные длины без модификации графа, но они останутся за рамками настоящего текста). Нужно найти путь между заданной парой вершин с наименьшей суммарной длиной рёбер.
Нами будет рассмотрен единственный алгоритм — алгоритм Дейкстры. Он представляет собой обычный обход в ширину с двумя модификациями:
- вместо множества посещённых вершин — таблица расстояний от начальной
- вместо граничной очереди — очередь с приоритетом
Напомним, что очередь с приоритетом отличается от очереди/стека тем, что:
- каждый элемент сопровождается приоритетом — элементом некоторого линейно упорядоченного множества
- операция изъятия элемента изымает элемент с наименьшим (или же наибольшим) приоритетом
Очередь с приоритетом
Эффективную очередь с приоритетом можно получить из упорядоченного мультисловаря (это когда допускается хранить множество элементов по одному ключу) или же из обычного словаря, преобразовав его в мультисловарь одним из двух способов:
- мультисловарь типа
k -> v
— это словарь типаk -> List v
, гдеList v
— тип списков элементов типаv
- мультисловарь типа
k -> v
— это словарь типа(k, int) -> v
Поясним второй способ подробнее: он интереснее первого и зачастую проще. Идея состоит в том, чтобы сопровождать каждый ключ уникальным числом. Это лишает нас операции «получить значение по ключу», но для упорядоченных словарей наличие такой операции необязательно: она отлично заменяется операцией «найти ключ не меньший, чем заданный».
Этот способ работает по той причине, что пары (в тех языках программирования, где они есть) упорядочены сначала по первой компоненте, а затем (в рамках классов равенства по первой компоненте) — по второй.
Без лишних слов Дейкстра
Возьмём стандартный обход в ширину (из прошлой главы):
std::set<int> component(Graph &g, int v) {
std::deque<int> border{v};
std::set<int> visited{};
while (border.size() > 0) {
int current_vertex = border.front(); border.pop_front();
if (visited.count(current_vertex)) { continue; }
visited.insert(current_vertex);
auto n = neighbors(g,current_vertex);
for (int i : n) {
if (visited.count(i) == 0) { border.push_back(i); }
}
}
return visited;
}
и немного модифицируем:
// всё делается в предположении, что Graph означает следующее:
using Graph = std::map<int, std::map<int, double>>;
std::map<int, double> distances(Graph &f, int v) {
std::map<std::pair<double,int>, int> border;
border[{0,0}] = v;
int unique = 1;
std::map<int, double> visited; // таблица расстояний
while (border.size() > 0) {
// it -- "указатель" на пару вида ((расстояние,мусор),вершина)
auto it = border.begin();
double d = (it->first).first;
int current_vertex = it->second;
border.erase(it);
if (visited.count(current_vertex)) { continue; }
visited[current_vertex] = d;
for (auto n : g[current_vertex]) {
int neighbor = n.first;
double edge_length = n.second;
if (visited.count(neighbor) == 0) {
double new_d = d + edge_length;
border[{new_d, unique++}] = neighbor;
}
}
}
return visited;
}
На каждом шагу алгоритм выбирает вершину, ближайшую к обработанному множеству. Ближайшую — по путям, все рёбра которых, кроме последнего, соединяют вершины обработанного множества.
Нетрудно заметить, что среди кратчайших путей до выбранной вершины есть путь, который имеет именно такой вид: если бы в нём было хотя бы два ребра, выходящих за пределы обработанного множества, он был бы не короче того, в котором одно такое ребро, причём кратчайшее из возможных.
Ну и для полноты картины приведём код на Haskell (без импортов и тех функций, которые не относятся к сути дела):
type Vertex = Int
type Distance = Double
type Graph = Map Vertex (Map Vertex Distance)
neighbors :: Vertex -> Graph -> [(Vertex, Distance)]
neighbors i g = case Map.lookup i g of
Nothing -> []
Just n -> Map.toList n
data Front = Front !Int !(Map (Distance, Int) Vertex)
frontEmpty = Front 0 Map.empty
frontPush (v,d) (Front i m) =
Front (i+1) (Map.insert (d,i) v m)
frontPop (Front i m) = case Map.minViewWithKey m of
Just ( ((d,_),v), m' ) -> Just ( (v,d), Front i m' )
Nothing -> Nothing
distances :: Vertex -> Graph -> Map Vertex Distance
distances i g = go (frontPush (i,0) frontEmpty) Map.empty
where
go front distances = case frontPop front of
Nothing -> distances
Just ( (v,d), front' )
| v `Map.member` distances -> go front' distances
| otherwise ->
let distances' = Map.insert v d distances
n = [(v,d) | (v,d) <- neighbors v g,
not (v `Map.member` distances')]
front'' = foldl' (flip frontPush) front' n
in go front'' distances'