Uploaded by Ксения Егорова

курсовая алгоритмы на глафах

advertisement
Оглавление
Введение ............................................................................................................... 2
Глава 1. Теоретическая часть ............................................................................... 3
Алгоритм Дейкстры ........................................................................................... 3
Пример использования алгоритма Дейкстры: ............................................. 4
Алгоритм Беллмана-Форда............................................................................... 6
Пример использования алгоритма Беллмана Форда .................................. 6
Алгоритм Флойда .............................................................................................. 8
............................................................................................................................ 9
Вывод ............................................................................................................... 10
Глава 2. Реализация ........................................................................................... 11
Алгоритм Дейкстры. ...................................................................................... 11
Алгоритм Беллмана-Форда .......................................................................... 14
Введение
В современном мире графы находят свое применение в большом количестве
областей, например,
транспортная логистика, социальные сети,
биоинформатика, навигация.
Основная задача при работе с графами – нахождение кратчайшего пути между
двумя вершинами. Существует множество алгоритмов, применяемых при
разных постановках задачи. Например, алгоритм Дейкстры применяется,
когда необходимо найти кратчайшее расстояние от одной вершины ко всем
остальным.
Наиболее популярные алгоритмы для нахождения кратчайшего пути:
Алгоритм Дейкстры
Алгоритм Флойда
Переборные Беллмана-Форда
Алгоритм поиска А*
Алгоритм Ли (волновой алгоритм)
Алгоритм Джонсона
В данной курсовой работе будут изучены различные алгоритмы поиска
кратчайших путей, их свойства и особенности. Будут рассмотрены
алгоритмы Дейкстры, Беллмана-Форда и Флойда.
Глава 1. Теоретическая часть
Алгоритм Дейкстры
Алгоритм Дейкстры применяется для нахождения пути между заданной
вершиной и остальными вершинами на неориентированных и
ориентированных графах. Для каждого ребра должен быть задан вес l,
обязательно неотрицательный.
Данный алгоритм может найти кратчайший путь между вершинами А и
В, если между ними существует хотя бы один путь, в ином случае будет
возвращено значение «бесконечность». Алгоритм основан на приписывании
вершинам 𝑉𝑖 меток d(𝑉𝑖 ), величины которых постепенно уменьшаются и
спустя время становятся постоянными.
Инициализация:
1. Задаем вершине 𝑉0 постоянное значение равное 0
2. Задаем остальным вершинам временные значения равные ∞.
3. Создадим массив отметок «посещенных» вершин и отметим 𝑉0 , как
посещенную вершину
Основной цикл алгоритма:
1. Находим вершину 𝑉𝑖 с минимальным расстоянием в неотмеченной
части графа
2. Отмечаем найденную вершину посещенной
3. Обновляем расстояние всех вершин 𝑉𝑗 , смежных с вершиной 𝑉𝑖 ,
если можно найти более короткий путь через 𝑉𝑖
4. Если количество отмеченных вершин меньше количество вершин в
графе, то повторяем цикл
На выходе алгоритм выдает массив расстояний от начальной вершины, до всех
остальных.
Сложность алгоритма – О(𝑛2 ), где n - количество вершин в графе.
Пример использования алгоритма Дейкстры:
Рисунок 1. Взвешенный неориентированный граф №1
Применим к графу №1 алгоритм Дейкстры, выбрав вершину 1 начальной
вершиной. Алгоритм приведен в таблице №1
Таблица 1. Алгоритм Дейкстры для неориентированного графа
Вершина
1
2
3
4
5
6
Пути
Шаг 1
0
∞
∞
∞
∞
∞
1
Шаг 2
∞
7
9
∞
∞
14
1->236
Шаг 3
∞
7
9
22
∞
14
1->2->34
Шаг 4
∞
∞
9
20
∞
11
1->3->46
Шаг 5
∞
∞
∞
∞
20
11
1->3->6>5
Итог
0
7
9
20
20
11
По окончании алгоритма получаем следующую матрицу чисел: [0, 7, 9, 20,
20, 11]. Построим дерево кратчайших путей, чтобы можно было проследить
Рисунок 2. Дерево кратчайших расстояний для
графа №1
путь до каждой вершины.
Алгоритм Беллмана-Форда
Алгоритм Беллмана–Форда – алгоритм нахождения кратчайшего пути во
взвешенном графе, но, в отличие от алгоритма Дейкстры, может применяться
в графах с отрицательными весами.
Если в графе присутствуют циклы отрицательного веса, то кратчайший
путь найдет не будет. Данный алгоритм позволяет легко определить, есть ли
в графе отрицательный цикл. Достаточно произвести лишнюю итерацию, и
если длина кратчайшего пути до како-либо вершины уменьшилась, то в графе
присутствует такой цикл.
Алгоритм:
1. Расстояние до исходной вершины ставится равным 0, а до
остальных принимается равным «бесконечность». Создается
массив dist[] размера V, где dist[i] равно расстояние до вершины 𝑉𝑖
2. Пока количество итераций меньше количества вершин,
производим для каждого ребра следующие действия: если
расстояние до вершины A больше чем расстояние до вершины B
+ вес ребра AB, то обновляем расстояние до А: dist[A] = dist[B] +
AB.
3. Если на повторе V, возможно повторить действия из шага 2, то в
графе присутствует цикл отрицательного веса.
Пример использования алгоритма Беллмана Форда
Рисунок 3. Взвешенный ориентированный граф №2
Будем проходить ребра в следующем порядке: (4, 2) -> (3, 4) -> (1, 4) -> (1,
2). Всего должны пройти каждое ребро |V|-1 раз, то есть 3 раза.
Таблица 2. Алгоритм Беллмана-Форда для ориентированного графа
Вершина
Начало
Цикл 1
Цикл 2
Цикл 3
Цикл 4
1
0
0
0
0
0
2
∞
4
4
-2
-2
3
∞
5
5
5
5
4
∞
∞
8
8
8
Цикл 4 является проверкой на наличие отрицательных циклов в графе.
Так как расстояния до вершин не изменились, то отрицательные циклы
отсутствуют.
Попробуем ввести в граф №2 отрицательный цикл и посмотрим, как
Рисунок 4. Граф с отрицательным циклом №3
будет работать алгоритм
Проходим Граф №3 в таком порядке: (4, 2) -> (3, 4) -> (1, 4) -> (1, 2) -> (2, 3).
Вершина
Начало
Цикл 1
Цикл 2
Цикл 3
Цикл 4
1
0
0
0
0
0
2
∞
4
4
-2
-2
3
∞
5
5
3
3
4
∞
∞
8
8
6
Расстояния до вершин в Цикле 4 уменьшилось, что свидетельствует о
наличии отрицательного цикла в графе.
Алгоритм Флойда
Алгоритм Флойда-Уоршелла (больше известный как алгоритм Флойда)
используется для поиска кратчайших путей во взвешенном графе. Но в
отличие от алгоритмов Дейкстры и Беллмана-Форда он ищет кратчайшие
расстояния между всеми парами вершин. Сложность работы алгоритма
O(|V|3), а в особо густых графах – О(V4).
Основная идея алгоритма Флойда заключается в том, что он строит
матрицу кратчайших путей для заданной сети. Эта матрица содержит
информацию о кратчайших путях между каждой парой вершин в сети.
Алгоритм:
1. Построение матрицы смежности графа. Матрица смежности
представляет собой квадратную матрицу размером |V|*|V|, где
элемент A[i, j] равен весу ребра из вершины i в вершину j (или
бесконечности, если ребра нет).
2. Нахождение кратчайших путей с помощью алгоритма ФордаБеллмана. Алгоритм Форда-Беллмана находит кратчайшие пути
между всеми парами вершин за время O(N^3).
3. Построение матрицы кратчайших путей. Матрица кратчайших путей
представляет собой квадратную матрицу размером |V|*|V|, где
элемент B[i, j] равен длине кратчайшего пути из вершины i в вершину
j.Пример использования алгоритма Флойда
Рисунок 5. Ориентированный граф №4
Рисунок 6. Матрицы кратчайших путей
Вывод
Сравнение слгоритмов Дейкстры и Беллмана-Форда:
Алгоритмы Беллмана — Форда и Дейкстры являются двумя наиболее
известными алгоритмами для поиска кратчайших путей. Оба этих алгоритма
используются в различных приложениях, таких как маршрутизация,
планирование.
1. Алгоритм
Беллмана-Форда
может
обрабатывать
графы
с
отрицательными весами
2. Алгоритм Дейкстры имеет меньшую вычислительную сложность O(𝑉 2 ),
чем алгоритм Беллмана-Форда O(|V||E|)
3. Алгоритм Беллмана может находить циклы с отрицательными весами.
Преимущества алгоритма Флойда:
 Эффективность: алгоритм Флойда имеет сложность O(n^3), что является
