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

Пошук у ширину (BFS)

Пошук у ширину — один з базових і фундаментальних алгоритмів пошуку на графах.

Завдяки тому, як працює цей алгоритм, шлях, який пошук у ширину знаходить до будь-якої вершини, є найкоротшим шляхом до цієї вершини, тобто шляхом, що містить найменшу кількість ребер у незважених графах.

Алгоритм працює за час O(n+m)O(n + m), де nn — кількість вершин, а mm — кількість ребер.

Коли підходить цей алгоритм?
  • Граф незважений (або всі ребра мають однакову вагу), а потрібен найкоротший шлях / мінімальна кількість кроків? (якщо ваги лише 00 та 110-1 BFS; якщо ваги довільні невід'ємні → Дейкстра)
  • Задачу можна подати як граф станів, де перехід між сусідніми станами «коштує» один крок (сітки, головоломки, ходи фігури)?
  • Достатньо обходу в порядку зростання відстані від джерела (компоненти зв'язності, найкоротший цикл), а не лексикографічного обходу вглиб? (якщо потрібен саме обхід вглиб → DFS)

Опис алгоритму

На вхід алгоритм отримує незважений граф та номер початкової вершини ss. Вхідний граф може бути орієнтованим або неорієнтованим — для алгоритму це не має значення.

Алгоритм можна уявити як поширення вогню графом: на нульовому кроці горить лише початкова вершина ss. На кожному кроці вогонь, що горить у кожній вершині, поширюється на всіх її сусідів. За одну ітерацію алгоритму «кільце вогню» розширюється у ширину на одну одиницю (звідси й назва алгоритму).

Точніше алгоритм можна сформулювати так: створимо чергу qq, яка міститиме вершини для обробки, та булевий масив used[]used[], що для кожної вершини вказує, чи її вже запалили (відвідали), чи ні.

Спочатку додаємо початкову вершину ss до черги та встановлюємо used[s]=trueused[s] = true, а для всіх інших вершин vv встановлюємо used[v]=falseused[v] = false. Потім виконуємо цикл, доки черга не спорожніє, і на кожній ітерації витягуємо вершину з початку черги. Перебираємо всі ребра, що виходять з цієї вершини, і якщо деякі з цих ребер ведуть до вершин, які ще не запалені, запалюємо їх і додаємо до черги.

У результаті, коли черга спорожніє, «кільце вогню» міститиме всі вершини, досяжні з початкової вершини ss, причому кожна вершина досягається найкоротшим можливим шляхом. Можна також обчислити довжини найкоротших шляхів (для цього достатньо підтримувати масив довжин шляхів d[]d[]), а також зберегти інформацію для відновлення всіх цих найкоротших шляхів (для цього необхідно підтримувати масив «батьків» p[]p[], який для кожної вершини зберігає вершину, з якої ми до неї дісталися).

Реалізація

Напишемо код описаного алгоритму на C++ та Java.

vector<vector<int>> adj; // представлення у вигляді списку суміжності
int n; // кількість вершин
int s; // початкова вершина

queue<int> q;
vector<bool> used(n);
vector<int> d(n), p(n);

q.push(s);
used[s] = true;
p[s] = -1;
while (!q.empty()) {
int v = q.front();
q.pop();
for (int u : adj[v]) {
if (!used[u]) {
used[u] = true;
q.push(u);
d[u] = d[v] + 1;
p[u] = v;
}
}
}

Якщо нам потрібно відновити та вивести найкоротший шлях від початкової вершини до деякої вершини uu, це можна зробити так:

if (!used[u]) {
cout << "No path!";
} else {
vector<int> path;
for (int v = u; v != -1; v = p[v])
path.push_back(v);
reverse(path.begin(), path.end());
cout << "Path: ";
for (int v : path)
cout << v << " ";
}

Застосування BFS

  • Знайти найкоротший шлях від початкової вершини до інших вершин у незваженому графі.

  • Знайти всі компоненти зв'язності в неорієнтованому графі за час O(n+m)O(n + m): Для цього ми просто запускаємо BFS, починаючи з кожної вершини, окрім вершин, які вже були відвідані під час попередніх запусків. Таким чином, ми виконуємо звичайний BFS з кожної вершини, але не скидаємо масив used[]used[] щоразу, коли отримуємо нову компоненту зв'язності, і загальний час роботи все одно становитиме O(n+m)O(n + m) (виконання кількох BFS на графі без обнулення масиву used[]used [] називається серією пошуків у ширину).

  • Пошук розв'язку задачі або гри з найменшою кількістю ходів, якщо кожен стан гри можна подати вершиною графа, а переходи з одного стану в інший — ребрами графа.

  • Пошук найкоротшого шляху в графі з вагами 0 або 1: Для цього потрібна лише невелика модифікація звичайного пошуку у ширину: замість того щоб підтримувати масив used[]used[], ми тепер перевіряємо, чи відстань до вершини менша за поточну знайдену відстань, і тоді, якщо поточне ребро має нульову вагу, додаємо його на початок черги, інакше — в кінець черги. Ця модифікація детальніше описана в статті 0-1 BFS.

  • Пошук найкоротшого циклу в орієнтованому незваженому графі: Запускаємо пошук у ширину з кожної вершини. Щойно ми спробуємо перейти з поточної вершини назад до початкової вершини, ми знайшли найкоротший цикл, що містить початкову вершину. У цей момент ми можемо зупинити BFS і почати новий BFS з наступної вершини. З усіх таких циклів (щонайбільше по одному з кожного BFS) вибираємо найкоротший.

  • Знайти всі ребра, що лежать на якомусь найкоротшому шляху між заданою парою вершин (a,b)(a, b). Для цього запускаємо два пошуки у ширину: один з aa і один з bb. Нехай da[]d_a [] — масив, що містить найкоротші відстані, отримані з першого BFS (з aa), а db[]d_b [] — масив, що містить найкоротші відстані, отримані з другого BFS з bb. Тепер для кожного ребра (u,v)(u, v) легко перевірити, чи лежить це ребро на якомусь найкоротшому шляху між aa і bb: критерієм є умова da[u]+1+db[v]=da[b]d_a [u] + 1 + d_b [v] = d_a [b].

  • Знайти всі вершини, що лежать на якомусь найкоротшому шляху між заданою парою вершин (a,b)(a, b). Щоб це зробити, запускаємо два пошуки у ширину: один з aa і один з bb. Нехай da[]d_a [] — масив, що містить найкоротші відстані, отримані з першого BFS (з aa), а db[]d_b [] — масив, що містить найкоротші відстані, отримані з другого BFS (з bb). Тепер для кожної вершини легко перевірити, чи лежить вона на якомусь найкоротшому шляху між aa і bb: критерієм є умова da[v]+db[v]=da[b]d_a [v] + d_b [v] = d_a [b].

  • Знайти найкоротший прохід парної довжини від початкової вершини ss до цільової вершини tt у незваженому графі: Для цього ми маємо побудувати допоміжний граф, вершинами якого є стани (v,c)(v, c), де vv — поточна вершина, c=0c = 0 або c=1c = 1 — поточна парність. Будь-яке ребро (u,v)(u, v) вихідного графа в цьому новому графі перетвориться на два ребра ((u,0),(v,1))((u, 0), (v, 1)) та ((u,1),(v,0))((u, 1), (v, 0)). Після цього ми запускаємо BFS, щоб знайти найкоротший прохід від початкової вершини (s,0)(s, 0) до кінцевої вершини (t,0)(t, 0).
    Зауваження: у цьому пункті недарма використано термін «прохід», а не «шлях», адже вершини в знайденому проході потенційно можуть повторюватися, щоб зробити його довжину парною. Задача пошуку найкоротшого шляху парної довжини є NP-повною в орієнтованих графах і розв'язною за лінійний час у неорієнтованих графах, але зі значно складнішим підходом.

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

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