Skip to Content

Burza

Análisis oficial 

Explicación

Análisis inicial

Primero intentemos transformar los términos del juego en algo menos arbitrario.

Como Stjepan no puede volver atrás porque marca todos los nodos que visita, solo puede moverse hacia abajo en el árbol, aumentando su profundidad a medida que avanza. Dado esto, podemos hacer una serie de observaciones:

  1. Daniel puede “cortar” de forma efectiva un subárbol entero marcando un nodo, porque entonces Stjepan no puede pasar por ese nodo. Observemos que este nodo tiene que estar en un nivel más profundo que el nivel actual de Stjepan.
  2. Si no podemos impedir que Stjepan alcance un nodo de profundidad kk (siendo la raíz de profundidad 00), entonces el juego no puede terminar siempre en a lo sumo kk movimientos.
  3. Como ya “perdimos” si Stjepan alcanza la profundidad kk, no nos importa nada por debajo de la profundidad kk. También podemos quitar cualquier nodo que no lleve a una profundidad de al menos kk. Observemos que a partir de aquí no consideraremos ninguno de estos nodos irrelevantes.

De la primera observación, vemos que una estrategia óptima sería marcar a lo sumo un nodo en cada profundidad (excluyendo la raíz).

Como marcar un nodo de profundidad dd deja al menos kd+1k - d + 1 nodos fuera de alcance y podemos marcar nodos desde la profundidad 11 hasta kk, si k(k+1)2n\frac{k \cdot (k + 1)}{2} \geq n, el juego siempre puede terminar en kk movimientos.

Eso reduce el valor máximo de kk que tenemos que considerar a alrededor de 30, que lamentablemente sigue siendo demasiado grande para una solución de tiempo exponencial. ¡Pero es algo! Veamos si podemos recortar más las cotas: 20 sería un buen punto de corte.

Análisis posterior

Para limitar el número máximo de pasos a un punto en el que la DP con máscaras de bits sea viable, tenemos que demostrar que un juego siempre puede terminar en kk movimientos si k2nk^2 \geq n.

Como cada movimiento parte un árbol en un montón de árboles más pequeños, podemos hacer una demostración por inducción .

Definimos nrn_r como el número total de nodos que aún no se han vuelto inválidos después de rr movimientos, y trt_r como el número de nodos válidos a los que Stjepan puede moverse después de rr movimientos (o sea, el número de nodos válidos de profundidad r+1r+1).

Por ejemplo, digamos que estábamos a profundidad 1 en el siguiente árbol:

Si cortamos el nodo 2, n1n_1 sería 3 y t1t_1 sería 1, ya que Stjepan solo puede moverse al nodo 3.

Nuestro caso base para la inducción es el siguiente:

nr(kr)2 n_r \leq (k-r)^2

Esto es cierto para r=0r=0. Ahora, solo tenemos que mostrar que, dado lo anterior,

nr+1(kr1)2=(kr)22r+2k1 n_{r+1} \leq (k-r-1)^2 = (k-r)^2 - 2r + 2k - 1

Si mostramos esto, sabremos que nr(kr)2n_r \leq (k-r)^2 para r<kr\lt k. Sustituyendo r=k1r = k - 1, obtenemos

nk1(k(k1))2=1 n_{k-1} \leq (k - (k - 1))^2 = 1

Esto garantizaría que después de k1k-1 movimientos, queda a lo sumo un nodo válido al que Stjepan pueda moverse. Como Daniel sería el primero en mover en el turno kk, pondría su marca final en este único nodo restante. Stjepan se quedaría entonces con cero nodos adyacentes válidos a los que pisar, impidiendo su kk-ésimo movimiento y asegurando la victoria de Daniel.

Para demostrar el paso inductivo, primero definamos una estrategia más concreta:

En cada nivel, cortar el nodo con el subárbol de mayor tamaño que aún no haya sido cortado. Si hay varios nodos con esta cualidad, cortar cualquiera de ellos.

Si seguimos la estrategia definida antes, en cada turno siempre cortaremos al menos nrtr\frac{n_r}{t_r} nodos y también dejaremos atrás tr1t_r - 1 nodos, ya que ahora están por encima de nuestra profundidad actualEl -1 es porque ya contamos el nodo inmediatamente debajo de nosotros en el término de la fracción. Quitarlos resultaría en sobrecontar.. Esto da la siguiente desigualdad:

nr+1nrnrtr(tr1) n_{r+1} \leq n_r - \frac{n_r}{t_r} - (t_r - 1)

Aunque esta es una relación significativa, los términos trt_r son un poco molestos. Sería genial si pudiéramos tener solo una relación entre nrn_r y nr+1n_{r+1}.

