Описание проблемы

Дана следующая схема дорог, связывающих города. Каждая дорога имеет цену, которую надо заплатить, чтобы по ней проехать.

схема дорог, связывающих города

Рисунок — схема дорог, связывающих города

Моя задача — добраться из города Москва до Новгород по минимальной цене.

надо добраться из Москвы до Новгорода по минимальной цене

Рисунок — надо добраться из Москвы до Новгорода по минимальной цене

💡 В решении моей проблемы поможет алгоритм Дейкстры.

Введу следующие ограничения ( допущения, дополнения ):

➡ Города буду называть вершинами ( узлами ).

➡ Дороги, связывающие города буду называть ребрами.

➡ Все дороги двусторонние. Это означает, если можно проехать из Москвы до Самары, то также можно и из Самары до Москвы.

➡ Стоимость проезда по дороге — положительная величина. Почему алгоритм Дейкстры работает некорректно для ребер отрицательного веса — об этом будет написано ниже.

Переформулирую проблему в терминах теории графов: необходимо найти кратчайший путь ( путь минимального веса ) от заданной вершины неориентированного графа до другой заданной вершины.

💡 Классический алгоритм Дейкстры находит кратчайшие пути от заданной вершины до всех остальных вершин графа.

Графический анализ алгоритма Дейкстры

Начальный этап — этап инициализации. До каждого города, кроме стартового, принимается расстояние, равное бесконечности ( $\infty$ ).

расстояние до всех вершин от заданной равно $\infty$

Рисунок — расстояние до всех вершин от заданной равно $\infty$

Также я хочу ввести следующие обозначения для статусов вершин ( названий городов ). Эти статусы будет постоянно меняться в процессе работы алгоритма Дейкстры. В качестве примера рассмотрю город Самара.

Название статуса Графическое обозначение
Непосещенная вершина ( город ) Самара
В процессе обработки Самара
Обработанная вершина ( город ) Самара

На этапе инициализации в работу включается стартовая вершина, то есть город Москва, поэтому графическое состояние схемы дорог будет следующим:

этап инициализации алгоритма Дейкстры

Рисунок — этап инициализации алгоритма Дейкстры

На следующем этапе необходимо выбрать один из городов, имеющих статус «В процессе обработки», но при этом у этого города должна быть минимальная метка стоимости. На данный момент только один город имеет такой статус. Это город Москва. Метка стоимости у этого города равна $0$. Это справедливо, так как добраться до Москвы ничего не стоит, так как изначально мы находимся в Москве.

И сейчас необходимо от этого города ( Москва ) пройти ко всем соседним связанным городам, у которых статус не равен «Обработанная вершина». Москва напрямую связана только с четырьмя городами:

    • Самара;
    • Петербург;
    • Чита;
    • Новгород.

При этом все эти четыре города имеют статус непосещенных.

перебор всех городов-соседей для города Москва

Рисунок — перебор всех городов-соседей для города Москва

➡ В какой последовательности перебирать смежные города? Абсолютно не важно! Можно сначала рассмотреть связь Москва Самара, а можно Москва Новгород и т.д. На конечный результат алгоритма Дейкстры это никак не повлияет.

Покажу, какие проверки происходят, когда делается анализ связи, например, Москва Чита. Текущая ценовая метка у города Чита равна $\infty$. Стоимость проезда между Москвой и Читой составляет $38$. Ценовая метка у города Москва равна $0$. Следовательно, чтобы добраться от Москвы до Читы достаточно потратить $0 + 38 = 38$, а это меньше, чем $\infty$.

💡 Поэтому меняем ценовую метку у города Чита на $38$.

найден более дешевый путь ( $38$ ) от Москвы до Читы

Рисунок — найден более дешевый путь ( $38$ ) от Москвы до Читы

Аналогично пересчитываются весовые метки у Самары, Петербурга и Новгорода.

➡ После этого город Москва считается посещенным и в дальнейших расчетах участия не принимает! А только что пересчитанные города-соседи принимают статус «В работе».

В результате схема дорог принимает такой вид:

схема дорог после обработки города Москва и ее соседей

Рисунок — схема дорог после обработки города Москва и ее соседей

На следующем этапе необходимо выбрать город, у которого статус «В работе», а также этот город должен иметь наименьшую метку стоимости. На данный момент $4$ города удовлетворяют этому условию. Выпишу их:

Название города Стоимость проезда до города
Чита $38$
Самара $14$
Петербург $5$
Новгород $65$

