Skip to Content

Introducción a grafos funcionales

Introducción

En un grafo funcional (functional graph), cada nodo tiene exactamente una arista saliente. También se lo conoce habitualmente como grafo de sucesores (successor graph).

Recursos
FuenteRecursoNotas
CPH16.3 - Successor Graphs

diagramas

Se puede pensar cada componente conexa de un grafo funcional como un árbol enraizado con todas las aristas dirigidas hacia la raíz, más una arista adicional que sale de la raíz.

Algoritmo de Floyd

El algoritmo de Floyd (Floyd’s Algorithm), también conocido habitualmente como el algoritmo de la tortuga y la liebre (Tortoise and Hare), es capaz de detectar ciclos en un grafo funcional en tiempo O(N)\mathcal{O}(N) y memoria O(1)\mathcal{O}(1) (sin contar el grafo en sí).

Recursos
FuenteRecursoNotas
CPH16.4 - Cycle Detection
CP25.7 - Cycle-Finding
VisuAlgoFloyd's Algorithm Visualization

Ejemplo - Cooperative Game

HechoFuenteNombreDificultadTagsSolución
CFCooperative GameDifícilFunctional Graphen el módulo
Pista 1

Resolver el problema cuando hay exactamente tres amigos.

Pista 2

El truco está en algún lugar del algoritmo de la página 166 de CPH.

Solución

Tutorial oficial 

Usando el algoritmo de Floyd, podemos hallar algún nodo del ciclo después de 2ctc2c\left\lceil \frac{t}{c}\right\rceil consultas. Luego podemos hallar el primer nodo del ciclo después de otras tt consultas.

#include <iostream> #include <string> #include <vector> using std::cout; using std::endl; using std::pair; using std::vector; vector<int> move_result(const vector<int> &to_move) { cout << "next "; for (int i = 0; i < to_move.size() - 1; i++) { cout << to_move[i] << ' '; } cout << to_move.back() << endl; int group_num; std::cin >> group_num; vector<int> groups(10); for (int g = 0; g < group_num; g++) { std::string group; std::cin >> group; for (char i : group) { groups[i - '0'] = g; } } return groups; } int main() { move_result({0, 1}); vector<int> groups = move_result({1}); while (groups[0] != groups[1]) { move_result({0, 1}); groups = move_result({1}); } while (groups[2] != groups[1]) { groups = move_result({0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); } cout << "done" << endl; }
import java.io.*; import java.util.*; public class CoopGame { static BufferedReader read = new BufferedReader(new InputStreamReader(System.in)); public static void main(String[] args) throws IOException { moveResult(new int[] {0, 1}); int[] groups = moveResult(new int[] {1}); while (groups[0] != groups[1]) { moveResult(new int[] {0, 1}); groups = moveResult(new int[] {1}); } while (groups[2] != groups[1]) { groups = moveResult(new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}); } System.out.println("done"); } static int[] moveResult(int[] toMove) throws IOException { System.out.print("next "); for (int i = 0; i < toMove.length - 1; i++) { System.out.print(toMove[i] + " "); } System.out.println(toMove[toMove.length - 1]); System.out.flush(); StringTokenizer groupST = new StringTokenizer(read.readLine()); int groupNum = Integer.parseInt(groupST.nextToken()); int[] groups = new int[10]; for (int g = 0; g < groupNum; g++) { String group = groupST.nextToken(); for (int i = 0; i < group.length(); i++) { groups[group.charAt(i) - '0'] = g; } } return groups; } }
def move_result(to_move): """ Type annotations break the grader for some reason, so here's the signature of the function: move_result(to_move: Iterable[int]) -> list[int] """ print(f"next {' '.join(str(i) for i in to_move)}", flush=True) res = input().split() groups = [0 for _ in range(10)] for g in range(int(res[0])): for i in res[g + 1]: groups[int(i)] = g return groups move_result([0, 1]) groups = move_result([1]) while groups[0] != groups[1]: move_result([0, 1]) groups = move_result([1]) while groups[2] != groups[1]: groups = move_result(range(10)) print("done")

¿Se ve por qué esto es equivalente al código mencionado en CPH?

