Skip to content

Virtual trees article added #330

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
303 changes: 303 additions & 0 deletions content/russian/cs/trees/virtual.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
---
title: Метод виртуальных деревьев
authors:
- '[Akram](https://linkedin.com/in/akram62)'
- '[Nyaan](https://atcoder.jp/users/Nyaan)'
date: 2025-02-27
created: 2025
prerequisites:
- binary-lifting
weight: 5
---

В этой статье рассматривается понятие *виртуального дерева*, доказываются его ключевые свойства, описывается эффективный алгоритм его построения, а затем метод применяется для решения конкретной задачи с использованием динамического программирования (ДП) на деревьях.

## Введение

Во многих задачах, связанных с деревьями, требуется работать с подмножеством вершин, сохраняя при этом структуру дерева, индуцированную их попарными наименьшими общими предками (LCA). Понятие *виртуального дерева* позволяет перейти от исходного дерева $T$ с $N$ вершинами к подструктуре, размер которой линейно зависит от размера выбранного множества $X$. Это существенно ускоряет алгоритмы, в частности, для подсчета ДП.

## Определение виртуального дерева

Пусть $T$ — заданное корневое дерево, а $X$ — некоторое подмножество его вершин. **Виртуальное дерево** $A(X)$ определяется следующим образом:
$$
A(X) = \{ \operatorname{lca}(x,y) \mid x,y \in X \},
$$
где $\operatorname{lca}(x,y)$ означает наименьшего общего предка вершин $x$ и $y$ в дереве $T$. В этом дереве между каждой парой вершин проводится ребро, если одна из них является предком другой в $T$.

Такое определение гарантирует, что в структуру попадают все вершины, «важные» для анализа $X$ (то есть все LCA для пар вершин из $X$), а связность наследуется от исходного дерева.

## Ключевые свойства и их доказательства

### Свойство 1: Оценка по размеру

**Утверждение.**
Для любого множества $X$ вершин дерева $T$ справедливо неравенство:
$$
\vert A(X) \vert \le 2 \vert X \vert - 1.
$$

**Доказательство.**
Выберем порядок обхода в глубину (DFS) и рассмотрим вершины множества $X$, расположенные в порядке обхода:
$$
x_1, x_2, \dots, x_m.
$$
Обозначим
$$
l = \operatorname{lca}(x_1, x_2).
$$
Далее необходимо доказать следующую лемму.

> **Лемма 1.** Для любого целого числа $n \ge 3$, если $\operatorname{lca}(x_1,x_n) \neq l$, то
$$
\operatorname{lca}(x_1,x_n) = \operatorname{lca}(x_2,x_n).
$$
>
> **Доказательство (от противного).**
> Предположим, что для некоторого $n \ge 3$ выполняется $\operatorname{lca}(x_1,x_n) \neq l$ и одновременно $\operatorname{lca}(x_1,x_n) \neq \operatorname{lca}(x_2,x_n)$. Пусть $k = \operatorname{lca}(x_1,x_n)$.
> Если $k$ является предком $l$, то по определению получаем, что и $\operatorname{lca}(x_1,x_n)$, и $\operatorname{lca}(x_2,x_n)$ равны $k$, что противоречит предположению. Следовательно, $k$ должно быть потомком $l$. Но тогда в порядке DFS должен получиться порядок $x_1, x_n, x_2$, что противоречит выбранному порядку обхода.
> Противоречие завершает доказательство леммы.

Возвращаясь к доказательству свойства, заметим, что согласно лемме, каждый LCA, возникающий при последовательном рассмотрении вершин $X$, равен либо:
1. самой вершине $x_1$,
2. $\operatorname{lca}(x_1,x_2)$, либо
3. $\operatorname{lca}(x_2,x_n)$ для некоторого $n$.

Таким образом, можно рекурсивно записать:
$$
A(\{x_1,\dots,x_m\}) = A(\{x_2,\dots,x_m\}) \cup \{ x_1, \operatorname{lca}(x_1,x_2) \}.
$$
Из этого следует, что
$$
\vert A(X) \vert \le \vert A(\{x_2,\dots,x_m\}) \vert + 2.
$$
Применяя индукцию по размеру множества $X$, получаем итоговую оценку:
$$
\vert A(X) \vert \le 2 \vert X \vert - 1.
$$
При этом, если дерево $T$ является совершенным двоичным деревом, а $X$ состоит из его листьев, неравенство достигается в точности.

### Свойство 2: Представление через последовательные LCA

**Утверждение.**
Пусть вершины $X$ упорядочены по порядку обхода DFS: $x_1, x_2, \dots, x_m$. Тогда
$$
A(X) = X \cup \{ \operatorname{lca}(x_i, x_{i+1}) \mid 1 \le i < m \}.
$$

