Продвинутые DFS и BFS

Топологическая сортировка, 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);
        }
    }
}
      
1 2 3 4 5 6
  • Обходит вглубь, использует стек (рекурсию)
  • Сложность: 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);
        }
    }
}
      
1 d=0 2 3 d=1 4 5 d=2
  • Обходит по слоям, использует очередь
  • Сложность: O(V + E)
  • Кратчайший путь в невзвешенном графе

Сегодняшний план

1. Топологическая сортировка — порядок зависимостей в DAG (DFS + BFS подходы)
2. 0-1 BFS — кратчайший путь с весами 0 и 1 (дек вместо очереди)
3. Мосты и точки сочленения — уязвимые места графа (продвинутый DFS)
4. Проверка двудольности — раскраска в два цвета (BFS)
Каждая тема = задача-мотивация → алгоритм → код → практика

Задача: порядок курсов

Вы — студент. Есть n курсов и m зависимостей: курс a нужно пройти до курса b. Найдите порядок, в котором можно пройти все курсы, соблюдая зависимости.
Если это невозможно (есть цикл зависимостей), выведите «IMPOSSIBLE».
Матан Программ. Линал Теорвер Алгоритмы ML

Правильный порядок: Матан, Программирование, Линал, Теорвер, Алгоритмы, ML

Определение

Топологическая сортировка ориентированного ациклического графа (DAG) — это линейный порядок вершин, такой что для каждого ребра (u, v) вершина u идёт раньше v.
1 2 3 4 5 6 Все стрелки направо → порядок корректен
Топологическая сортировка возможна тогда и только тогда, когда граф не содержит цикла (т.е. является DAG).

Топосорт через DFS

  1. Запускаем DFS из каждой непосещённой вершины
  2. Когда DFS покидает вершину (после всех потомков), добавляем её в список
  3. Ответ — обратный порядок (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

ШагДействиеСтек DFSorder
1dfs(1): идём в 2[1, 2][]
2dfs(2): идём в 4[1, 2, 4][]
3dfs(4): идём в 6[1, 2, 4, 6][]
4dfs(6): тупик, выходим[1, 2, 4][6]
5dfs(4): выходим[1, 2][6, 4]
6dfs(2): выходим[1][6, 4, 2]
7dfs(1)→3→5→(6 vis)→выход 5, 3, 1[][6,4,2,5,3,1]
8reverse[1,3,5,2,4,6]

А если есть цикл?

В ориентированном графе нужен другой подход — три цвета:

  • ⬜ Белый (0): ещё не посещена
  • 🔘 Серый (1): в процессе (в стеке DFS)
  • ⬛ Чёрный (2): полностью обработана
Если при DFS мы приходим в серую вершину — найден цикл (обратное ребро).

vector<int> color; // 0 = белый, 1 = серый, 2 = чёрный
bool has_cycle = false;

void dfs(int v) {
    color[v] = 1;
    for (int u : adj[v]) {
        if (color[u] == 1) { has_cycle = true; return; }
        if (color[u] == 0) dfs(u);
    }
    color[v] = 2;
}
  

Топосорт через BFS: алгоритм Кана

  1. Посчитать входящую степень (in-degree) каждой вершины
  2. Добавить в очередь все вершины с in-degree = 0
  3. Пока очередь не пуста: достать v, добавить в ответ, для каждого соседа u уменьшить in-degree[u]. Если стало 0 — в очередь.
  4. Если обработали все 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 вершин — цикл

Сложность: O(V + E)

Пример: алгоритм Кана

DAG: 1→2, 1→3, 2→4, 3→4, 3→5, 4→6, 5→6  |  in-deg: 1:0, 2:1, 3:1, 4:2, 5:1, 6:2

ШагОчередьБерёмДействиеorder
0[1]in-deg[1]=0[]
1[]1in-deg[2]--, in-deg[3]--[1]
2[2, 3]2in-deg[4]--[1, 2]
3[3]3in-deg[4]--, in-deg[5]--[1, 2, 3]
4[4, 5]4in-deg[6]--[1, 2, 3, 4]
5[5]5in-deg[6]--[1, 2, 3, 4, 5]
6[6]6[1,2,3,4,5,6]

Этот порядок отличается от DFS-варианта — обе сортировки корректны

Два подхода: когда что?

DFSКан (BFS)
СложностьO(V + E)O(V + E)
Обнаружение циклаТри цвета (серая вершина)order.size() < n
Лекс. мин. порядокСложноЛегко (priority_queue)
DP на DAGУдобно (при выходе)Менее удобно
РеализацияРекурсивнаяИтеративная
На олимпиадах чаще используют DFS-вариант — он короче. Но алгоритм Кана лучше, когда нужен лексикографически минимальный порядок или рекурсия может переполнить стек.

Задача: Course Schedule

CSES 1679 «Course Schedule»
cses.fi/problemset/task/1679

Дано 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";
  

Практика: топологическая сортировка

#ЗадачаТемаПодсказка
1 CSES 1679 «Course Schedule» Базовый топосорт DFS или Кан, проверить цикл
2 CSES 1681 «Game Routes» Топосорт + DP dp[v] = сумма dp[u] по входящим
3 CSES 1680 «Longest Flight Route» Топосорт + DP (макс) dp[v] = max(dp[u]+1), восстановить путь
4 CF 510C «Fox and Names» Топосорт на алфавите Граф из пар соседних слов

Задача: минимальная стоимость

Дан граф, где каждое ребро имеет вес 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)