➡ Минимальную стоимость проезда имеет Петербург ( $5$ единиц ). Поэтому подвергаю обработке именно этот город по аналогии, как это было сделано с Москвой.

Необходимо от этого города ( Петербург ) пройти ко всем соседним связанным городам, у которых статус не равен «Обработанная вершина». Петербург напрямую связан только с четырьмя городами:

  • Москва;
  • Самара;
  • Новосибирск;
  • Екатеринбург.

Но Москва имеет статус «Обработанная вершина», поэтому в вычислениях она участия не принимает. По факту остается лишь $3$ соседа для Петербурга.

Рисунок - перебор всех городов-соседей для города Петербург

Рисунок — перебор всех городов-соседей для города Петербург

💡 Обращаю особое внимание на пару смежных городов ПетербургСамара. Самара, несмотря на то, что имеет статус «В работе» принимает участие в вычислениях кратчайшего пути.

Пересчитываем стоимости проезда до Самары, Новосибирска и Екатеринбурга. Всем этим городам ставим статус «В работе», а узел-Петербург принимает статус «Обработан» и в дальнейших вычислениях, как и Москва, он уже участие не принимает.

схема дорог после обработки города Петербург и его соседей

Рисунок — схема дорог после обработки города Петербург и его соседей

На следующем этапе необходимо выбрать город, у которого статус «В работе», а также этот город должен иметь наименьшую метку стоимости. На данный момент $5$ городов удовлетворяют этому условию. Выпишу их:

Название города Стоимость проезда до города
Чита $38$
Самара $14$
Новгород $65$
Новосибирск
$20$
Екатеринбург
$48$

➡ Минимальную стоимость проезда имеет Самара ( $14$ единиц ). Поэтому подвергаю обработке именно этот город по аналогии, как это было сделано с Москвой и Петербургом.

Необходимо от этого города ( Самара ) пройти ко всем соседним связанным городам, у которых статус не равен «Обработанная вершина». Самара напрямую связана только с тремя городами:

  • Москва;
  • Петербург;
  • Чита.

Но Москва и Петербург имеет статус «Обработанная вершина», поэтому в вычислениях они участия не принимают. По факту остается лишь $1$ сосед для Самары.

перебор всех городов-соседей для города Самара

Рисунок — перебор всех городов-соседей для города Самара

Пересчитываю стоимость от Самары до Читы: $14 + 9 = 23 < 38$ — да, следовательно, надо переписать ценовую метку для Читы, а сам город принимает статус «Посещен».

схема дорог после обработки города Самара и ее соседей

Рисунок — схема дорог после обработки города Самара и ее соседей

На следующем этапе снова выбирается город, имеющий статус «В работе» и минимальную ценовую метку. Это город Новосибирск. Происходит пересчет стоимости пути до его городов-соседей, у которых статус отличен от «Посещен».

схема дорог после обработки города Новосибирск и его соседей

Рисунок — схема дорог после обработки города Новосибирск и его соседей

Сейчас следует пересчитать город Чита ( имеет минимальную ценовую метку, равную $23$ ) и ее возможных соседей. Кстати, все смежные города для Читы уже обработаны ( Самара, Москва, Новосибирск ), поэтому город просто принимает статус «Посещен».

схема дорог после обработки города Чита и ее соседей

Рисунок — схема дорог после обработки города Чита и ее соседей

Из двух оставшихся необработанных городов берем Екатеринбург, так как его ценовая метка ( $26$ ) меньше, чем у Новгорода ( $41$ ). После пересчета Екатеринбурга получаем следующую картину:

схема дорог после обработки города Екатеринбург и его соседей

Рисунок — схема дорог после обработки города Екатеринбург и его соседей

И на завершающем этапе пересчитывается последний необработанный город. Это Новгород. Очевидно, что ценовые метки не меняются.

схема дорог после обработки города Новгород и его соседей

Рисунок — схема дорог после обработки города Новгород и его соседей

В результате у каждого города сформирована минимальная ценовая метка, которая показывает, сколько потребуется заплатить, чтобы добраться от Москвы до этого города:

# Город Минимальная стоимость проезда
$1$ Москва $0$
$2$ Петербург $5$
$3$ Новосибирск $20$
$4$ Самара $14$
$5$ Новгород $30$
$6$ Чита $23$
$7$ Екатеринбург $26$

Мне нужно было узнать, какова минимальная стоимость пути из Москвы в Новгород. Алгоритм Дейкстры дал мне ответ на этот вопрос: придется заплатить $30$ единиц.