**Доказательство.**
Начнём с рекурсивного представления:
$$
A(\{x_1,\dots,x_m\}) = A(\{x_2,\dots,x_m\}) \cup \{ x_1, \operatorname{lca}(x_1,x_2) \}.
$$
Раскроем его рекурсивно:
$$
\begin{aligned}
A(\{x_1,\dots,x_m\}) &= A(\{x_2,\dots,x_m\}) \cup \{ x_1, \operatorname{lca}(x_1,x_2) \} \\
&= A(\{x_3,\dots,x_m\}) \cup \{ x_1, \operatorname{lca}(x_1,x_2), x_2, \operatorname{lca}(x_2,x_3) \} \\
&\quad \vdots \\
&= \{ x_1, \operatorname{lca}(x_1,x_2), x_2, \dots, x_{m-1}, \operatorname{lca}(x_{m-1},x_m), x_m \}.
\end{aligned}
$$
Таким образом, получаем требуемое представление.

## Построение виртуального дерева

Дано множество вершин $X$. Виртуальное дерево $A(X)$ можно построить за время
$$
\mathcal{O}\left(|X| (\log |X| + \log N)\right),
$$
при этом необходимо выполнить предпосчет исходного дерева $T$ за $\mathcal{O}(N \log N)$ для обеспечения быстрых запросов LCA.

**Основные шаги построения:**

1. **Предподсчет.**
С помощью обхода в глубину (DFS) или методов, таких как двоичные подъемы или HLD, вычисляем времена входа в вершины и LCA для любых двух вершин.

2. **Сортировка вершин.**
Сортируем вершины множества $X$ в порядке их появления при обходе DFS (используя время входа).

3. **Добавление LCA.**
Для последовательных вершин $x_i$ и $x_{i+1}$ вычисляем $\operatorname{lca}(x_i,x_{i+1})$. Объединение $X$ и этих LCA даёт множество вершин $A(X)$ (согласно свойству 2).

4. **Построение дерева.**
Получив множество вершин $A(X)$, отсортированных по порядку DFS, можно пройти по последовательности, используя стек, для восстановления структуры дерева. При обходе поддерживается стек, верхний элемент которого является текущей вершиной, а если новая вершина не является потомком вершины на вершине стека, производится «выталкивание» элементов до нахождения соответствующего предка, после чего производится соединение.

Такой алгоритм гарантирует, что виртуальное дерево содержит $\mathcal{O}(|X|)$ вершин, что позволяет эффективно применять ДП.

## Применение: подсчет поддеревьев в раскрашенном дереве

### Условие задачи

Дано дерево $T$ с $N$ вершинами, пронумерованными от $1$ до $N$. Ребро $i$ соединяет вершины $u[i]$ и $v[i]$. Каждая вершина $i$ раскрашена в цвет $A[i]$.
Необходимо найти (по модулю 998244353) количество (непустых) подмножеств $S$ вершин дерева $T$, удовлетворяющих условию:
- Индуцированный граф $G[S]$ является деревом.
- Все вершины $G[S]$ со степенью 1 имеют один и тот же цвет.

### Основная идея решения

Основная идея заключается в разбиении задачи по цветам. Для каждого цвета $c$ рассматривается множество вершин $X$, раскрашенных в $c$, и строится виртуальное дерево для $X$. Затем на полученном дереве выполняется ДП для подсчета допустимых поддеревьев, у которых все вершины со степенью 1 имеют цвет $c$. Итоговый ответ получается суммированием результатов по всем цветам.

Благодаря построению виртуального дерева, хотя исходное дерево содержит $N$ вершин, каждое виртуальное дерево для конкретного цвета имеет размер $\mathcal{O}(|X|)$, что позволяет выполнять DP за приемлемое время.

Получаем, что суммарная сложность предпосчета и построения виртуальных деревьев не превышает $\mathcal{O}(N \log N + \sum_{c \in C} |X_c| (\log |X_c| + \log N)) = \mathcal{O}(N \log N)$, где $C$ это множество различных цветов вершин и $X_c$ это множество вершин с цветом $c$.

### Реализация решения

Ниже приведён полный код на C++ с комментариями, описывающими основные компоненты алгоритма:

