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

## Введение↵

Во многих задачах, связанных с деревьями, требуется работать с подмножеством вершин, сохраняя при этом структуру дерева, индуцированную их попарными наименьшими общими предками (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)$ можно построить за время↵
$$↵
O\left(|X| (\log |X| + \log N)\right),↵
$$↵
при этом необходимо выполнить предпосчет исходного дерева $T$ за $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, можно пройти по последовательности, используя стек, для восстановления структуры дерева. При обходе поддерживается стек, верхний элемент которого является текущей вершиной, а если новая вершина не является потомком вершины на вершине стека, производится «выталкивание» элементов до нахождения соответствующего предка, после чего производится соединение.↵

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

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

### Условие [задачи](https://atcoder.jp/contests/abc340/tasks/abc340_g)↵

Дано дерево $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$ вершин, каждое виртуальное дерево для конкретного цвета имеет размер $O(|X|)$, что позволяет выполнять DP за приемлемое время.↵

Получаем, что суммарная сложность предпосчета и построения виртуальных деревьев не превышает $O(N \log N + \sum_{c \in C} |X_c| (\log |X_c| + \log N)) = 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.↵

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

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

Статья основана на [разборе](https://atcoder.jp/contests/abc340/editorial/9256) задачи 
с Atcoder (ABC340G) от [user:Nyaan,2025-02-26].

История

 
 
 
 
Правки
 
 
  Rev. Язык Кто Когда Δ Комментарий
ru2 Русский spike1236 2025-02-26 22:50:11 12 (опубликовано)
en2 Английский spike1236 2025-02-26 22:49:37 8 (published)
en1 Английский spike1236 2025-02-26 22:48:38 13272 Initial revision for English translation (saved to drafts)
ru1 Русский spike1236 2025-02-26 22:44:18 13451 Первая редакция (сохранено в черновиках)