Árbol virtual
| Fuente | Recurso | Notas |
|---|---|---|
| YouTube | CP Tutorial: Virtual/Auxiliary Tree | |
| CF | Virtual trees method | |
| OIWiki | OIWiki - Virtual Tree | |
| YKW | YouKn0wWho Academy - Virtual Tree / Auxiliary Tree |
A menudo, al ejecutar operaciones o programación dinámica sobre árboles, solo necesitamos llevar la cuenta de unos pocos nodos clave y sus relaciones. Si podemos aislar los pocos nodos relevantes que mantienen intacta la estructura del árbol, conservando a la vez las relaciones entre los nodos clave, podemos reducir de forma significativa nuestra complejidad temporal.
Ejemplo - Leaf Color
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| AC | Leaf Color | Normal | Virtual Tree | en el módulo |
Solución naive
Podemos sumar la cantidad de conjuntos de vértices que satisfacen la condición de que los vértices de grado tienen color para todo para obtener una respuesta.
Definimos como la cantidad de subgrafos inducidos que son árboles enraizados en tales que todas las hojas tienen color . Al hacer las transiciones, inicializamos . Las transiciones son las siguientes:
-
Para cada hijo de , podemos o bien adjuntar uno de los subgrafos inducidos que son árboles que contienen a , o ignorar por completo:
-
Sin embargo, si , entonces tenemos que quitar el caso en que el subgrafo inducido es solo mismo, porque viola la condición:
Para extraer nuestra respuesta, podemos iterar sobre cada raíz posible de nuestro subgrafo inducido.
- Si , entonces podemos sumar directamente a nuestra respuesta.
- En caso contrario, hay que asegurarse de que no tenga grado en ninguno de los subgrafos inducidos que sumamos. Así, restamos todos los casos en que está unido a exactamente uno de sus hijos.
Así, nuestra respuesta será
La complejidad temporal de este enfoque es , porque para cada color iteramos sobre todos los nodos.
Aquí hay un código (TLE) que demuestra este enfoque:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int MAX_N = 2e5 + 1;
constexpr int MOD = 998244353;
int n, a[MAX_N];
ll dp[MAX_N], ans;
vector<int> adj[MAX_N];
int dfs_dp(int v, int p, int c) {
dp[v] = 1;
for (int u : adj[v]) {
if (u == p) continue;
dp[v] *= (dfs_dp(u, v, c) + 1);
dp[v] %= MOD;
}
if (a[v] != c) dp[v]--;
ans += dp[v];
ans %= MOD;
if (a[v] != c) {
for (int u : adj[v]) {
if (u == p) continue;
ans += MOD - dp[u];
ans %= MOD;
}
}
return dp[v];
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) { cin >> a[i]; }
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
u--;
v--;
adj[u].push_back(v);
adj[v].push_back(u);
}
for (int col = 1; col <= n; col++) { dfs_dp(0, 0, col); }
cout << ans;
}Optimización con árbol virtual
Para acelerar nuestro enfoque de programación dinámica, podemos observar que nuestras transiciones de programación dinámica son relativamente simples cuando involucran ciertos tipos de nodos.
El caso más simple es cuando un nodo no tiene nodos de color en su subárbol. Aquí, simplemente concluimos , porque es imposible construir un subgrafo inducido en el subárbol de con hojas de color .
Otro caso de transiciones simples es cuando solo un hijo de tiene algún nodo de color . Entonces, a partir de las transiciones de DP ya establecidas, observamos que es equivalente a .
Sea el conjunto de nodos excluyendo estos dos casos. Más formalmente, si hacemos , entonces
Definimos como el árbol virtual o árbol auxiliar de . También podemos definir , el padre virtual de , como el nodo más bajo en tal que y , y como un hijo virtual de .
Aquí hay un ejemplo de un árbol virtual, con los nodos de coloreados de púrpura, y el resto del árbol virtual en azul:

