← Вернуться в блог

Обратный BFS и проверка маршрута за O(1) на шаг — задача «Navigation System» РАЗБОР

  • 1900
  • bfs
  • grafy
  • shortest-path
  • reverse-graph
  • multi-query

Когда задача звучит «обработать KK вопросов про кратчайшие пути на одном графе с nn вершинами и mm рёбрами», у участника есть выбор. Можно запускать BFS на каждый вопрос — это O(K(n+m))O(K \cdot (n + m)) и при n,m,K2105n, m, K \sim 2 \cdot 10^5 совершенно не уложится. А можно запустить одно BFS один раз и подготовить структуру, по которой каждый вопрос отвечается за O(1)O(1) — итог O(n+m+K)O(n + m + K), с огромным запасом по времени.

Эта задача — отличный учебный кейс под технику. Граф направленный, у нас есть фиксированная цель tt, и нам нужно по уже выбранному маршруту покомпонентно проанализировать каждый шаг: «по кратчайшему пути в tt или нет, и были ли альтернативы». Один трюк — построить обратный граф и пустить BFS из tt по нему, получая массив dist[v] = длина кратчайшего пути из vv в tt. Дальше каждый шаг маршрута становится тривиальной проверкой.


Что дано

В Берёзовске nn перекрёстков, пронумерованных от 11 до nn, и mm односторонних дорог. По дорогам можно проехать из любого перекрёстка в любой другой. Поликарп живёт у перекрёстка ss, работает у перекрёстка tt.

Сегодня он выбрал маршрут p1,p2,,pkp_1, p_2, \ldots, p_k, где p1=sp_1 = s, pk=tp_k = t, а каждое pi+1p_{i+1} — соседний с pip_i по дороге pipi+1p_i \to p_{i+1}.

Когда Поликарп выезжает с какого-то перекрёстка uu, навигатор показывает какой-нибудь кратчайший путь из uu в tt (если их несколько, выбирается любой — система не подсказывает, какой именно). Если Поликарп едет по тому ребру, которое навигатор показал — путь не пересчитывается. Если едет в другую сторону — навигатор пересчитывает кратчайший путь от новой вершины.

Найти минимальное и максимальное возможное число пересчётов.

Ограничения

  • 2n21052 \le n \le 2 \cdot 10^5
  • 1m21051 \le m \le 2 \cdot 10^5
  • 2kn2 \le k \le n
  • Между двумя вершинами не больше одной дороги в каждом направлении.
  • Из любой вершины достижима любая другая.

Формат ввода

n m
u_1 v_1
…
u_m v_m
k
p_1 p_2 … p_k

Формат вывода

Два целых числа через пробел: минимальное и максимальное количество пересчётов.

Пример

Вход:

6 9
1 5
5 4
1 2
2 3
3 4
4 1
2 6
6 4
4 2
4
1 2 3 4

Выход:

1 2

Разберём пример руками

Список рёбер: 151 \to 5, 545 \to 4, 121 \to 2, 232 \to 3, 343 \to 4, 414 \to 1, 262 \to 6, 646 \to 4, 424 \to 2. Цель — t=4t = 4. Маршрут — 12341 \to 2 \to 3 \to 4.

Вычислим dist[v] — кратчайшее расстояние из vv в t=4t = 4:

  • dist[4]=0\text{dist}[4] = 0 (мы уже в tt).
  • dist[5]=1\text{dist}[5] = 1 (есть дорога 545 \to 4).
  • dist[3]=1\text{dist}[3] = 1 (есть дорога 343 \to 4).
  • dist[6]=1\text{dist}[6] = 1 (есть дорога 646 \to 4).
  • dist[1]=2\text{dist}[1] = 2 (через 1541 \to 5 \to 4 за два шага).
  • dist[2]=2\text{dist}[2] = 2 (через 2342 \to 3 \to 4 или 2642 \to 6 \to 4).

Теперь анализируем шаги маршрута.

