Задача поиска кратчайшего маршурта. Модификации алгоритма Дейсктры. Дельта-шаг. Алгоритм А*#
Параллелизация алгоритма Дейкстры[1]#
Рассмотрим алгоритм для направленного графа авторов Краузер, Мельхорн, Мейер, Сандерс, 1998 г.
Подход предполагает разделение графа на более мелкие подграфы и распараллеливание выполнения алгоритма Дейкстры на каждом подграфе с использованием подхода распараллеливания с общей памятью
Что подлежит параллелизации?#
Основные компоненты алгоритма, которые можно распределить между процессами:
- Обработка вершин
- Распределение множества вершин
V
между процессами (V = V₁ ∪ V₂ ∪ ... ∪ Vₚ
) -
Локальные вычисления для "своих" вершин
-
Работа с очередями
-
Параллельное обслуживание трех типов очередей:
Q_d
(основные расстояния)Q_in
(входящие ребра)Q_out
(исходящие ребра)
-
Обновление расстояний
- Независимая обработка смежных вершин
- Пакетное обновление значений
Инициализация#
Для всех v ∈ Vᵢ положить:
d[v] = ∞
s — стартовая вершина
d[s] = 0
S = ∅
Добавить s в очереди Qᵈᵢ, Qᵢₙᵢ, Qₒᵤₜᵢ
Основной цикл#
Пока хотя бы одна из очередей Qᵈᵢ не пуста:
- На каждом
процессе
i: - Извлечь из Qₒᵤₜᵢ вершину с минимальным приоритетом → значение Lᵢ
-
Выполнить редукцию: L = min{ Lᵢ | i = 1, ..., P }
-
Аналогично:
- Извлечь из Qᵈᵢ вершину с минимальным приоритетом → значение Mᵢ
-
Выполнить редукцию: M = min{ Mᵢ | i = 1, ..., P }
-
На каждом
процессе
: - Найти множество вершин Rᵢ, для которых d[v] ≤ L или in[v] ≤ M
- Для этих вершин расстояние считается окончательно найденным
-
Обновить S: S = S ∪ ⋃Rᵢ
-
Удалить вершины из Rᵢ из очередей Qᵈᵢ, Qᵢₙᵢ, Qₒᵤₜᵢ
-
Сформировать множество пар (z, x), где:
- z — необработанная вершина, смежная с вершинами из Rᵢ
-
x — вес ребра, ведущего к z
-
Для каждой пары (z, x) ∈ Zᵢ × Xᵢ:
- Если d[z] > d[u] + x, то обновить: d[z] = d[u] + x
-
Обновить приоритеты вершины z в соответствующих очередях (своего или чужого процесса):
Qᵈᵢ, Qᵢₙᵢ, Qₒᵤₜᵢ -
Выполнить синхронизацию и проверить критерий останова
Реализация в коде#
-
Установка количества потоков:
omp_set_num_threads(num_threads);
— задает количество потоков для выполнения параллельных операций в OpenMP.
-
Инициализация данных:
- Создаются вектора
dist
(для хранения расстояний) иvisited
(для отслеживания посещенных вершин). - Каждому потоку выделяется очередь с приоритетом (
priority_queue
), которая используется для обработки вершин графа.
- Создаются вектора
-
Основной цикл обработки в параллельном блоке:
#pragma omp parallel
— обозначает начало параллельного блока, в котором выполняются все потоки.- В каждом потоке идет извлечение вершины из очереди текущего потока с помощью
local_queues[tid].top()
. - Если очередь текущего потока пуста, то происходит проверка очередей других потоков для нахождения работы.
-
Критическая секция для извлечения данных:
- Для синхронизации доступа к общим данным используется
#pragma omp critical
, чтобы избежать одновременного доступа нескольких потоков к одной очереди.
- Для синхронизации доступа к общим данным используется
-
Блокировка и синхронизация:
- В случае, если работы нет, используется
#pragma omp barrier
для ожидания завершения работы всех потоков, после чего через#pragma omp single
проверяется, есть ли еще задачи в других очередях. - Если все очереди пусты, выполнение завершается.
- В случае, если работы нет, используется
-
Обработка вершин:
- Вершины, которые еще не были посещены, помечаются как "в процессе обработки".
- Входные данные для обработки — это вершины графа и их соседи, которые добавляются в очереди для последующего обновления.
-
Обновление расстояний и добавление соседей в очереди:
- Для каждого соседа проверяется, нужно ли обновить расстояние. Если расстояние обновляется, сосед добавляется в очередь соответствующего потока.
-
Завершение работы:
- В конце каждого потока, после обработки всех вершин, посещенная вершина помечается как "обработанная" с помощью
#pragma omp atomic write
, что гарантирует корректное обновление данных в многопоточном окружении.
- В конце каждого потока, после обработки всех вершин, посещенная вершина помечается как "обработанная" с помощью
-
Измерение времени выполнения:
auto start = high_resolution_clock::now();
— начало измерения времени.auto end = high_resolution_clock::now();
— завершение измерения времени.- Время выполнения измеряется в миллисекундах с использованием
duration_cast<milliseconds>
.
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <random>
#include <chrono>
#include <omp.h>
#include <fstream>
#include <algorithm>
using namespace std;
using namespace std::chrono;
struct QueueNode {
int vertex;
double distance;
bool operator>(const QueueNode& other) const {
return distance > other.distance;
}
};
struct CRSGraph {
vector<int> row_ptr;
vector<int> col_ind;
vector<double> weights;
int n = 0;
CRSGraph() = default;
explicit CRSGraph(int size) : n(size) {}
};
CRSGraph generate_random_graph(int n, double density) {
CRSGraph graph(n);
graph.row_ptr.resize(n + 1);
graph.row_ptr[0] = 0;
random_device rd;
mt19937 gen(rd());
uniform_real_distribution<> weight_dist(1.0, 100.0);
bernoulli_distribution edge_dist(density);
for (int i = 0; i < n; ++i) {
vector<int> cols;
vector<double> wts;
for (int j = 0; j < n; ++j) {
if (i != j && edge_dist(gen)) {
cols.push_back(j);
wts.push_back(weight_dist(gen));
}
}
graph.row_ptr[i + 1] = graph.row_ptr[i] + cols.size();
graph.col_ind.insert(graph.col_ind.end(), cols.begin(), cols.end());
graph.weights.insert(graph.weights.end(), wts.begin(), wts.end());
}
return graph;
}
double run_sequential_dijkstra(const CRSGraph& graph, int source) {
const int n = graph.n;
vector<double> dist(n, numeric_limits<double>::max());
dist[source] = 0;
priority_queue<QueueNode, vector<QueueNode>, greater<QueueNode>> pq;
pq.push({ source, 0 });
auto start = high_resolution_clock::now();
while (!pq.empty()) {
QueueNode node = pq.top();
pq.pop();
int u = node.vertex;
if (node.distance > dist[u]) continue;
for (int i = graph.row_ptr[u]; i < graph.row_ptr[u + 1]; ++i) {
int v = graph.col_ind[i];
double weight = graph.weights[i];
if (dist[v] > dist[u] + weight) {
dist[v] = dist[u] + weight;
pq.push({ v, dist[v] });
}
}
}
auto end = high_resolution_clock::now();
return duration_cast<milliseconds>(end - start).count();
}
double run_parallel_dijkstra(const CRSGraph& graph, int source, int num_threads) {
omp_set_num_threads(num_threads);
const int n = graph.n;
vector<double> dist(n, numeric_limits<double>::max());
vector<char> visited(n, 0);
dist[source] = 0;
int max_threads = num_threads;
vector<priority_queue<QueueNode, vector<QueueNode>, greater<QueueNode>>> local_queues(max_threads);
local_queues[0].push({ source, 0 });
auto start = high_resolution_clock::now();
#pragma omp parallel
{
int tid = omp_get_thread_num();
while (true) {
QueueNode current;
bool has_work = false;
if (!local_queues[tid].empty()) {
#pragma omp critical
{
if (!local_queues[tid].empty()) {
current = local_queues[tid].top();
local_queues[tid].pop();
has_work = true;
}
}
}
if (!has_work) {
for (int i = 0; i < max_threads; ++i) {
if (i == tid) continue;
#pragma omp critical
{
if (!local_queues[i].empty()) {
current = local_queues[i].top();
local_queues[i].pop();
has_work = true;
}
}
if (has_work) break;
}
}
if (!has_work) {
#pragma omp barrier
#pragma omp single
{
bool any_work = false;
for (auto& q : local_queues) {
if (!q.empty()) any_work = true;
}
if (!any_work) {
#pragma omp flush
}
}
break;
}
if (visited[current.vertex] == 2) continue;
bool should_process = false;
#pragma omp critical
{
if (visited[current.vertex] == 0) {
visited[current.vertex] = 1;
should_process = true;
}
}
if (!should_process) continue;
int row_start = graph.row_ptr[current.vertex];
int row_end = graph.row_ptr[current.vertex + 1];
for (int i = row_start; i < row_end; ++i) {
int neighbor = graph.col_ind[i];
double weight = graph.weights[i];
double new_dist = dist[current.vertex] + weight;
bool updated = false;
#pragma omp critical
{
if (new_dist < dist[neighbor]) {
dist[neighbor] = new_dist;
updated = true;
}
}
if (updated) {
int target_thread = neighbor % max_threads;
#pragma omp critical
{
local_queues[target_thread].push({ neighbor, dist[neighbor] });
}
}
}
#pragma omp atomic write
visited[current.vertex] = 2;
}
}
auto end = high_resolution_clock::now();
return duration_cast<milliseconds>(end - start).count();
}
void run_benchmark_and_save_results() {
vector<int> sizes = { 1000, 5000, 10000, 20000 };
vector<double> densities = { 0.1, 0.3, 0.5 };
vector<int> thread_counts = { 1, 2, 4, 8 };
int repeats = 3;
ofstream out("speedup_results.csv");
out << "Size,Density,Threads,SeqTime(ms),ParTime(ms),Speedup\n";
for (int size : sizes) {
for (double density : densities) {
// Генерация графа
auto gen_start = high_resolution_clock::now();
CRSGraph graph = generate_random_graph(size, density);
auto gen_end = high_resolution_clock::now();
cout << "Generated graph: size=" << size << ", density=" << density
<< " in " << duration_cast<milliseconds>(gen_end - gen_start).count() << " ms\n";
// Выбор исходной вершины
int source = 0;
while (graph.row_ptr[source + 1] - graph.row_ptr[source] == 0) {
source = (source + 1) % size;
}
// Последовательная версия
double seq_time = 0;
for (int i = 0; i < repeats; ++i) {
seq_time += run_sequential_dijkstra(graph, source);
}
seq_time /= repeats;
// Параллельные версии
for (int threads : thread_counts) {
double par_time = 0;
for (int i = 0; i < repeats; ++i) {
par_time += run_parallel_dijkstra(graph, source, threads);
}
par_time /= repeats;
double speedup = seq_time / par_time;
out << size << "," << density << "," << threads << ","
<< seq_time << "," << par_time << "," << speedup << "\n";
cout << "Size: " << size << ", Density: " << density
<< ", Threads: " << threads << ", Speedup: " << speedup << "\n";
}
}
}
out.close();
}
int main() {
run_benchmark_and_save_results();
cout << "Benchmark completed. Results saved to speedup_results.csv\n";
return 0;
}
Алгоритм дельта-шага#
Note
Алгоритм дельта-шаган предназначен для решения задачи поиска кратчайшего пути на графе. Для заданного ориентированного взвешенного графа с неотрицательными весами алгоритм находит кратчайшие расстояния от выделенной вершины-источника до всех остальных вершин графа. Алгоритм изначально проектировался с целью эффективной параллелизации.
Идея: модифицировать алгоритм Дейкстры, сгруппировав вершины с близкими значениями расстояний, чтобы пересчитывать их одновременно.
- Вводится параметр Δ > 0 для разделения рёбер:
- Лёгкие: вес < Δ
- Тяжёлые: вес ≥ Δ
- Вершины группируются в "карманы" (buckets) по текущей оценке расстояния:
- 𝐵[𝑖] содержит вершины с 𝑑[𝑣] ∈ [𝑖Δ, (𝑖+1)Δ)
Псевдокод алгоритма#
- Предобработка:
-
Для каждой вершины 𝑣:
- 𝐿[𝑣] ← все рёбра с весом < Δ
- 𝐻[𝑣] ← все рёбра с весом ≥ Δ
-
Инициализация:
- ∀𝑣 ∈ 𝑉: 𝑑[𝑣] ← ∞
- 𝑑[𝑠] ← 0
- 𝐵[0] ← {𝑠}
-
𝑖 ← 0
-
Основной цикл (пока ∃ непустые карманы):
- 𝑆 ← ∅
- Пока 𝐵[𝑖] ≠ ∅:
- Req ← {(𝑢, 𝑥) | 𝑣 ∈ 𝐵[𝑖], (𝑣,𝑢) ∈ 𝐿[𝑣], 𝑥 = 𝑑[𝑣] + 𝑤(𝑣,𝑢)}
- 𝑆 ← 𝑆 ∪ 𝐵[𝑖]
- 𝐵[𝑖] ← ∅
- Для каждого (𝑢, 𝑥) ∈ Req:
- Если 𝑑[𝑢] > 𝑥:
- Удалить 𝑢 из 𝐵[⌊𝑑[𝑢]/Δ⌋]
- 𝑑[𝑢] ← 𝑥
- Добавить 𝑢 в 𝐵[⌊𝑥/Δ⌋]
- Req ← {(𝑢, 𝑥) | 𝑣 ∈ 𝑆, (𝑣,𝑢) ∈ 𝐻[𝑣], 𝑥 = 𝑑[𝑣] + 𝑤(𝑣,𝑢)}
- Для каждого (𝑢, 𝑥) ∈ Req:
- Если 𝑑[𝑢] > 𝑥:
- Удалить 𝑢 из 𝐵[⌊𝑑[𝑢]/Δ⌋]
- 𝑑[𝑢] ← 𝑥
- Добавить 𝑢 в 𝐵[⌊𝑥/Δ⌋]
Или более алгоритмически:
Входные данные:
граф с вершинами V, рёбрами E с весами W;
вершина-источник u;
параметр Δ > 0.
Выходные данные: расстояния d(v) до каждой вершины v ∈ V от вершины u.
procedure DeltaStepping(V, E, W, u, Δ):
LightEdges := { e ∈ E | W(e) ≤ Δ }
HeavyEdges := { e ∈ E | W(e) > Δ }
for each v ∈ V do d(v) := ∞
Relax(u, 0)
while any({ Buckets(i) ≠ ∅ }):
Bucket := first({ Buckets(i) ≠ ∅ })
Deleted := ∅
while Bucket ≠ ∅:
Requests := FindRequests(Bucket, LightEdges)
Deleted := Deleted ∪ Bucket
Bucket := ∅
RelaxAll(Requests)
RelaxAll(FindRequests(Deleted, HeavyEdges))
procedure Relax(v, x):
if x < d(v):
OldBucket := B(⌊d(v) / Δ⌋)
NewBucket := B(⌊x / Δ⌋)
OldBucket := OldBucket \ {v}
NewBucket := NewBucket ∪ {v}
d(v) := x
procedure RelaxAll(R):
for each (v, x) ∈ R do Relax(v, x)
function FindRequests(V', E'):
return { (w, d(w) + W(v, w)) | v ∈ V' and (v, w) ∈ E'}
Анализ сложности#
Для случайных графов с весами ∈ [0,1] и Δ = Θ(1/𝑚):
Среднее время работы = O(n + m + dL)
где:
- 𝑑 — максимальная степень вершины
- 𝐿 — длина максимального кратчайшего пути
Особенности реализации#
- Структуры данных:
- Карманы: динамические массивы с O(1) на вставку/удаление
-
Множество Req:
- Основной массив пар (𝑢, 𝑥)
- Массив меток для проверки принадлежности
-
Выбор Δ:
- Оптимально: Δ ≈ среднему весу рёбер
- Крайние случаи:
- Δ → ∞ → Дейкстра
- Δ → 0 → Bellman-Ford
Пример работы алгоритм дельта-шага#
Пошаговая демонстрация работы алгоритма Δ-Шага#
Рассмотрим выполнение алгоритма на небольшом графе. Вершина-источник — A, параметр Δ = 3.
В начале алгоритма все вершины, кроме источника, имеют бесконечные оценки расстояний:
Вершина | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
Расстояние | 0 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
Карманы:
B[0]
охватывает диапазон [0, 2]B[1]
охватывает диапазон [3, 5]B[2]
охватывает диапазон [6, 8]
Начальное состояние:
- B[0] = {A}
- Остальные карманы пусты
Шаг 1. Обработка легких ребер из B[0]
Обрабатываются легкие ребра, инцидентные вершине A:
- A — B, A — G, A — E
Результат:
- B, G, E помещаются в B[1]
Поскольку B[0]
опустел, обрабатывается тяжелое ребро:
- A — D
Новые расстояния:
Вершина | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
Расстояние | 0 | 3 | ∞ | 5 | 3 | ∞ | 3 |
Шаг 2. Обработка легких ребер из B[1]
Обрабатываются легкие ребра от B, E, G:
- G — C
Результат:
- C добавляется в B[2]
Поскольку B[1]
опустел, обрабатывается тяжелое ребро:
- E — F
Обновленные расстояния:
Вершина | A | B | C | D | E | F | G |
---|---|---|---|---|---|---|---|
Расстояние | 0 | 3 | 6 | 5 | 3 | 8 | 3 |
Шаг 3. Обработка кармана B[2]
Обработка B[2]
не приводит к улучшению расстояний.
Завершение
Алгоритм завершает работу, так как все карманы пусты.
Преимущества и недостатки#
Преимущества | Недостатки |
---|---|
Эффективен для разреженных графов | Зависит от выбора Δ |
Возможность параллелизации | Доп. расходы на управление карманами |
Уменьшает число операций с очередью | Сложность реализации структур |
Примечание: За счет разбиения на легкие и тяжелые ребра по карманам возможна параллелизация
Реализация последовательного дельта-шага#
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <cmath>
#include <unordered_map>
#include <unordered_set>
using namespace std;
const int INF = numeric_limits<int>::max();
class DeltaStepping {
struct Edge {
int to;
int weight;
};
int delta;
vector<vector<Edge>> light_edges;
vector<vector<Edge>> heavy_edges;
vector<int> dist;
vector<unordered_set<int>> buckets;
int max_bucket;
public:
DeltaStepping(const vector<vector<pair<int, int>>>& graph, int delta)
: delta(delta) {
int n = graph.size();
light_edges.resize(n);
heavy_edges.resize(n);
dist.assign(n, INF);
// Предобработка: разделяем ребра на легкие и тяжелые
for (int u = 0; u < n; ++u) {
for (const auto& [v, w] : graph[u]) {
if (w <= delta) {
light_edges[u].push_back({v, w});
} else {
heavy_edges[u].push_back({v, w});
}
}
}
// Инициализация карманов
max_bucket = 0;
buckets.resize(1);
}
void relax(int v, int x, unordered_set<int>& new_bucket) {
if (x < dist[v]) {
// Удаляем из старого кармана (если был там)
if (dist[v] != INF) {
int old_bucket = dist[v] / delta;
if (old_bucket < buckets.size()) {
buckets[old_bucket].erase(v);
}
}
// Обновляем расстояние
dist[v] = x;
// Добавляем в новый карман
int new_bucket_idx = x / delta;
// Увеличиваем количество карманов при необходимости
if (new_bucket_idx >= buckets.size()) {
buckets.resize(new_bucket_idx + 1);
max_bucket = new_bucket_idx;
}
// Помечаем для добавления
new_bucket.insert(v);
}
}
vector<int> shortest_path(int source) {
dist[source] = 0;
buckets[0].insert(source);
while (true) {
// Находим первый непустой карман
int current_bucket = -1;
for (int i = 0; i <= max_bucket; ++i) {
if (!buckets[i].empty()) {
current_bucket = i;
break;
}
}
if (current_bucket == -1) break;
unordered_set<int> new_bucket;
unordered_set<int> S;
// Обрабатываем легкие ребра
while (!buckets[current_bucket].empty()) {
unordered_set<int> current_vertices = move(buckets[current_bucket]);
S.insert(current_vertices.begin(), current_vertices.end());
// Релаксация легких ребер
for (int v : current_vertices) {
for (const Edge& e : light_edges[v]) {
relax(e.to, dist[v] + e.weight, new_bucket);
}
}
}
// Добавляем вершины в карман
for (int v : new_bucket) {
buckets[dist[v] / delta].insert(v);
}
new_bucket.clear();
// Обрабатываем тяжелые ребра
for (int v : S) {
for (const Edge& e : heavy_edges[v]) {
relax(e.to, dist[v] + e.weight, new_bucket);
}
}
// Добавляем вершины после обработки тяжелых ребер
for (int v : new_bucket) {
buckets[dist[v] / delta].insert(v);
}
}
return dist;
}
};
int main() {
// Пример графа: список смежности {вершина, вес}
vector<vector<pair<int, int>>> graph = {
{{1, 2}, {2, 6}}, // 0
{{2, 3}, {3, 5}}, // 1
{{3, 2}, {4, 4}}, // 2
{{4, 1}}, // 3
{} // 4
};
int delta = 3; // Выбираем порог delta
DeltaStepping ds(graph, delta);
int source = 0;
vector<int> distances = ds.shortest_path(source);
cout << "Кратчайшие расстояния от вершины " << source << ":\n";
for (int i = 0; i < distances.size(); ++i) {
cout << "До " << i << ": " << distances[i] << endl;
}
return 0;
}
Параллельный алгоритм дельта-шага#
Синхронизация: потоки (процессы) обмениваются подмножествами \text{Req} , чтобы каждый обрабатывал запросы только для своих локальных вершин. Дубли удаляются при формировании \text{Req} .
Дополнительно синхронизируются:
- переход к следующему непустому карману,
- проверка критерия останова.
Предобработка (в параллель): для всех v \in V сформировать списки L[v] и H[v]
Инициализация (в параллель): для всех v \in V : d[v] = \infty , B[0] = \{s\}
Основной цикл:
- На каждом потоке k : S[k] = ∅
- Пока карман B[i] не пуст:
- Параллельно: для локальных вершин из B[i] построить \text{Req}[k] без дублей
- S[k] = S[k] ∪ B[i] , B[i] = ∅
- Обменяться \text{Req}[k] , выполнить релаксацию локально
- Параллельно: построить \text{Req}[k] из рёбер H[v] , где v \in S[k]
- Обменяться \text{Req}[k] , выполнить релаксацию
- Синхронизация: найти следующий непустой карман
Пример тестирования кода:
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
#include <algorithm>
#include <set>
#include <memory>
#include <cmath>
#include <omp.h>
#include <random>
#include <chrono>
using namespace std;
const int INF = numeric_limits<int>::max();
// Graph class for Dijkstra's implementation
class Graph {
int V;
vector<vector<pair<int, int>>> adj;
public:
Graph(int V) : V(V), adj(V) {}
void addEdge(int u, int v, int w) {
adj[u].push_back({ v, w });
adj[v].push_back({ u, w });
}
void dijkstra(int src) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
vector<int> dist(V, INF);
pq.push({ 0, src });
dist[src] = 0;
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
for (auto& edge : adj[u]) {
int v = edge.first;
int weight = edge.second;
if (dist[v] > dist[u] + weight) {
dist[v] = dist[u] + weight;
pq.push({ dist[v], v });
}
}
}
// Commented out to reduce output clutter during measurements
// cout << "Dijkstra Results:\nVertex\tDistance\n";
// for (int i = 0; i < V; ++i)
// cout << i << "\t" << dist[i] << "\n";
}
};
// Delta-Stepping implementation
void relax(int u, int v, int weight, vector<int>& distances, int delta, vector<set<int>>& buckets) {
int old_distance = distances[v];
int new_distance = distances[u] + weight;
if (new_distance < old_distance) {
distances[v] = new_distance;
int old_bucket = old_distance / delta;
int new_bucket = new_distance / delta;
if (old_distance != INF) {
buckets[old_bucket].erase(v);
}
buckets[new_bucket].insert(v);
}
}
void delta_stepping(int source, const vector<vector<pair<int, int>>>& graph, vector<int>& distances, int delta) {
int n = graph.size();
distances.assign(n, INF);
distances[source] = 0;
int max_buckets = 3 * n;
vector<set<int>> buckets(max_buckets);
buckets[0].insert(source);
for (int i = 0; i < max_buckets; ++i) {
while (!buckets[i].empty()) {
set<int> S = buckets[i];
buckets[i].clear();
// Light edges
#pragma omp parallel for schedule(dynamic)
for (int u : S) {
for (const auto& edge : graph[u]) {
if (edge.second <= delta) {
#pragma omp critical
relax(u, edge.first, edge.second, distances, delta, buckets);
}
}
}
// Heavy edges
#pragma omp parallel for schedule(dynamic)
for (int u : S) {
for (const auto& edge : graph[u]) {
if (edge.second > delta) {
#pragma omp critical
relax(u, edge.first, edge.second, distances, delta, buckets);
}
}
}
}
}
}
// Function to generate a random graph with given size and density
vector<vector<pair<int, int>>> generateRandomGraph(int size, double density) {
vector<vector<pair<int, int>>> graph(size);
random_device rd;
mt19937 gen(rd());
uniform_int_distribution<> weight_dist(1, 100);
uniform_real_distribution<> prob_dist(0.0, 1.0);
for (int u = 0; u < size; ++u) {
for (int v = u + 1; v < size; ++v) {
if (prob_dist(gen) < density) {
int weight = weight_dist(gen);
graph[u].push_back({ v, weight });
graph[v].push_back({ u, weight });
}
}
}
// Ensure the graph is connected by adding at least one edge per vertex
for (int u = 0; u < size; ++u) {
if (graph[u].empty()) {
int v = (u + 1) % size;
int weight = weight_dist(gen);
graph[u].push_back({ v, weight });
graph[v].push_back({ u, weight });
}
}
return graph;
}
void runComparison(int n, double density) {
cout << "\n=== Comparing algorithms for n = " << n << ", density = " << density << " ===\n";
// Generate random graph
auto adjList = generateRandomGraph(n, density);
// Create Graph object for Dijkstra
Graph g(n);
for (int u = 0; u < adjList.size(); u++) {
for (const auto& edge : adjList[u]) {
if (u < edge.first) { // Avoid adding edges twice
g.addEdge(u, edge.first, edge.second);
}
}
}
// Run Dijkstra's algorithm
double start = omp_get_wtime();
g.dijkstra(0);
double dijkstra_time = omp_get_wtime() - start;
printf("Dijkstra's time: %f seconds\n", dijkstra_time);
// Run Delta-Stepping algorithm
vector<int> distances;
int delta = 10; // Fixed delta value for simplicity
start = omp_get_wtime();
delta_stepping(0, adjList, distances, delta);
double delta_time = omp_get_wtime() - start;
// Commented out to reduce output clutter during measurements
// cout << "Delta-Stepping Results:\nVertex\tDistance\n";
// for (int i = 0; i < distances.size(); ++i)
// cout << i << "\t" << distances[i] << "\n";
printf("Delta-Stepping time: %f seconds\n", delta_time);
cout << "\nSpeedup: " << dijkstra_time / delta_time << "x\n";
}
int main() {
omp_set_num_threads(4); // Use 4 threads for parallel sections
// Test parameters
vector<int> sizes = { 10000, 20000, 30000, 50000 };
vector<double> densities = { 0.1, 0.3, 0.5, 0.7, 0.9 };
// Run comparisons for all combinations
for (int n : sizes) {
for (double d : densities) {
runComparison(n, d);
}
}
return 0;
}
Алгоритм А зведочка#
Алгоритм A* является одним из наиболее популярных и эффективных алгоритмов поиска пути, используемых в различных областях, таких как робототехника, игры и навигационные системы. Он сочетает в себе преимущества алгоритмов поиска по ширине и жадного поиска, что позволяет ему находить оптимальные решения с минимальными затратами времени и ресурсов.
A* — это модификация алгоритма Дейкстры, оптимизированная для единственной конечной точки. Алгоритм Дейкстры может находить пути ко всем точкам, A* находит путь к одной точке. Он отдаёт приоритет путям, которые ведут ближе к цели.
A* пошагово просматривает все пути, ведущие от начальной вершины в конечную, пока не найдёт минимальный. Как и все информированные алгоритмы поиска, он просматривает сначала те маршруты, которые «кажутся» ведущими к цели. Он использует эвристическую функцию, которая помогает выбрать наиболее перспективное направление поиска.
g(n)
— стоимость пути от начальной вершины до вершиныn
.h(n)
— эвристическая оценка расстояния отn
до цели.f(n) = g(n) + h(n)
— общая оценка стоимости пути через вершинуn
.
Алгоритм выбирает вершины с наименьшим значением f(n)
.
A*(start, goal):
openSet ← {start}
gScore[start] ← 0
fScore[start] ← h(start)
while openSet is not empty:
current ← вершина в openSet с наименьшим fScore[current]
if current == goal:
return восстановить путь
openSet.remove(current)
for каждый сосед neighbor of current:
tentative_gScore ← gScore[current] + расстояние(current, neighbor)
if tentative_gScore < gScore[neighbor]:
cameFrom[neighbor] ← current
gScore[neighbor] ← tentative_gScore
fScore[neighbor] ← gScore[neighbor] + h(neighbor)
if neighbor not in openSet:
openSet.add(neighbor)
return "Путь не найден"
Список источников#
- А А. Аль-саиди, И О. Темкин, В И. Алтай, А Ф. Алмунтафеки, А Н. Мохедхуссин Повышение эффективности алгоритма Дейкстры с помощью технологий параллельных вычислений с библиотекой OpenMP // ИВД. 2023. №8 (104). URL: https://cyberleninka.ru/article/n/povyshenie-effektivnosti-algoritma-deykstry-s-pomoschyu-tehnologiy-parallelnyh-vychisleniy-s-bibliotekoy-openmp (дата обращения: 21.04.2025).