Топологическая сортировка, 0-1 BFS, мосты и точки сочленения, двудольность
IOI Prep Camp — Abbas Aliyev — апрель 2026
📄 Раздаточный материал: DFS & BFS basics (Day 1)
DFS — напоминание
vector<bool> visited;
vector<vector<int>> adj;
void dfs(int v) {
visited[v] = true;
for (int u : adj[v]) {
if (!visited[u]) {
dfs(u);
}
}
}
Обходит вглубь, использует стек (рекурсию)
Сложность: O(V + E)
Связность, компоненты, flood fill, циклы
BFS — напоминание
vector<int> dist(n + 1, -1);
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int v = q.front();
q.pop();
for (int u : adj[v]) {
if (dist[u] == -1) {
dist[u] = dist[v] + 1;
q.push(u);
}
}
}
Обходит по слоям, использует очередь
Сложность: O(V + E)
Кратчайший путь в невзвешенном графе
Сегодняшний план
1. Топологическая сортировка — порядок зависимостей в DAG (DFS + BFS подходы)
2. 0-1 BFS — кратчайший путь с весами 0 и 1 (дек вместо очереди)
3. Мосты и точки сочленения — уязвимые места графа (продвинутый DFS)
4. Проверка двудольности — раскраска в два цвета (BFS)
Каждая тема = задача-мотивация → алгоритм → код → практика
Задача: порядок курсов
Вы — студент. Есть n курсов и m зависимостей: курс a нужно пройти до курса b. Найдите порядок, в котором можно пройти все курсы, соблюдая зависимости.
Если это невозможно (есть цикл зависимостей), выведите «IMPOSSIBLE».
Правильный порядок: Матан, Программирование, Линал, Теорвер, Алгоритмы, ML
Определение
Топологическая сортировка ориентированного ациклического графа (DAG) — это линейный порядок вершин, такой что для каждого ребра (u, v) вершина u идёт раньше v.
Топологическая сортировка возможна тогда и только тогда, когда граф не содержит цикла (т.е. является DAG).
Топосорт через DFS
Запускаем DFS из каждой непосещённой вершины
Когда DFS покидает вершину (после всех потомков), добавляем её в список
Ответ — обратный порядок (reverse)
Вершина попадает в ответ после всех вершин, до которых из неё можно дойти. Значит в обратном порядке она стоит перед ними — как раз то, что нужно.
Код: топосорт через DFS
int n, m;
vector<vector<int>> adj;
vector<bool> visited;
vector<int> order;
void dfs(int v) {
visited[v] = true;
for (int u : adj[v]) {
if (!visited[u]) dfs(u);
}
order.push_back(v); // при выходе из вершины
}
void topological_sort() {
visited.assign(n + 1, false);
for (int v = 1; v <= n; v++)
if (!visited[v]) dfs(v);
reverse(order.begin(), order.end());
}
⬆ Ключевая строка — order.push_back(v) при ВЫХОДЕ, не при входе
Сложность: O(V + E)
Пример: DFS топосорт
DAG: 1→2, 1→3, 2→4, 3→4, 3→5, 4→6, 5→6
Шаг
Действие
Стек DFS
order
1
dfs(1): идём в 2
[1, 2]
[]
2
dfs(2): идём в 4
[1, 2, 4]
[]
3
dfs(4): идём в 6
[1, 2, 4, 6]
[]
4
dfs(6): тупик, выходим
[1, 2, 4]
[6]
5
dfs(4): выходим
[1, 2]
[6, 4]
6
dfs(2): выходим
[1]
[6, 4, 2]
7
dfs(1)→3→5→(6 vis)→выход 5, 3, 1
[]
[6,4,2,5,3,1]
8
reverse
—
[1,3,5,2,4,6]
А если есть цикл?
В ориентированном графе нужен другой подход — три цвета:
⬜ Белый (0): ещё не посещена
🔘 Серый (1): в процессе (в стеке DFS)
⬛ Чёрный (2): полностью обработана
Если при DFS мы приходим в серую вершину — найден цикл (обратное ребро).
Посчитать входящую степень (in-degree) каждой вершины
Добавить в очередь все вершины с in-degree = 0
Пока очередь не пуста: достать v, добавить в ответ, для каждого соседа u уменьшить in-degree[u]. Если стало 0 — в очередь.
Если обработали все n вершин — топосорт найден. Иначе — цикл!
Вершина «готова», когда все её зависимости уже обработаны. in-degree = 0 означает «все зависимости выполнены».
Код: алгоритм Кана
vector<int> topological_sort_kahn(int n, vector<vector<int>>& adj) {
vector<int> in_degree(n + 1, 0);
for (int v = 1; v <= n; v++)
for (int u : adj[v]) in_degree[u]++;
queue<int> q;
for (int v = 1; v <= n; v++)
if (in_degree[v] == 0) q.push(v);
vector<int> order;
while (!q.empty()) {
int v = q.front(); q.pop();
order.push_back(v);
for (int u : adj[v])
if (--in_degree[u] == 0) q.push(u);
}
if ((int)order.size() != n) return {}; // цикл!
return order;
}
⬆ order.size() != n — если обработали меньше n вершин — цикл
Этот порядок отличается от DFS-варианта — обе сортировки корректны
Два подхода: когда что?
DFS
Кан (BFS)
Сложность
O(V + E)
O(V + E)
Обнаружение цикла
Три цвета (серая вершина)
order.size() < n
Лекс. мин. порядок
Сложно
Легко (priority_queue)
DP на DAG
Удобно (при выходе)
Менее удобно
Реализация
Рекурсивная
Итеративная
На олимпиадах чаще используют DFS-вариант — он короче. Но алгоритм Кана лучше, когда нужен лексикографически минимальный порядок или рекурсия может переполнить стек.
Дано n курсов и m зависимостей. Для каждой зависимости (a, b) — курс a нужно пройти до курса b. Найти порядок прохождения всех курсов. Если невозможно — вывести «IMPOSSIBLE». n ≤ 105, m ≤ 2·105. Лимит: 1 сек.
Прямое применение топологической сортировки
DFS или Кан
Если order.size() < n — цикл → IMPOSSIBLE
Задача: подсчёт путей в DAG
CSES 1681 «Game Routes» cses.fi/problemset/task/1681
DAG из n вершин и m рёбер. Количество путей от 1 до n по модулю 109+7.
n ≤ 105, m ≤ 2·105.
Идея: Топосорт + DP. dp[v] = кол-во путей от 1 до v. dp[1] = 1.
// После топосорта
vector<long long> dp(n + 1, 0);
dp[1] = 1;
for (int v : order)
for (int u : adj[v])
dp[u] = (dp[u] + dp[v]) % MOD;
cout << dp[n] << "\n";
Дан граф, где каждое ребро имеет вес 0 или 1. Найти кратчайший путь от вершины s до вершины t. n, m ≤ 105. Лимит: 1 сек.
Какие алгоритмы вы знаете?
BFS (невзвешенный граф) — НЕ работает, веса не равны
Dijkstra — работает, но O((V+E) log V), избыточно
0-1 BFS — O(V + E), идеальный выбор
Идея 0-1 BFS
Обычный BFS: queue (FIFO) — все рёбра равны
0-1 BFS: deque (двусторонняя очередь)
Ребро веса 0: добавляем в начало дека (push_front)
Ребро веса 1: добавляем в конец дека (push_back)
Дек всегда содержит вершины с расстояниями d и d+1. Вершины с d — в начале, с d+1 — в конце. Из начала всегда берём минимальное расстояние.
Почему не обычный BFS?
Обычный BFS (неправильно)
0-1 BFS (правильно)
BFS считает число рёбер, а не сумму весов. При весах 0 два ребра могут быть «дешевле» одного.
Код: 0-1 BFS
// adj[v] = список пар {сосед, вес} (вес = 0 или 1)
vector<vector<pair<int,int>>> adj(n + 1);
vector<int> dist(n + 1, INT_MAX);
deque<int> dq;
dist[s] = 0;
dq.push_back(s);
while (!dq.empty()) {
int v = dq.front(); dq.pop_front();
for (auto [u, w] : adj[v]) {
if (dist[v] + w < dist[u]) {
dist[u] = dist[v] + w;
if (w == 0) dq.push_front(u); // вес 0: в начало
else dq.push_back(u); // вес 1: в конец
}
}
}
⬆ push_front / push_back — ключевое отличие от BFS
Сложность: O(V + E)
Пример: 0-1 BFS
1—1→2, 1—0→3, 3—0→2, 2—1→4, 3—1→4, 4—0→5
Шаг
Дек
Берём
Обновления
dist
0
[1]
—
—
1:0, ост: ∞
1
[]
1
3:0+0=0 (front), 2:0+1=1 (back)
2:1, 3:0
2
[3, 2]
3
2:0+0=0<1 (front!), 4:0+1=1 (back)
2:0, 4:1
3
[2, 2, 4]
2 (d=0)
4:0+1=1, не лучше
без изм.
4
[2, 4]
2 (d=1)
пропуск
без изм.
5
[4]
4
5:1+0=1 (front)
5:1
6
[5]
5
—
0,0,0,1,1
Типичное применение: сетка
Сетка n×m. Ячейки: пустые (.) или стены (#). Вопрос: минимум стен для слома, чтобы дойти от (1,1) до (n,m)?
Сетка n×m. Стоите в (r, c). Ходы вверх/вниз не ограничены. Максимум x ходов влево и y ходов вправо. Сколько ячеек достижимо? n, m ≤ 2000. Лимит: 2 сек.
Идея:
Вверх/вниз — вес 0
Влево/вправо — вес 1
0-1 BFS минимизирует число «дорогих» ходов
Ячейка достижима, если ходы влево ≤ x и вправо ≤ y
n учеников и m пар друзей. Разделить на две команды, чтобы друзья были в разных. Вывести номер команды (1 или 2). Если невозможно — «IMPOSSIBLE». n ≤ 105, m ≤ 2·105.
Прямая задача на двудольность
BFS-раскраска: color[v] + 1 = номер команды
Конфликт → IMPOSSIBLE
Обработать все компоненты!
Зачем двудольность на олимпиадах?
Разделение на группы — прямая проверка
Максимальное паросоче��ание — алгоритм Куна работает на двудольных графах
Раскраска — двудольность = хроматическое число 2
Шахматная доска — чёрные/белые клетки = двудольный граф
Паттерн: 2 группы + «соседи в разных» → двудольность
Если в задаче фигурируют 2 группы/команды/цвета — подумайте: двудольность?