push_front (w=0) push_back (w=1) d d d d+1 d+1 d+1 pop_front →
Дек всегда содержит вершины с расстояниями d и d+1. Вершины с d — в начале, с d+1 — в конце. Из начала всегда берём минимальное расстояние.

Почему не обычный BFS?

Обычный BFS (неправильно)

1 2 3 w=0 w=1 w=0 dist[3] = 1 ✗

0-1 BFS (правильно)

1 2 3 w=0 w=1 w=0 dist[3] = 0 ✓
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[]13:0+0=0 (front), 2:0+1=1 (back)2:1, 3:0
2[3, 2]32: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]45:1+0=1 (front)5:1
6[5]50,0,0,1,1

Типичное применение: сетка

Сетка n×m. Ячейки: пустые (.) или стены (#).
Вопрос: минимум стен для слома, чтобы дойти от (1,1) до (n,m)?

  • Каждая клетка — вершина
  • Ребро к . — вес 0
  • Ребро к # — вес 1
  • Кратчайший путь = мин. стен
  • Решаем 0-1 BFS!
S T

Задача: Labyrinth

Codeforces 1063B «Labyrinth»
codeforces.com/problemset/problem/1063/B

Сетка n×m. Стоите в (r, c). Ходы вверх/вниз не ограничены. Максимум x ходов влево и y ходов вправо. Сколько ячеек достижимо?
n, m ≤ 2000. Лимит: 2 сек.

Идея:

  • Вверх/вниз — вес 0
  • Влево/вправо — вес 1
  • 0-1 BFS минимизирует число «дорогих» ходов
  • Ячейка достижима, если ходы влево ≤ x и вправо ≤ y

Практика: 0-1 BFS

#ЗадачаТемаПодсказка
1 CF 1063B «Labyrinth» 0-1 BFS на сетке Вверх/вниз — 0, влево/вправо — 1
2 CF 590C «Three States» Мультиисточниковый BFS от каждого государства
3 CSES 1202 «Investigation» Дейкстра (для сравнения) Решить Дейкстрой, подумать: можно ли 0-1 BFS?

Задача: уязвимости сети

1 2 3 4 5 6 7 МОСТ МОСТ

Мост — ребро, удаление которого увеличивает число компонент связности

Точка сочленения — вершина, удаление которой разрушает связность

Наивный подход: слишком медленный

  1. Для каждого ребра (u, v): удалить, проверить связность DFS, вернуть
  2. Если стало больше компонент — мост

Сложность: O(E · (V + E))

n = 105, m = 2·105 → ~1010 операций — TLE

Можно ли за один DFS?

Инструмент: времена входа (tin)

При DFS каждой вершине присваиваем время входа — номер при первом посещении.


int timer = 0;
vector<int> tin;

void dfs(int v, int parent) {
    tin[v] = timer++;
    // ...
}
      
1 tin=0 2 tin=1 3 tin=4 4 tin=2 5 tin=3

Обратные рёбра и функция low

  • Рёбра дерева DFS — по которым мы проходим
  • Обратные рёбра — к уже посещённым предкам
low[v] = min из:
• tin[v]
• tin[u] для каждого обратного ребра v→u
• low[u] для каждого потомка u в дереве DFS
A 0/0 B 1/0 C 2/0 обратное tin/low Обратное ребро C→A «понижает» low[C] до 0 и low «поднимается» к B и A Интуиция: low[v] = насколько «высоко» можно добраться

Критерий: когда ребро — мост?

Ребро дерева DFS (v, u), где v — родитель u, является мостом тогда и только тогда, когда low[u] > tin[v].

Мост (low[u] > tin[v])

v u tin=2 low=3 Нет пути наверх

Не мост (low[u] ≤ tin[v])

v u tin=2 low=1 Есть обратное ребро

Код: поиск мостов


int timer = 0;
vector<int> tin, low;
vector<bool> visited;
vector<pair<int,int>> bridges;

void dfs(int v, int parent) {
    visited[v] = true;
    tin[v] = low[v] = timer++;
    for (int u : adj[v]) {
        if (u == parent) continue;
        if (visited[u]) {
            low[v] = min(low[v], tin[u]);
        } else {
            dfs(u, v);
            low[v] = min(low[v], low[u]);
            if (low[u] > tin[v]) bridges.push_back({v, u});
        }
    }
}
  

⬆ Разница с обычным DFS: tin, low, и проверка low[u] > tin[v]

Сложность: O(V + E)

Пример: мосты

Граф: 1-2, 2-3, 3-1, 3-4, 4-5, 5-6, 6-4, 6-7

ВершинаtinlowРодитель
100-1
2101
3202
4333
5434
6535
7666

Проверяем: ребро 3-4: low[4]=3 > tin[3]=2 → Мост!

Ребро 6-7: low[7]=6 > tin[6]=5 → Мост!

Остальные — не мосты (обратные рёбра 3→1 и 6→4)

Подводный камень: кратные рёбра

Если между u и v два ребра, ни одно не мост. Но if (u == parent) continue пропустит все рёбра к родителю!

Решение: передавать номер ребра, не parent:


// adj[v] = список пар {сосед, номер_ребра}
void dfs(int v, int parent_edge) {
    visited[v] = true;
    tin[v] = low[v] = timer++;
    for (auto [u, edge_id] : adj[v]) {
        if (edge_id == parent_edge) continue;
        // ... (остальное как раньше)
    }
}
  
На олимпиадах лучше сразу писать с номером ребра — безопаснее.

Точки сочленения

Точка сочленения — вершина, удаление которой разрушает связность.

Два случая:

  1. v — корень DFS: точка сочленения ⟺ у v ≥ 2 детей в дереве DFS
  2. v — не корень: точка сочленения ⟺ ∃ ребёнок u: low[u] ≥ tin[v]
Отличие от мостов:
• Мост: low[u] > tin[v] (строгое)
• Точка сочленения: low[u] tin[v] (нестрогое)

Код: точки сочленения


int timer = 0;
vector<int> tin, low;
vector<bool> visited;
set<int> cut_points;

void dfs(int v, int parent) {
    visited[v] = true;
    tin[v] = low[v] = timer++;
    int children = 0;

    for (int u : adj[v]) {
        if (u == parent) continue;
        if (visited[u]) {
            low[v] = min(low[v], tin[u]);
        } else {
            children++;
            dfs(u, v);
            low[v] = min(low[v], low[u]);
            if (parent != -1 && low[u] >= tin[v]) {
                cut_points.insert(v);
            }
        }
    }

    if (parent == -1 && children > 1) {
        cut_points.insert(v);
    }
}
  

⬆ Отличия от мостов: children, , проверка корня

Пример: точки сочленения

Тот же граф: 1-2, 2-3, 3-1, 3-4, 4-5, 5-6, 6-4, 6-7

ВершинаtinlowДети в DFSТочка?
1 (корень)001Нет (1 ребёнок)
2101Нет
3201Да! low[4]=3 ≥ tin[3]=2
4331Да! low[5]=3 ≥ tin[4]=3
5431Нет
6531Да! low[7]=6 ≥ tin[6]=5
7660Нет

Точки сочленения: 3, 4, 6

Задача: мосты

e-olymp 1943 «Мосты» (или аналогичная)
e-olymp.com/ru/problems/1943

Дан неориентированный связный граф. Найти все мосты.
n ≤ 105, m ≤ 3·105. Лимит: 2 сек.

Прямое применение алгоритма с tin/low.

Практика: мосты и точки сочленения

#ЗадачаТемаПодсказка
1 e-olymp 1943 «Мосты» Поиск мостов Прямое применение tin/low
2 CF 1000E «We Need More Bosses» Мосты + конденсация Сжать 2-рёберно-связные, найти диаметр
3 CF 193A «Cutting Figure» Точки сочленения на сетке Найти, сколько клеток удалить для разъединения

Задача: разделить на две команды

Есть n людей и m пар врагов. Можно ли разделить всех на две команды, чтобы враги были в разных?
1 3 5 2 4 6 Команда 1 Команда 2 Все рёбра между разными командами
Двудольный граф — граф, вершины которого можно разбить на две доли, так что каждое ��ебро соединяет вершины из разных долей.

Когда граф двудольный?

Теорема: Граф двудольный тогда и только тогда, когда он не содержит циклов нечётной длины.

Двудольный (все циклы чётные)

1 2 3 4

Не двудольный (треугольник)

1 2 3? конфликт!

На практике мы не ищем циклы — пробуем раскрасить BFS-ом.

Проверка двудольности: BFS

  1. Массив color[], -1 = не раскрашен
  2. Для каждой непосещённой вершины v: BFS, красим v в 0
  3. Для каждого соседа u:
    • Не раскрашен → красим в 1 - color[v]
    • Раскрашен и color[u] == color[v]конфликт!
  4. Нет конфликтов → двудольный
  5. Обходим все компоненты!
Это обычный BFS + проверка: «сосед того же цвета = нечётный цикл = не двудольный»

Код: проверка двудольности


int n, m;
vector<vector<int>> adj;
vector<int> color;

bool bfs_bipartite(int start) {
    queue<int> q;
    color[start] = 0;
    q.push(start);
    while (!q.empty()) {
        int v = q.front(); q.pop();
        for (int u : adj[v]) {
            if (color[u] == -1) { color[u] = 1 - color[v]; q.push(u); }
            else if (color[u] == color[v]) return false;
        }
    }
    return true;
}

bool is_bipartite() {
    color.assign(n + 1, -1);
    for (int v = 1; v <= n; v++)
        if (color[v] == -1 && !bfs_bipartite(v)) return false;
    return true;
}
  

1 - color[v] — переключает 0 ↔ 1

Сложность: O(V + E)

Пример: двудольный

Граф: 1-2, 1-4, 2-3, 3-4, 4-5, 5-6

ШагОчередьВершинаЦветКонфликт?
0[1]1: 0
1[2, 4]12: 1, 4: 1нет
2[4, 3]23: 0нет
3[3, 5]43: уже 0 (ok), 5: 0нет
4[5]34: уже 1 (ok)нет
5[6]56: 1нет

Двудольный! Доли: {1,3,5} и {2,4,6}

Пример: НЕ двудольный

Граф: 1-2, 2-3, 3-1, 3-4, 4-5

ШагОчередьВерш.Цвет?
0[1]1: 0
1[2, 3]12: 1, 3: 1нет
2[3]23: 1, [2]=1ДА!
1: 0 2: 1 3: 1 !

Не двудольный! Нечётный цикл 1-2-3

Задача: Building Teams

CSES 1668 «Building Teams»
cses.fi/problemset/task/1668

n учеников и m пар друзей. Разделить на две команды, чтобы друзья были в разных. Вывести номер команды (1 или 2). Если невозможно — «IMPOSSIBLE».
n ≤ 105, m ≤ 2·105.
  • Прямая задача на двудольность
  • BFS-раскраска: color[v] + 1 = номер команды
  • Конфликт → IMPOSSIBLE
  • Обработать все компоненты!

Зачем двудольность на олимпиадах?

  1. Разделение на группы — прямая проверка
  2. Максимальное паросоче��ание — алгоритм Куна работает на двудольных графах
  3. Раскраска — двудольность = хроматическое число 2
  4. Шахматная доска — чёрные/белые клетки = двудольный граф
  5. Паттерн: 2 группы + «соседи в разных» → двудольность
Если в задаче фигурируют 2 группы/команды/цвета — подумайте: двудольность?

Практика: двудольность

#ЗадачаТемаПодсказка
1 CSES 1668 «Building Teams» Базовая двудольность BFS-раскраска
2 CF 1144F «Graph Without Long Directed Paths» Двудольность + ориентация Ориентировать рёбра от одной доли к другой
3 CF 687A «NP-Hard Problem» Прямая проверка BFS/DFS раскраска, вывод долей

Итоги: что мы изучили

ТемаАлгоритмСложностьБазовая идея
Топосорт (DFS)DFS + post-order reverseO(V+E)Добавляем при выходе, reverse
Топосорт (Кан)BFS + in-degreeO(V+E)Снимаем «слои» без зависимостей
0-1 BFSBFS + dequeO(V+E)push_front для 0, push_back для 1
МостыDFS + tin/lowO(V+E)low[u] > tin[v]
Точки сочлененияDFS + tin/lowO(V+E)low[u] ≥ tin[v], корень: дети > 1
ДвудольностьBFS + 2 цветаO(V+E)Сосед того же цвета = не двудольный
Все алгоритмы — O(V + E). Каждый — модификация базового DFS или BFS.

Все задачи для практики

Начните с базовых задач в каждой теме. Переходите к сложным после уверенного решения базовых.

Вопросы?

  • Топологическая сортировка (DFS + Кан)
  • 0-1 BFS (дек)
  • Мосты и точки сочленения (tin/low)
  • Проверка двудольности (BFS-раскраска)

Abbas Aliyev — alievabbas1@gmail.com

Раздаточный материал Day 1 + эта презентация = полный справочник по DFS/BFS для IOI