одним из самых эффективных алгоритмов для нахождения кратчайших
путей.
 Простота: алгоритм довольно простой и интуитивный, его легко понять
и реализовать.
 Универсальность: алгоритм работает для графов с положительным и
отрицательным весом ребер, а также для графов со специфическими
ограничениями (например, для транспортных сетей).
Недостатки:
 Сложность: несмотря на свою эффективность, алгоритм Флойда может
быть слишком медленным для больших графов, так как его сложность
зависит от n^3.
 Потребность в памяти: для хранения промежуточных результатов
алгоритм требует O(n^2) памяти, что может стать проблемой для
больших графов.
Глава 2. Реализация
Структура данных: Ориентированный и неориентированный граф
Алгоритмы: алгоритм Дейкстры, алгоритм Беллмана-Форда, алгоритм Флойда
Язык программирования С++
Описываю структуру и алгоритмы программы.
1. Структура Flight – хранит
продолжительности пути
информацию
Алгоритм Дейкстры.
void Dijkstra(int start, int to, vector<int>& pat, vector<int>& lens, bool a)
{
int n = matrix.size();
// Массив для хранения времени до каждой вершины
vector<int> time(n, 1000000);
// Приоритетная очередь для хранения вершин с наименьшим временем
priority_queue<pair<int, int>> pq;
// Инициализация начальной вершины
time[start] = 0;
pq.push({ 0, start });
// Массив для хранения предыдущей вершины
vector<int> prev(n, -1);
if (a == 1) //lenght
{
while (!pq.empty())
{
int u = pq.top().second;
pq.pop();
// Обход смежных вершин
for (int v = 0; v < n; ++v)
{
if (matrix[u][v].lenght < 0)
throw DijkstraExeption();
if (matrix[u][v].lenght > 0) // Проверка наличия рейса между вершинами
{
int newTime = time[u] + matrix[u][v].lenght;
// Обновление времени до вершины, если найден более короткий путь
if (newTime < time[v])
{
time[v] = newTime;
prev[v] = u;
pq.push({ time[v], v });
}
}
}
}
}
о
стоимости
и
if (a == 0) //price
{
while (!pq.empty())
{
int v = pq.top().second;
pq.pop();
// Обход смежных вершин
for (int u = 0; u < n; ++u)
{
if (matrix[u][v].lenght < 0)
throw DijkstraExeption();
if (matrix[u][v].price > 0) // Проверка наличия рейса между вершинами
{
int newTime = time[v] + matrix[u][v].price;
// Обновление времени до вершины, если найден более короткий путь
if (newTime < time[u])
{
time[u] = newTime;
prev[u] = v;
pq.push({ time[u], u });
}
}
}
}
}
vector<int> path;
for (int i = to-1; i != -1; i = prev[i])
{
path.push_back(i+1);
}
lens = time;
pat = path; }
Функция используется для нахождения кратчайших путей в графе с
положительными весами рёбер. Она работает за O(n^2), где n - количество
вершин в графе.
В данном случае функция принимает следующие параметры:
start - начальная вершина, с которой начинается поиск кратчайшего пути.
to - конечная вершина, до которой ищется кратчайший путь.
pat - массив, содержащий информацию о пути до каждой вершины (т.е. pat[i]
указывает на вершину, предшествующую вершине i в кратчайшем пути).
lens - массив длин рёбер графа.
a - параметр, который используется для определения того, какой из вариантов
пути надо вычислить (если a == 1, то функция вычисляет стоимость
кратчайшего пути, иначе - продолжительность).
Функция работает следующим образом:
1.Создается массив time, в котором для каждой вершины хранится время до
этой вершины от начальной вершины. Изначально все значения в массиве
равны максимальному значению int (1000000).
2.Создается приоритетная очередь pq, в которой хранятся вершины с
наименьшим временем. Начальная вершина помещается в очередь с
временем 0.
3.Создается массив prev, в котором хранится предыдущая вершина для
каждого пути. Изначально все значения массива равны -1.
4.На каждой итерации цикла из очереди извлекается вершина с
минимальным временем. Если время до этой вершины больше
максимального значения int, то цикл сразу переходит к следующей итерации.
5.Для каждой вершины в графе проверяется, можно ли уменьшить время до
нее, пройдя через текущую вершину. Если можно, то время до этой вершины
уменьшается, она помещается в очередь pq с новым временем, и в массив
prev записывается предыдущая вершина.
6.Когда все вершины будут обработаны, массив prev будет содержать
информацию о кратчайшем пути от начальной вершины до каждой вершины
графа. Функция изменяет массив lens, заполняя его кратчайшими путями от
одной вершины к остальным, а также массив path, выводя в него кратчайший
путь др определенной вершины.
Также для алгоритма Дейкстры были написаны две функции.
Функция BestPricе запускает алгоритм Дейкстры и высчитывает
минимальную стоимость, BestLength – минимальную продолжительность
void BestPrice(int start, int to)
{
vector<int> path, lens;
Dijkstra(start, to, path, lens, 1);
cout << "The Best Price:" << '\n' << "Path: ";
print(path);
cout << "Path Lengths: ";
print(lens);
cout << '\n';
}
void BestLenght(int start, int to)
{
vector<int> path, lens;
Dijkstra(start, to, path, lens, 0);
cout << "The Best Length:" << '\n' << "Path: ";
print(path);
cout << "Path Lengths: ";
print(lens);
cout << '\n'; }
Алгоритм Беллмана-Форда
Алгоритм Беллмана-Форда работает в основном с списками
смежностей, поэтому прежде чем приступить к написанию самого алгоритма,
необходимо создать функцию, переводящую матрицу смежности в списки
смежности
Было написано две функции: для ориентированного и
неориентированного графов. Разница состоит в том, что в
неориентированном графе не рассматриваем ребра дважды (Например: (5, 2)
и (2, 5)), так как значение у них будет одно, а в списке появятся лишние записи.
В ориентированном же графе наоборот, рассматриваем абсолютно все ребра,
так как, к примеру, ребро
(1, 3) может иметь вес 10, а ребро (3, 1) будет
иметь вес 7.
Также для данной функции мы написали допольнительный метод,
который высчитывает количество ребер в графе (в данном случае, он написан
для ориентированного графа)
х – отвечает за значения в списке смежностей. При х = 0, рассматривается
продолжительность пути, иначе – стоимость.
int edjes(bool x)
{
int n = matrix.size();
int e = 0;
if (x==0){
for(int i = 0; i<n; i++)
{
for (int j=0; j<n; j++)
{
if(matrix[i][j].lenght!=0)
e++;
}
}}
if (x==1){
for(int i = 0; i<n; i++)
{
for (int j=0; j<n; j++)
{
if(matrix[i][j].price!=0)
e++;
}
}}
return e;
}
vector<vector<int>> OrientWeightedList(bool x)
{
vector<vector<int>> weightedList(edjes(x));
if (x==0){ //lenght
int n = 0;
for (int i = 0; i < matrix.size(); i++)
{
for (int j = 0; j < matrix[i].size(); j++)
{
if (matrix[i][j].lenght != 0)
{
weightedList[n].push_back(i);
weightedList[n].push_back(j);
weightedList[n].push_back(matrix[i][j].lenght);
n++;
}
}
}}
if (x==1){ //price
int n = 0;
for (int i = 0; i < matrix.size(); i++)
{
for (int j = 0; j < matrix[i].size(); j++)
{
if (matrix[i][j].price != 0)
{
weightedList[n].push_back(i);
weightedList[n].push_back(j);
weightedList[n].push_back(matrix[i][j].price);
n++;
}
}
}}
return weightedList;
}
Рассмотрим сам алгоритм Беллмана-Форда.
Функция находит кратчайшие пути в графе с рёбрами с отрицательным
весом. Она работает за O (|V||E|) времени, где |V| - количество вершин, |E|
- количество рёбер в графе.
Параметры функции:
Start, x – параметры аналогичны алгоритму Дейкстры.
Создаётся список смежностей, переменная v, равная количеству
вершин, и переменная p, равная числу рёбер.
Создается вектор dis, который будет хранить расстояния до всех вершин.
Начальное значение расстояния равно 100000. Инициализируется расстояние
до стартовой вершины, оно равно 0.
В цикле for происходит обработка всех вершин, кроме последней. Для
каждой вершины проверяются все смежные вершины. Если расстояние до
смежной вершины меньше текущего расстояния плюс вес ребра, то
расстояние обновляется.
Далее алгоритм проверяет наличие отрицательного цикла в графе. Цикл
обработки вершин запускается еще раз, и если значения в массиве dis
уменьшаются, то в графе присутствует цикл отрицательного веса. В этом
случае обрабатывается исключение, но в своем коде я оповещаю о том, что
найдет цикл отрицательного веса, чтобы показать, что алгоритм работает
верно.
vector<int> BellmanFord(int start, bool x)
{
vector<vector<int>> graph = OrientWeightedList(x);
cout << "Adjacency List" << '\n';
for (int i = 0; i < graph.size(); i++)
{
print(graph[i]);
}
// инициализируем расстояние до всех вершин
int v = matrix.size();
int p = edjes(x);
vector<int> dis(v, 100000);
// инициализируем начальную вершину
dis[start] = 0;
for (int i = 0; i < v - 1; i++)
{
for (int j = 0; j < p; j++)
{
if (dis[graph[j][0]] != 1000000 && dis[graph[j][0]] + graph[j][2] < dis[graph[j][1]])
dis[graph[j][1]] = dis[graph[j][0]] + graph[j][2];
}
}
for (int i = 0; i < p; i++) {
int x = graph[i][0];
int y = graph[i][1];
int weight = graph[i][2];
if (dis[x] != 1000000 && dis[x] + weight < dis[y])
{
//throw BellmanFordExeption();
cout << "!!!Graph contains negative weight cycle!!!" << endl;
}
}
cout << "Path Lengths " << start << ": ";
return dis;
}
Алгоритм Флойда
Это функция выполняет алгоритм нахождения всех кратчайших путей между
всеми парами вершин во взвешенном графе. Она принимает один параметр
x, который определяет, как заполняются веса рёбер во входном графе. Если x
равен 0, то веса рёбер равны их продолжительности, если x равен 1, то веса
рёбер равны их стоимости.
Функция сначала создаёт матрицу смежности graph, где каждый элемент
представляет собой вес ребра между двумя вершинами. Затем она выполняет
алгоритм Флойда-Уолшера, который обновляет веса рёбер, чтобы получить
кратчайшие пути между всеми парами вершин.
На первом шаге для каждой пары вершин (u, v) находится минимальное
расстояние между ними, учитывая только рёбра, выходящие из вершины u.
На втором шаге для каждой вершины v находится минимальное расстояние,
учитывая только рёбра, исходящие из вершины v.
На третьем шаге находятся минимальные расстояния между всеми парами
вершин, учитывая все рёбра.
void Floyd(bool x)
{
int inf = 100000;
int v = matrix.size();
vector<vector<int>> graph(v);
if (x==0){
for (int i = 0; i<v; i++)
{
for (int j = 0; j<v; j++)
{
if (matrix[i][j].lenght == 0)
graph[i].push_back(inf);
else graph[i].push_back(matrix[i][j].lenght);
}
}}
if (x==1){
for (int i = 0; i<v; i++)
{
for (int j = 0; j<v; j++)
{
if (matrix[i][j].price == 0)
graph[i].push_back(inf);
else graph[i].push_back(matrix[i][j].price);
}
}}
for (int k = 0; k < v; k++)
{
for (int i = 0; i < v; i++)
{
for (int j = 0; j < v; j++)
{
if (graph[i][j] > (graph[i][k] + graph[k][j]) && (graph[k][j] != inf && graph[i][k] != inf))
{graph[i][j] = graph[i][k] + graph[k][j];}
}
}
}
cout << "The Path Matrix: " << '\n';
for(int i = 0; i<v; i++)
{
for(int j = 0; j<v; j++)
{
if (i == j) {graph[i][j] = 0;}
if (graph[i][j] == inf)
cout << "inf" << " ";
else cout << graph[i][j] << " ";
}
cout << '\n';
}
cout << '\n';
}
Результат работы алгоритмов на заранее подобранных графах:
Dijkstra Algorithm
The Best Length:
Path: 4 5 1
Path Lengths: 0 7 12 12 4 7
The Best Price:
Path: 5 6 3 1
Path Lengths: 0 7 9 20 20 11
Bellman-Ford Algorithm
Adjacency List
014
025
233
3 1 -10
Path Lengths 0: 0 -2 5 8
Graph contains negative weight cycle
Adjacency List
014
025
125
233
3 1 -10
!!!Graph contains negative weight cycle!!!
Path Lengths 0: 0 -6 1 4
Floyd Algorithm
The Path Matrix:
0 5 8 9
inf 0 3 4
inf inf 0 1
inf inf inf 0
The Path Matrix:
0 5 3 8
-5 0 -2 3
-3 2 0 5
-8 -3 -5 0
Floyd Algorithm from 1 Chapter:
The Path Matrix:
0 7 5
3 0 8
5 2 0
Download