Дерево-рюкзак
Состояние — вершина и размер. Выглядит как куб, а на деле квадрат — если писать границы циклов честно.
4 мин
Задача: дано дерево и число . Удалить минимальное число рёбер так, чтобы хотя бы одна из образовавшихся компонент имела размер ровно .
Слово «ровно» убивает жадность — это тот же признак, по которому рюкзак не решается жадно. Значит, размер входит в состояние.
— минимальное число удалённых рёбер в поддереве при условии, что компонента, содержащая , имеет размер ровно .
Пересчёт
Детей добавляем по одному, каждый раз строя новый слой из старого. Для очередного ребёнка есть два варианта.
Ребро оставляем. Тогда к нашей компоненте приклеивается компонента размера :
Ребро удаляем. Тогда всё поддерево отваливается, а мы платим одно ребро:
void dfs(int v, int p) {
sz[v] = 1;
dp[v].assign(2, INF);
dp[v][1] = 0; // пока только сама вершина
for (int u : g[v]) if (u != p) {
dfs(u, v);
vector<int> nd(sz[v] + sz[u] + 1, INF);
for (int a = 1; a <= sz[v]; a++) { // границы — вот это важно
if (dp[v][a] >= INF) continue;
nd[a] = min(nd[a], dp[v][a] + 1); // ребро вырезаем
for (int b = 1; b <= sz[u]; b++)
if (dp[u][b] < INF)
nd[a + b] = min(nd[a + b], dp[v][a] + dp[u][b]);
}
sz[v] += sz[u];
dp[v] = nd;
}
}
Ответ — минимум по всем вершинам величины плюс единица за отрезание от родителя (для корня — плюс ноль). Перебор по всем нужен потому, что нужная компонента может висеть где угодно; её самая высокая вершина и будет тем , которое мы переберём.
Проверено: на 30 000 деревьев до девяти вершин совпало с перебором всех подмножеств удаляемых рёбер.
Асимптотика: не куб, а квадрат
На первый взгляд получается : перебираем ребро, потом , потом .
Первое уточнение очевидно: вершин с их детьми не , а ребро суммарно. Остаётся : ребро, , .
Но если писать границы честно — до текущего размера уже собранной части, до размера поддерева , — получается .
Доказательство красивое. Перебор — это выбор одной вершины из уже собранной части. Перебор — выбор одной вершины из поддерева нового ребёнка. Значит, время работы равно числу рассмотренных пар вершин .
Каждая пара рассматривается ровно один раз: на уровне их наименьшего общего предка. Именно там и впервые оказываются в разных объединяемых частях. Пар всего — отсюда квадрат.
Проверка
Измерено число выполнений внутренней строки:
| бамбук | звезда | случайное дерево | ||
|---|---|---|---|---|
| 100 | 4 950 | 4 950 | 4 950 | 4 950 |
| 300 | 44 850 | 44 850 | 44 850 | 44 850 |
| 1 000 | 499 500 | 499 500 | 499 500 | 499 500 |
| 3 000 | 4 498 500 | 4 498 500 | 4 498 500 | 4 498 500 |
Не «примерно квадрат», а ровно , и одинаково на любой форме дерева. Ровно то, что обещает рассуждение про пары.
Что будет, если написать границы небрежно
Если перебирать for (a = 1; a <= n; a++) и for (b = 1; b <= n; b++) вместо sz[v] и sz[u], ответ останется верным, а работа станет кубической. На это разница между 4,5 миллиона операций и 27 миллиардами.
Это тот редкий случай, когда асимптотика зависит не от алгоритма, а от аккуратности записи цикла.
Тот же приём в других задачах
Схема «состояние — вершина и количество, слияние детей по одному» решает целый класс:
- выбрать ровно вершин поддерева с максимальной суммой весов;
- разбить дерево на компоненты размера не больше , минимизируя число разрезов;
- посчитать число способов покрасить дерево так, чтобы чёрных вершин было ровно .
Во всех случаях оценка та же: при честных границах.