a = succ(x) b = succ(succ(x)) while a != b: a = succ(a) b = succ(succ(b))
a = succ(x); b = succ(succ(x)); while (a != b) { a = succ(a); b = succ(succ(b)); }
a = succ(x); b = succ(succ(x)); while (a != b) { a = succ(a); b = succ(succ(b)); }

bb corresponde al amigo 11 y aa corresponde al amigo 00.

a = x while a != b: a = succ(a) b = succ(b) first = a
a = x; while (a != b) { a = succ(a); b = succ(b); } first = a;
a = x; while (a != b) { a = succ(a); b = succ(b); } first = a;

aa corresponde a los amigos 292\ldots 9 y bb corresponde a los amigos 00 y 11.

Ejemplo - Badge

HechoFuenteNombreDificultadTagsSolución
CFDiv 2 B - BadgeMuy fácilFunctional Graphen el módulo

Aunque las restricciones permiten una solución O(N2)\mathcal{O}(N^2), ¡es posible hacerlo en solo O(N)\mathcal{O}(N)!

Solución 1

#include <iostream> #include <vector> using std::cout; using std::endl; using std::vector; vector<int> p; vector<int> ans; bool in_cycle = false; void dfs(int n) { if (ans[n] != -2) { // it seems we've come back to something we've visited- a cycle! if (ans[n] == -1) { in_cycle = true; ans[n] = n; } return; // either way, this one's already been processed } ans[n] = -1; // set a marker for our dfs dfs(p[n]); // check if we're back at the initial cycle node if (ans[n] != -1) { // if so, now we're no longer in the cycle in_cycle = false; } else { // set our answer depending on if we're in a cycle or not ans[n] = in_cycle ? n : ans[p[n]]; } } int main() { int n; std::cin >> n; p = vector<int>(n); for (int &i : p) { std::cin >> i; i--; } ans = vector<int>(n, -2); // -2 is our initial no-answer value for (int i = 0; i < n; i++) { // in_cycle is always reset to false at the end of each DFS dfs(i); } for (int i = 0; i < n; i++) { cout << (ans[i] + 1) << " \n"[i == n - 1]; } }
import java.io.*; import java.util.*; public class Badge { public static int[] p; public static int[] ans; public static boolean in_cycle; public static void dfs(int n) { if (ans[n] != -2) { // it seems we've come back to something we've visited- a cycle! if (ans[n] == -1) { in_cycle = true; ans[n] = n; } return; // either way, this one's already been processed } ans[n] = -1; // set a marker for our dfs dfs(p[n]); // check if we're back at the initial cycle node if (ans[n] != -1) { // if so, now we're no longer in the cycle in_cycle = false; } else { // set our answer depending on if we're in a cycle or not ans[n] = in_cycle ? n : ans[p[n]]; } } public static void main(String[] args) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(reader.readLine()); p = new int[n]; StringTokenizer st = new StringTokenizer(reader.readLine()); for (int i = 0; i < n; i++) { p[i] = Integer.parseInt(st.nextToken()) - 1; } reader.close(); ans = new int[n]; Arrays.fill(ans, -2); // -2 is our initial no-answer value for (int i = 0; i < n; i++) { // in_cycle is always reset to false at the end of each DFS dfs(i); } for (int i = 0; i < n - 1; i++) { System.out.print((ans[i] + 1) + " "); } System.out.println(ans[n - 1] + 1); } }
import sys sys.setrecursionlimit(10**5) n = int(input()) p = [int(i) - 1 for i in input().split()] assert n == len(p) def dfs(n: int) -> None: global in_cycle if ans[n] != -2: if ans[n] == -1: in_cycle = True ans[n] = n return ans[n] = -1 dfs(p[n]) if ans[n] != -1: in_cycle = False else: ans[n] = n if in_cycle else ans[p[n]] in_cycle = False ans = [-2 for _ in range(n)] for i in range(n): dfs(i) print(" ".join(str(i + 1) for i in ans))

Este código genera la respuesta de forma independiente para cada componente conexa. Notemos que usa indexación desde 0, no desde 1.

Intentemos simular el algoritmo sobre el siguiente grafo dirigido en el Graph Editor  de CSAcademy.

0 1 1 2 2 3 3 4 4 2 5 6 6 1
  • En el primer paso, hacemos las siguientes llamadas recursivas: dfs(0) -> dfs(1) -> dfs(2) -> dfs(3) -> dfs(4) -> dfs(2), y nos detenemos porque ans[2] = -1. Como llegamos a 2 por segunda vez, sabemos que 2 forma parte de un ciclo y ans[2] = 2. De forma similar, ans[3] = 3 y ans[4] = 4 porque forman parte del ciclo. En cambio, ans[0] = ans[1] = 2 porque ninguno de los dos forma parte del ciclo.

  • Más tarde, hacemos las siguientes llamadas recursivas cuando empezamos en el vértice 5: dfs(5) -> dfs(6) -> dfs(1). Ya sabemos que ans[1] = 2, así que ans[5] = ans[6] = 2 también.

Solución 2

floyd(x) genera las respuestas para todos los vértices de la componente conexa que contiene a x. Notemos que esto requiere listas de adyacencia inversas. En el código, estas se guardan en la variable radj.

#include <bits/stdc++.h> using namespace std; int n; vector<int> adj, ans; vector<vector<int>> radj; void fill_radj(int x) { for (auto &child : radj[x]) { /* * As all nodes in the cycle are processed in function floyd, * the recursive call will only start at the nodes which * combine the cycle with the acyclic part of the connected component * where one of its outgoing arrows points to the node that is not * processed yet. */ if (ans[child] == -1) { ans[child] = ans[x]; fill_radj(child); } } } void floyd(int x) { int y = x; do { // find a cycle using Floyd's algorithm x = adj[x]; y = adj[adj[y]]; } while (y != x); do { // set ans[x] = x for all x along cycle ans[x] = x; x = adj[x]; } while (y != x); do { // set ans'es for all x not along cycle fill_radj(x); x = adj[x]; } while (y != x); } int main() { cin >> n; adj.assign(n, -1); for (auto &e : adj) { cin >> e; e--; } /* * For each node, we have to use a vector to store its children; * at nodes combining the cycle with other parts of the connected component, * there would be more than one outgoing arrow in the reversed adjacency * list */ radj.assign(n, {}); for (int i = 0; i < n; i++) radj[adj[i]].push_back(i); ans.assign(n, -1); // we run Floyd's algorithm for each connected component for (int i = 0; i < n; i++) if (ans[i] == -1) floyd(i); for (auto &a : ans) cout << a + 1 << " "; }
import java.io.*; import java.util.*; public class Badge { static int[] adj; static int[] ans; /* * For each node, we need a list to store its children; at nodes * combining the cycle with other part of the connected component, there * would be more than one outgoing arrow in the reversed adjacency list */ static List<List<Integer>> radj; public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); int n = Integer.parseInt(in.readLine()); adj = new int[n]; ans = new int[n]; radj = new ArrayList<>(); StringTokenizer st = new StringTokenizer(in.readLine()); for (int i = 0; i < n; i++) { adj[i] = Integer.parseInt(st.nextToken()) - 1; ans[i] = -1; radj.add(new ArrayList<>()); } for (int i = 0; i < n; i++) { radj.get(adj[i]).add(i); } for (int i = 0; i < n; i++) { // run Floyd's algorithm on every connected component if (ans[i] == -1) { floyd(i); } } for (int i = 0; i < n; i++) { System.out.print(ans[i] + 1 + " "); } } private static void floyd(int x) { int a = adj[x]; int b = adj[adj[x]]; // find a cycle using Floyd's algorithm while (a != b) { a = adj[a]; b = adj[adj[b]]; } // for each node a in the cycle, the answer ans[a] will be a as well do { ans[a] = a; a = adj[a]; } while (a != b); // for each node a that has outgoing arrow(s) pointing to the acyclic // part we set their answers with fillRadj do { fillRadj(a); a = adj[a]; } while (a != b); } private static void fillRadj(int x) { for (int child : radj.get(x)) { /* * As all nodes in the cycle are processed in method floyd, the * recursive call will only start at the nodes which combine the * cycle with the acyclic part of the connected component, where one * of its outgoing arrows points to the node that is not processed * yet */ if (ans[child] == -1) { ans[child] = ans[x]; fillRadj(child); } } } }
n = int(input()) adj = [i - 1 for i in list(map(int, input().split()))] radj = [[] for _ in range(n)] ans = [-1] * n def fill_radj(x: int) -> None: global ans """ As all nodes in the cycle are processed in function floyd, the recursive call will only start at the nodes which combine the cycle with the acyclic part of the connected component where one of its outgoing arrows points to the node that is not processed yet. """ for child in radj[x]: if ans[child] == -1: ans[child] = ans[x] fill_radj(child) def floyd(x: int) -> None: global ans y = x # find cycle with floyd's while True: x = adj[x] y = adj[adj[y]] if y == x: break # set answer for the cycle while True: ans[x] = x x = adj[x] if y == x: break # set answer for students not in cycle while True: fill_radj(x) x = adj[x] if y == x: break for i in range(n): radj[adj[i]].append(i) for i in range(n): if ans[i] == -1: floyd(i) print(" ".join(str(i + 1) for i in ans))

También es posible usar floyd(x) para generar las respuestas de todos los vértices de la componente conexa que contiene a x sin usar listas de adyacencia.

#include <bits/stdc++.h> using namespace std; vector<int> res; vector<int> arr; /* * fills up all vertices from curr to first_cycle, * whose answer has already been calculated */ void fill_up(int curr, int first_cycle) { while (curr != first_cycle) { res[curr] = res[first_cycle]; curr = arr[curr]; } } int floyd(int curr) { int a = arr[curr]; int b = arr[arr[curr]]; // find cycle using Floyd's algo while (a != b) { /* * while finding cycle, if a node is found out to be already * calculated, go to the fill_up function, where, the answer from * vertex curr to this vertex a is ans[a] */ if (res[a] != -1) { fill_up(curr, a); return 0; } a = arr[a]; b = arr[arr[b]]; } a = curr; while (a != b) { a = arr[a]; b = arr[b]; } int cycle_first = a; a = curr; while (a != cycle_first) { res[a] = cycle_first; a = arr[a]; } a = cycle_first; // for each node in the cycle, the answer would be itself do { res[a] = a; a = arr[a]; } while (a != cycle_first); return 0; } int main() { int n; cin >> n; arr.resize(n + 1); for (int i = 1; i <= n; i++) { cin >> arr[i]; } res.resize(n + 1, -1); for (int i = 1; i <= n; i++) { if (res[i] == -1) { floyd(i); } } for (int i = 1; i <= n; i++) { cout << res[i] << " \n"[i == n]; } }
import java.io.*; import java.util.*; public class Badge { static int[] ans; static int[] arr; public static void main(String[] args) throws IOException { BufferedReader x = new BufferedReader(new InputStreamReader(System.in)); StringTokenizer st = new StringTokenizer(x.readLine()); int n = Integer.parseInt(st.nextToken()); arr = new int[n + 1]; st = new StringTokenizer(x.readLine()); for (int i = 1; i <= n; i++) { arr[i] = Integer.parseInt(st.nextToken()); } ans = new int[n + 1]; Arrays.fill(ans, -1); for (int i = 1; i <= n; i++) { if (ans[i] == -1) { floyd(i); } } StringBuilder output = new StringBuilder(); for (int i = 1; i <= n; i++) { output.append(ans[i]).append(" "); } output.setLength(output.length() - 1); System.out.println(output); } /** * fills up all vertices from curr to firstCycle, * whose answer has already been calculated */ static void fillUp(int curr, int firstCycle) { while (curr != firstCycle) { ans[curr] = ans[firstCycle]; curr = arr[curr]; } } static int floyd(int curr) { int a = arr[curr]; int b = arr[arr[curr]]; // find cycle using Floyd's algo while (a != b) { /* * while finding cycle, if a node is found out to be already * calculated, go to the fillUp function, where, the answer from * vertex curr to this vertex a is ans[a] */ if (ans[a] != -1) { fillUp(curr, a); return 0; } a = arr[a]; b = arr[arr[b]]; } a = curr; while (a != b) { a = arr[a]; b = arr[b]; } int cycleFirst = a; a = curr; while (a != cycleFirst) { ans[a] = cycleFirst; a = arr[a]; } a = cycleFirst; // for each node in the cycle, the answer would be itself do { ans[a] = a; a = arr[a]; } while (a != cycleFirst); return 0; } }
def fill_up(curr: int, first_cycle: int): """ Fills up all vertices from curr to first_cycle, whose answer has already been calculated. """ while curr != first_cycle: res[curr] = res[first_cycle] curr = arr[curr] def floyd(curr: int): a = arr[curr] b = arr[arr[curr]] # Find cycle using Floyd's cycle-finding algorithm while a != b: if res[a] != -1: """ While finding cycle, if a node is found to be already calculated, go to the fill_up function where the answer from vertex curr to this vertex a is res[a]. """ fill_up(curr, a) return a = arr[a] b = arr[arr[b]] a = curr while a != b: a = arr[a] b = arr[b] cycle_first = a a = curr while a != cycle_first: res[a] = cycle_first a = arr[a] # For each node in the cycle, the answer would be itself a = cycle_first while True: res[a] = a a = arr[a] if a == cycle_first: break n = int(input()) arr = list(map(int, input().split())) arr = [0] + arr res = [-1] * (n + 1) for i in range(1, n + 1): if res[i] == -1: floyd(i) print(*res[1:])

Contar ciclos

El siguiente código cuenta la cantidad de ciclos en un grafo de este tipo. La «pila» contiene nodos que pueden alcanzar el nodo actual. Si el nodo actual apunta a un nodo v en la pila (on_stack[v] es verdadero), entonces sabemos que se ha creado un ciclo. Sin embargo, si el nodo actual apunta a un nodo v que ya fue visitado pero no está en la pila, entonces sabemos que la cadena actual de nodos apunta a un ciclo que ya fue considerado.

bool visited[MAXN]; bool on_stack[MAXN]; int number_of_cycles = 0; int next_node[MAXN]; void dfs(int n) { visited[n] = on_stack[n] = true; int u = next_node[n]; if (on_stack[u]) { number_of_cycles++; } else if (!visited[u]) { dfs(u); } on_stack[n] = false; } int main() { // read input, etc for (int i = 1; i <= N; i++) { if (!visited[i]) { dfs(i); } } }
import java.io.*; import java.util.*; public class CountCycles { static boolean[] visited = new boolean[MAXN]; static boolean[] onStack = new boolean[MAXN]; static int numberOfCycles = 0; static int[] nextNode = new int[MAXN]; public static void main(String[] args) throws IOException { // Take in input for (int i = 1; i != N; i++) { if (!visited[i]) { dfs(i); } } } public static void dfs(int n) { visited[n] = onStack[n] = true; int u = nextNode[n]; if (onStack[u]) { numberOfCycles++; } else if (!visited[u]) { dfs(u); } onStack[n] = false; } }
vis = [False] * MAXN on_stack = [False] * MAXN next_node = [0] * MAXN number_of_cycles = 0 def dfs(n: int) -> None: global number_of_cycles vis[n] = on_stack[n] = True if on_stack[next_node[n]]: number_of_cycles += 1 elif not vis[next_node[n]]: dfs(next_node[n]) on_stack[n] = 0 # read input, etc. for i in range(MAXN): if not vis[i]: dfs(i)

KK-ésimo sucesor

Como se describe brevemente en CPH 16.3, el kk-ésimo sucesor de cierto nodo en un grafo funcional se puede hallar en tiempo O(logk)\mathcal{O}(\log k) usando binary jumping (elevación binaria), dado un preprocesamiento de tiempo O(nlogu)\mathcal{O}(n \log u) donde uu es la longitud máxima de cada salto. Ver el módulo de Platino para más detalles.

Problemas

HechoFuenteNombreDificultadTagsSolución
SilverThe Bovine ShuffleFácilFunctional GraphSolución
CSESPlanets CyclesFácilFunctional GraphSolución
SilverVisitsNormalSCCSolución
Old SilverLuxury River CruiseNormalFunctional GraphSolución
IOITropical GardenMuy difícilFunctional GraphSolución

Quiz

Pregunta 1/4

¿Qué es un grafo funcional (grafo de sucesores)?

Se pueden encontrar problemas adicionales que involucran grafos funcionales en los módulos de DP sobre árboles y Binary Jumping.