Шаг 1: 121 \to 2. dist[1]=2\text{dist}[1] = 2, ожидаем шаг в вершину с dist = 1. Среди соседей 11 (исходящих рёбер): 55 (dist=1\text{dist}=1), 22 (dist=2\text{dist}=2). Только 55 — оптимальный. Пользователь идёт в 22, что не оптимально. Навигатор пересчитывает гарантированно — это forced rebuild: min_rb++, max_rb++.

Шаг 2: 232 \to 3. dist[2]=2\text{dist}[2] = 2, ожидаем шаг в dist = 1. Соседи 22: 33 (dist=1\text{dist}=1), 66 (dist=1\text{dist}=1). Два оптимальных соседа. Пользователь идёт в 33 — это оптимально. Но навигатор мог показать 66, и тогда после фактического выбора 33 пришлось бы пересчитать. То есть пересчёт возможен, но не гарантирован: max_rb++ (в пессимистическом сценарии), min_rb остаётся (в оптимистическом — навигатор «угадал» 33).

Шаг 3: 343 \to 4. dist[3]=1\text{dist}[3] = 1, ожидаем шаг в dist = 0. Соседи 33: только 44. Один оптимальный сосед, пользователь идёт ровно в него — пересчёт исключён. Ни min_rb, ни max_rb не меняются.

Итог: min_rb=1\text{min\_rb} = 1, max_rb=2\text{max\_rb} = 2. Совпадает с ответом «1 2». ✓

Из этого ручного прогона видно, что на шаге uwu \to w нужно знать только две вещиdist[u], dist[w] и количество соседей xx вершины uu с dist[x] = dist[u] - 1. Эту информацию даёт ровно одно BFS по обратному графу.


Идея решения

Алгоритм в одну фразу.

Построить обратный граф GRG^R, запустить BFS из tt — получить dist[v] для всех vv. Затем пройти по маршруту и для каждой пары (u,w)=(pi,pi+1)(u, w) = (p_i, p_{i+1}) определить тип шага: «forced rebuild» / «optional rebuild» / «no rebuild».

Почему обратный граф

В исходном графе мы хотим знать «сколько шагов от vv до tt». BFS даёт расстояния от стартовой вершины ко всем остальным. Но запускать BFS из каждого vv — это O(n(n+m))O(n \cdot (n + m)), не уложится.

Хитрость стандартная: развернём все рёбра. В графе GRG^R путь vtv \to \ldots \to t длины dd в GG становится путём tvt \to \ldots \to v длины dd в GRG^R. Поэтому BFS из tt в GRG^R даёт dist[v] = длина кратчайшего пути из vv в tt в исходном графе.

Это работает, потому что на BFS ребро — единичной стоимости, и направление хода роли не играет. Для Дейкстры с весами трюк тот же.

Почему классификация шагов корректна

Рассмотрим шаг uwu \to w в маршруте.

  • Если dist[w]dist[u]1\text{dist}[w] \ne \text{dist}[u] - 1. Шаг не лежит на кратчайшем пути из uu в tt. Какой бы оптимальный путь навигатор ни предложил, его «следующий шаг» — вершина с dist = dist[u] - 1, не ww. Значит после фактического хода в ww навигатор пересчитывает. Это forced: оба счётчика +1.
  • Если dist[w]=dist[u]1\text{dist}[w] = \text{dist}[u] - 1, и среди соседей uu ровно одна вершина с dist = dist[u] - 1. Этот сосед — ww. Навигатор показывает ровно ww как следующий шаг, пользователь туда и едет — пересчёта нет ни в каком сценарии.
  • Если dist[w]=dist[u]1\text{dist}[w] = \text{dist}[u] - 1, и среди соседей uu есть другая вершина ww' с dist = dist[u] - 1, www' \ne w. Навигатор показывает один из оптимальных шагов; это либо ww (повезло, пересчёта нет), либо www' \ne w (после фактического хода в ww пересчёт). Пересчёт опционален: min не растёт (повезло), max растёт (не повезло).

Этим анализ исчерпывается. Каждый шаг попадает ровно в один из трёх случаев.

Сложность

  • BFS по графу с nn вершинами и mm рёбрами: O(n+m)O(n + m).
  • Проход по маршруту — K1K - 1 шагов, на каждом счётчик соседей uu. Если хранить только cnt[u] = «сколько соседей uu с dist = dist[u] - 1», заранее посчитанный за O(m)O(m), шаг — O(1)O(1). Итого O(n+m+K)O(n + m + K).

