0 - 1 пошук в ширину (або 0 - 1 BFS) — це модифікація класичного алгоритму пошуку в ширину (BFS), який використовується для знаходження найкоротших шляхів у графах. Відмінність цього алгоритму в тому, що ваги ребер у графі можуть приймати лише значення або , що дозволяє значно оптимізувати пошук.
Постановка задачі. Уявімо, що у нас є зважений граф, у якому ваги ребер — це або , або . Наприклад, такий граф можна зустріти, коли ми працюємо з містом, в якому деякі дороги безкоштовні (вага ), а за інші доводиться платити (вага ). Ми хочемо знайти найкоротший шлях від однієї вершини графа до іншої з урахуванням цих ваг.
Проблема стандартного алгоритму пошуку в ширину. Класичний алгоритм пошуку в ширину (BFS) має оцінку складності O(V + E) і використовується для пошуку найкоротших шляхів у графі, але він підходить лише для графів, де всі ребра мають однакову вагу. У випадку, якщо ваги ребер різні, такий підхід вже неефективний — для цих цілей зазвичай застосовують алгоритм Дейкстри з оцінкою складності O(V^2 + E) або O(E \cdot log_2V). Але для графів з вагами лише і існує швидший підхід — 0 - 1 BFS.
0-1 BFS Алгоритм
У 0 - 1 BFS ми використовуємо двосторонню чергу (deque). Ідея полягає в наступному:
Ми починаємо з початкової вершини і додаємо її до черги з початковою відстанню, що дорівнює .
При обробці кожної вершини (яка витягується з голови черги) розглядаємо її сусідів :
Якщо у ребра вага , то сусідня вершина поміщається на початок черги з поточною відстанню (dist[v] = dist[u]).
Якщо у ребра вага , то сусідня вершина поміщається в кінець черги зі збільшенням відстані на одиницю (dist[v] = dist[u] + 1).
Це дозволяє ефективно підтримувати правильний порядок обробки вершин: вершини, досяжні через ребра з меншою вагою, обробляються раніше.
Таким чином, використання двосторонньої черги гарантує, що ми завжди маємо доступ до вершини з мінімальною відстанню на початку черги, а складність роботи алгоритму залишається O(V + E).
Релаксація ребер при 0 - 1 пошуку в ширину схожа на класичну релаксацію в алгоритмі Дейкстри, але відрізняється використанням двосторонньої черги (deque) замість черги з пріоритетами (priority queue) для впорядкування вершин на основі ваги ребер.
Алгоритм 0-1 BFS (на відміну від класичного BFS) не гарантує, що кожна вершина буде відвідана з мінімальною відстанню з першого разу, особливо якщо до однієї й тієї самої вершини можна дістатися кількома шляхами. Тому релаксація ребер обов'язкова.
Розглянемо наступний приклад. Ініціалізуємо масив найкоротших відстаней та чергу :
Розглянемо ребра, що виходять з вершини . Релаксуємо ребра та . Вершина буде занесена на початок черги, а вершина — у кінець черги.
Однак значення dist[2] = 1 не є остаточним. Пройдемо по ребру 3 - 4 і встановимо dist[4] = 0, queue = (4, 2). Потім розглянемо ребро 4 - 2 і значення встановиться рівним . Таким чином, приймало два різних значення в результаті релаксації (спочатку , потім ).
Приклад
Граф, наведений у прикладі, має вигляд:
Довжина найкоротшого шляху з вершини у дорівнює , а сам найкоротший шлях має вигляд: 1 - 2 - 3.
Реалізація алгоритму
Оголосимо константу нескінченність.
#define INF 0x3F3F3F3FОголосимо масив найкоротших відстаней і список суміжності графа .
vector\u003cint\u003e dist;
vector\u003cvector\u003cpair\u003cint, int\u003e\u003e\u003e g;Функція bfs реалізує пошук у ширину з вершини .
void bfs(int start)
{Ініціалізуємо масив найкоротших відстаней .
dist = vector\u003cint\u003e(n + 1, INF);
dist[start] = 0;Оголосимо чергу і занесемо в неї стартову вершину .
deque\u003cint\u003e q;
q.push_back(start);Продовжуємо алгоритм пошуку в ширину, поки черга не порожня.
while (!q.empty())
{Витягуємо з голови черги вершину .
int v = q.front(); q.pop_front();Перебираємо ребра, що виходять з вершини .
for (auto edge : g[v])
{
int to = edge.first;
int w = edge.second;Поточне ребро (v, to) має вагу . Проводимо його релаксацію.
if (dist[to] \u003e dist[v] + w)
{Якщо ребро релаксує, то перераховуємо найкоротшу відстань dist[to].
dist[to] = dist[v] + w;Залежно від ваги ребра додаємо вершину в кінець або на початок черги.
if (w == 1)
q.push_back(to);
else
q.push_front(to);
}
}
}
}Основна частина програми. Читаємо вхідні дані.
scanf("%d %d %d %d", \u0026n, \u0026m, \u0026s, \u0026d);
g.resize(n + 1);Читаємо граф.
for (i = 0; i \u003c m; i++)
{
scanf("%d %d %d", \u0026a, \u0026b, \u0026w);
g[a].push_back({ b, w });
g[b].push_back({ a, w });
}Запускаємо пошук у ширину зі стартової вершини .
bfs(s);Виводимо найкоротшу відстань до вершини .
printf("%d\n", dist[d]);