Условие задачи
На дереве живут термиты. Ваша задача убить их всех. Дерево является неориентированным связным графом с $n$ вершинами и $n — 1$ ребрами. Чтобы убить термитов, Вам следует отравить некоторые вершины. Если термит попадает на вершину с ядом, то он немедленно умирает. Вы не знаете, где изначально находятся термиты. Но Вы знаете, что термиты каждый раз попадают в случайную соседнюю вершину. Однако если термит прошел ребро $(u, v)$, то следующее ребро должно отличаться от $(v, u)$ за исключением случая, когда термит попадает в лист (в этом случае термит поворачивается и возвращается назад). Вам следует отравить минимальное количество вершин так, чтобы термиты попали в отравленные вершины после конечного числа шагов.
Входные данные
Первая строка содержит одно целое число $n$ $(1 \leqslant n \leqslant 100000)$. Следующая строка содержит $n — 1$ целое число $p_{i} (2 \leqslant i \leqslant n)$, означающее что ребро соединяет $p_{i}$ и $i$.
Выходные данные
Выведите минимальное количество отравленных вершин.
Тесты
№ | Входные данные | Выходные данные |
1 | 1 | 1 |
2 | 2 1 |
1 |
3 | 8 1 1 2 1 2 3 2 |
2 |
4 | 5 1 2 1 4 |
1 |
5 | 16 1 2 3 4 5 3 7 1 9 9 11 11 13 13 15 |
3 |
6 | 10 1 2 3 3 1 2 3 7 9 |
2 |
7 | 8 1 1 3 3 1 6 6 |
2 |
Код
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
#include <bits/stdc++.h> using namespace std; const int NMAX = 100001; vector<int> tree [NMAX]; vector<int> killed (NMAX, 0); vector<bool> used (NMAX, 0); vector<int> dist (NMAX, 0); int ans = 0; void kills(int v, int p) { if (v == p) kills(tree[v][0], v); if (tree[v].size() == 2) for (auto to : tree[v]) if (to != p) kills(to, v); if (tree[v].size() >= 3) killed[v]++; return; } void goup(int v, int p) { if (v == p) { for (auto to : tree[v]) if (dist[to] < dist[v]) goup(to, v); killed[v] = 0; return; } if (tree[v].size() == 2) for (auto to : tree[v]) if (to != p) goup(to, v); if (tree[v].size() >= 3) { killed[v]++; return; } } int main() { int n,v; cin>>n; if (n <= 5) {cout<<1; return 0;} // Частный случай tree[1].push_back(0); // Корень тоже должен быть "развилкой" vector<int> leaves; for (int i = 1; i < n; i++) { cin>>v; tree[v].push_back(i+1); tree[i+1].push_back(v); //Заполнение графа в том виде, в котором его дают } for (int i = 1; i <= n; i++) { if (tree[i].size() == 1 || (i == 1 && tree[i].size() == 2)) leaves.push_back(i); //Запоминаем все листья } queue<int> q; dist[1] = 0; q.push(1); used[1] = true; while (!q.empty()) { int qv = q.front(); q.pop(); used[qv] = 1; for (auto to : tree[qv]) //BFS заполняющий вектор dist { if (!used[to]) { q.push(to); dist[to] = dist[qv]+1; } } } for (auto l : leaves) { kills(l, l); // Первый этап - запросы от каждого листа } int maxdist = -1; for (int i = 1; i <= n; i++) maxdist = max(maxdist, dist[i]); // Определение максимального "уровня" дерева int wentup = 1; while (wentup) { wentup = 0; for (int l = maxdist; l > 0; l--) { for (int i = 2; i <= n; i++) { if (killed[i] == 1 && dist[i] == l) { goup(i, i); //Этап 2 - поднятие "недошедших" запросов wentup++; } } } } for (int i = 1; i <= n; i++) if (killed[i] >= 2) ans++; //Финальный подсчет "отравленных" развилок cout<<ans; return 0; } |
Решение задачи
Поскольку в задаче речь идет о дереве, циклов в нем нет по определению. Значит, единственным способом для термита ходить «вечно» будет путь между двумя листами, в которых он сможет разворачиваться. Фактически, задача сводится к вопросу «Какое минимальное количество вершин в дереве нужно отравить, чтобы нельзя было добраться из любого листа в другой лист не пройдя через отравленные?».
Определим для этого $3$ типа вершин: лист, развилка и обычная вершина. Листом назовем вершину, у которой нет детей (всего $1$ связь с другой вершиной). Обычные вершины — те, у которых ровно $2$ связи (для нашего термита это пути вниз или вверх). Развилкой назовем вершину, у которой $3$ или больше связей с другими. Будем считать корень тоже развилкой, даже если у него всего $2$ связи, или листом, если одна. Через развилки можно ходить из одного листа в другой, либо «вверх» — в сторону корня.
Первый этап
Очевидно, выгоднее всего «закрывать» развилки. А среди них — те, которые соединяют несколько листов напрямую. Пусть каждый лист отправляет «запрос» вверх по дереву на закрытие ближайшей к нему развилки. Когда «запрос» доходит до развилки, он тут же записывается на её счёт. Таким образом, в дереве выше вершина $4$ будет иметь $2$ запроса — от листов $5$ и $6$, а корень — $1$ запрос от листа $3$.
Теперь, просто считаем количество вершин с количеством запросов $\geqslant2$ и «закрываем» их.
Второй этап
Увы, первый этап не идеален и может «не донести» запросы в нужное место, т.к. некоторые развилки (а именно — соединяющие лист и другую развилку) могут остаться с одним запросом и не быть закрытыми. Если таких много, термит все еще может ходить между листами. Например, в таком дереве:
Вершина $2$ и корень получают по $1$ запросу и остаются открытыми, а у термита остается путь между листами $10$ и $6$.
Для предотвращения таких случаев, пробежимся по дереву «снизу вверх» — от самого нижнего уровня до верхнего и для каждой развилки, у которой ровно $1$ запрос, сместим его вверх аналогично первому этапу — до ближайшей развилки. Будем выполнять этот шаг, пока есть такие вершины (с $1$ запросом).
В итоге, все запросы «соединятся» в нужных развилках, значение в них станет $\geqslant2$ и эти развилки нужно будет тоже закрыть. Для дерева выше, будет закрыт корень.
Осталось посчитать кол-во закрытых.
Описание алгоритма
Дерево будем хранить в массиве векторов tree. Количество запросов для вершины $i$ хранится в killed[i]. Стандартный вектор used для поиска в ширину и dist- вектор расстояний от корня до вершин, которые и будут определяться с помощью BFS.
Функция kills предназначена для того, чтобы донести запрос от листа до развилки. Она рассматривает $3$ случая:
- v == p — текущая вершина совпадает с той, из которой пришли. Это крайний случай, говорящий о том, что мы только начали и находимся в листе. Тогда, идем в единственно возможном направлении — tree[v][0].
- tree[v].size == 2 — вершина обычного типа, просто идем «вверх», выбирая из двух путей тот, что не совпадает с предыдущей вершиной.
- tree[v].size >= 3 — попали в развилку. Увеличиваем ее значение killed[v] и выходим из рекурсии.
Функция goup отличается от kills лишь тем, что при v == p выбирает из всех направлений то, которое ближе к корню, используя dist.
Подготовка
Можно заметить, что для всех деревьев из $5$ или менее вершин ответ будет $1$. Проверим это сразу при вводе n. Далее, осторожно считываем дерево в массив векторов (см. Входные данные). В следующем цикле, определяем листья и запоминаем их в вектор leaves. Нужно учесть то, что корень может быть листом, если у него всего $2$ связи — одна с деревом, а другая — искусственно созданная нами в $0$ вершину. Последний шаг — запустить поиск в ширину из корня, который заполнит вектор dist расстояниями от корня до вершин.
Первый этап
Просто запускаем kills (l, l) из каждого листа l для «отправки» запросов в ближайшие развилки.
Второй этап
Определяем максимальную «глубину» дерева — максимальное расстояние вершины от корня. Далее, для каждого уровня от самого нижнего до корня, при определении вершины со значением killed[i] == 1 запускаем goup (i, i), а в переменной wentup считаем количество таких случаев. Как только их не останется — while выйдет из цикла.
Наконец, осталось просто посчитать количество вершин, у которых значение
killed[i] >= 2.
Задача на e-olymp
Код решения на ideone
Засчитанное решение на e-olymp