```cpp
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2e5 + 10;
const int LOGN = 20;
const int MOD = 998244353;

vector<int> g[MAXN]; // Список смежности заданного графа
vector<int> ver[MAXN]; // Для каждого цвета ver[c] хранит вершины с цветом c
vector<int> aux_g[MAXN]; // Список смежности для виртуального дерева
int col[MAXN]; // Цвет каждой вершины
int tmr, n; // Глобальный счетчик времени и количество вершин
int up[LOGN][MAXN], dep[MAXN], tin[MAXN]; // Для подсчета LCA и времени входа

// DP массивы для динамического программирования на виртуальном дереве
int dp[MAXN][2], sum[MAXN];

// Функция предподсчета: DFS для вычисления tin, массива up и dep
void dfs_precalc(int v, int p) {
tin[v] = ++tmr;
up[0][v] = p;
dep[v] = dep[p] + 1;
for (int i = 1; i < LOGN; ++i)
up[i][v] = up[i - 1][up[i - 1][v]];
for (auto to : g[v])
if (to != p)
dfs_precalc(to, v);
}

// Функция для вычисления LCA с использованием двоичных подъемов
int getlca(int x, int y) {
if (dep[x] < dep[y]) swap(x, y);
for (int i = LOGN - 1; i >= 0; --i)
if (dep[up[i][x]] >= dep[y])
x = up[i][x];
if (x == y) return x;
for (int i = LOGN - 1; i >= 0; --i)
if (up[i][x] != up[i][y]) {
x = up[i][x];
y = up[i][y];
}
return up[0][x];
}

// DFS по виртуальному дереву для выполнения DP.
// Параметр c — целевой цвет, для которого производится подсчет.

void dfs_calc(int v, int p, int c, int &ans) {
dp[v][0] = dp[v][1] = 0;
sum[v] = 0;
for(auto to : aux_g[v]) {
if(to == p) continue;
dfs_calc(to, v, c, ans);
// Переходы DP: объединение текущего состояния с результатом из поддерева.
int nxt0 = (dp[v][0] + sum[to]) % MOD;
int nxt1 = ((dp[v][0] + dp[v][1]) * 1ll * sum[to] % MOD + dp[v][1]) % MOD;
dp[v][0] = nxt0;
dp[v][1] = nxt1;
}
sum[v] = (dp[v][0] + dp[v][1]) % MOD;
if(col[v] == c) {
// Если вершина имеет целевой цвет, она может участвовать в корректном поддереве.
sum[v] = (sum[v] + 1) % MOD;
ans = (ans + sum[v]) % MOD;
} else {
ans = (ans + dp[v][1]) % MOD;
}
}

// Функция для построения виртуального дерева для цвета c и выполнения DP.
void calc_aux(int c, int &ans) {
auto p = ver[c];
if (p.empty()) return;
// Сортируем вершины по времени входа (tin) — порядку обхода DFS.
sort(p.begin(), p.end(), [&](const int a, const int b) { return tin[a] < tin[b]; });
vector<int> verstk = {1}; // Инициализируем стек с корнем дерева T (вершина 1).
aux_g[1].clear();
auto add = [&](int u, int v) {
aux_g[u].push_back(v);
aux_g[v].push_back(u);
};
// Обрабатываем каждую вершину из множества p, поддерживая стек для построения виртуального дерева.
for (auto u : p) {
if (u == 1) continue;
int lca = getlca(u, verstk.back());
if (lca != verstk.back()) {
while (verstk.size() >= 2 && tin[lca] < tin[verstk[verstk.size() - 2]]) {
add(verstk.back(), verstk[verstk.size() - 2]);
verstk.pop_back();
}
if (verstk.size() >= 2 && tin[lca] != tin[verstk[verstk.size() - 2]]) {
aux_g[lca].clear();
add(verstk.back(), lca);
verstk.back() = lca;
} else {
add(verstk.back(), lca);
verstk.pop_back();
}
}
aux_g[u].clear();
verstk.push_back(u);
}
while (verstk.size() > 1) {
add(verstk.back(), verstk[verstk.size() - 2]);
verstk.pop_back();
}
// Выполняем DP по виртуальному дереву, начиная с корня 1.
return dfs_calc(1, 0, c, ans);
}

int main() {
ios_base::sync_with_stdio(0);
cin.tie(0);
cin >> n;
// Считываем цвета и группируем вершины по цвету.
for (int i = 1; i <= n; ++i) {
cin >> col[i];
ver[col[i]].push_back(i);
}
for (int i = 1; i < n; ++i) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
dfs_precalc(1, 0);
int ans = 0;
// Для каждого возможного цвета обрабатываем соответствующее виртуальное дерево.
for (int i = 1; i <= n; ++i)
calc_aux(i, ans);
cout << ans;
return 0;
}
```

### Объяснение реализации

- **Предподсчет.**
Функция `dfs_precalc` выполняет обход дерева $T$ в глубину, вычисляя время входа (`tin`), глубину и заполняя таблицу двоичных подъемов `up` для быстрых запросов LCA.

- **Вычисление LCA.**
Функция `getlca` реализует двоичные подъемы для быстрого нахождения наименьшего общего предка двух вершин.

- **Построение виртуального дерева.**
Функция `calc_aux` для каждого цвета $c$ берёт множество вершин `ver[c]`, сортирует его по `tin` и с помощью стека строит виртуальное дерево. При этом для каждой пары последовательных вершин вычисляется LCA, что соответствует свойству 2.

- **Динамическое программирование.**
Функция `dfs_calc` выполняет обход виртуального дерева и объединяет результаты поддеревьев согласно переходам DP. Состояния DP рассчитаны таким образом, что учитывается вклад вершины, если она имеет целевой цвет, и производится подсчет только тех поддеревьев, где все листья имеют один цвет.

- **Сбор результата.**
Функция `main` считывает входные данные, выполняет предподсчет, и затем суммирует результаты для каждого цвета, выводя итоговый ответ по модулю 998244353.

## Заключение

В данной статье мы подробно рассмотрели понятие виртуального дерева, доказали его основные свойства, описали эффективный алгоритм построения и продемонстрировали применение этой техники для решения задачи на ДП в деревьях. Преимущество данного подхода состоит в том, что он позволяет свести задачу к работе с подструктурой размером $\mathcal{O}(|X|)$, что существенно ускоряет вычисления даже при большом размере исходного дерева.