Skip to main content

Що таке Breadth-First Search (пошук у ширину)?

Коротка відповідь

Breadth-First Search (BFS, пошук у ширину) - це алгоритм обходу графа/дерева, який відвідує вершини «по шарах» від стартової вершини, використовуючи чергу. Він гарантує знаходження найкоротшого шляху за кількістю ребер у неважених графах і працює за O(V+E) за часом і O(V) за пам'яттю.

Розгорнута відповідь

Визначення

BFS - це алгоритм, який починає обхід із заданої стартової вершини і послідовно відвідує всі вершини на відстані 1 ребро, потім відстані 2 ребра і так далі. Для керування порядком відвідування використовується черга (FIFO). Такий «шаровий» обхід дозволяє коректно вимірювати мінімальну кількість ребер від джерела до кожної досяжної вершини в неваженому графі.

Ключова ідея

  • Помістити стартову вершину в чергу, позначити її як відвідану, встановити відстань до неї рівною 0.
  • Поки черга не порожня: витягти вершину u з початку черги, «розширити» її - пройти по всіх сусідах v.
  • Якщо сусід v ще не відвіданий: позначити відвідування при додаванні в чергу, записати батька v = u, відстань dist[v] = dist[u] + 1, покласти v у кінець черги.

Властивості і гарантії

  • Шаровість: вершини відвідуються в порядку незростання найкоротшої відстані від джерела.
  • Найкоротші шляхи в неважених графах: BFS знаходить мінімальну кількість ребер від джерела до кожної досяжної вершини.
  • Працює і для орієнтованих, і для неорієнтованих графів (з урахуванням напрямку ребер).
  • Підходить для дерев (дає обхід по рівнях, level-order traversal).

Складність

  • Час: O(V + E), де V - кількість вершин, E - кількість ребер.
  • Пам'ять: O(V) для черги, масивів dist/visited/parent.

Структури даних

  • Черга (FIFO) - керує порядком обходу рівнів.
  • visited - позначає, що вершину вже поставлено в чергу (важливо позначати при додаванні, а не при вилученні).
  • dist - відстань у ребрах від джерела до вершини.
  • parent - батько вершини в дереві BFS для відновлення шляху.

Псевдокод (JavaScript)

function bfs(adj, start) { const n = adj.length; const dist = Array(n).fill(Infinity); const parent = Array(n).fill(-1); const visited = Array(n).fill(false); const q = []; let head = 0; // реалізація черги через масив і покажчик голови q.push(start); visited[start] = true; dist[start] = 0; while (head < q.length) { const u = q[head++]; for (const v of adj[u]) { if (!visited[v]) { visited[v] = true; // позначаємо при додаванні parent[v] = u; // запам'ятовуємо дерево BFS dist[v] = dist[u] + 1; // відстань по шарах q.push(v); } } } return { dist, parent, visited }; }

Відновлення шляху (по parent)

Після BFS можна відновити найкоротший шлях від s до t, підіймаючись по parent від t до s і розвертаючи послідовність.

function getPath(parent, s, t) { const path = []; for (let v = t; v !== -1; v = parent[v]) path.push(v); path.reverse(); return path[0] === s ? path : []; // якщо t недосяжний, повернемо порожній шлях }

Приклад: найкоротший шлях у неорієнтованому графі

Граф заданий списками суміжності. Знайдемо найкоротший шлях від 0 до 5.

const adj = [ /*0*/ [1, 2], /*1*/ [0, 3, 4], /*2*/ [0, 4], /*3*/ [1, 5], /*4*/ [1, 2, 5], /*5*/ [3, 4] ]; const { dist, parent } = bfs(adj, 0); console.log('dist to 5:', dist[5]); // 3 console.log('path 0->5:', getPath(parent, 0, 5)); // наприклад [0,1,3,5] або [0,2,4,5]

Приклад: обхід дерева по рівнях (Level-Order Traversal)

Для дерев BFS відповідає обходу по рівнях: спочатку корінь, потім усі вузли глибини 1, потім глибини 2 і так далі.

class Node { constructor(val, left = null, right = null) { this.val = val; this.left = left; this.right = right; } } function levelOrder(root) { if (!root) return []; const res = []; const q = [root]; let head = 0; while (head < q.length) { const levelSize = q.length - head; const level = []; for (let i = 0; i < levelSize; i++) { const node = q[head++]; level.push(node.val); if (node.left) q.push(node.left); if (node.right) q.push(node.right); } res.push(level); } return res; } // Приклад дерева: 1 // / \ // 2 3 // / \ \ // 4 5 6 const root = new Node(1, new Node(2, new Node(4), new Node(5)), new Node(3, null, new Node(6))); console.log(levelOrder(root)); // [[1],[2,3],[4,5,6]]

BFS на сітці (grid): найкоротший шлях із перешкодами

Клітини зі значенням 0 - прохідні, 1 - перешкоди. Ходимо в 4 напрямках. BFS видасть довжину найкоротшого шляху в кроках.

function shortestPathGrid(grid, start, goal) { const m = grid.length, n = grid[0].length; const dirs = [[1,0],[-1,0],[0,1],[0,-1]]; const dist = Array.from({ length: m }, () => Array(n).fill(Infinity)); const q = []; let head = 0; const [sx, sy] = start, [gx, gy] = goal; if (grid[sx][sy] === 1 || grid[gx][gy] === 1) return -1; dist[sx][sy] = 0; q.push([sx, sy]); while (head < q.length) { const [x, y] = q[head++]; if (x === gx && y === gy) return dist[x][y]; for (const [dx, dy] of dirs) { const nx = x + dx, ny = y + dy; if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] === 0 && dist[nx][ny] === Infinity) { dist[nx][ny] = dist[x][y] + 1; q.push([nx, ny]); } } } return -1; // шляху до цілі немає } const grid = [ [0,0,1,0], [0,0,0,0], [1,0,1,0], [0,0,0,0] ]; console.log(shortestPathGrid(grid, [0,0], [3,3]));

Варіації і застосування

  • Найкоротші шляхи в неважених графах і сітках (у кроках/ребрах).
  • Визначення досяжності і відстаней від джерела до всіх вершин.
  • Перевірка зв'язності/підрахунок компонент зв'язності (запускаючи BFS із невідвіданих вершин).
  • Перевірка двочастковості графа: розфарбування по рівнях (чергування кольорів).
  • Мультиджерельний BFS: стартуємо з множини вершин одразу (відстань 0), корисно для задач «хвильового» поширення.

BFS vs DFS

  • BFS використовує чергу і йде по шарах; DFS використовує стек/рекурсію і йде в глибину.
  • BFS знаходить найкоротший шлях у неважених графах; DFS такої гарантії не дає.
  • Пам'ять: BFS може споживати більше пам'яті на широких рівнях; DFS зазвичай економніший за пам'яттю.

Коли BFS не підходить

  • Важені графи з різними вагами: потрібен Дейкстра (або 0-1 BFS для ваг 0/1, або A* з евристикою).
  • Дуже широкі графи/рівні - високе споживання пам'яті.

Часті помилки

  • Позначати visited занадто пізно (при вилученні з черги), через що вузли можуть потрапити в чергу кілька разів.
  • Використовувати стек замість черги - отримаєте DFS, а не BFS.
  • Забувати ініціалізувати dist/parent/visited для кожного запуску BFS.
  • Неправильно враховувати напрямок ребер в орієнтованих графах.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.