Skip to Content

Hotels

Las dos pistas siguientes se aplican a los tres enfoques:

Pista 1

Para cualesquiera tres nodos aa, bb y cc, los tres caminos aba \rightarrow b, bcb \rightarrow c y cac \rightarrow a tienen un único nodo en común.

Pista 2

Sea rr el nodo común mencionado. La terna (a,b,c)(a, b, c) es buena si y solo si las longitudes de los caminos rar \rightarrow a, rbr \rightarrow b y rcr \rightarrow c son iguales.

Solución en video

Por David Zhou

Nota: la solución en video puede no ser la misma que las demás soluciones. Código en C++, Python y Java.

Solución en video

Video de YouTube (abT6JItO6b8)

Solución 1 - Sumas de prefijos/sufijos

Pista 3

¿Crees que tienes una solución O(N3)\mathcal{O}(N^3) que no implica recorrer todas las ternas de nodos? Si es así, ¿es realmente O(N3)\mathcal{O}(N^3) o es en realidad O(N2)\mathcal{O}(N^2) amortizado?

Solución

Explicación

Como NN es pequeño, podemos calcular el número de ternas buenas con nodo común rr, para todo rr de 1 a NN.

Para algún rr, enraizamos el árbol en rr. Luego podemos hacer DFS para hallar las profundidades de los subárboles de los hijos de rr, y la cantidad de nodos a cada profundidad en cada subárbol. Sea \texttt{at\\_depth}_i[d] la cantidad de nodos a profundidad dd en el subárbol del hijo ii.

Cada nodo de una terna buena debe estar a la misma profundidad y en subárboles distintos, así que la cantidad de ternas buenas que aporta la profundidad dd del subárbol ii es:

\left(\sum_{j < i}\texttt{at\\_depth}_j[d]\right) \cdot \texttt{at\\_depth}_i[d] \cdot \left(\sum_{j > i}\texttt{at\\_depth}_j[d]\right)

Podemos recorrer cada par válido (i,d)(i, d) en tiempo O(N)\mathcal{O}(N) amortizado, ya que cada par corresponde a al menos un nodo del árbol.

¿Pero cómo calcular \sum_{j < i}\texttt{at\\_depth}_j[d] y \sum_{j > i}\texttt{at\\_depth}_j[d] rápidamente?

Nótese que solo tenemos que calcular una de estas sumas, y que se parecen mucho a sumas de prefijos/sufijos. En mi solución, calculo \sum_{j > i}\texttt{at\\_depth}_j[d] usando sumas de sufijos. Llamemos a este valor afteri[d]\texttt{after}_i[d].

Como solo nos importan los afteri[d]\texttt{after}_i[d] de los pares válidos (i,d)(i, d), podemos mantener un arreglo global que guarde los valores máximos de afteri[d]\texttt{after}_i[d] hasta el momento (con todos los elementos en 0 al inicio). Esto nos permite calcular todos los afteri[d]\texttt{after}_i[d] interesantes en tiempo O(N)\mathcal{O}(N).

Como alternativa, podemos ordenar los hijos de rr por sus profundidades y luego calcular afteri[d]\texttt{after}_i[d] de forma directa, lo cual sigue siendo lo bastante rápido para pasar.

Implementación

Complejidad temporal: O(N2)\mathcal{O}(N^2)

#include <bits/stdc++.h> using namespace std; vector<int> graph[5001]; void find_dep(int node, int parent, pair<int, vector<int>> &root, int depth = 0) { if (depth == root.first) { root.first++; root.second.push_back(1); } else root.second[depth]++; for (int i : graph[node]) if (i != parent) find_dep(i, node, root, depth + 1); } int main() { int n; scanf("%d", &n); for (int i = 1; i < n; i++) { int a, b; scanf("%d %d", &a, &b); graph[a].push_back(b); graph[b].push_back(a); } if (n < 4) { printf("0\n"); return 0; } long long ans = 0; for (int i = 1; i <= n; i++) { int m = graph[i].size(); vector<pair<int, vector<int>>> dep(m); vector<vector<int>> aft(m); for (int j = 0; j < m; j++) find_dep(graph[i][j], i, dep[j]); vector<int> tot(max_element(dep.begin(), dep.end())->first, 0); for (int j = m - 1; ~j; j--) { aft[j].resize(dep[j].first); for (int k = 0; k < dep[j].first; k++) { aft[j][k] = tot[k]; tot[k] += dep[j].second[k]; } } for (int j = 0; j < m; j++) { for (int k = 0; k < dep[j].first; k++) { ans += (tot[k] - dep[j].second[k] - aft[j][k]) * dep[j].second[k] * aft[j][k]; } } } printf("%lld\n", ans); return 0; }

Hay que usar std::vectors para garantizar memoria O(N)\mathcal{O}(N).

Solución 2 - DP

Extra - Una solución O(NlogN)\mathcal{O}(N \log N)

Ver el editorial polaco  para más detalles.