Дополнительно можно еще посмотреть сам маршрут ( кратчайший путь ) из Москвы в Новгород:

кратчайший путь ( минимальный по цене ) из Москвы в Новгород

Рисунок — кратчайший путь ( минимальный по цене ) из Москвы в Новгород

Маршрут минимальной стоимости: Москва Петербург Новосибирск Екатеринбург Новгород. Маршрут состоит из $5$ городов и его стоимость $30$ единиц.

💡 Вообще, задача классического алгоритма Дейкстры — нахождение кратчайших путей от заданной вершины до всех остальных. Речи не идет о том, чтобы восстановить сам этот минимальный путь. Но, как правило, это нужно уметь делать. И я покажу, как получить сам кратчайший путь ( перечислением посещенных вершин ), но чуть позже, когда пойдет детальный анализ структур данных.

Почему алгоритм Дейкстры не работает на графах, имеющих ребра отрицательного веса

➡ Это очень популярный вопрос на все возможных программистских форумах и, в том числе, от моих учеников.

Для этого я рассмотрю небольшой неориентированный взвешенный граф, состоящий буквально из $4$ вершин. Одно из ребер будет иметь отрицательный вес.

неориентированный взвешенный граф для алгоритма Дейкстры

Рисунок — неориентированный взвешенный граф, имеющий одно ребро отрицательного веса, для алгоритма Дейкстры

➡ Ребро между вершинами $2$ и $4$ имеет отрицательный вес, равный $-8$.



Например, я попробую найти кратчайший путь между вершинами $3$ и $1$. Очевидно, для реализации задуманного, мне нужно будет запустить алгоритм Дейкстры от вершины $3$.

Инициализация вершин графа перед запуском алгоритма Дейкстры

Рисунок — инициализация вершин графа перед запуском алгоритма Дейкстры

После обработки стартовой вершины ( под номером $3$ ) получим такое состояние графа:

состояние графа после обработки вершины $3$

Рисунок — состояние графа после обработки вершины $3$

Среди вершин, имеющих статус «В работе», вершина номер $2$ имеет наименьшую ценовую метку, равную $5$. Поэтому обрабатываю теперь вершину $2$ и получаю:

состояние графа после обработки вершины $2$

Рисунок — состояние графа после обработки вершины $2$

Среди вершин, имеющих статус «В работе», вершина номер $4$ имеет наименьшую ценовую метку, равную $-3$. Поэтому обрабатываю теперь вершину $4$.

💡 Но есть нюанс! В соответствии с алгоритмом Дейкстры вершины $2$ и $3$ уже обработаны и в последующих вычислениях участия не принимают. То есть получается, что кратчайший путь от вершины $3$ до вершины $2$ составляет $5$ единиц. Но разве это справедливо?! Очевидно, если прокладывать маршрут к вершине $2$ через $4$, то его протяженность составит $6 + ( -8 ) = -2$ ед. А значение $-2$ математически меньше, чем $+5$. Следовательно, алгоритм Дейкстры некорректно рассчитал расстояние от стартового узла графа ( от узла $3$ ) до узла $2$.

правильный маршрут минимальной протяженности между $3$ и $2$

Рисунок — правильный маршрут минимальной протяженности между вершинами $3$ и $2$

Другими словами, вершина $2$ «не знала», что от вершины $4$ найдется ребро, которое пересчитает расстояние до $2$ в сторону уменьшения. При положительных весах это невозможно, а вот отрицательные весовые значения нарушают это правило.

➡ По этой причине алгоритм Дейкстры некорректно работает, когда граф имеет отрицательные ребра.

Но есть еще нюанс! На рассматриваемом графе элементы $2$ и $4$ соединяет ребро отрицательного веса. По факту эти вершины образуют цикл отрицательного веса. Из этого следует, что можно до бесконечности «прыгать» между этими вершинами постепенно уменьшая длину маршрута.

Лично я запомнил раз и навсегда: если нужен алгоритм Дейкстры, то необходимо запрещать отрицательные веса в графе. Иначе результат будет непредсказуем ( а по факту — неправилен ).

Структуры данных для алгоритма Дейкстры

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

Покажу, какие структуры данных мне потребуется, чтобы «наложить» алгоритм Дейкстры на эту схему дорог:

схема дорог, связывающих города

Рисунок — схема дорог, связывающих города

Формат входных данных

Существует $4$ фундаментальных формата входных данных:

  • ввод с клавиатуры;
  • считывание из заданного текстового/двоичного файла;
  • случайная генерация;
  • по формуле.

