Перейти до основного вмісту

Алгоритм Д'Есопо–Папе

Дано граф із nn вершинами та mm ребрами з вагами wiw_i, а також початкову вершину v0v_0. Потрібно знайти найкоротший шлях від вершини v0v_0 до кожної іншої вершини.

У більшості випадків алгоритм Д'Есопо–Папе працює швидше за алгоритм Дейкстри та алгоритм Беллмана–Форда, і до того ж він працює з від'ємними ребрами. Проте він не працює з від'ємними циклами.

Коли підходить цей алгоритм?
  • Потрібні найкоротші шляхи з однієї вершини, і граф може містити ребра від'ємної ваги (але без від'ємних циклів)? (якщо всі ваги невід'ємні → надійніший Дейкстра)
  • Граф «дружній» (не спеціально побудований як контрприклад), бо на зловмисних входах алгоритм може деградувати до експоненційного часу? (якщо потрібна гарантована оцінка часу на від'ємних вагах → Беллман-Форд)
  • У графі немає від'ємних циклів? (для їх пошуку → Пошук від'ємного циклу)

Опис

Нехай масив dd містить довжини найкоротших шляхів, тобто did_i — це поточна довжина найкоротшого шляху від вершини v0v_0 до вершини ii. Спочатку цей масив заповнено нескінченністю для кожної вершини, окрім dv0=0d_{v_0} = 0. Після завершення алгоритму цей масив міститиме найкоротші відстані.

Нехай масив pp містить поточних предків, тобто pip_i — це безпосередній предок вершини ii на поточному найкоротшому шляху від v0v_0 до ii. Так само як і масив dd, масив pp поступово змінюється протягом роботи алгоритму, а наприкінці набуває своїх остаточних значень.

Тепер перейдемо до самого алгоритму. На кожному кроці підтримуються три множини вершин:

  • M0M_0 — вершини, для яких відстань уже обчислено (хоча вона може й не бути остаточною)
  • M1M_1 — вершини, для яких відстань обчислюється зараз
  • M2M_2 — вершини, для яких відстань ще не обчислено

Вершини з множини M1M_1 зберігаються у двосторонній черзі (деку).

На кожному кроці алгоритму ми беремо вершину з множини M1M_1 (з початку черги). Нехай uu — обрана вершина. Ми поміщаємо цю вершину uu до множини M0M_0. Потім перебираємо всі ребра, що виходять із цієї вершини. Нехай vv — другий кінець поточного ребра, а ww — його вага.

  • Якщо vv належить до M2M_2, то vv вставляється до множини M1M_1 шляхом додавання її в кінець черги. dvd_v встановлюється рівним du+wd_u + w.
  • Якщо vv належить до M1M_1, то ми намагаємось покращити значення dvd_v: dv=min(dv,du+w)d_v = \min(d_v, d_u + w). Оскільки vv уже є в M1M_1, нам не потрібно вставляти її до M1M_1 та черги.
  • Якщо vv належить до M0M_0 і якщо dvd_v можна покращити dv>du+wd_v > d_u + w, то ми покращуємо dvd_v і вставляємо вершину vv назад до множини M1M_1, розміщуючи її на початку черги.

І, звісно, з кожним оновленням у масиві dd ми також маємо оновлювати відповідний елемент у масиві pp.

Реалізація

Ми використовуватимемо масив mm, щоб зберігати, у якій множині наразі перебуває кожна вершина.

struct Edge {
int to, w;
};

int n;
vector<vector<Edge>> adj;

const int INF = 1e9;

void shortest_paths(int v0, vector<int>& d, vector<int>& p) {
d.assign(n, INF);
d[v0] = 0;
vector<int> m(n, 2);
deque<int> q;
q.push_back(v0);
p.assign(n, -1);

while (!q.empty()) {
int u = q.front();
q.pop_front();
m[u] = 0;
for (Edge e : adj[u]) {
if (d[e.to] > d[u] + e.w) {
d[e.to] = d[u] + e.w;
p[e.to] = u;
if (m[e.to] == 2) {
m[e.to] = 1;
q.push_back(e.to);
} else if (m[e.to] == 0) {
m[e.to] = 1;
q.push_front(e.to);
}
}
}
}
}

Складність

Зазвичай алгоритм працює досить швидко — у більшості випадків навіть швидше за алгоритм Дейкстри. Проте існують випадки, для яких алгоритм виконується експоненційний час, що робить його непридатним у найгіршому випадку. Для довідки див. обговорення на Stack Overflow та Codeforces.