Уроки D2-D7 (14 файлов): деревья/куча, epoll, потоки, gdb, графы, L2-L3, ДП, TCP, bash, ядро, docker, мок-интервью + cards_src 613
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
# D4, часть 1. Графы: обход, топологическая сортировка, кратчайшие пути (урок)
|
||||
|
||||
Это не проверка, а урок: сначала разбираем механизм, потом решаешь `tasks/12_dijkstra`. Граф —
|
||||
это не абстракция «для собеседований», а модель любой сети связей: маршрутизаторы и линки,
|
||||
процессы и их зависимости, файлы и их include-цепочки.
|
||||
|
||||
## 1. Представления графа: матрица смежности vs список смежности
|
||||
|
||||
Граф — это набор вершин `V` и рёбер `E`. Два способа хранить связи в памяти:
|
||||
|
||||
```c++
|
||||
// матрица смежности: adj[u][v] = вес ребра (или 0/inf, если ребра нет)
|
||||
std::vector<std::vector<int>> adj(n, std::vector<int>(n, 0));
|
||||
|
||||
// список смежности: adj[u] = список (сосед, вес)
|
||||
std::vector<std::vector<std::pair<int,int>>> adj(n);
|
||||
```
|
||||
|
||||
**Матрица смежности:** память O(V²) независимо от числа рёбер, проверка «есть ли ребро u→v» —
|
||||
O(1) прямым обращением по индексу, но перебор всех соседей вершины — всегда O(V), даже если
|
||||
соседей всего два. Подходит для **плотных** графов (E близко к V²) и когда нужна частая
|
||||
проверка «есть ли ребро».
|
||||
|
||||
**Список смежности:** память O(V+E) — платишь только за реально существующие рёбра, перебор
|
||||
соседей вершины — O(deg(v)), то есть ровно столько операций, сколько соседей. Подходит для
|
||||
**разреженных** графов (типичный случай: сети, графы зависимостей), где E ≈ V, а не V².
|
||||
`tasks/12_dijkstra` — 100 000 вершин и 200 000 рёбер: матрица заняла бы 10¹⁰ ячеек и не влезла
|
||||
бы в память, поэтому единственный вариант — список смежности.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Память матрицы смежности для V вершин? — O(V²)
|
||||
- base | Память списка смежности? — O(V+E)
|
||||
- core | Сложность перебора всех соседей вершины в списке смежности? — O(deg(v)), в матрице — всегда O(V)
|
||||
- core | Почему для графа 100 000 вершин нужен список смежности, а не матрица? — матрица заняла бы 10¹⁰ ячеек, не влезает в память
|
||||
|
||||
Почему дальше: раз связи хранятся как список соседей, естественный первый вопрос — как обойти
|
||||
весь граф, начиная с одной вершины.
|
||||
|
||||
## 2. BFS: обход в ширину и кратчайший путь по числу рёбер
|
||||
|
||||
```c++
|
||||
std::vector<int> bfs(int src, const std::vector<std::vector<int>>& adj) {
|
||||
std::vector<int> dist(adj.size(), -1);
|
||||
std::queue<int> q;
|
||||
dist[src] = 0;
|
||||
q.push(src);
|
||||
while (!q.empty()) {
|
||||
int u = q.front(); q.pop();
|
||||
for (int v : adj[u])
|
||||
if (dist[v] == -1) { // ещё не посещена
|
||||
dist[v] = dist[u] + 1;
|
||||
q.push(v);
|
||||
}
|
||||
}
|
||||
return dist;
|
||||
}
|
||||
```
|
||||
|
||||
BFS раскрывает граф слоями: сначала все вершины на расстоянии 1 ребро от источника, потом на
|
||||
расстоянии 2, и так далее — это следствие того, что структура данных — **очередь** (FIFO):
|
||||
вершина, добавленная раньше, обрабатывается раньше, поэтому к моменту, когда очередь дошла до
|
||||
вершин расстояния k, все вершины расстояния <k уже обработаны. Отсюда ключевое свойство: **в
|
||||
невзвешенном графе BFS даёт кратчайший путь по числу рёбер** — первый раз, когда вершина
|
||||
помечена посещённой, это гарантированно по кратчайшему пути.
|
||||
|
||||
Каждая вершина кладётся в очередь ровно один раз (проверка `dist[v] == -1` это гарантирует), и
|
||||
для каждой вершины перебираются все её соседи один раз — суммарно **O(V+E)**: O(V) на
|
||||
инициализацию и постановку/снятие каждой вершины из очереди, O(E) на суммарный перебор всех
|
||||
рёбер по всем вершинам (каждое ребро просматривается один или два раза, в зависимости от
|
||||
ориентированности).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность BFS на списке смежности? — O(V+E)
|
||||
- base | Какую структуру данных использует BFS? — очередь (FIFO)
|
||||
- core | Что гарантированно находит BFS в невзвешенном графе? — кратчайший путь по числу рёбер от источника
|
||||
- core | Почему BFS даёт кратчайший путь именно из-за очереди, а не из-за чего-то ещё? — FIFO-порядок раскрывает граф строго по слоям расстояния, вершина расстояния k не может обработаться раньше всех вершин расстояния <k
|
||||
|
||||
Почему дальше: если вместо очереди использовать стек (или рекурсию), обход идёт не слоями, а
|
||||
«вглубь» одной ветки до конца — это DFS, и он даёт другие гарантии.
|
||||
|
||||
## 3. DFS: обход в глубину
|
||||
|
||||
```c++
|
||||
void dfs(int u, const std::vector<std::vector<int>>& adj, std::vector<bool>& visited) {
|
||||
visited[u] = true;
|
||||
for (int v : adj[u])
|
||||
if (!visited[v]) dfs(v, adj, visited); // рекурсия = неявный стек вызовов
|
||||
}
|
||||
```
|
||||
|
||||
DFS идёт максимально глубоко по одной ветке, прежде чем откатиться и попробовать соседнюю —
|
||||
это следствие того, что структура данных — **стек** (LIFO), явный или неявный (стек вызовов
|
||||
рекурсии). Сложность та же, что у BFS — **O(V+E)**: те же рассуждения про однократное
|
||||
посещение каждой вершины и однократный перебор рёбер, разница только в порядке обхода, не в
|
||||
асимптотике.
|
||||
|
||||
DFS не гарантирует кратчайший путь (может найти путь в 10 рёбер, когда есть путь в 2), зато
|
||||
даёт естественный механизм для задач, где важен порядок «сначала потомки, потом сам узел» —
|
||||
топологическая сортировка через постфиксный обход, обнаружение циклов, поиск компонент
|
||||
связности.
|
||||
|
||||
**Ловушки**
|
||||
- Рекурсивный DFS на графе с длинной цепочкой (например, список из 100 000 вершин,
|
||||
выстроенных в цепь) → глубина рекурсии 100 000 → переполнение стека вызовов (stack
|
||||
overflow), не ловится компилятором, падает в рантайме. Решение — итеративный DFS с явным
|
||||
`std::stack`.
|
||||
- Забыть пометить вершину посещённой до рекурсивного спуска, а не после → на графе с циклом
|
||||
уйдёт в бесконечную рекурсию.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность DFS на списке смежности? — O(V+E)
|
||||
- base | Какую структуру данных использует DFS? — стек (явный или стек вызовов рекурсии)
|
||||
- core | Гарантирует ли DFS кратчайший путь? — нет, в отличие от BFS
|
||||
- deep | Почему рекурсивный DFS опасен на графе-цепочке из 100 000 вершин? — глубина рекурсии равна длине цепочки, стек вызовов переполняется
|
||||
|
||||
Почему дальше: раз BFS/DFS помечают вершины посещёнными за один проход, естественно
|
||||
использовать это для подсчёта изолированных друг от друга частей графа — компонент
|
||||
связности.
|
||||
|
||||
## 4. Связные компоненты
|
||||
|
||||
Механизм: перебираем все вершины `0..n-1`; если вершина ещё не посещена — запускаем от неё
|
||||
BFS или DFS, это помечает посещёнными всю компоненту, к которой она принадлежит, и
|
||||
увеличиваем счётчик компонент на 1. Суммарная сложность по всем запускам всё равно
|
||||
**O(V+E)** — каждая вершина и каждое ребро обрабатываются ровно один раз за весь перебор,
|
||||
несмотря на то что BFS/DFS запускается несколько раз (по числу компонент), а не один.
|
||||
|
||||
Задача **Number of Islands** — это ровно связные компоненты на сетке: клетка `'1'` —
|
||||
вершина, соседние по 4 направлениям (вверх/вниз/влево/вправо) клетки `'1'` — рёбра. BFS/DFS
|
||||
(flood fill) от каждой ещё не посещённой клетки `'1'` закрашивает весь остров, число запусков
|
||||
= число островов. Граф здесь не хранится явно списком смежности — соседи вычисляются на лету
|
||||
по координатам, но асимптотика та же: O(rows·cols).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность подсчёта связных компонент через BFS/DFS от каждой непосещённой вершины? — O(V+E), суммарно по всем запускам
|
||||
- core | Что в задаче Number of Islands является «вершиной» и «ребром»? — вершина — клетка `'1'`, ребро — соседство по 4 направлениям
|
||||
|
||||
Почему дальше: связные компоненты не различают направление рёбер. В ориентированном графе
|
||||
зависимостей («B зависит от A») важен порядок — какую вершину обработать раньше другой. Это
|
||||
топологическая сортировка.
|
||||
|
||||
## 5. Топологическая сортировка: алгоритм Кана
|
||||
|
||||
Топологический порядок существует только для **направленного ациклического графа (DAG)** —
|
||||
если есть цикл (A зависит от B, B зависит от A), непротиворечивого порядка «раньше/позже»
|
||||
построить нельзя в принципе.
|
||||
|
||||
```c++
|
||||
std::vector<int> kahn_toposort(int n, const std::vector<std::vector<int>>& adj) {
|
||||
std::vector<int> indeg(n, 0);
|
||||
for (int u = 0; u < n; ++u)
|
||||
for (int v : adj[u]) indeg[v]++; // степень входа: сколько рёбер входит в v
|
||||
|
||||
std::queue<int> q;
|
||||
for (int u = 0; u < n; ++u)
|
||||
if (indeg[u] == 0) q.push(u); // вершины без зависимостей — стартовые
|
||||
|
||||
std::vector<int> order;
|
||||
while (!q.empty()) {
|
||||
int u = q.front(); q.pop();
|
||||
order.push_back(u);
|
||||
for (int v : adj[u])
|
||||
if (--indeg[v] == 0) q.push(v); // все зависимости v уже обработаны
|
||||
}
|
||||
return order; // order.size() < n -> в графе есть цикл
|
||||
}
|
||||
```
|
||||
|
||||
Механизм: **степень входа** вершины — число рёбер, входящих в неё, то есть число
|
||||
нерассмотренных зависимостей. Вершина с `indeg == 0` не зависит ни от кого необработанного —
|
||||
её можно поставить в порядок прямо сейчас. Когда вершина обработана, она «снимает»
|
||||
зависимость со всех своих соседей (`--indeg[v]`); как только у соседа не осталось
|
||||
необработанных зависимостей, он тоже готов — кладём его в очередь. Сложность **O(V+E)**: та
|
||||
же логика, что у BFS — каждая вершина обрабатывается один раз, каждое ребро уменьшает
|
||||
`indeg` один раз.
|
||||
|
||||
**Как проявляется цикл:** если в графе есть цикл, все вершины цикла имеют `indeg > 0` до
|
||||
конца работы алгоритма (каждая ждёт кого-то из цикла, а тот ждёт её) — они никогда не попадут
|
||||
в очередь. Проверка: **`order.size() < n`** после завершения — значит, часть вершин не
|
||||
обработана, в графе цикл. Это и есть способ решить задачу **Course Schedule**: курсы —
|
||||
вершины, пререквизит «A перед B» — ребро A→B; если топологический порядок покрывает все
|
||||
курсы, все курсы можно пройти, иначе где-то циклическая зависимость.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Для какого типа графа определена топологическая сортировка? — направленный ациклический граф (DAG)
|
||||
- base | Сложность алгоритма Кана? — O(V+E)
|
||||
- core | Что такое степень входа вершины в алгоритме Кана? — число входящих в неё рёбер (нерассмотренных зависимостей)
|
||||
- core | Как алгоритм Кана обнаруживает цикл? — счётчик обработанных вершин `order.size()` меньше `n` после завершения
|
||||
- core | Как задача Course Schedule сводится к топологической сортировке? — курсы — вершины, пререквизит — направленное ребро, цикл = невозможно пройти все курсы
|
||||
|
||||
**Ловушки**
|
||||
- Забыть, что `order.size() < n` — единственный надёжный признак цикла (пустая очередь сама
|
||||
по себе не значит успех) → код считает граф с циклом корректно отсортированным.
|
||||
- Спутать степень входа со степенью выхода при инициализации очереди → в очередь попадут не
|
||||
те вершины, порядок будет неверным с первого шага.
|
||||
|
||||
Почему дальше: топологическая сортировка отвечает на вопрос «в каком порядке», но не «на
|
||||
каком расстоянии». Если рёбрам приписать веса, следующий естественный вопрос — кратчайший
|
||||
путь по сумме весов, а не по числу рёбер.
|
||||
|
||||
## 6. Дейкстра: кратчайшие пути с неотрицательными весами
|
||||
|
||||
Условие применимости — **все веса рёбер ≥ 0** (`tasks/12_dijkstra` явно требует `w >= 0`,
|
||||
при этом вес 0 допустим). Причина ограничения — алгоритм жадный: как только вершина извлечена
|
||||
с минимальным текущим расстоянием, она считается **окончательно решённой** и больше не
|
||||
пересматривается. Это верно только если из невыбранных вершин путь до неё не может стать
|
||||
короче — а если есть отрицательное ребро, путь через ещё не рассмотренную вершину теоретически
|
||||
мог бы уменьшить уже «зафиксированное» расстояние, и жадность ломается: алгоритм даст неверный
|
||||
ответ, а не просто отработает медленнее.
|
||||
|
||||
**Наивная реализация** (без очереди с приоритетом): на каждом из V шагов ищем среди
|
||||
непосещённых вершину с минимальным `dist` линейным перебором — O(V) на шаг, всего **O(V²)**.
|
||||
Годится для плотных графов, где E ≈ V².
|
||||
|
||||
**С `priority_queue`** (мин-куча по `dist`): каждое расслабление ребра — это `push` в кучу
|
||||
O(log V), таких расслаблений всего O(E), плюс O(V) извлечений минимума — итого **O((V+E) log V)**.
|
||||
Для `tasks/12_dijkstra` (100 000 вершин, 200 000 рёбер) это единственный вариант, укладывающийся
|
||||
в 2 секунды: V² здесь — 10¹⁰ операций, а (V+E)·log V — около 300 000 · 17 ≈ 5·10⁶.
|
||||
|
||||
```c++
|
||||
long long shortest_path(int n,
|
||||
const std::vector<std::tuple<int,int,int>>& edges, // (u, v, w)
|
||||
int src, int dst) {
|
||||
if (src == dst) return 0;
|
||||
std::vector<std::vector<std::pair<int,long long>>> adj(n);
|
||||
for (auto& [u, v, w] : edges) adj[u].push_back({v, w}); // ориентированное ребро
|
||||
|
||||
std::vector<long long> dist(n, -1);
|
||||
using P = std::pair<long long,int>; // (расстояние, вершина)
|
||||
std::priority_queue<P, std::vector<P>, std::greater<P>> pq; // мин-куча
|
||||
dist[src] = 0;
|
||||
pq.push({0, src});
|
||||
|
||||
while (!pq.empty()) {
|
||||
auto [d, u] = pq.top(); pq.pop();
|
||||
if (dist[u] != -1 && d != dist[u]) continue; // устаревшая запись — "ленивое" удаление
|
||||
if (u == dst) return d;
|
||||
for (auto& [v, w] : adj[u]) {
|
||||
long long nd = d + w;
|
||||
if (dist[v] == -1 || nd < dist[v]) {
|
||||
dist[v] = nd;
|
||||
pq.push({nd, v});
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1; // недостижима
|
||||
}
|
||||
```
|
||||
|
||||
**«Ленивое» удаление устаревших записей** — `std::priority_queue` не умеет напрямую
|
||||
уменьшать ключ уже лежащего элемента (нет операции decrease-key), поэтому вместо обновления
|
||||
кладут в кучу новую пару `(nd, v)` при каждом улучшении расстояния — старая запись остаётся в
|
||||
куче. Когда до неё доходит очередь на извлечение, проверка `if (d != dist[v]) continue;`
|
||||
находит, что для `v` уже известно расстояние лучше, чем в этой записи, и просто пропускает
|
||||
её. Куча может временно раздуться до O(E) записей вместо O(V), но асимптотика O((V+E) log V)
|
||||
не меняется, потому что каждая запись — это одна операция `push` в ответ на одно расслабление
|
||||
ребра.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Обязательное условие применимости Дейкстры? — все веса рёбер неотрицательны (w ≥ 0)
|
||||
- base | Сложность наивной Дейкстры (без кучи)? — O(V²)
|
||||
- core | Сложность Дейкстры с `priority_queue`? — O((V+E) log V)
|
||||
- core | Почему отрицательное ребро ломает Дейкстру? — вершина считается решённой сразу после извлечения минимума, а отрицательное ребро из ещё не рассмотренной вершины теоретически могло бы уменьшить это «зафиксированное» расстояние
|
||||
- core | Что проверяет `if (d != dist[v]) continue;` в реализации на куче? — что извлечённая запись не устарела (для v уже нашли путь короче)
|
||||
- deep | Почему куча может держать до O(E) записей вместо O(V)? — при каждом улучшении расстояния в кучу кладётся новая запись вместо обновления старой (нет decrease-key)
|
||||
|
||||
**Ловушки**
|
||||
- Забыть проверку устаревшей записи (`if (d != dist[v]) continue;`) → для одной вершины
|
||||
расслабление соседей выполняется несколько раз лишний раз, результат остаётся верным, но
|
||||
сложность деградирует к худшему случаю.
|
||||
- Применить Дейкстру к графу с отрицательным ребром без проверки условия → неверный
|
||||
(заниженный или завышенный, в зависимости от структуры) ответ без явной ошибки —
|
||||
тестами не всегда ловится, если отрицательное ребро не лежит на кратчайшем пути.
|
||||
|
||||
Почему дальше: если в графе всё же есть отрицательные веса, нужен алгоритм без жадного
|
||||
«фиксирования» результата — Беллман-Форд.
|
||||
|
||||
## 7. Беллман-Форд: отрицательные веса
|
||||
|
||||
Механизм: вместо жадного выбора минимума **расслабляем все E рёбер `V-1` раз подряд**. После
|
||||
k-й полной итерации гарантированно найдены все кратчайшие пути, использующие не более k
|
||||
рёбер; кратчайший путь без отрицательных циклов не может состоять больше чем из `V-1` ребра
|
||||
(иначе он повторял бы вершину), поэтому `V-1` итераций достаточно, чтобы расстояния
|
||||
стабилизировались. Сложность — **O(V·E)**: `V-1` проходов по всем E рёбрам.
|
||||
|
||||
Дополнительная V-я итерация по всем рёбрам служит детектором **отрицательного цикла**: если
|
||||
после `V-1` итераций расстояние для какого-то ребра всё ещё можно уменьшить, значит в графе
|
||||
есть цикл с отрицательной суммой весов — кратчайший путь в принципе не определён (можно
|
||||
крутиться по циклу бесконечно, уменьшая сумму). Дейкстра такой цикл не заметит и просто даст
|
||||
неверный ответ; Беллман-Форд явно сигнализирует о его существовании.
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Сложность Беллман-Форда? — O(V·E)
|
||||
- base | Сколько раз алгоритм проходит по всем рёбрам в основном цикле? — V-1 раз
|
||||
- core | Как Беллман-Форд обнаруживает отрицательный цикл? — если расстояние ещё можно уменьшить на дополнительной V-й итерации, в графе есть отрицательный цикл
|
||||
- core | Почему V-1 итераций достаточно? — кратчайший путь без отрицательных циклов не может содержать больше V-1 ребра (иначе повторяет вершину)
|
||||
|
||||
Почему дальше: и Дейкстра, и Беллман-Форд работают на ориентированных рёбрах с весами. Для
|
||||
неориентированных рёбер есть отдельная задача — построить минимальный связывающий набор
|
||||
рёбер (остовное дерево) без циклов; для неё нужна структура, которая быстро отвечает «эти две
|
||||
вершины уже соединены?» — union-find.
|
||||
|
||||
## 8. Union-Find (Disjoint Set Union): ранг, сжатие пути, Kruskal
|
||||
|
||||
Структура хранит разбиение вершин на непересекающиеся множества (компоненты) и поддерживает
|
||||
две операции: `find(x)` — найти представителя множества, `union(x, y)` — объединить два
|
||||
множества.
|
||||
|
||||
```c++
|
||||
struct DSU {
|
||||
std::vector<int> parent, rank_;
|
||||
DSU(int n) : parent(n), rank_(n, 0) {
|
||||
for (int i = 0; i < n; ++i) parent[i] = i;
|
||||
}
|
||||
int find(int x) {
|
||||
if (parent[x] != x) parent[x] = find(parent[x]); // сжатие пути (path compression)
|
||||
return parent[x];
|
||||
}
|
||||
bool unite(int x, int y) {
|
||||
x = find(x); y = find(y);
|
||||
if (x == y) return false; // уже в одном множестве — цикл
|
||||
if (rank_[x] < rank_[y]) std::swap(x, y);
|
||||
parent[y] = x;
|
||||
if (rank_[x] == rank_[y]) rank_[x]++; // объединение по рангу
|
||||
return true;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**Сжатие пути:** при каждом `find` все вершины на пути до корня перепривязываются напрямую к
|
||||
корню — следующий `find` для любой из них займёт один шаг вместо повторного прохода всей
|
||||
цепочки. **Объединение по рангу:** при слиянии корень с меньшим рангом (примерной оценкой
|
||||
высоты дерева) подвешивается под корень с большим — это не даёт деревьям расти в глубину без
|
||||
необходимости. По отдельности каждый приём даёт логарифмическое улучшение, вместе — амортизи-
|
||||
рованная сложность операции становится **O(α(n))**, где α — обратная функция Аккермана: она
|
||||
растёт настолько медленно, что для любого практически представимого n (меньше числа атомов во
|
||||
вселенной) α(n) ≤ 4. На практике это и называют «почти O(1)».
|
||||
|
||||
**Kruskal** (минимальное остовное дерево): сортируем все рёбра по весу — O(E log E); идём по
|
||||
рёбрам от меньшего веса к большему, для каждого ребра `(u,v)` проверяем `find(u) != find(v)` —
|
||||
если вершины ещё не в одной компоненте, добавляем ребро в остов и делаем `unite(u,v)` (иначе
|
||||
ребро создало бы цикл — пропускаем). Итоговая сложность — **O(E log E)** (сортировка
|
||||
доминирует над почти O(1) операциями union-find).
|
||||
|
||||
Задача **Redundant Connection**: дан граф-дерево из n вершин и n рёбер (то есть ровно одно
|
||||
лишнее ребро, замыкающее единственный цикл) — нужно найти это лишнее ребро. Решение —
|
||||
union-find: идём по рёбрам по порядку, для каждого делаем `unite`; первое ребро, для которого
|
||||
`unite` вернул `false` (обе вершины уже в одной компоненте до объединения), и есть искомое —
|
||||
оно замыкает цикл.
|
||||
|
||||
Задача **Network Delay Time**: рёбра с весами (время передачи сигнала), нужно время, за
|
||||
которое сигнал от вершины k дойдёт до всех остальных. Это ровно Дейкстра от источника k;
|
||||
ответ — максимум из всех найденных кратчайших расстояний (если какая-то вершина осталась
|
||||
недостижима — ответ -1).
|
||||
|
||||
**Факты для карточек**
|
||||
- base | Какие два приёма дают почти-константную сложность union-find? — сжатие пути и объединение по рангу
|
||||
- base | Амортизированная сложность операции union-find с обоими приёмами? — O(α(n)), обратная функция Аккермана, практически O(1)
|
||||
- core | Сложность алгоритма Kruskal и что в ней доминирует? — O(E log E), доминирует сортировка рёбер
|
||||
- core | Как union-find решает Redundant Connection? — первое ребро, для которого `unite` вернул false (вершины уже в одной компоненте), и есть лишнее
|
||||
- core | Как Network Delay Time сводится к Дейкстре? — вершина k — источник, ответ — максимум из всех кратчайших расстояний, -1 если что-то недостижимо
|
||||
- deep | Каково верхнее ограничение обратной функции Аккермана для практических n? — α(n) ≤ 4 для любого n, меньшего числа атомов во вселенной
|
||||
|
||||
**Ловушки**
|
||||
- Забыть сжатие пути в `find` → дерево может выродиться в цепочку, `find` деградирует к O(n)
|
||||
на несбалансированных входных данных.
|
||||
- В Kruskal забыть проверку `find(u) != find(v)` перед добавлением ребра → в остов попадёт
|
||||
ребро, замыкающее цикл, результат перестанет быть деревом.
|
||||
|
||||
Ссылки на задачи этого дня: `tasks/12_dijkstra` (Дейкстра на `priority_queue`, ленивое
|
||||
удаление, условие неотрицательных весов).
|
||||
|
||||
<details>
|
||||
<summary>Проверь себя</summary>
|
||||
|
||||
1. Граф на 100 000 вершин и 200 000 рёбер. Почему для него используют список смежности, а не
|
||||
матрицу, и во сколько раз (порядок величины) список экономнее по памяти?
|
||||
<details><summary>Ответ</summary>Матрица заняла бы V²=10¹⁰ ячеек, список — V+E≈300 000
|
||||
ячеек: разница на порядки (≈33 000 раз), матрица физически не влезает в разумную память.</details>
|
||||
|
||||
2. Почему BFS, а не DFS, используют для поиска кратчайшего пути в невзвешенном графе?
|
||||
<details><summary>Ответ</summary>BFS раскрывает граф слоями через очередь (FIFO) — все
|
||||
вершины расстояния k обрабатываются раньше вершин расстояния k+1, поэтому первое посещение
|
||||
вершины гарантированно происходит по кратчайшему пути. DFS идёт вглубь одной ветки и такой
|
||||
гарантии не даёт.</details>
|
||||
|
||||
3. В алгоритме Кана после обработки все вершины кроме трёх остались с `indeg > 0`. Что это
|
||||
означает и как это связано с количеством обработанных вершин?
|
||||
<details><summary>Ответ</summary>Эти три (и, возможно, другие зависимые от них) вершины
|
||||
образуют цикл — они никогда не наберут `indeg == 0`, поэтому `order.size() < n`, что и есть
|
||||
признак цикла в графе.</details>
|
||||
|
||||
4. Граф с одним ребром веса -5 в остальном с неотрицательными весами. Почему нельзя просто
|
||||
«запустить Дейкстру и она сработает, если это ребро не на пути к целевой вершине»?
|
||||
<details><summary>Ответ</summary>Нельзя гарантировать заранее, какие вершины уже
|
||||
«зафиксированы» жадным выбором к моменту, когда алгоритм дойдёт до отрицательного ребра —
|
||||
если оно ведёт в уже решённую вершину, её расстояние должно было бы уменьшиться, но
|
||||
Дейкстра его не пересмотрит; корректность в общем случае не гарантирована, поэтому условие
|
||||
w ≥ 0 обязательно для всех рёбер, а не только на пути к конкретной вершине.</details>
|
||||
|
||||
5. Почему сложность Kruskal — O(E log E), если union-find работает почти за O(1)?
|
||||
<details><summary>Ответ</summary>Потому что перед объединением рёбра нужно отсортировать по
|
||||
весу — это O(E log E) и доминирует над суммарной почти константной стоимостью всех
|
||||
union-find операций O(E·α(V)).</details>
|
||||
|
||||
</details>
|
||||
|
||||
## Материалы
|
||||
|
||||
- Документация `std::priority_queue` (мин-куча в реализации Дейкстры) — https://en.cppreference.com/w/cpp/container/priority_queue
|
||||
Reference in New Issue
Block a user