➡ Вводить с клавиатуры постоянно названия всех городов и стоимость проезда между ними утомительно. Перед каждым запуском программы придется проводить практически один и тот же ввод. Этот вариант отбросим.

➡ Случайно генерировать данные достаточно неудобно. Нужно иметь мощную базу городов, которую где-то нужно хранить. В целом этот вариант приемлем, но его также отбросим.

➡ Формульный ввод. Для задания графов этот вариант практически неприменим. Отбрасываем.

💡 Считывать информацию о городах и весах ребер из текстового файла удобно. Также эту информацию легко редактировать. Поэтому я выбираю именно этот формат входных данных!

Для рассматриваемой схемы дорог входной текстовый файл будет иметь вид:

входной текстовый файл с данными для алгоритма Дейкстры

Рисунок — входной текстовый файл с данными для алгоритма Дейкстры

входной текстовый файл состоит из $3$ блоков данных

Рисунок — входной текстовый файл состоит из $3$ блоков данных

 

 

Где хранить информацию о городах

Во-первых, перед тем, как запускать на выполнение алгоритм Дейкстры, необходимо знать количество вершин графа. То есть число узлов графа должно быть известно. В моем примере $7$ вершин, имеющих следующие метки:

Номер вершины Метка вершины
$0$ Москва
$1$ Петербург
$2$ Новосибирск
$3$ Самара
$4$ Новгород
$5$ Чита
$6$ Екатеринбург

➡ Нумерацию вершин графа я неспроста взял от $0$. Это связано с тем, что в языке «чистый» Си первый элемент любого одномерного массива имеет индекс, равный $0$.

То есть в программе у меня будет специальный массив, состоящий из $7$ элементов. А значениями элементов этого массива будет информация о городах.

Каждый город ( вершина графа ) должна включать такую полезную информацию:

А вот, собственно, одномерный массив вершин графа:

Где хранить информацию о дорогах между городами и стоимости проезда по ним

Это не самый легкий момент в проектировании. Есть много разных способов. Я выберу наиболее простой и понятный вариант — задействую весовую матрицу.

весовая матрица заданного графа

Рисунок — весовая матрица заданного графа

➡ Можно говорить, что заданный граф я отражаю в весовую матрицу. Иногда удобнее отражать граф в список смежности и т.д.

Реализация алгоритма Дейкстры на языке Си

Приведу полный код программы, который выполняет следующие действия:

➡ считывание данных из заданного текстового файла;

➡ поиск кратчайших путей от заданное вершины графа до всех остальных, используя алгоритм Дейкстры;

➡ вывод результата в понятной форме.

➡ Программа под $350$ строк кода. А на языке Си так всегда — не бывает коротких программ, так как каждый чих нужно программировать, в отличие, например, от языка С++, где есть куча готовых шаблонов.

Напомню, что алгоритм Дейкстры я накладываю на следующий граф:

схема дорог, связывающих города

Рисунок — схема дорог, связывающих города

Тестирую программу для нахождения кратчайшего пути между Москвой и Новгородом:

результаты работы программы ( поиск минимального пути между Москвой и Новгородом )

Рисунок — результаты работы программы ( поиск минимального пути между Москвой и Новгородом )

А что будет, если какая-то вершина графа недостижима от заданной

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

Что в этом случае вернет представленная программа в качестве ответа?

Ответить просто на этот вопрос, если помнить инициализацию вершин графа перед началом запуска алгоритма Дейкстры. Всем вершинам присваивалась ценовая метка, равная «плюс бесконечности»:

Следовательно, если после выполнения алгоритмы Дейкстры минимальное расстояние до какой-либо вершины равное $\infty$, значит, проехать до этой вершины физически невозможно!

 

 

Хочу это проверить на практике. Добавлю $8$-ой город, например, Челябинск. Это будет изолированная вершина графа. И попробую найти самый дешевый путь от Петербурга до Челябинска.

в схему дорог добавлен город Челябинск, который не связан ни с одним городом

Рисунок — в схему дорог добавлен город Челябинск, который не связан ни с одним городом

Автоматически совсем чуть-чуть меняется и входной текстовый файл с информацией о городах и связях между ними:

входной текстовый файл теперь содержит информацию о Челябинске

Рисунок — входной текстовый файл теперь содержит информацию о Челябинске

В результате программа выдает следующие результаты:

результаты, когда кратчайший путь невозможно проложить

Рисунок — результаты, когда кратчайший путь невозможно проложить

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