Альтернативно — на каждом шаге можно посчитать cnt[u] лениво, но запас не нужен: одна предобработка cnt чище и быстрее.


Решение: псевдокод

прочитать n, m
построить adj_rev[]  # обратный граф: для u → v добавляем v → u в adj_rev
прочитать k и маршрут p[1..k]

t ← p[k]
dist[1..n] ← бесконечность; dist[t] ← 0
очередь BFS ← {t}
пока очередь не пуста:
    u ← очередь.pop_front()
    для каждой v ∈ adj_rev[u]:
        если dist[v] = бесконечность:
            dist[v] ← dist[u] + 1
            очередь.push_back(v)

# Подсчёт оптимальных соседей в исходном графе (а не обратном)
cnt[1..n] ← 0
для каждого ребра u → v исходного графа:
    если dist[v] = dist[u] - 1:
        cnt[u] += 1

min_rb ← 0
max_rb ← 0
для i от 1 до k - 1:
    u ← p[i]
    w ← p[i+1]
    если dist[w] ≠ dist[u] - 1:
        min_rb += 1
        max_rb += 1
    иначе если cnt[u] >= 2:
        max_rb += 1
вывести min_rb, max_rb

Сложность: O(n+m+k)O(n + m + k) по времени и памяти.


Код решения

Комментарии по реализации.

  • Очередь BFS. В Python — deque из collections обязателен: popleft() за O(1)O(1) против O(n)O(n) у list.pop(0), на n=2105n = 2 \cdot 10^5 разница катастрофическая. В C++ — queue<int> из STL.
  • Маркер «не достигнуто». INF = -1 для непосещённых вершин — работает в любых арифметических операциях, не нужно сравнивать с float('inf') или INT_MAX. Граф связный по условию, после BFS все dist[v] >= 0.
  • Тип данных. int достаточно: расстояния не превышают n2105n \le 2 \cdot 10^5, счётчики тоже.
  • Память. Два массива смежности (fwd и rev) — O(n+m)O(n + m), для n+m=4105n + m = 4 \cdot 10^5 это 8\sim 8 Мб, проходит с запасом.
  • Быстрый ввод. Python — sys.stdin.buffer.read().split(), единое чтение всего ввода (на m2105m \le 2 \cdot 10^5 обычный input() в цикле медленнее в 5\sim 5 раз). C++ — ios::sync_with_stdio(false); cin.tie(nullptr); обязательно.
  • Внимание: cnt[u] считается по исходным рёбрам uvu \to v, не по обратным. Легко перепутать.

Проверим на примере из условия

Вход:

6 9
1 5
5 4
1 2
2 3
3 4
4 1
2 6
6 4
4 2
4
1 2 3 4

BFS из t=4t = 4 по обратному графу. Обратные рёбра (из vv в uu для исходного uvu \to v):

  • 515 \to 1 (из 151 \to 5)
  • 454 \to 5 (из 545 \to 4)
  • 212 \to 1 (из 121 \to 2)
  • 323 \to 2 (из 232 \to 3)
  • 434 \to 3 (из 343 \to 4)
  • 141 \to 4 (из 414 \to 1)
  • 626 \to 2 (из 262 \to 6)
  • 464 \to 6 (из 646 \to 4)
  • 242 \to 4 (из 424 \to 2)

BFS:

  • Слой 0: {4}\{4\}. dist[4] = 0.
  • Соседи 44 в обратном графе: 55 (через 454 \to 5), 33 (через 434 \to 3), 66 (через 464 \to 6).
  • Слой 1: {5,3,6}\{5, 3, 6\}. dist[5] = dist[3] = dist[6] = 1.
  • Соседи 55: 11. Соседи 33: 22. Соседи 66: 22 (уже в слое 2 будет).
  • Слой 2: {1,2}\{1, 2\}. dist[1] = dist[2] = 2.

Итого: dist = [_, 2, 2, 1, 0, 1, 1] (индекс с 1).

