Когда задача звучит «обработать вопросов про кратчайшие пути на одном графе с вершинами и рёбрами», у участника есть выбор. Можно запускать BFS на каждый вопрос — это и при совершенно не уложится. А можно запустить одно BFS один раз и подготовить структуру, по которой каждый вопрос отвечается за — итог , с огромным запасом по времени.
Эта задача — отличный учебный кейс под технику. Граф направленный, у нас есть фиксированная цель , и нам нужно по уже выбранному маршруту покомпонентно проанализировать каждый шаг: «по кратчайшему пути в или нет, и были ли альтернативы». Один трюк — построить обратный граф и пустить BFS из по нему, получая массив dist[v] = длина кратчайшего пути из в . Дальше каждый шаг маршрута становится тривиальной проверкой.
Что дано
В Берёзовске перекрёстков, пронумерованных от до , и односторонних дорог. По дорогам можно проехать из любого перекрёстка в любой другой. Поликарп живёт у перекрёстка , работает у перекрёстка .
Сегодня он выбрал маршрут , где , , а каждое — соседний с по дороге .
Когда Поликарп выезжает с какого-то перекрёстка , навигатор показывает какой-нибудь кратчайший путь из в (если их несколько, выбирается любой — система не подсказывает, какой именно). Если Поликарп едет по тому ребру, которое навигатор показал — путь не пересчитывается. Если едет в другую сторону — навигатор пересчитывает кратчайший путь от новой вершины.
Найти минимальное и максимальное возможное число пересчётов.
Ограничения
- Между двумя вершинами не больше одной дороги в каждом направлении.
- Из любой вершины достижима любая другая.
Формат ввода
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
Разберём пример руками
Список рёбер: , , , , , , , , . Цель — . Маршрут — .
Вычислим dist[v] — кратчайшее расстояние из в :
- (мы уже в ).
- (есть дорога ).
- (есть дорога ).
- (есть дорога ).
- (через за два шага).
- (через или ).
Теперь анализируем шаги маршрута.
Шаг 1: . , ожидаем шаг в вершину с dist = 1. Среди соседей (исходящих рёбер): (), (). Только — оптимальный. Пользователь идёт в , что не оптимально. Навигатор пересчитывает гарантированно — это forced rebuild: min_rb++, max_rb++.
Шаг 2: . , ожидаем шаг в dist = 1. Соседи : (), (). Два оптимальных соседа. Пользователь идёт в — это оптимально. Но навигатор мог показать , и тогда после фактического выбора пришлось бы пересчитать. То есть пересчёт возможен, но не гарантирован: max_rb++ (в пессимистическом сценарии), min_rb остаётся (в оптимистическом — навигатор «угадал» ).
Шаг 3: . , ожидаем шаг в dist = 0. Соседи : только . Один оптимальный сосед, пользователь идёт ровно в него — пересчёт исключён. Ни min_rb, ни max_rb не меняются.
Итог: , . Совпадает с ответом «1 2». ✓
Из этого ручного прогона видно, что на шаге нужно знать только две вещи — dist[u], dist[w] и количество соседей вершины с dist[x] = dist[u] - 1. Эту информацию даёт ровно одно BFS по обратному графу.
Идея решения
Алгоритм в одну фразу.
Построить обратный граф , запустить BFS из — получить
dist[v]для всех . Затем пройти по маршруту и для каждой пары определить тип шага: «forced rebuild» / «optional rebuild» / «no rebuild».
Почему обратный граф
В исходном графе мы хотим знать «сколько шагов от до ». BFS даёт расстояния от стартовой вершины ко всем остальным. Но запускать BFS из каждого — это , не уложится.
Хитрость стандартная: развернём все рёбра. В графе путь длины в становится путём длины в . Поэтому BFS из в даёт dist[v] = длина кратчайшего пути из в в исходном графе.
Это работает, потому что на BFS ребро — единичной стоимости, и направление хода роли не играет. Для Дейкстры с весами трюк тот же.
Почему классификация шагов корректна
Рассмотрим шаг в маршруте.
- Если . Шаг не лежит на кратчайшем пути из в . Какой бы оптимальный путь навигатор ни предложил, его «следующий шаг» — вершина с
dist = dist[u] - 1, не . Значит после фактического хода в навигатор пересчитывает. Это forced: оба счётчика +1. - Если , и среди соседей ровно одна вершина с
dist = dist[u] - 1. Этот сосед — . Навигатор показывает ровно как следующий шаг, пользователь туда и едет — пересчёта нет ни в каком сценарии. - Если , и среди соседей есть другая вершина с
dist = dist[u] - 1, . Навигатор показывает один из оптимальных шагов; это либо (повезло, пересчёта нет), либо (после фактического хода в пересчёт). Пересчёт опционален: min не растёт (повезло), max растёт (не повезло).
Этим анализ исчерпывается. Каждый шаг попадает ровно в один из трёх случаев.
Сложность
- BFS по графу с вершинами и рёбрами: .
- Проход по маршруту — шагов, на каждом счётчик соседей . Если хранить только
cnt[u]= «сколько соседей сdist = dist[u] - 1», заранее посчитанный за , шаг — . Итого .
Альтернативно — на каждом шаге можно посчитать 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
Сложность: по времени и памяти.
Код решения
Комментарии по реализации.
- Очередь BFS. В Python —
dequeизcollectionsобязателен:popleft()за против уlist.pop(0), на разница катастрофическая. В C++ —queue<int>из STL. - Маркер «не достигнуто».
INF = -1для непосещённых вершин — работает в любых арифметических операциях, не нужно сравнивать сfloat('inf')илиINT_MAX. Граф связный по условию, после BFS всеdist[v] >= 0. - Тип данных.
intдостаточно: расстояния не превышают , счётчики тоже. - Память. Два массива смежности (
fwdиrev) — , для это Мб, проходит с запасом. - Быстрый ввод. Python —
sys.stdin.buffer.read().split(), единое чтение всего ввода (на обычныйinput()в цикле медленнее в раз). C++ —ios::sync_with_stdio(false); cin.tie(nullptr);обязательно. - Внимание:
cnt[u]считается по исходным рёбрам , не по обратным. Легко перепутать.
Проверим на примере из условия
Вход:
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 из по обратному графу. Обратные рёбра (из в для исходного ):
- (из )
- (из )
- (из )
- (из )
- (из )
- (из )
- (из )
- (из )
- (из )
BFS:
- Слой 0: .
dist[4] = 0. - Соседи в обратном графе: (через ), (через ), (через ).
- Слой 1: .
dist[5] = dist[3] = dist[6] = 1. - Соседи : . Соседи : . Соседи : (уже в слое 2 будет).
- Слой 2: .
dist[1] = dist[2] = 2.
Итого: dist = [_, 2, 2, 1, 0, 1, 1] (индекс с 1).
Подсчёт cnt[u] (соседи в исходном графе с dist = dist[u] - 1):
- ,
dist[1] = 2, target = 1. Соседи : (dist=1✓), (dist=2✗).cnt[1] = 1. - ,
dist[2] = 2, target = 1. Соседи : (dist=1✓), (dist=1✓).cnt[2] = 2. - ,
dist[3] = 1, target = 0. Соседи : (dist=0✓).cnt[3] = 1. - ,
dist[4] = 0— пропускаем (уже в ). - ,
dist[5] = 1, target = 0. Соседи : (dist=0✓).cnt[5] = 1. - ,
dist[6] = 1, target = 0. Соседи : (dist=0✓).cnt[6] = 1.
Проход по маршруту .
| Шаг | равны? | min_rb | max_rb | |||||
|---|---|---|---|---|---|---|---|---|
| 1 | 1 | 2 | 1 | 2 | нет | — | +1 | +1 |
| 2 | 2 | 3 | 1 | 1 | да | 2 | 0 | +1 |
| 3 | 3 | 4 | 0 | 0 | да | 1 | 0 | 0 |
Итог: , . Ответ: 1 2 ✓.
Крайние случаи
1. Маршрут длины 2 ()
Только один шаг . Соседи — все вершины с ребром . Если ребро существует и других соседей с dist = dist[s] - 1 нет — пересчётов 0. Если есть несколько оптимумов — max_rb = 1, min_rb = 0. Если не на кратчайшем пути (теоретически возможно: → напрямую, но кратчайший — через цепочку) — forced rebuild, min_rb = max_rb = 1. Код обрабатывает корректно.
2. — вершина с dist[u] = 1, и
dist[u] = 1, target = 0. Соседи с dist = 0 — это в точности соседи , ведущие в (поскольку dist[t] = 0 и она единственная). Если есть только — cnt[u] = 1, повезло. Если имеет несколько рёбер, ведущих в вершины с dist = 0, — это может произойти только если в графе несколько копий , что невозможно (вершины уникальны). Значит из вершины с dist[u] = 1 единственный оптимальный сосед — .
Замечание: формально может быть, что у есть несколько рёбер в одну и ту же вершину (мульти-граф), но условие задачи запрещает это. Поэтому шаг в из вершины с dist = 1 всегда «no rebuild».
3. Граница массива
Маршрут гарантированно валиден ( — реальное ребро), поэтому dist[w] всегда определена и конечна. Проверка dist[u] == 0 (в ) на середине маршрута невозможна, так как и других в маршруте быть не должно (маршрут — обычно простой, но даже если не простой, проблема не возникает: попадаем в , дальше идём, формально допустимо, но обычно не случается).
4. Память
Для суммарно три массива смежности (fwd, rev_g с дублированием рёбер) и dist, cnt, path размера . Общий расход — , памяти хватает.
Типичные ошибки
- Запустить BFS из вместо . Тогда получим
dist_from_s[v]= расстояние от до . Это не то: нам нужно расстояние от каждого до , а не от . Симптом: на «лёгких» примерах ответ кажется правильным (если — единственный кратчайший путь), но на деревообразных графах ломается. - Забыть развернуть рёбра. Если запустить BFS из по исходному графу, получим расстояние от до . На направленном графе это совсем другая величина: рёбра идут не туда, и
dist[v]= для большинства (поскольку из обычно никуда не уйти, либо уйти не в ). - Перепутать
cntс количеством рёбер из вообще.cnt[u]— только количество оптимальных соседей, то есть тех сdist = dist[u] - 1. Соседей у может быть много, но не все они на кратчайшем пути. - Поставить
>вместо>=в условииcnt[u] >= 2. Если оптимальный сосед один и это — пересчёта нет ни в каком сценарии. Если их два или больше — есть шанс пересчитать.cnt[u] >= 2, не> 2. - Учесть пересчёт на шаг , где
dist[w] = dist[u] - 1иcnt[u] = 1. Опасная мысль: «может, навигатор показал что-то ещё». Нет: еслиcnt[u] = 1иdist[w] = dist[u] - 1, единственный оптимальный сосед — это . Навигатор обязан показать ровно . - Использовать
list.pop(0)вместоdeque.popleft()в Python. Стандартныйlistне поддерживает удаление с начала, и BFS на улетает в TL.from collections import deque— обязательная привычка для BFS-задач. - Объявить
distкакint-массив инициализацией всеми нулями вместо «бесконечности». Тогда вершина и непосещённые вершины не отличаются. На связном графе по условию всеdist[v]определены, но в более общем случае — критическая ошибка. Привычка ставить «маркер не посещения» (-1илиINT_MAX) полезная.
Анализ сложности
- BFS: по времени, по памяти.
- Подсчёт
cnt: , проход по всем рёбрам. - Проход по маршруту: .
- Итог: по времени, по памяти.
При — это операций. На Python — около – секунды, на C++ — десятки миллисекунд.
Что ещё полезно потренировать
Задачи на одно BFS как предобработка для множественных запросов. Все — с возраст-нейтральных платформ.
- Multi-source BFS / shortest paths from sources. Запустить BFS из всех выделенных вершин одновременно, получив
dist[v]= расстояние до ближайшей выделенной. Стандартный приём для задач типа «лесные пожары» / «пожарные машины» / «больницы и пациенты». - 0-1 BFS (deque-BFS) — обобщение BFS на графы с весами рёбер . То же , но
dequeсappendleft()для рёбер веса 0 иappend()для рёбер веса 1. Полезно на задачах с «бесплатными» переходами (телепорты, повторное использование рельсов). - Reverse BFS / DFS на DAG для подсчёта числа кратчайших путей. Аналог нашего подхода, но вместо «сколько оптимальных соседей у » — «сколько кратчайших путей через ». Ответ — произведение по слоям.
- Codeforces Div.2 D с тегом
bfs+shortest paths— десятки задач 1700–2100, где основная трудность — придумать что брать за состояние в BFS. Тренировка на 5–10 таких задачах быстро ставит автоматизм.
Следующий шаг — задачи, где BFS нужно расширить до состояний с дополнительными параметрами (например, BFS на пересечении графа и маленького конечного автомата). Это уже полоса 2100+ и стандартный мост от «чистого BFS» к графовому DP.
Итого
- Идея: BFS из по обратному графу даёт
dist[v]= расстояние от до в исходном графе. - Анализ маршрута: для каждого шага сверяем
dist[w]сdist[u] - 1. Если не совпали — forced rebuild. Если совпали и оптимальных соседей у не один — optional rebuild. - Сложность: .
- Главная ошибка: забыть развернуть рёбра или запустить BFS не из той вершины. Сигнал —
dist[v]получается «не туда». - Универсальный приём: одна предобработка , далее запросов по каждый. Применимо везде, где «много вопросов про кратчайшие пути на одном графе».