14. Теория графов. Поиск в ширину (BFS).
1. Теория графов. Поиск в ширину (BFS).
Сегодня все занятие мы посвятим решению одной задачи. Точнее, мы решим вроде бы только одну задачу, а потом поймем, что это же решение можно применить (практически без изменений) для решения сотен и даже тысяч очень актуальных и важных алгоритмических задач из реальной жизни.
Условие задачи: Есть N городов. Некоторые из этих городов могут быть попарно связаны друг с другом дорогами. Всего дорог М. Каждая дорога связывает два города и имеет длину (в километрах). Необходимо ответить на вопрос, можно ли проехать по имеющимся дорогам из города X в город Y, и, если да, то какое минимальное количество километров между городами надо проехать.
Исходные данные:
Сначала вводится N – количество городов.
Затем М – количество дорог.
Далее для каждой дороги вводится номер города, из которого она идет, номер города, в который она идет, и расстояние в километрах. Города нумеруются, начиная с 0.
Потом вводится Х – номер города, из которого планируется начать движение.
И Y – номер города, в котором планируется закончить движение.
Выводится текст «Дороги нет!», если попасть из города Х в город Y по имеющимся дорогам нельзя или число – минимальное количество километров, которые надо проехать между городами, чтобы попасть из горда Х в город Y.
Мы будем использовать один из основных алгоритмов на графах - поиск в ширину.
В результате поиска в ширину находится путь кратчайшей длины в графе, т.е. путь, содержащий наименьший суммарный вес рёбер.
2. Поиск в ширину (BFS). Описание алгоритма.
По сути поиск в ширину в графе очень похож на решение задачи «принц и принцесса», которую мы с вами разбирали раньше. Но, если принц мог двигаться всегда вправо, влево, вниз или вверх на свободные клетки, то в графе путь из каждой вершины задается ребрами (кстати, один из способов решения задачи «принц и принцесса» и состоит в том, чтобы представить лабиринт в виде графа и решать его поиском в ширину).
На вход алгоритма подаётся заданный граф (невзвешенный), и номер стартовой вершины Х. Граф может быть как ориентированным, так и неориентированным, для алгоритма это не важно.
Сам алгоритм можно понимать, как процесс "поджигания" графа: на нулевом шаге поджигаем только вершину Х. На каждом следующем шаге огонь с каждой уже горящей вершины перекидывается на всех её соседей; то есть за одну итерацию алгоритма происходит расширение "кольца огня" в ширину на единицу (отсюда и название алгоритма).
Более строго это можно представить следующим образом. Создадим очередь, в которую будут помещаться горящие вершины, а также заведём булевский массив used[], в котором для каждой вершины будем отмечать, горит она уже или нет (или иными словами, была ли она посещена).
Изначально в очередь помещается только вершина Х, и used[Х]=true, а для всех остальных вершин used[i]=false. Затем алгоритм представляет собой цикл: пока очередь не пуста, достать из её головы одну вершину, просмотреть все рёбра, исходящие из этой вершины, и если какие-то из просмотренных вершин ещё не горят, то поджечь их и поместить в конец очереди.
В итоге, когда очередь опустеет, обход в ширину обойдёт все достижимые из Х вершины, причём до каждой дойдёт кратчайшим путём.
Для взвешенного графа алгоритм немного усложняется. Вместо булевского массива used[] заведем массив длин путей path[], элементам которого изначально присвоим бесконечность. Длине пути в исходную вершину присвоим 0: path[Х]=0. Для расчета кратчайших путей придется постоянно проверять, можем ли мы попасть в очередную вершину за более короткий путь и, если да, обновлять значение минимального пути и добавлять эту вершину в очередь еще раз, чтобы заново пересчитать минимальные пути.
В итоге мы будем иметь длины кратчайших путей из указанной вершины Х во все остальные вершины графа (в том числе в вершину Y). Если длина пути в конкретную вершину осталась равна бесконечности, значит в эту вершину попасть нельзя.
Граф будем хранить с помощью матрицы смежности (у нас достаточно много ребер).
int start = 0; int end = 0; Console.Write("Введите N="); int n = Convert.ToInt32(Console.ReadLine()); int[,] graf = new int[n, n]; Console.Write("Введите M="); int m = Convert.ToInt32(Console.ReadLine()); for (int i = 0; i < m; i++) { Console.Write("Номер города, из которого идет дорога: "); start = Convert.ToInt32(Console.ReadLine()); Console.Write("Номер города, в который идет дорога: "); end = Convert.ToInt32(Console.ReadLine()); Console.Write("Длина дороги: "); int size = Convert.ToInt32(Console.ReadLine()); graf[start, end] = size; graf[end, start] = size; } Console.Write("Введите X="); start = Convert.ToInt32(Console.ReadLine()); Console.Write("Введите Y="); end = Convert.ToInt32(Console.ReadLine()); int[] path = new int[n]; for (int i = 0; i < n; i++) { path[i] = Int32.MaxValue; } Queue<int> list = new Queue<int>(); path[start] = 0; list.Enqueue(start); while (list.Count > 0) { int u = list.Dequeue(); for (int i = 0; i < n; i++) { if (graf[u, i] > 0) { if (path[u] + graf[u, i] < path[i]) { path[i] = path[u] + graf[u, i]; list.Enqueue(i); } } } } if (path[end] != Int32.MaxValue) { Console.WriteLine("Ответ: "+path[end]); } else { Console.WriteLine("Дороги нет!"); }
Задание*: Попробуйте самостоятельно вывести в консоль кратчайший маршрут движения от города Х в город Y (список номеров городов, через которые надо проехать).
Этот код можно применять при решении огромного количества задач: поиск пути, расчет минимального времени, логистика перевозок, оптимизация работы автоматических линий в цехах предприятий и многое другое.
Итоги занятия.
Сегодня мы познакомились с самым базовым из алгоритмов теории графов – поиском в ширину, который может применяться при решении огромного количества задач на графы.
А на следующем занятии поймем, что ни один поиск в ширину не может оправдать походов «налево»! Только вперед! И только в правильном направлении!