Подсчёт cnt[u] (соседи uu в исходном графе с dist = dist[u] - 1):

  • u=1u = 1, dist[1] = 2, target = 1. Соседи 11: 55 (dist=1 ✓), 22 (dist=2 ✗). cnt[1] = 1.
  • u=2u = 2, dist[2] = 2, target = 1. Соседи 22: 33 (dist=1 ✓), 66 (dist=1 ✓). cnt[2] = 2.
  • u=3u = 3, dist[3] = 1, target = 0. Соседи 33: 44 (dist=0 ✓). cnt[3] = 1.
  • u=4u = 4, dist[4] = 0 — пропускаем (уже в tt).
  • u=5u = 5, dist[5] = 1, target = 0. Соседи 55: 44 (dist=0 ✓). cnt[5] = 1.
  • u=6u = 6, dist[6] = 1, target = 0. Соседи 66: 44 (dist=0 ✓). cnt[6] = 1.

Проход по маршруту 12341 \to 2 \to 3 \to 4.

Шагuuwwdist[u]1\text{dist}[u] - 1dist[w]\text{dist}[w]равны?cnt[u]\text{cnt}[u]min_rbmax_rb
11212нет+1+1
22311да20+1
33400да100

Итог: min_rb=1\text{min\_rb} = 1, max_rb=2\text{max\_rb} = 2. Ответ: 1 2 ✓.


Крайние случаи

1. Маршрут длины 2 (k=2k = 2)

Только один шаг sts \to t. Соседи ss — все вершины vv с ребром svs \to v. Если ребро sts \to t существует и других соседей с dist = dist[s] - 1 нет — пересчётов 0. Если есть несколько оптимумов — max_rb = 1, min_rb = 0. Если sts \to t не на кратчайшем пути (теоретически возможно: sstt напрямую, но кратчайший — через цепочку) — forced rebuild, min_rb = max_rb = 1. Код обрабатывает корректно.

2. uu — вершина с dist[u] = 1, и w=tw = t

dist[u] = 1, target = 0. Соседи uu с dist = 0 — это в точности соседи uu, ведущие в tt (поскольку dist[t] = 0 и она единственная). Если есть только ttcnt[u] = 1, повезло. Если uu имеет несколько рёбер, ведущих в вершины с dist = 0, — это может произойти только если в графе несколько копий tt, что невозможно (вершины уникальны). Значит из вершины uu с dist[u] = 1 единственный оптимальный сосед — tt.

Замечание: формально может быть, что у uu есть несколько рёбер в одну и ту же вершину (мульти-граф), но условие задачи запрещает это. Поэтому шаг в tt из вершины с dist = 1 всегда «no rebuild».

3. Граница массива

Маршрут гарантированно валиден (pipi+1p_i \to p_{i+1} — реальное ребро), поэтому dist[w] всегда определена и конечна. Проверка dist[u] == 0tt) на середине маршрута невозможна, так как pk=tp_k = t и других tt в маршруте быть не должно (маршрут — обычно простой, но даже если не простой, проблема не возникает: попадаем в tt, дальше идём, формально допустимо, но обычно не случается).

4. Память

Для n+m=4105n + m = 4 \cdot 10^5 суммарно три массива смежности (fwd, rev_g с дублированием рёбер) и dist, cnt, path размера O(n)O(n). Общий расход — O(n+m)O(n + m), памяти хватает.


