Skip to Content

Críticos

HechoFuenteNombreDificultadTagsSolución
CSESCritical CitiesDifícilen el módulo
Recursos
FuenteRecursoNotas
PrincetonDominator slides

¡Hechas por el creador del algoritmo mismo!

WikiDominator

Definición de Wikipedia

BlogDominator Tree
BlogVisualizing Dominators

Enfoque inicial

Estos nodos críticos de los que habla el problema se conocen comúnmente como dominadores. Definamos dom(u)\texttt{dom}(u) como el conjunto de nodos que dominan al nodo uu.

El dominador del nodo de partida es él mismo, y el conjunto de dominadores de cualquier otro nodo uu es la intersección del conjunto de dominadores de todos los ancestros pp del nodo uu.

dom(u)={u if u is the starting pointu(pancestor(u)dom(p))otherwise \texttt{dom}(u)= \begin{cases} u\quad\text{ if } u \text{ is the starting point} \\ {u}\cup \left(\bigcap_{p \in \texttt{ancestor}(u)} \texttt{dom}(p)\right)\quad\text{otherwise} \end{cases}

Implementación

El siguiente código usa la recurrencia de arriba. Sin embargo, es demasiado lento y usa demasiada memoria. ¡Intentaremos optimizar esto a continuación!

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

#include <bitset> #include <iostream> #include <vector> using std::bitset; using std::cout; using std::endl; using std::vector; const int MAX_N = 1e5; int main() { int city_num; int flight_num; std::cin >> city_num >> flight_num; vector<vector<int>> rev_adj(city_num); for (int f = 0; f < flight_num; f++) { int a, b; std::cin >> a >> b; // notice that we need the edges in reverse rev_adj[--b].push_back(--a); } vector<bitset<MAX_N>> dom(city_num); dom[0].set(0); // the dominator of the starting node is itself for (int c = 1; c < city_num; c++) { dom[c].set(); } bool changed; // iteratively update the dom sets of each node do { changed = false; // make a new vector where the updated doms reside vector<bitset<MAX_N>> new_dom(city_num); new_dom[0].set(0); for (int c = 1; c < city_num; c++) { bitset<MAX_N> updated = bitset<MAX_N>().set(); for (int prev : rev_adj[c]) { updated &= dom[prev]; } updated.set(c); if (updated != dom[c]) { changed = true; } new_dom[c] = updated; } dom = new_dom; } while (changed); vector<int> critical; for (int c = 0; c < city_num; c++) { if (dom[city_num - 1][c]) { critical.push_back(c); } } cout << critical.size() << '\n'; for (int i = 0; i < critical.size(); i++) { cout << critical[i] + 1 << " \n"[i == critical.size() - 1]; } }

Optimizar con árboles

En este enfoque, vamos a construir el árbol de dominadores  del grafo.

Antes de discutir esto, definamos algunos términos que usaremos a lo largo de este módulo:

  • Un nodo uu domina estrictamente a otro nodo vv si uu domina a vv y uvu \ne v.
  • Sea el dominador inmediato de uu, o idom(u)\texttt{idom}(u), el único nodo vv tal que domina estrictamente al nodo uu y todo otro dominador del nodo uu domina estrictamente al nodo vv.
  • Sea e(u)e(u) el tiempo de entrada en el nodo uu haciendo un tour de Euler.
  • Sea el semi-dominador de uu, o sdom(u)\texttt{sdom}(u), un vv tal que hay un camino de vv a uu y e(i)e(u)e(i) \ge e(u) para cada nodo intermedio ii a lo largo del camino de uu a vv, excluyendo los extremos. Si hay múltiples nodos que cumplen este requisito, tomamos el nodo vv con el e(v)e(v) más pequeño.
  • Sea el dominador relativo de uu, o rdom(u)\texttt{rdom}(u), el vértice xsdom(u)x \ne \texttt{sdom}(u) en el camino de sdom(u)\texttt{sdom}(u) a uu en el árbol del tour de Euler con el número de nodo sdom más bajo. A diferencia del sdom, los empates en esta función se pueden romper de forma arbitraria.

Un árbol de dominadores es un árbol donde los hijos de cada nodo son aquellos nodos que domina inmediatamente. El nodo de partida es la raíz del árbol.

Dada esta definición, podemos ver que si un nodo domina a otro, entonces el primero es un ancestro del segundo en el árbol de dominadores. Así, la respuesta al problema de CSES es el conjunto de todos los nodos que yacen en el camino de la raíz al nodo nn en el árbol.

El siguiente grafo muestra el sdom de cada nodo. Las aristas de color pleno representan las aristas que forman parte del árbol DFS.

Graph2

Propiedades importantes

Las demostraciones de estas propiedades están más adelante en el módulo. Para todas estas propiedades, sea uu un nodo que no es el nodo de partida.

  1. sdom(u)\texttt{sdom}(u) es un ancestro propio de uu en el árbol DFS.
  2. Si rdom(u)=u\texttt{rdom}(u)=u, entonces idom(u)=sdom(u)\texttt{idom}(u)=\texttt{sdom}(u).
  3. Si no, entonces idom(u)=idom(rdom(u))\texttt{idom}(u)=\texttt{idom}(\texttt{rdom}(u))

Visión general del algoritmo

Antes de entrar en los detalles, aquí hay un esquema breve de cómo exactamente vamos a construir este árbol de dominadores.

  1. Calcular el sdom de cada nodo excepto el de partida.
  2. Calcular el rdom de cada nodo excepto el de partida.
  3. Visitar todos los vértices en el árbol DFS y calcular su dominador inmediato usando la segunda y tercera propiedades listadas arriba. Observar que por cómo definimos el rdom de un nodo, un recorrido en preorden siempre visitará el rdom de un nodo antes que el nodo mismo si los dos no son iguales.

El primer y segundo pasos son terriblemente vagos: ¡aclaremos eso ahora!

Calcular sdom\texttt{sdom}

Podemos calcular sdom(u)\texttt{sdom}(u) como el nodo mínimo en la intersección de los siguientes grupos:

  1. Todos los nodos yy tales que hay una arista de yy a uu y e(y)e(u)e(y) \le e(u).
  2. Todos los valores de sdom(x)\texttt{sdom}(x) donde xx es cualquier nodo tal que hay una arista de uu a xx y e(x)>e(u)e(x) > e(u). Para ser más precisos matemáticamente, podemos definir este grupo como {sdom(x)  (u,x)E and e(x)>e(u)} \{\texttt{sdom}(x)\ |\ (u, x) \in E\text{ and }e(x)>e(u)\}

La demostración  de por qué esto funciona está fuera del alcance de este módulo.

Implementar sdom\texttt{sdom}

Primero hacemos un recorrido DFS en preorden del grafo desde el nodo fuente y rastrearemos todos los tiempos de entrada de los nodos.

Luego, calculamos el sdom de todos los nodos aplicando la fórmula mencionada en la sección anterior. Para hacer esto, iteramos sobre el recorrido en orden inverso y mantenemos los nodos que hemos recorrido en un DSU.

Sin embargo, el DSU que usamos para este algoritmo va a ser un poco distinto. Unimos nodos de forma usual, pero la función find difiere.

Digamos que xx es la raíz de la componente sobre la que llamamos find. Si el nodo sobre el que llamamos find resulta ser x, entonces devolvemos x como de costumbre. Sin embargo, si es algún otro nodo, entonces devolvemos un nodo con el sdom mínimo que yace en el camino de uu a xx.

Para procesar el nodo uu iteramos sobre todos los nodos vv que tienen una arista dirigida hacia él. Si vv viene antes que uu en el recorrido en preorden, entonces vv es un ancestro de uu y no habría sido procesado hasta ahora. En ese caso, find(v) devolvería vv mismo.

Si no, entonces find(v) devolvería un nodo xx que yace en el camino de vv a la raíz en su componente DSU con el sdom más pequeño.

Si esta explicación todavía resulta un poco confusa, hay pseudocódigo en la diapositiva 33 de las diapositivas de Princeton dadas al comienzo de este módulo.

Calcular rdom\texttt{rdom}

El rdom de un nodo es el nodo con el sdom que viene más temprano en el recorrido. Como esto se reduce a hallar el mínimo de un valor a lo largo de un cierto camino en un árbol, podemos implementarlo usando una aumentación de binary jumping.

Implementación

Complejidad temporal: O((N+M)logN)\mathcal{O}((N+M) \cdot \log N)

#include <algorithm> #include <cassert> #include <cmath> #include <cstdint> #include <functional> #include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; // BeginCodeSnip{Tree Class for rdom} template <typename T> class Tree { private: const int root = 0; const vector<int> &parents; const vector<T> &vals; const int log2dist; vector<vector<int>> pow2ends; vector<vector<T>> pow2mins; vector<int> depth; public: Tree(const vector<int> &parents, const vector<T> &vals) : parents(parents), vals(vals), log2dist(std::ceil(std::log2(parents.size() + 1))), pow2ends(parents.size(), vector<int>(log2dist + 1)), pow2mins(parents.size(), vector<T>(log2dist + 1)) { assert(parents[root] == -1); vector<vector<int>> children(parents.size()); for (int n = 0; n < parents.size(); n++) { if (parents[n] != -1) { children[parents[n]].push_back(n); } } depth = vector<int>(parents.size()); vector<int> frontier{root}; while (!frontier.empty()) { int curr = frontier.back(); frontier.pop_back(); for (int n : children[curr]) { depth[n] = depth[curr] + 1; frontier.push_back(n); } } for (int n = 0; n < parents.size(); n++) { pow2mins[n][0] = vals[n]; pow2ends[n][0] = parents[n]; } for (int p = 1; p <= log2dist; p++) { for (int n = 0; n < parents.size(); n++) { int halfway = pow2ends[n][p - 1]; if (halfway == -1) { pow2ends[n][p] = -1; pow2mins[n][p] = pow2mins[n][p - 1]; } else { pow2ends[n][p] = pow2ends[halfway][p - 1]; pow2mins[n][p] = std::min(pow2mins[n][p - 1], pow2mins[halfway][p - 1]); } } } } /** * @return the min value on the path from the ancestor to its descendant, * not including the ancestor. */ T min_val(int ancestor, int desc) { int dist = depth[desc] - depth[ancestor]; T ret = vals[desc]; int at = desc; for (int pow = 0; pow <= log2dist; pow++) { if ((dist & (1 << pow)) != 0) { ret = std::min(ret, pow2mins[at][pow]); at = pow2ends[at][pow]; } } assert(at == ancestor); return ret; } }; // EndCodeSnip int main() { int city_num; int flight_num; std::cin >> city_num >> flight_num; vector<vector<int>> adj(city_num); vector<vector<int>> rev_adj(city_num); for (int f = 0; f < flight_num; f++) { int a, b; std::cin >> a >> b; adj[--a].push_back(--b); rev_adj[b].push_back(a); } // Variables used for the Euler Tour vector<int> visit_order; // visit_time is initialized with INF so nodes that aren't reachable // during the DFS won't mess up our sdom calculation vector<int> visit_time(city_num, INT32_MAX); vector<int> visit_parent(city_num, -1); vector<bool> visited(city_num, false); std::function<void(int, int)> dfs; dfs = [&](int at, int prev) { if (visited[at]) { return; } visited[at] = true; visit_time[at] = visit_order.size(); visit_order.push_back(at); visit_parent[at] = prev; for (int next : adj[at]) { dfs(next, at); } }; dfs(0, -1); // We use a function-based interface instead of a class-based one due to // heavy reliance on previously calculated values vector<int> dsu_parent(city_num, -1); vector<int> dsu_min(city_num, INT32_MAX); std::function<int(int)> find; find = [&](int x) { if (dsu_parent[x] == -1) { return x; } if (dsu_parent[dsu_parent[x]] != -1) { int parent_res = find(dsu_parent[x]); if (visit_time[parent_res] < visit_time[dsu_min[x]]) { dsu_min[x] = parent_res; } dsu_parent[x] = dsu_parent[dsu_parent[x]]; } assert(dsu_min[x] != INT32_MAX); return dsu_min[x]; }; vector<int> sdom(city_num, -1); for (int i = visit_order.size() - 1; i > 0; i--) { int c = visit_order[i]; // Iterate over all nodes with a directed edge to c for (int from : rev_adj[c]) { int find_res = find(from); // Take the node with the earliest visit time in the traversal if (sdom[c] == -1 || visit_time[find_res] < visit_time[sdom[c]]) { sdom[c] = find_res; } } // Link c to its parent in the DFS tree dsu_parent[c] = visit_parent[c]; dsu_min[c] = sdom[c]; } // Initialize the values in the tree vector<std::pair<int, int>> sdom_times(city_num, {INT32_MAX, -1}); for (int c : visit_order) { if (c != 0) { // The tree takes the minimum of these pairs, but what we // really want is the node itself- therefore, we store both sdom_times[c] = {visit_time[sdom[c]], c}; } } Tree tree(visit_parent, sdom_times); vector<int> rdom(city_num, -1); for (int c : visit_order) { if (c != 0) { // Use the definition of the rdom rdom[c] = tree.min_val(sdom[c], c).second; } } // Use the properties previously given to compute the idom vector<int> idom(city_num, -1); for (int c : visit_order) { if (c != 0) { idom[c] = rdom[c] == c ? sdom[c] : idom[rdom[c]]; } } // Trace the idom tree from the finish node to the start node, // finding all dominators along the way vector<int> critical{0}; int at = city_num - 1; while (at != 0) { critical.push_back(at); at = idom[at]; } std::sort(critical.begin(), critical.end()); cout << critical.size() << '\n'; for (int i = 0; i < critical.size() - 1; i++) { cout << critical[i] + 1 << ' '; } cout << critical.back() + 1 << endl; }

Ciclos

HechoFuenteNombreDificultadTagsSolución
CFThe Meeting Place Cannot Be Changed 2Difícilen el módulo

Explicación

El problema pide hallar un nodo de intersección de todos los ciclos, si existe.

Primero, quitemos los nodos que no pertenecen a ningún ciclo. Para hacer esto, podemos quitar recursivamente los nodos sin ninguna arista saliente. Por “recursivamente” queremos decir que después de terminar con un nodo podemos revisar sus padres para ver si los padres tampoco tienen aristas salientes.

Ahora nuestro grafo es básicamente un montón de ciclos. Asumiendo que la intersección de todos los ciclos no es vacía, reducimos los ciclos nodo por nodo. El último nodo en pie es la intersección.

Implementación

Complejidad temporal: O(N)\mathcal{O}(N)

#include <functional> #include <iostream> #include <unordered_set> #include <vector> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(NULL); int n, m; cin >> n >> m; vector<unordered_set<int>> in(n), out(n); for (int i = 0; i < m; i++) { int x, y; cin >> x >> y; out[--x].insert(--y); in[y].insert(x); } // Recursively remove nodes without outgoing edges function<void(int)> dfs = [&](int node) { if (!out[node].empty()) { return; } for (int p : in[node]) { out[p].erase(node); dfs(p); } in[node].clear(); }; // Check for nodes with no outgoing edges for (int i = 0; i < n; i++) { if (out[i].empty()) { dfs(i); } } // Reduce cycles by popping out nodes vector<int> todo; vector<bool> seen(n); for (int i = 0; i < n; i++) { if ((int)out[i].size() == 1) { todo.push_back(i); seen[i] = true; } } while (!todo.empty()) { int node = todo.back(); todo.pop_back(); int next = *out[node].begin(); if (node == next) { continue; } in[next].erase(node); in[next].insert(in[node].begin(), in[node].end()); for (int p : in[node]) { out[p].erase(node); out[p].insert(next); if ((int)out[p].size() == 1 && !seen[p]) { todo.push_back(p); seen[p] = true; } } out[node].clear(); in[node].clear(); } // Get the last node standing. // If there are multiple nodes, then there is no intersection int ans = -1; for (int i = 0; i < n; i++) { if (!out[i].empty()) { if (ans == -1) { ans = i + 1; } else { ans = -1; break; } } } cout << ans << endl; }
HechoFuenteNombreDificultadTagsSolución
CFUseful RoadsDifícil
KattisSki ResortDifícil