Afortunadamente, mediante manipulación algebraica, es posible obtener la siguiente desigualdad:

nrnrtr(tr1)nr2nr+1 n_r - \frac{n_r}{t_r} - (t_r - 1) \leq n_r - 2\sqrt{n_r} + 1

Encadenando esto con la desigualdad de arriba, obtenemos

nr+1nr2nr+1 n_{r+1} \leq n_r - 2\sqrt{n_r} + 1

Como sabemos que nr(kr)2n_r \leq (k-r)^2 y por extensión nrkr\sqrt{n_r} \leq k-r, podemos sustituir esos términos para demostrar la expresión deseada:

nr+1(kr)22(kr)+1=(kr1)2 n_{r+1} \leq (k-r)^2-2(k-r) + 1=(k-r-1)^2

¡Y listo! Como mostramos que cualquier caso en el que k2nk^2 \geq n resulta en una victoria, solo tenemos que manejar el caso en el que k<20k \lt 20, para el cual basta la DP con máscaras de bits.

DP con máscaras de bits

Observemos que, como un árbol es un grafo planar , cualquier nodo que cortemos cubrirá un segmento continuo de hojas.

Tomemos el siguiente árbol como ejemplo:

Ningún nodo puede cubrir solo, por ejemplo, 3 y 14. Si puede cubrir esos dos entonces también debe cubrir el nodo 1.

Esto nos lleva a nuestro estado de DP. Sea \texttt{max\\_cover}[S] el número máximo de hojas que podemos cubrir desde el inicio dado que tomamos un nodo de cada profundidad especificada en el subconjunto.

Para nuestra transición, intentamos añadir profundidades a todos los subconjuntos previos e iteramos por todos los nodos de esas profundidades.

Por ejemplo, si quitar un nodo podría cubrir de la tercera hoja a la quinta y nuestro subconjunto previo actual puede cubrir hasta la cuarta hoja, entonces juntar esos dos da una configuración que puede cubrir hasta la quinta hoja.

Implementación

Complejidad temporal: O(2nn)\mathcal{O}(2^{\sqrt{n}} \cdot \sqrt{n})

#include <functional> #include <iostream> #include <map> #include <vector> using std::cout; using std::endl; using std::vector; int main() { int node_num; int max_turns; std::cin >> node_num >> max_turns; vector<vector<int>> neighbors(node_num); for (int e = 0; e < node_num - 1; e++) { int n1, n2; std::cin >> n1 >> n2; neighbors[--n1].push_back(--n2); neighbors[n2].push_back(n1); } if (max_turns * max_turns >= node_num) { cout << "DA" << endl; return 0; } std::map<int, int> lost; vector<vector<int>> cutoff_cover(node_num); vector<vector<int>> depth_nodes(max_turns + 1); std::function<void(int, int, int)> process_nodes; process_nodes = [&](int at, int prev, int depth) { depth_nodes[depth].push_back(at); if (depth == max_turns) { lost[at] = lost.size(); cutoff_cover[at] = {at}; return; // no nos importa nada más allá de esta profundidad } for (int n : neighbors[at]) { if (n != prev) { process_nodes(n, at, depth + 1); cutoff_cover[at].insert(cutoff_cover[at].end(), cutoff_cover[n].begin(), cutoff_cover[n].end()); } } }; process_nodes(0, 0, 0); // intervals[n] el intervalo de hojas que podemos cubrir si cortamos el nodo n vector<std::pair<int, int>> intervals; for (const vector<int> &cc : cutoff_cover) { if (cc.empty()) { intervals.push_back({-1, -1}); } else { intervals.push_back({lost[cc.front()] + 1, lost[cc.back()] + 1}); } } /* * max_cover[ss] contiene el # máx. de hojas que podemos cubrir * desde el inicio dado que * solo cortamos nodos de las profundidades especificadas en los subconjuntos ss */ vector<int> max_cover(1 << max_turns); max_cover[0] = 0; for (int ss = 1; ss < (1 << max_turns); ss++) { int &curr = max_cover[ss]; // recorremos cada profundidad previa posible for (int to_add = 0; to_add < max_turns; to_add++) { if ((ss & (1 << to_add)) != 0) { int prev = max_cover[ss & ~(1 << to_add)]; // y todos los nodos de dicha profundidad for (int n : depth_nodes[to_add + 1]) { // ver si podemos cubrir más nodos que antes if (intervals[n].first <= prev + 1) { curr = std::max(curr, intervals[n].second); } } } } if (max_cover[ss] == lost.size()) { cout << "DA" << endl; return 0; } } cout << "NE" << endl; }