A partir de aquí, podemos calcular nuestros valores de DP de forma similar a antes, excepto que solo procesamos nodos en , y consideramos hijos virtuales en lugar de hijos directos.
Observar que al calcular nuestra respuesta, también solo necesitamos considerar nodos en . Los nodos que no están en no pueden ser la raíz porque solo uno de sus hijos tiene nodos de color en su subárbol, lo que significa que terminaríamos con un nodo de grado que no es de color .
Así, nuestra complejidad temporal será , ya que iteramos sobre el conjunto para cada color distinto.
En la siguiente sección, demostramos que está acotado por , y también mostramos cómo construir un árbol virtual.
Construcción del árbol virtual
Un método bien conocido para construir el árbol virtual del conjunto es el siguiente:
- Ordenar los puntos de por orden DFS, y agregarlos a
- Calcular el LCA de cualesquiera dos puntos clave adyacentes en , y agregarlo a
- Construir el árbol virtual según la relación ancestro-descendente del árbol original
Para una demostración informal de correctitud, considerar lo siguiente:
Observar que un vértice debe o bien satisfacer o existir al menos dos hijos distintos de con puntos clave en sus subárboles.
El primer caso se resuelve de inmediato en el paso 1. En el segundo caso, como propiedad del orden DFS, deben existir puntos clave en el subárbol de tales que están en subárboles distintos y son adyacentes en el orden DFS cuando solo se consideran los puntos clave. Como es , nuestra construcción necesariamente se cumple. Un corolario de nuestra construcción es que está de hecho acotado por : de hecho, una cota ajustada es .
Implementación
Abajo hay una implementación para la tarea, que también contiene código general para construir árboles virtuales. Como es , y ordenamos por orden DFS, nuestra complejidad temporal final es .
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int MAX_N = 2e5 + 1;
constexpr int LG = 18;
constexpr int MOD = 998244353;
int n, a[MAX_N];
ll dp[MAX_N], ans;
int tin[MAX_N], tout[MAX_N], d[MAX_N], lift[MAX_N][LG], timer;
vector<int> adj[MAX_N], vadj[MAX_N], at_a[MAX_N];
void dfs(int v, int p) {
at_a[a[v]].push_back(v);
tin[v] = timer++;
lift[v][0] = p;
for (int i = 1; i < LG; i++) { lift[v][i] = lift[lift[v][i - 1]][i - 1]; }
for (int u : adj[v]) {
if (u == p) { continue; }
dfs(u, v);
}
tout[v] = timer++;
}
int is_ancestor(int u, int v) { return tin[u] <= tin[v] && tout[v] <= tout[u]; }
int lca(int u, int v) {
if (is_ancestor(u, v)) { return u; }
if (is_ancestor(v, u)) { return v; }
for (int i = LG - 1; i >= 0; i--) {
if (!is_ancestor(lift[u][i], v)) { u = lift[u][i]; }
}
return lift[u][0];
}
bool sort_tin(const int &a, const int &b) { return tin[a] < tin[b]; }
vector<int> vtree(const vector<int> &key) {
if (key.empty()) return {};
vector<int> res = key;
sort(res.begin(), res.end(), sort_tin);
for (int i = 1; i < (int)key.size(); i++) {
res.push_back(lca(res[i - 1], res[i]));
}
sort(res.begin(), res.end(), sort_tin);
res.erase(unique(res.begin(), res.end()), res.end());
for (int v : res) { vadj[v].clear(); }
for (int i = 1; i < (int)res.size(); i++) {
vadj[lca(res[i - 1], res[i])].push_back(res[i]);
}
return res;
}
int main() {
cin >> n;
for (int i = 0; i < n; i++) { cin >> a[i]; }
for (int i = 1; i < n; i++) {
int u, v;
cin >> u >> v;
u--;
v--;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(0, 0);
for (int col = 1; col <= n; col++) {
vector<int> vt = vtree(at_a[col]);
reverse(vt.begin(), vt.end());
for (int v : vt) {
dp[v] = 1;
for (int u : vadj[v]) {
dp[v] *= (dp[u] + 1);
dp[v] %= MOD;
}
if (a[v] != col) { dp[v]--; }
ans += dp[v];
ans %= MOD;
if (a[v] != col) {
for (int u : vadj[v]) {
ans += MOD - dp[u];
ans %= MOD;
}
}
}
}
cout << ans << '\n';
}Problemas
| Hecho | Fuente | Nombre | Dificultad | Tags | Solución |
|---|---|---|---|---|---|
| Gym | ★ Color the Tree | Normal | Virtual Tree | Solución | |
| CF | Kingdom and its Cities | Difícil | Virtual Tree | — | |
| CF | Chaotic V. | Difícil | Virtual Tree | — | |
| Gym | Bridges: The Final Battle | Insano | Virtual Tree, Block Cut Tree | — |