Типичные ошибки

  1. Запустить BFS из ss вместо tt. Тогда получим dist_from_s[v] = расстояние от ss до vv. Это не то: нам нужно расстояние от каждого vv до tt, а не от ss. Симптом: на «лёгких» примерах ответ кажется правильным (если sts \to t — единственный кратчайший путь), но на деревообразных графах ломается.
  2. Забыть развернуть рёбра. Если запустить BFS из tt по исходному графу, получим расстояние от tt до vv. На направленном графе это совсем другая величина: рёбра идут не туда, и dist[v] = \infty для большинства vv (поскольку из tt обычно никуда не уйти, либо уйти не в ss).
  3. Перепутать cnt с количеством рёбер из uu вообще. cnt[u] — только количество оптимальных соседей, то есть тех с dist = dist[u] - 1. Соседей у uu может быть много, но не все они на кратчайшем пути.
  4. Поставить > вместо >= в условии cnt[u] >= 2. Если оптимальный сосед один и это ww — пересчёта нет ни в каком сценарии. Если их два или больше — есть шанс пересчитать. cnt[u] >= 2, не > 2.
  5. Учесть пересчёт на шаг uwu \to w, где dist[w] = dist[u] - 1 и cnt[u] = 1. Опасная мысль: «может, навигатор показал что-то ещё». Нет: если cnt[u] = 1 и dist[w] = dist[u] - 1, единственный оптимальный сосед uu — это ww. Навигатор обязан показать ровно ww.
  6. Использовать list.pop(0) вместо deque.popleft() в Python. Стандартный list не поддерживает O(1)O(1) удаление с начала, и BFS на n=2105n = 2 \cdot 10^5 улетает в TL. from collections import deque — обязательная привычка для BFS-задач.
  7. Объявить dist как int-массив инициализацией всеми нулями вместо «бесконечности». Тогда вершина tt и непосещённые вершины не отличаются. На связном графе по условию все dist[v] определены, но в более общем случае — критическая ошибка. Привычка ставить «маркер не посещения» (-1 или INT_MAX) полезная.

Анализ сложности

  • BFS: O(n+m)O(n + m) по времени, O(n+m)O(n + m) по памяти.
  • Подсчёт cnt: O(m)O(m), проход по всем рёбрам.
  • Проход по маршруту: O(k)O(k).
  • Итог: O(n+m+k)O(n + m + k) по времени, O(n+m)O(n + m) по памяти.

При n,m,k2105n, m, k \le 2 \cdot 10^5 — это 6105\le 6 \cdot 10^5 операций. На Python — около 0.30.30.50.5 секунды, на C++ — десятки миллисекунд.


Что ещё полезно потренировать

Задачи на одно BFS как предобработка для множественных запросов. Все — с возраст-нейтральных платформ.

  • Multi-source BFS / shortest paths from KK sources. Запустить BFS из всех KK выделенных вершин одновременно, получив dist[v] = расстояние до ближайшей выделенной. Стандартный приём для задач типа «лесные пожары» / «пожарные машины» / «больницы и пациенты».
  • 0-1 BFS (deque-BFS) — обобщение BFS на графы с весами рёбер {0,1}\{0, 1\}. То же O(n+m)O(n + m), но deque с appendleft() для рёбер веса 0 и append() для рёбер веса 1. Полезно на задачах с «бесплатными» переходами (телепорты, повторное использование рельсов).
  • Reverse BFS / DFS на DAG для подсчёта числа кратчайших путей. Аналог нашего подхода, но вместо «сколько оптимальных соседей у uu» — «сколько кратчайших путей через uu». Ответ — произведение по слоям.
  • Codeforces Div.2 D с тегом bfs + shortest paths — десятки задач 1700–2100, где основная трудность — придумать что брать за состояние в BFS. Тренировка на 5–10 таких задачах быстро ставит автоматизм.

Следующий шаг — задачи, где BFS нужно расширить до состояний с дополнительными параметрами (например, BFS на пересечении графа и маленького конечного автомата). Это уже полоса 2100+ и стандартный мост от «чистого BFS» к графовому DP.


Итого

  • Идея: BFS из tt по обратному графу даёт dist[v] = расстояние от vv до tt в исходном графе.
  • Анализ маршрута: для каждого шага uwu \to w сверяем dist[w] с dist[u] - 1. Если не совпали — forced rebuild. Если совпали и оптимальных соседей у uu не один — optional rebuild.
  • Сложность: O(n+m+k)O(n + m + k).
  • Главная ошибка: забыть развернуть рёбра или запустить BFS не из той вершины. Сигнал — dist[v] получается «не туда».
  • Универсальный приём: одна предобработка O(n+m)O(n + m), далее KK запросов по O(1)O(1) каждый. Применимо везде, где «много вопросов про кратчайшие пути на одном графе».

Попробуй разобрать похожие задачи

В CodePal AI-партнёр подсказывает идею, а не ответ. Разбор в диалоге, код проверяется в браузере.