Пошук в ширину
Пошук в ширину (обхід в ширину, BFS) - це один з основних алгоритмів на графах.
В результаті пошуку в ширину знаходиться шлях найкоротшої довжини у незваженому графі, тобто шлях, що містить найменшу кількість ребер.
Алгоритм працює за , де - кількість вершин, - кількість ребер.
Алгоритм
На вхід алгоритму подається незважений граф і номер стартової вершини . Граф може бути як орієнтованим, так і неорієнтованим, для алгоритму це не важливо.
Сам алгоритм можна сприймати як процес "підпалювання" графа: на нульовому кроці підпалюємо тільки вершину . На кожному наступному кроці вогонь з кожної вершини, що вже горить, поширюється на всіх її сусідів; тобто за одну ітерацію алгоритму відбувається розширення "кільця вогню" в ширину на одиницю (звідси і назва алгоритму).
Формальний опис алгоритму. Створимо чергу , в яку будуть додаватися вершини, що горять, а також заведемо булевий масив , в якому для кожної вершини будемо відзначати, горить вона вже чи ні (або іншими словами, чи була вона переглянутою).
Спершу в чергу додається тільки вершина , і , а для всіх інших вершин . Потім алгоритм представляє собою цикл: поки черга не порожня, дістати з її голови одну вершину, переглянути всі ребра, що виходять з цієї вершини, і якщо якісь з переглянутих вершин (на протилежних кінцях ребер) ще не горять, то підпалити їх і додати в кінець черги.
В результаті, коли черга стане пустою, обхід в ширину обійде всі досяжні з вершини, причому до кожної дійде найкоротшим шляхом. Також можна порахувати довжини найкоротших шляхів (для чого треба завести масив довжин шляхів ), і компактно зберегти інформацію, якої достатню для відновлення всіх цих найкоротших шляхів (для цього треба завести масив "предків" , в якому для кожної вершини зберігати номер вершини, з якої ми потрапили в цю вершину).
Реалізація
Реалізуємо вищеописаний алгоритм на мові C++.
Вхідні дані:
vector<vector<int>> g; // граф, список суміжності
int n; // кількість вершин
int s; // стартова вершина (вершини нумеруються з нуля)
// читання графа
...
Сам обхід:
queue<int> q;
vector<bool> visited(n);
vector<int> d(n), p(n);
q.push(s);
visited[s] = true;
p[s] = -1;
while (!q.empty()) {
int v = q.front();
q.pop();
for (int i = 0; i < g[v].size(); i++) {
int to = g[v][i];
if (!visited[to]) {
visited[to] = true;
q.push(to);
d[to] = d[v] + 1;
p[to] = v;
}
}
}
Якщо потрібно відновити і вивести найкоротший шлях до якоїсь вершини , то це можна зробити наступним чином:
if (!visited[to]) {
cout << "No path!";
} else {
vector<int> path;
for (int v = to; v != -1; v = p[v]) {
path.push_back(v);
}
reverse(path.begin(), path.end());
cout << "Path: ";
for (int i = 0; i < path.size(); i++) {
cout << path[i] + 1 << " ";
}
}
Застосування
Пошук найкоротшого шляху у незваженому графі.
Описано у цій статті.
Пошук компонент зв'язності у графі за .
Для цього запускаємо обхід в ширину від кожної вершини, за винятком вершин, що залишилися відвіданими () після попередніх запусків. Таким чином, ми виконуємо звичайний запуск в ширину від кожної вершини, але не зануляємо кожний раз масив , за рахунок чого ми кожний раз будемо обходити нову компоненту зв'язності, а сумарний час роботи алгоритму буде як і раніше (такі декілька запусків обходу на графі без занулення масиву називається серією обходів в ширину).
Знаходження розв'язку будь-якої задачі з найменшим числом ходів, якщо кожний стан системи можна уявити вершиною графа, а переходи з одного стану в інші - ребрами графа.
Класичний приклад - гра, де робот рухається по полю, при цьому він може переміщати ящики, що знаходяться на цьому ж полі, і потрібно за найменше число ходів пересунути ящики в необхідні позиції. Розв'язується обходом в ширину по графу, де станом (вершиною) є набір координат: координати робота, і координати всіх коробок.
Знаходження найкоротшого шляху в 0-1-графі (зважений граф, але з вагами рівними тільки 0 або 1).
Достатньо трохи модифікувати пошук в ширину: якщо поточне ребро нульової ваги, і відбувається покращення відстані до якоїсь вершини, то цю вершину додаємо не в кінець, а в початок черги.
Знаходження найкоротшого циклу в орієнтованому незваженому графі.
Для цього запускаємо пошук в ширину з кожної вершини. Як тільки в процесі обходу ми намагаємося піти з поточної вершини по якомусь ребру до вже відвіданої вершини, то це означає, що ми знайшли найкоротший цикл, і зупиняємо обхід в ширину. Серед всіх таких знайдених циклів (по одному від кожного запуску обходу) вибираємо найкоротший.
Знайти всі ребра, що лежать на будь-якому найкоротшому шляху між заданою парою вершин .
Для цього треба запустити 2 пошуки в ширину: з , і з . Позначимо через масив найкоротших відстаней, отриманий в результаті першого обходу, а через - в результаті другого обходу. Тепер для будь-якого ребра легко перевірити чи лежить він на будь-якому найкоротшому шляху: критерієм буде умова .
Знайти всі вершини, що лежать на будь-якому найкоротшому шляху між заданою парою вершин .
Для цього треба запустити 2 пошуки в ширину: з , і з . Позначимо через масив найкоротших відстаней, отриманий в результаті першого обходу, а через - в результаті другого обходу. Тепер для будь-якої вершини легко перевірити чи лежить вона на будь-якому найкоротшому шляху: критерієм буде умова .
Знайти найкоротший парний шлях в графі (тобто шлях парної довжини).
Для цього треба побудувати допоміжний граф, вершинами якого будуть стани , де - номер поточної вершини, - поточна парність. Будь-яке ребро вихідного графа в новому графі перетвориться в два ребра та . Після цього на цьому графі треба обходом в ширину знайти найкоротший шлях з стартової вершини в кінцеву, з парністю, рівною 0.
Задачі
e-olymp - 292 - Ведмідь Міша | Розв'язки: C++
e-olymp - 2384 - Сортуюча гра | Розв'язки: C++
e-olymp - 2621 - Шлях короля | Розв'язки: C++