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

0-1 BFS

Добре відомо, що знайти найкоротші шляхи від однієї стартової вершини до всіх інших можна за O(E)O(|E|) за допомогою пошуку в ширину (BFS) у незваженому графі, тобто відстань — це мінімальна кількість ребер, які потрібно пройти від джерела до іншої вершини. Такий граф можна також трактувати як зважений, у якому кожне ребро має вагу 11. Якщо ж не всі ребра графа мають однакову вагу, нам потрібен більш загальний алгоритм, як-от Дейкстри, що працює за час O(V2+E)O(|V|^2 + |E|) або O(ElogV)O(|E| \log |V|).

Проте якщо ваги обмежені сильніше, ми часто можемо досягти кращого результату. У цій статті ми покажемо, як за допомогою BFS розв'язати задачу SSSP (найкоротший шлях від однієї вершини, single-source shortest path) за O(E)O(|E|), якщо вага кожного ребра дорівнює 00 або 11.

Коли підходить цей алгоритм?
  • Вага кожного ребра дорівнює рівно 00 або 11? (якщо всі ваги однакові → BFS; якщо ваги довільні невід'ємні → Дейкстра)
  • Усі ваги невід'ємні (немає від'ємних ребер чи циклів)? (якщо ні → Беллман-Форд)
  • Ваги обмежені малою константою kk, а не лише {0,1}\{0,1\}? (тоді підходить узагальнення — алгоритм Діала, описаний нижче)

Алгоритм

Розробити цей алгоритм ми можемо, уважно вивчивши алгоритм Дейкстри й поміркувавши над наслідками, які накладає наш особливий граф. Загальний вигляд алгоритму Дейкстри такий (тут для черги з пріоритетом використано set):

d.assign(n, INF);
d[s] = 0;
set<pair<int, int>> q;
q.insert({0, s});
while (!q.empty()) {
int v = q.begin()->second;
q.erase(q.begin());

for (auto edge : adj[v]) {
int u = edge.first;
int w = edge.second;

if (d[v] + w < d[u]) {
q.erase({d[u], u});
d[u] = d[v] + w;
q.insert({d[u], u});
}
}
}

Ми можемо помітити, що різниця між відстанями від джерела s до двох інших вершин у черзі відрізняється щонайбільше на одиницю. Зокрема, ми знаємо, що d[v]d[u]d[v]+1d[v] \le d[u] \le d[v] + 1 для кожного uQu \in Q. Причина цього в тому, що під час кожної ітерації ми додаємо до черги лише вершини з рівною відстанню або з відстанню, більшою на одиницю. Припустимо, що в черзі існує така uu, що d[u]d[v]>1d[u] - d[v] > 1; тоді uu мала би бути додана до черги через іншу вершину tt з d[t]d[u]1>d[v]d[t] \ge d[u] - 1 > d[v]. Однак це неможливо, оскільки алгоритм Дейкстри перебирає вершини в порядку зростання відстані.

Це означає, що порядок черги виглядає так:

Q=vd[v],,ud[v],md[v]+1nd[v]+1Q = \underbrace{v}_{d[v]}, \dots, \underbrace{u}_{d[v]}, \underbrace{m}_{d[v]+1} \dots \underbrace{n}_{d[v]+1}

Ця структура настільки проста, що нам не потрібна справжня черга з пріоритетом, тобто використання збалансованого бінарного дерева було б надмірністю. Ми можемо просто скористатися звичайною чергою й додавати нові вершини на початок, якщо відповідне ребро має вагу 00, тобто якщо d[u]=d[v]d[u] = d[v], або в кінець, якщо ребро має вагу 11, тобто якщо d[u]=d[v]+1d[u] = d[v] + 1. У такий спосіб черга весь час залишається відсортованою.

vector<int> d(n, INF);
d[s] = 0;
deque<int> q;
q.push_front(s);
while (!q.empty()) {
int v = q.front();
q.pop_front();
for (auto edge : adj[v]) {
int u = edge.first;
int w = edge.second;
if (d[v] + w < d[u]) {
d[u] = d[v] + w;
if (w == 1)
q.push_back(u);
else
q.push_front(u);
}
}
}

Алгоритм Діала

Ми можемо розширити цей підхід ще далі, якщо дозволимо ребрам мати більші ваги. Якщо кожне ребро графа має вагу k\le k, то відстані вершин у черзі відрізнятимуться від відстані vv до джерела щонайбільше на kk. Тож ми можемо тримати k+1k + 1 кошиків для вершин у черзі, і щоразу, коли кошик, що відповідає найменшій відстані, спорожнюється, ми робимо циклічний зсув, щоб отримати кошик з наступною більшою відстанню. Це розширення називається алгоритмом Діала.

Задачі для практики

Відеоматеріали