Алгоритм Дейкстри
Нам задано орієнтований або неорієнтований зважений граф з вершинами та ребрами. Ваги всіх ребер невід'ємні. Також задано початкову вершину . У цій статті ми розглянемо, як знайти довжини найкоротших шляхів від початкової вершини до всіх інших вершин, а також вивести самі найкоротші шляхи.
Цю задачу також називають задачею про найкоротші шляхи з однієї вершини (single-source shortest paths problem).
- Усі ваги ребер невід'ємні? (якщо є від'ємні ребра → Беллман-Форд)
- Потрібні найкоротші шляхи з однієї вершини, а не між усіма парами? (якщо потрібні всі пари при малому → Флойд-Воршелл)
- Ваги довільні (не лише )? (якщо ваги лише та → швидший 0-1 BFS; якщо граф незважений → BFS)
- Граф щільний? Ця реалізація за оптимальна для щільних графів; (для розріджених графів → версія з купою/set)
Алгоритм
Ось алгоритм, описаний нідерландським комп'ютерним науковцем Едсгером В. Дейкстрою у 1959 році.
Створимо масив , у якому для кожної вершини зберігатимемо поточну довжину найкоротшого шляху від до у . Спочатку , а для всіх інших вершин ця довжина дорівнює нескінченності. У реалізації за нескінченність обирають достатньо велике число (яке гарантовано більше за будь-яку можливу довжину шляху).
Окрім того, ми підтримуємо булів масив , у якому для кожної вершини зберігається, чи вона позначена. Спочатку всі вершини непозначені:
Алгоритм Дейкстри виконується протягом ітерацій. На кожній ітерації як вершина обирається непозначена вершина, що має найменше значення :
Очевидно, що на першій ітерації буде обрано початкову вершину .
Обрана вершина позначається. Далі з вершини виконуються релаксації: розглядаються всі ребра виду , і для кожної вершини алгоритм намагається покращити значення . Якщо довжина поточного ребра дорівнює , то код релаксації має вигляд:
Після того як усі такі ребра розглянуто, поточна ітерація завершується. Нарешті, після ітерацій усі вершини будуть позначені, і алгоритм завершується. Ми стверджуємо, що знайдені значення є довжинами найкоротших шляхів від до всіх вершин .
Зауважимо, що якщо деякі вершини недосяжні з початкової вершини , то значення для них залишаться нескінченними. Очевидно, що на кількох останніх ітераціях алгоритм обиратиме саме ці вершини, але жодної корисної роботи для них зроблено не буде. Тому алгоритм можна зупинити, щойно обрана вершина має нескінченну відстань до неї.
Відновлення найкоротших шляхів
Зазвичай потрібно знати не лише довжини найкоротших шляхів, а й самі найкоротші шляхи. Подивимося, як підтримувати достатньо інформації, щоб відновити найкоротший шлях від до будь-якої вершини. Ми підтримуватимемо масив попередників , у якому для кожної вершини значення — це передостання вершина в найкоротшому шляху від до . Тут ми використовуємо той факт, що якщо взяти найкоротший шлях до деякої вершини і прибрати з цього шляху , то отримаємо шлях, що закінчується у вершині , і цей шлях буде найкоротшим для вершини . Цей масив попередників можна використати для відновлення найкоротшого шляху до будь-якої вершини: починаючи з , ми повторно беремо попередника поточної вершини, доки не дійдемо до початкової вершини , і отримуємо потрібний найкоротший шлях із вершинами, перерахованими у зворотному порядку. Отже, найкоротший шлях до вершини дорівнює:
Побудувати цей масив попередників дуже просто: при кожній успішній релаксації, тобто коли для деякої обраної вершини відбувається покращення відстані до деякої вершини , ми оновлюємо вершину-попередника для вершиною :
Доведення
Основне твердження, на якому ґрунтується коректність алгоритму Дейкстри, таке:
Після того як будь-яка вершина стає позначеною, поточна відстань до неї є найкоротшою і більше не змінюватиметься.
Доведення проводиться методом індукції. Для першої ітерації це твердження очевидне: єдина позначена вершина — це , і відстань до неї справді є довжиною найкоротшого шляху до . Тепер припустимо, що це твердження істинне для всіх попередніх ітерацій, тобто для всіх уже позначених вершин; доведемо, що воно не порушується після завершення поточної ітерації. Нехай — вершина, обрана на поточній ітерації, тобто — це вершина, яку алгоритм позначить. Тепер нам треба довести, що справді дорівнює довжині найкоротшого шляху до неї .
Розглянемо найкоротший шлях до вершини . Цей шлях можна розбити на дві частини: , яка складається лише з позначених вершин (принаймні початкова вершина є частиною ), і решту шляху (вона може містити позначену вершину, але завжди починається з непозначеної вершини). Позначимо першу вершину шляху як , а останню вершину шляху як .
Спочатку доведемо наше твердження для вершини , тобто доведемо, що . Це майже очевидно: на одній з попередніх ітерацій ми обрали вершину і виконали з неї релаксацію. Оскільки (через вибір вершини ) найкоротший шлях до — це найкоротший шлях до плюс ребро , релаксація з встановила значення рівним довжині найкоротшого шляху .
Оскільки ваги ребер невід'ємні, довжина найкоротшого шляху (яку ми щойно довели рівною ) не перевищує довжину найкоротшого шляху до вершини . З огляду на те, що (бо алгоритм Дейкстри не міг знайти коротшого шляху, ніж найкоротший можливий), отримуємо нерівність:
З іншого боку, оскільки обидві вершини і непозначені, і на поточній ітерації було обрано вершину , а не , отримуємо ще одну нерівність:
З цих двох нерівностей робимо висновок, що , а тоді з раніше знайдених рівностей отримуємо:
Що й треба було довести.
Реалізація
Алгоритм Дейкстри виконує ітерацій. На кожній ітерації він обирає непозначену вершину з найменшим значенням , позначає її та перевіряє всі ребра , намагаючись покращити значення .
Час роботи алгоритму складається з:
- пошуків вершини з найменшим значенням серед непозначених вершин
- спроб релаксації
Для найпростішої реалізації цих операцій на кожній ітерації пошук вершини потребує операцій, а кожну релаксацію можна виконати за . Отже, підсумкова асимптотика алгоритму:
Ця складність оптимальна для щільного графа, тобто коли . Однак у розріджених графах, коли значно менше за максимальну кількість ребер , задачу можна розв'язати зі складністю . Алгоритм і реалізацію можна знайти у статті Алгоритм Дейкстри на розріджених графах.
- C++
- Python
- TypeScript
- Go
const int INF = 1000000000;
vector<vector<pair<int, int>>> adj;
void dijkstra(int s, vector<int> & d, vector<int> & p) {
int n = adj.size();
d.assign(n, INF);
p.assign(n, -1);
vector<bool> u(n, false);
d[s] = 0;
for (int i = 0; i < n; i++) {
int v = -1;
for (int j = 0; j < n; j++) {
if (!u[j] && (v == -1 || d[j] < d[v]))
v = j;
}
if (d[v] == INF)
break;
u[v] = true;
for (auto edge : adj[v]) {
int to = edge.first;
int len = edge.second;
if (d[v] + len < d[to]) {
d[to] = d[v] + len;
p[to] = v;
}
}
}
}
INF = 1000000000
# adj — список суміжності: adj[v] містить пари (to, len)
def dijkstra(s: int, adj: list[list[tuple[int, int]]]) -> tuple[list[int], list[int]]:
n = len(adj)
d = [INF] * n # відстані від s
p = [-1] * n # попередники
u = [False] * n # позначені вершини
d[s] = 0
for _ in range(n):
# обираємо непозначену вершину з найменшою відстанню
v = -1
for j in range(n):
if not u[j] and (v == -1 or d[j] < d[v]):
v = j
if d[v] == INF:
break
u[v] = True
for to, length in adj[v]:
# релаксація ребра (v, to)
if d[v] + length < d[to]:
d[to] = d[v] + length
p[to] = v
return d, p
const INF = 1000000000;
// adj — список суміжності: adj[v] містить пари [to, len]
function dijkstra(
s: number,
adj: [number, number][][],
): { d: number[]; p: number[] } {
const n = adj.length;
const d: number[] = new Array(n).fill(INF); // відстані від s
const p: number[] = new Array(n).fill(-1); // попередники
const u: boolean[] = new Array(n).fill(false); // позначені вершини
d[s] = 0;
for (let i = 0; i < n; i++) {
// обираємо непозначену вершину з найменшою відстанню
let v = -1;
for (let j = 0; j < n; j++) {
if (!u[j] && (v === -1 || d[j] < d[v])) {
v = j;
}
}
if (d[v] === INF) {
break;
}
u[v] = true;
for (const [to, len] of adj[v]) {
// релаксація ребра (v, to)
if (d[v] + len < d[to]) {
d[to] = d[v] + len;
p[to] = v;
}
}
}
return { d, p };
}
const INF = 1000000000
// adj — список суміжності: adj[v] містить пари {to, len}
func dijkstra(s int, adj [][][2]int) (d, p []int) {
n := len(adj)
d = make([]int, n) // відстані від s
p = make([]int, n) // попередники
u := make([]bool, n) // позначені вершини
for i := range d {
d[i] = INF
p[i] = -1
}
d[s] = 0
for i := 0; i < n; i++ {
// обираємо непозначену вершину з найменшою відстанню
v := -1
for j := 0; j < n; j++ {
if !u[j] && (v == -1 || d[j] < d[v]) {
v = j
}
}
if d[v] == INF {
break
}
u[v] = true
for _, edge := range adj[v] {
to, length := edge[0], edge[1]
// релаксація ребра (v, to)
if d[v]+length < d[to] {
d[to] = d[v] + length
p[to] = v
}
}
}
return d, p
}
Тут граф зберігається як список суміжності: для кожної вершини значення містить список ребер, що виходять з цієї вершини, тобто список pair<int,int>, де перший елемент пари — це вершина на іншому кінці ребра, а другий елемент — вага ребра.
Функція приймає початкову вершину і два вектори, які будуть використані як значення, що повертаються.
Найперше код ініціалізує масиви: відстаней , позначок і попередників . Потім він виконує ітерацій. На кожній ітерації обирається вершина , яка має найменшу відстань серед усіх непозначених вершин. Якщо відстань до обраної вершини дорівнює нескінченності, алгоритм зупиняється. Інакше вершина позначається, і перевіряються всі ребра, що виходять з цієї вершини. Якщо релаксація вздовж ребра можлива (тобто відстань можна покращити), то відстань і попередник оновлюються.
Після виконання всіх ітерацій масив зберігає довжини найкоротших шляхів до всіх вершин, а масив зберігає попередників усіх вершин (крім початкової вершини ). Шлях до будь-якої вершини можна відновити так:
- C++
- Python
- TypeScript
- Go
vector<int> restore_path(int s, int t, vector<int> const& p) {
vector<int> path;
for (int v = t; v != s; v = p[v])
path.push_back(v);
path.push_back(s);
reverse(path.begin(), path.end());
return path;
}
def restore_path(s: int, t: int, p: list[int]) -> list[int]:
path = []
# йдемо від t назад по попередниках, доки не дійдемо до s
v = t
while v != s:
path.append(v)
v = p[v]
path.append(s)
path.reverse() # розвертаємо, щоб шлях ішов від s до t
return path
function restorePath(s: number, t: number, p: number[]): number[] {
const path: number[] = [];
// йдемо від t назад по попередниках, доки не дійдемо до s
for (let v = t; v !== s; v = p[v]) {
path.push(v);
}
path.push(s);
path.reverse(); // розвертаємо, щоб шлях ішов від s до t
return path;
}
func restorePath(s, t int, p []int) []int {
path := []int{}
// йдемо від t назад по попередниках, доки не дійдемо до s
for v := t; v != s; v = p[v] {
path = append(path, v)
}
path = append(path, s)
// розвертаємо, щоб шлях ішов від s до t
for i, j := 0, len(path)-1; i < j; i, j = i+1, j-1 {
path[i], path[j] = path[j], path[i]
}
return path
}
Джерела
- Edsger Dijkstra. A note on two problems in connexion with graphs [1959]
- Thomas Cormen, Charles Leiserson, Ronald Rivest, Clifford Stein. Introduction to Algorithms [2005]
Задачі для практики
- Timus - Ivan's Car [Difficulty:Medium]
- Timus - Sightseeing Trip
- SPOJ - SHPATH [Difficulty:Easy]
- Codeforces - Dijkstra? [Difficulty:Easy]
- Codeforces - Shortest Path
- Codeforces - Jzzhu and Cities
- Codeforces - The Classic Problem
- Codeforces - President and Roads
- Codeforces - Complete The Graph
- TopCoder - SkiResorts
- TopCoder - MaliciousPath
- SPOJ - Ada and Trip
- LA - 3850 - Here We Go(relians) Again
- GYM - Destination Unknown (D)
- UVA 12950 - Even Obsession
- GYM - Journey to Grece (A)
- UVA 13030 - Brain Fry
- UVA 1027 - Toll
- UVA 11377 - Airport Setup
- Codeforces - Dynamic Shortest Path
- UVA 11813 - Shopping
- UVA 11833 - Route Change
- SPOJ - Easy Dijkstra Problem
- LA - 2819 - Cave Raider
- UVA 12144 - Almost Shortest Path
- UVA 12047 - Highest Paid Toll
- UVA 11514 - Batman
- Codeforces - Team Rocket Rises Again
- UVA - 11338 - Minefield
- UVA 11374 - Airport Express
- UVA 11097 - Poor My Problem
- UVA 13172 - The music teacher
- Codeforces - Dirty Arkady's Kitchen
- SPOJ - Delivery Route
- SPOJ - Costly Chess
- CSES - Shortest Routes 1
- CSES - Flight Discount
- CSES - Flight Routes