awesome-everything EN
↑ Обратно к восхождению

Алгоритмы с нуля

Графы: чтение кода и багов

Суть Читайте код графа и лог сборки, предсказывайте поведение и выбирайте самое результативное исправление: баг с visited-set, баг расстояний в BFS, Dijkstra на отрицательных рёбрах и цикл в topological sort.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

Баги в графах прячутся в том, где вы помечаете вершину, из какой очереди извлекаете и каким весам доверяете. Прочтите каждый фрагмент, предскажите его поведение на циклическом или взвешенном графе и выберите исправление, которое senior-инженер сделает первым.

Цель

Отработайте цикл, который вы запускаете на каждом дефекте графа: проследите фрагмент на маленьком циклическом или взвешенном примере, найдите нарушенный инвариант и возьмите наименьшее корректное исправление.

Фрагмент 1 — DFS, который зависает

function dfs(v, adj, visited) {
  for (const w of adj[v]) {   // пометка — ПОСЛЕ рекурсии ниже
    if (!visited.has(w)) {
      dfs(w, adj, visited);
    }
  }
  visited.add(v);             // помечается только на выходе
}
// в adj есть цикл: { 0: [1], 1: [2], 2: [0] }
Викторина

Запустите это на циклическом графе 0->1->2->0, начиная с 0. Что произойдёт и как исправить?

Фрагмент 2 — BFS кратчайшего пути, дающий неверные расстояния

function bfs(src, adj) {
  const dist = { [src]: 0 };
  const queue = [src];
  while (queue.length) {
    const v = queue.shift();
    for (const u of adj[v]) {
      dist[u] = dist[v] + 1;   // ставим каждый раз, когда видим u
      queue.push(u);           // и ставим в очередь каждый раз
    }
  }
  return dist;
}
Викторина

На графе, где у вершины несколько входящих рёбер, этот BFS завышает расстояния и может не завершиться на цикле. В чём именно баг?

Фрагмент 3 — Dijkstra на графе с ребром-возвратом

// adj как [сосед, вес]: A:[[B,1]], B:[[C,-3]], A:[[C,4]]
// settled — Set; как только вершину извлекли, она окончательна
const [d, u] = heap.pop();
if (settled.has(u)) continue;
settled.add(u);                 // расстояние u ЗАФИКСИРОВАНО здесь
for (const [v, w] of adj[u]) {
  if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; heap.push([dist[v], v]); }
}
Викторина

С ребром B->C веса -3 Dijkstra фиксирует C через A->C (стоимость 4) до того, как обнаружит A->B->C (стоимость 1 + (-3) = -2). В чём корневая причина и какое исправление верное?

Фрагмент 4 — topological sort, возвращающий мусор на цикле

function topo(adj) {
  const inDeg = {};
  for (const v in adj) inDeg[v] = 0;
  for (const v in adj) for (const w of adj[v]) inDeg[w]++;
  const q = Object.keys(adj).filter(v => inDeg[v] === 0);
  const order = [];
  while (q.length) {
    const v = q.shift(); order.push(v);
    for (const w of adj[v]) if (--inDeg[w] === 0) q.push(w);
  }
  return order;                 // возвращает order безусловно
}
// adj = { a:[b], b:[c], c:[a], d:[a] }  // a->b->c->a — цикл
Викторина

На этом графе (a->b->c->a — цикл, плюс d->a) что вернёт topo и какое исправление верное?

Итог

Каждый фрагмент нарушил один инвариант. DFS должен помечать вершину на ВХОДЕ, до рекурсии, иначе цикл входит снова вечно. BFS кратчайшего пути должен защищать первое достижение (пометка на enqueue), чтобы более длинный поздний маршрут не перезаписал кратчайший. Шаг фиксации Dijkstra верен только при неотрицательных весах; отрицательное ребро означает Bellman-Ford. А topological sort по Кану должен проверять число оставшихся — короткий вывод это цикл, а не корректный порядок. Проследите на крошечном циклическом или взвешенном графе, назовите нарушенный инвариант и примените наименьшее исправление.

Продолжить восхождение ↑Графы: соберите движок зависимостей и маршрутизации
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.