Semana 6 — Árboles de expansión mínima y DFS

Objetivos

  • Demostrar la propiedad del corte y usarla para justificar Kruskal y Prim.
  • Usar los tiempos de finalización de DFS para el orden topológico.
  • Calcular las componentes fuertemente conexas en tiempo lineal y demostrar por qué funciona.

Sesión A — Árboles de expansión mínima: propiedad del corte, Kruskal y Prim

Lectura: Apuntes, semana 6.

Apoyo: Erickson, cap. 7, en especial secs. 7.2 (The Only Minimum Spanning Tree Algorithm), 7.4 (Prim) y 7.5 (Kruskal). CLRS, capítulo Minimum Spanning Trees, 4.ª edición.

Temas:

  • El problema: conectar todo al menor costo. Por qué la solución es un árbol.
  • La propiedad del corte: la arista más barata que cruza un corte está en un árbol de expansión mínima. Demostración por intercambio, con pesos distintos.
  • Empates: qué cambia cuando hay pesos repetidos.
  • Kruskal: ordenar aristas y unir con union-find. Costo O(E log E).
  • Prim: crecer desde un nodo con una cola de prioridad. Costo O(E log V) en un grafo simple.
  • Prim frente a Dijkstra: casi el mismo código, con otro problema y otra clave.
  • Agrupar: quitar las k − 1 aristas más caras de un árbol de expansión mínima da k grupos con la máxima separación.
  • Error común: creer que un árbol de expansión mínima contiene los caminos mínimos entre sus nodos.

Sesión B — DFS: orden topológico y componentes fuertemente conexas

Lectura: Apuntes, semana 6.

Apoyo: Erickson, secs. 6.1 (Preorder and Postorder), 6.2 (Detecting Cycles), 6.3 (Topological Sort), 6.5 (Strong Connectivity) y 6.6 (Strong Components in Linear Time). CLRS, capítulo Elementary Graph Algorithms, 4.ª edición.

Temas:

  • DFS con tiempos de inicio y fin, y tipos de arista. Una arista hacia atrás equivale a un ciclo.
  • Orden topológico: terminar en orden inverso. Demostración con los tiempos de finalización.
  • Componentes fuertemente conexas: el grafo de componentes es un DAG.
  • Kosaraju-Sharir: DFS sobre el grafo invertido para obtener un orden, y DFS sobre el original en ese orden. Por qué funciona.
  • Costo Θ(V + E).
  • Recursión profunda: DFS sobre un millón de nodos puede agotar la pila. Versión con una pila explícita.
  • Error común: procesar el grafo sin invertirlo y obtener componentes mezcladas.

Laboratorio — Diseñar una red · Los prerrequisitos de una malla

Problema A: dadas las sedes de una empresa y el costo de cada tramo de fibra posible, calcular el costo mínimo de conectarlas todas, o decir que no se puede.

Problema B: dada una malla curricular como lista de cursos y prerrequisitos, decir el mínimo de semestres para terminarla, o listar los cursos atrapados en un ciclo. Uno de los casos es la propuesta de malla de la carrera.

  1. Resolver A con Kruskal y union-find.
  2. Resolver B con orden topológico y componentes fuertemente conexas.
  3. Pasar todos los casos, incluido uno con recursión profunda.

Preguntas de repaso

  1. Demuestren la propiedad del corte, suponiendo pesos distintos.
  2. Den un grafo donde el árbol de expansión mínima no contenga el camino mínimo entre dos nodos.
  3. Si se suma una constante a todos los pesos, ¿cambia el árbol de expansión mínima? ¿Y los caminos mínimos?
  4. Demuestren que, en un DAG, si hay una arista de u a v, entonces v termina antes que u en DFS.
  5. Corran Kosaraju-Sharir a mano sobre un grafo pequeño de su elección.

Código de la semana

Compilar y probar dentro del contenedor del curso con make y make test.

Ejemplos (10)Pruebas (1)Laboratorio (6)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

clustering_demo.cppDescargar clustering_demo.cpp
// Agrupar con máxima separación: ocho puntos en el plano. El peso de cada arista es la
// distancia al cuadrado, un entero, que ordena igual que la distancia.
#include <iostream>
#include <string>
#include <vector>

#include "digraph.h"
#include "mst.h"

namespace {

struct Point {
    int x;
    int y;
};

std::vector<Edge> complete_graph(const std::vector<Point>& points) {
    std::vector<Edge> edges;
    int n = static_cast<int>(points.size());
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            int dx = points[i].x - points[j].x;
            int dy = points[i].y - points[j].y;
            edges.push_back({i, j, dx * dx + dy * dy});
        }
    }
    return edges;
}

}  // namespace

int main() {
    std::vector<Point> points = {{0, 0}, {0, 2}, {2, 1}, {10, 0}, {11, 2}, {12, 0}, {5, 9}, {6, 10}};
    int n = static_cast<int>(points.size());
    std::vector<Edge> edges = complete_graph(points);

    std::cout << "Puntos:";
    for (int i = 0; i < n; i++) {
        std::cout << " " << i << "=(" << points[i].x << "," << points[i].y << ")";
    }
    std::cout << "\n\nÁrbol de expansión mínima, de la arista más barata a la más cara:\n ";
    for (const Edge& e : sorted_by_weight(kruskal(n, edges))) {
        std::cout << " " << e.u << "-" << e.v << ":" << e.weight;
    }
    std::cout << "\n\n";

    for (int k = 2; k <= 4; k++) {
        std::vector<int> group = max_spacing_groups(n, edges, k);
        long long partitions = 0;
        int best = best_spacing_brute(n, edges, k, &partitions);
        std::cout << "k = " << k << ": " << groups_text(group) << "\n";
        std::cout << "  separación " << spacing(edges, group) << ", fuerza bruta " << best << " entre " << partitions
                  << " particiones\n";
    }

    std::vector<Edge> sorted = sorted_by_weight(edges);
    sorted.resize(sorted.size() - 2);
    std::cout << "\nIdea que falla, k = 3: quitar las 2 aristas más caras del grafo completo\n";
    std::cout << "  quedan " << count_components(n, sorted) << " grupo\n";
}
deep_recursion.cppDescargar deep_recursion.cpp
// DFS sobre un camino de n nodos, primero con pila explícita y luego con recursión.
//   build/deep_recursion n
#include <cstdlib>
#include <iostream>
#include <vector>

#include "digraph.h"

namespace {

// noinline: si el compilador la mezcla consigo misma, ya no se mide un marco por nodo.
__attribute__((noinline)) void visit(const Digraph& g, std::vector<bool>& marked, int u, int& finished) {
    marked[u] = true;
    for (int v : g[u]) {
        if (!marked[v]) visit(g, marked, v, finished);
    }
    finished++;
}

}  // namespace

int main(int argc, char** argv) {
    int n = argc > 1 ? std::atoi(argv[1]) : 1000000;
    Digraph path(static_cast<std::size_t>(n));
    for (int i = 0; i + 1 < n; i++) path[i].push_back(i + 1);

    DfsTimes t = dfs_iterative(path);
    std::cout << "con pila explícita: terminaron " << t.finish_order.size() << " nodos" << std::endl;

    std::vector<bool> marked(static_cast<std::size_t>(n), false);
    int finished = 0;
    visit(path, marked, 0, finished);
    std::cout << "con recursión: terminaron " << finished << " nodos" << std::endl;
}
dfs_traces.cppDescargar dfs_traces.cpp
// DFS con tiempos: la tabla de la clase, los tipos de arista, el orden topológico y el atajo.
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>

#include "digraph.h"

namespace {

std::string order_text(const std::vector<int>& order) {
    std::string text;
    for (int v : order) text += std::to_string(v) + " ";
    if (!text.empty()) text.pop_back();
    return text;
}

}  // namespace

int main() {
    Digraph g = {{1, 2, 3}, {2}, {0}, {2}, {3, 5}, {4}};
    std::vector<ClassifiedEdge> kinds;
    DfsTimes t = dfs_recursive(g, &kinds);
    std::cout << "Grafo con ciclos:\n";
    std::cout << "  nodo  inicio  fin\n";
    for (int v = 0; v < static_cast<int>(g.size()); v++) {
        std::printf("  %4d  %6d  %3d\n", v, t.start[v], t.finish[v]);
    }
    std::cout << "  arista  tipo\n";
    for (const ClassifiedEdge& e : kinds) {
        std::printf("  %d -> %d  %s\n", e.u, e.v, kind_name(e.kind));
    }

    // Tareas para armar un programa: toda arista va de una tarea a otra que la necesita.
    Digraph dag = {{1, 2}, {3}, {3, 4}, {5}, {5}, {}};
    DfsTimes d = dfs_recursive(dag);
    std::cout << "\nDAG: 0->1 0->2 1->3 2->3 2->4 3->5 4->5\n";
    std::cout << "  nodo  inicio  fin\n";
    for (int v = 0; v < static_cast<int>(dag.size()); v++) {
        std::printf("  %4d  %6d  %3d\n", v, d.start[v], d.finish[v]);
    }
    std::cout << "  terminan en este orden: " << order_text(d.finish_order) << "\n";
    std::vector<int> topo = reverse_finish_order(dag);
    std::cout << "  orden decreciente de fin: " << order_text(topo) << "  "
              << (is_topological(dag, topo) ? "es topológico" : "no es topológico") << "\n";

    Digraph with_cycle = {{1}, {2}, {0, 3}, {}};
    std::vector<int> not_topo = reverse_finish_order(with_cycle);
    std::cout << "\nCon un ciclo: 0->1 1->2 2->0 2->3\n";
    std::cout << "  orden decreciente de fin: " << order_text(not_topo) << "  "
              << (is_topological(with_cycle, not_topo) ? "es topológico" : "no es topológico") << "\n";

    Digraph small = {{1, 2}, {2}, {}};
    std::vector<int> shortcut = stack_shortcut_order(small);
    std::vector<int> shortcut_reversed(shortcut.rbegin(), shortcut.rend());
    DfsTimes real = dfs_iterative(small);
    std::cout << "\nEl atajo de la pila, en 0->1 0->2 1->2:\n";
    std::cout << "  pila con (nodo, siguiente), terminan: " << order_text(real.finish_order)
              << "  al revés: " << order_text(reverse_finish_order(small)) << "  "
              << (is_topological(small, reverse_finish_order(small)) ? "topológico" : "no topológico") << "\n";
    std::cout << "  atajo, orden al sacar: " << order_text(shortcut) << "  "
              << (is_topological(small, shortcut) ? "topológico" : "no topológico") << "\n";
    std::cout << "  atajo, al revés:       " << order_text(shortcut_reversed) << "  "
              << (is_topological(small, shortcut_reversed) ? "topológico" : "no topológico") << "\n";
}
digraph.hDescargar digraph.h
// Grafos dirigidos: DFS con tiempos, tipos de arista, orden topológico y componentes
// fuertemente conexas. Un grafo es una lista de adyacencia con nodos de 0 a n - 1.
#pragma once

#include <algorithm>
#include <cstddef>
#include <functional>
#include <string>
#include <utility>
#include <vector>

using Digraph = std::vector<std::vector<int>>;

inline Digraph make_digraph(int n, const std::vector<std::pair<int, int>>& arcs) {
    Digraph g(static_cast<std::size_t>(n));
    for (auto [u, v] : arcs) g[u].push_back(v);
    return g;
}

inline Digraph reversed(const Digraph& g) {
    Digraph r(g.size());
    for (int u = 0; u < static_cast<int>(g.size()); u++) {
        for (int v : g[u]) r[v].push_back(u);
    }
    return r;
}

// Tiempos de un DFS completo. Cada inicio y cada fin avanzan el reloj en 1, desde 1.
struct DfsTimes {
    std::vector<int> start;
    std::vector<int> finish;
    std::vector<int> finish_order;  // los nodos, en el orden en que terminaron
};

enum class EdgeKind { tree, back, forward, cross };

inline const char* kind_name(EdgeKind kind) {
    switch (kind) {
        case EdgeKind::tree: return "árbol";
        case EdgeKind::back: return "atrás";
        case EdgeKind::forward: return "adelante";
        case EdgeKind::cross: return "cruce";
    }
    return "";
}

struct ClassifiedEdge {
    int u;
    int v;
    EdgeKind kind;
};

// Versión recursiva, la que se demuestra. Inicia en los nodos de 0 a n - 1.
inline DfsTimes dfs_recursive(const Digraph& g, std::vector<ClassifiedEdge>* kinds = nullptr) {
    int n = static_cast<int>(g.size());
    DfsTimes t;
    t.start.assign(static_cast<std::size_t>(n), 0);
    t.finish.assign(static_cast<std::size_t>(n), 0);
    int clock = 0;
    std::function<void(int)> visit = [&](int u) {
        t.start[u] = ++clock;
        for (int v : g[u]) {
            if (t.start[v] == 0) {
                if (kinds != nullptr) kinds->push_back({u, v, EdgeKind::tree});
                visit(v);
            } else if (kinds != nullptr) {
                EdgeKind kind = t.finish[v] == 0        ? EdgeKind::back
                                : t.start[u] < t.start[v] ? EdgeKind::forward
                                                          : EdgeKind::cross;
                kinds->push_back({u, v, kind});
            }
        }
        t.finish[u] = ++clock;
        t.finish_order.push_back(u);
    };
    for (int u = 0; u < n; u++) {
        if (t.start[u] == 0) visit(u);
    }
    return t;
}

// Versión con pila explícita. Cada elemento guarda lo mismo que un marco de llamada: el nodo y
// por cuál arista iba. Un nodo termina recién cuando se le acaban las aristas.
inline DfsTimes dfs_iterative(const Digraph& g) {
    int n = static_cast<int>(g.size());
    DfsTimes t;
    t.start.assign(static_cast<std::size_t>(n), 0);
    t.finish.assign(static_cast<std::size_t>(n), 0);
    int clock = 0;
    std::vector<std::pair<int, std::size_t>> stack;
    for (int root = 0; root < n; root++) {
        if (t.start[root] != 0) continue;
        t.start[root] = ++clock;
        stack.push_back({root, 0});
        while (!stack.empty()) {
            auto [u, next] = stack.back();
            if (next == g[u].size()) {
                t.finish[u] = ++clock;
                t.finish_order.push_back(u);
                stack.pop_back();
                continue;
            }
            stack.back().second++;
            int v = g[u][next];
            if (t.start[v] == 0) {
                t.start[v] = ++clock;
                stack.push_back({v, 0});
            }
        }
    }
    return t;
}

// El atajo: meter todos los vecinos en la pila y anotar cada nodo al sacarlo.
// Visita los mismos nodos, pero no sabe cuándo termina nadie.
inline std::vector<int> stack_shortcut_order(const Digraph& g) {
    int n = static_cast<int>(g.size());
    std::vector<bool> marked(static_cast<std::size_t>(n), false);
    std::vector<int> order;
    std::vector<int> stack;
    for (int root = 0; root < n; root++) {
        stack.push_back(root);
        while (!stack.empty()) {
            int u = stack.back();
            stack.pop_back();
            if (marked[u]) continue;
            marked[u] = true;
            order.push_back(u);
            for (int v : g[u]) {
                if (!marked[v]) stack.push_back(v);
            }
        }
    }
    return order;
}

// Orden topológico: los nodos en orden decreciente de fin. Solo significa algo en un DAG.
inline std::vector<int> reverse_finish_order(const Digraph& g) {
    std::vector<int> order = dfs_iterative(g).finish_order;
    std::reverse(order.begin(), order.end());
    return order;
}

inline bool is_topological(const Digraph& g, const std::vector<int>& order) {
    std::vector<int> position(g.size());
    for (int i = 0; i < static_cast<int>(order.size()); i++) position[order[i]] = i;
    for (int u = 0; u < static_cast<int>(g.size()); u++) {
        for (int v : g[u]) {
            if (position[u] >= position[v]) return false;
        }
    }
    return true;
}

// Recorre desde cada nodo de order todavía sin grupo. comp[v] es el número del recorrido que
// lo alcanzó. Este paso no necesita tiempos: solo qué se alcanza.
inline std::vector<int> label_in_order(const Digraph& g, const std::vector<int>& order) {
    std::vector<int> comp(g.size(), -1);
    int next = 0;
    std::vector<int> stack;
    for (int root : order) {
        if (comp[root] != -1) continue;
        comp[root] = next;
        stack.push_back(root);
        while (!stack.empty()) {
            int u = stack.back();
            stack.pop_back();
            for (int v : g[u]) {
                if (comp[v] == -1) {
                    comp[v] = next;
                    stack.push_back(v);
                }
            }
        }
        next++;
    }
    return comp;
}

// Kosaraju-Sharir: DFS sobre el grafo invertido para obtener el orden, y recorrido sobre el
// original en orden decreciente de fin. Las componentes salen de sumidero a fuente.
inline std::vector<int> strong_components(const Digraph& g) {
    std::vector<int> order = dfs_iterative(reversed(g)).finish_order;
    std::reverse(order.begin(), order.end());
    return label_in_order(g, order);
}

// El error: sacar el orden del mismo grafo, sin invertirlo.
inline std::vector<int> components_without_reversing(const Digraph& g) {
    return label_in_order(g, reverse_finish_order(g));
}

// Fuerza bruta: la matriz de quién alcanza a quién por un camino de al menos una arista.
inline std::vector<std::vector<bool>> reach_matrix(const Digraph& g) {
    int n = static_cast<int>(g.size());
    std::vector<std::vector<bool>> reach(static_cast<std::size_t>(n), std::vector<bool>(static_cast<std::size_t>(n), false));
    for (int s = 0; s < n; s++) {
        std::vector<int> stack = {s};
        while (!stack.empty()) {
            int u = stack.back();
            stack.pop_back();
            for (int v : g[u]) {
                if (!reach[s][v]) {
                    reach[s][v] = true;
                    stack.push_back(v);
                }
            }
        }
    }
    return reach;
}

// Fuerza bruta: componentes por la definición. u y v van juntos si cada uno alcanza al otro.
inline std::vector<int> components_brute(const Digraph& g) {
    int n = static_cast<int>(g.size());
    auto reach = reach_matrix(g);
    std::vector<int> comp(static_cast<std::size_t>(n), -1);
    int next = 0;
    for (int u = 0; u < n; u++) {
        if (comp[u] != -1) continue;
        comp[u] = next;
        for (int v = u + 1; v < n; v++) {
            if (reach[u][v] && reach[v][u]) comp[v] = next;
        }
        next++;
    }
    return comp;
}

// Dos numeraciones de grupos son la misma partición si van juntos los mismos pares.
inline bool same_partition(const std::vector<int>& a, const std::vector<int>& b) {
    for (std::size_t i = 0; i < a.size(); i++) {
        for (std::size_t j = i + 1; j < a.size(); j++) {
            if ((a[i] == a[j]) != (b[i] == b[j])) return false;
        }
    }
    return true;
}

inline std::string groups_text(const std::vector<int>& comp) {
    int groups = comp.empty() ? 0 : *std::max_element(comp.begin(), comp.end()) + 1;
    std::string text;
    for (int c = 0; c < groups; c++) {
        text += '{';
        bool first = true;
        for (int v = 0; v < static_cast<int>(comp.size()); v++) {
            if (comp[v] != c) continue;
            if (!first) text += ' ';
            text += std::to_string(v);
            first = false;
        }
        text += "} ";
    }
    if (!text.empty()) text.pop_back();
    return text;
}
mst_bench.cppDescargar mst_bench.cpp
// Kruskal contra Prim con grafos al azar conexos, compilado con -O2. Toma el menor de 3 intentos.
// Las entradas y descartes de la cola no dependen de la máquina; los tiempos sí.
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>

#include "mst.h"

namespace {

std::vector<Edge> random_connected(int n, int m, unsigned seed) {
    std::mt19937 rng(seed);
    std::vector<Edge> edges;
    edges.reserve(static_cast<std::size_t>(m));
    for (int v = 1; v < n; v++) {
        edges.push_back({static_cast<int>(rng() % static_cast<unsigned>(v)), v, static_cast<int>(rng() % 1000000000U)});
    }
    while (static_cast<int>(edges.size()) < m) {
        int a = static_cast<int>(rng() % static_cast<unsigned>(n));
        int b = static_cast<int>(rng() % static_cast<unsigned>(n));
        if (a != b) edges.push_back({a, b, static_cast<int>(rng() % 1000000000U)});
    }
    return edges;
}

template <typename Run>
double best_ms(Run run) {
    double best = 1e18;
    for (int attempt = 0; attempt < 3; attempt++) {
        auto begin = std::chrono::steady_clock::now();
        run();
        auto end = std::chrono::steady_clock::now();
        best = std::min(best, std::chrono::duration<double, std::milli>(end - begin).count());
    }
    return best;
}

}  // namespace

int main() {
    std::printf("%8s %9s %11s %9s %11s %10s %10s\n", "V", "E", "kruskal ms", "prim ms", "entradas", "descartes", "costo igual");
    for (auto [n, m] : std::vector<std::pair<int, int>>{{50000, 200000}, {200000, 800000}, {800000, 3200000},
                                                        {100000, 3200000}, {3000, 3200000}}) {
        std::vector<Edge> edges = random_connected(n, m, 2026);
        long long kruskal_cost = 0;
        long long prim_cost = 0;
        long long pushes = 0;
        long long stale = 0;
        double k_ms = best_ms([&] { kruskal_cost = total_weight(kruskal(n, edges)); });
        double p_ms = best_ms([&] {
            PrimResult result = prim(n, edges, 0);
            prim_cost = total_weight(result.tree);
            pushes = result.pushes;
            stale = result.stale_pops;
        });
        std::printf("%8d %9d %11.1f %9.1f %11lld %10lld %10s\n", n, m, k_ms, p_ms, pushes, stale,
                    kruskal_cost == prim_cost ? "sí" : "NO");
    }
}
mst_traces.cppDescargar mst_traces.cpp
// El grafo de la clase: Kruskal y Prim paso a paso, y las ideas parecidas que fallan.
#include <cstdio>
#include <iostream>
#include <string>
#include <vector>

#include "mst.h"

namespace {

const int kNodes = 7;

std::vector<Edge> class_graph() {
    return {{0, 1, 7}, {0, 3, 5}, {1, 2, 8}, {1, 3, 9}, {1, 4, 6}, {2, 4, 4},
            {3, 4, 15}, {3, 5, 10}, {4, 5, 12}, {4, 6, 11}, {5, 6, 13}};
}

std::string edges_text(const std::vector<Edge>& edges) {
    std::string text;
    for (const Edge& e : edges) {
        text += std::to_string(e.u) + "-" + std::to_string(e.v) + ":" + std::to_string(e.weight) + " ";
    }
    if (!text.empty()) text.pop_back();
    return text;
}

void kruskal_trace(const std::vector<Edge>& edges) {
    std::cout << "Kruskal:\n";
    std::cout << "  arista  peso  se toma  grupos después\n";
    UnionFind sets(kNodes);
    std::vector<Edge> so_far;
    for (const Edge& e : sorted_by_weight(edges)) {
        bool taken = sets.unite(e.u, e.v);
        if (taken) so_far.push_back(e);
        std::printf("  %d-%d %7d  %s      %d\n", e.u, e.v, e.weight, taken ? "sí" : "no", sets.sets());
    }
    std::cout << "  costo " << total_weight(so_far) << ", " << so_far.size() << " aristas\n";
}

void prim_trace(const std::vector<Edge>& edges) {
    std::cout << "\nPrim desde 0 (arista que sale de la cola, y qué pasa):\n";
    using Entry = std::tuple<int, int, int>;
    auto adj = adjacency(kNodes, edges);
    std::vector<bool> in_tree(kNodes, false);
    std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> heap;
    long long pushes = 0;
    long long stale = 0;
    long long cost = 0;
    auto add_node = [&](int u) {
        in_tree[u] = true;
        for (const Neighbor& nb : adj[u]) {
            if (!in_tree[nb.to]) {
                heap.push({nb.weight, nb.to, u});
                pushes++;
            }
        }
    };
    add_node(0);
    while (!heap.empty()) {
        auto [weight, v, from] = heap.top();
        heap.pop();
        if (in_tree[v]) {
            stale++;
            std::printf("  %d-%d:%-3d descartada: %d ya está en el árbol\n", from, v, weight, v);
            continue;
        }
        add_node(v);
        cost += weight;
        std::printf("  %d-%d:%-3d entra %d, quedan %zu en la cola\n", from, v, weight, v, heap.size());
    }
    std::cout << "  costo " << cost << ", entradas " << pushes << ", descartadas " << stale << ", aristas del grafo "
              << edges.size() << "\n";
}

void show_idea(const std::string& name, const std::vector<Edge>& chosen) {
    std::cout << name << "\n";
    std::cout << "  aristas: " << edges_text(chosen) << "\n";
    std::cout << "  costo " << total_weight(chosen) << ", grupos " << count_components(kNodes, chosen) << ", "
              << (is_spanning_tree(kNodes, chosen) ? "es árbol de expansión" : "no es árbol de expansión") << "\n";
}

}  // namespace

int main() {
    std::vector<Edge> edges = class_graph();
    kruskal_trace(edges);
    prim_trace(edges);

    auto spanning = all_spanning_trees(kNodes, edges);
    auto minimum = all_minimum_trees(kNodes, edges);
    long long best = 0;
    for (int c : minimum[0]) best += edges[c].weight;
    std::cout << "\nFuerza bruta: " << spanning.size() << " árboles de expansión, mínimo " << best << ", "
              << minimum.size() << " de ese costo\n\n";

    show_idea("Las n - 1 aristas más baratas:", cheapest_edges_only(kNodes, edges));
    show_idea("Cada nodo elige su arista más barata:", cheapest_per_node(kNodes, edges));
    show_idea("Árbol de caminos mínimos desde 0:", dijkstra(kNodes, edges, 0).tree);
}
mst_vs_paths.cppDescargar mst_vs_paths.cpp
// Un árbol de expansión mínima no contiene los caminos mínimos, y sumar una constante
// afecta a un problema y no al otro.
#include <iostream>
#include <string>
#include <vector>

#include "mst.h"

namespace {

std::string edges_text(const std::vector<Edge>& edges) {
    std::string text;
    for (const Edge& e : edges) {
        text += std::to_string(e.u) + "-" + std::to_string(e.v) + ":" + std::to_string(e.weight) + " ";
    }
    if (!text.empty()) text.pop_back();
    return text;
}

long long distance(int n, const std::vector<Edge>& edges, int from, int to) {
    return dijkstra(n, edges, from).dist[to];
}

std::vector<Edge> add_constant(std::vector<Edge> edges, int c) {
    for (Edge& e : edges) e.weight += c;
    return edges;
}

}  // namespace

int main() {
    std::vector<Edge> triangle = {{0, 1, 2}, {1, 2, 2}, {0, 2, 3}};
    std::vector<Edge> mst = kruskal(3, triangle);
    std::vector<Edge> paths = dijkstra(3, triangle, 0).tree;
    std::cout << "Triángulo: 0-1:2 1-2:2 0-2:3\n";
    std::cout << "  árbol de expansión mínima: " << edges_text(mst) << " costo " << total_weight(mst) << "\n";
    std::cout << "  de 0 a 2 dentro de ese árbol: " << distance(3, mst, 0, 2) << "\n";
    std::cout << "  de 0 a 2 en el grafo:         " << distance(3, triangle, 0, 2) << "\n";
    std::cout << "  árbol de caminos desde 0:  " << edges_text(paths) << " costo " << total_weight(paths) << "\n";

    std::vector<Edge> square = {{0, 1, 1}, {1, 2, 1}, {0, 2, 3}, {2, 3, 2}, {1, 3, 4}};
    std::cout << "\nSumar una constante c a todas las aristas de 0-1:1 1-2:1 0-2:3 2-3:2 1-3:4\n";
    std::cout << " c  árbol de expansión mínima    costo  distancia 0 a 2  camino\n";
    for (int c : {0, 1, 2, 10}) {
        std::vector<Edge> shifted = add_constant(square, c);
        std::vector<Edge> tree = kruskal(4, shifted);
        long long direct = shifted[2].weight;
        long long by_one = shifted[0].weight + shifted[1].weight;
        long long d = distance(4, shifted, 0, 2);
        std::string route = direct < by_one ? "0-2" : (by_one < direct ? "0-1-2" : "empate");
        std::string kept;
        for (const Edge& e : tree) kept += std::to_string(e.u) + "-" + std::to_string(e.v) + " ";
        std::cout << (c < 10 ? " " : "") << c << "  " << kept << std::string(29 - kept.size(), ' ')
                  << total_weight(tree) << (total_weight(tree) < 10 ? "      " : "     ") << d
                  << (d < 10 ? "                " : "               ") << route << "\n";
    }
}
mst.hDescargar mst.h
// Árboles de expansión mínima: Kruskal, Prim, agrupamiento y las ideas que fallan.
#pragma once

#include <algorithm>
#include <cstddef>
#include <functional>
#include <limits>
#include <numeric>
#include <queue>
#include <tuple>
#include <utility>
#include <vector>

struct Edge {
    int u;
    int v;
    int weight;
};

inline long long total_weight(const std::vector<Edge>& edges) {
    long long total = 0;
    for (const Edge& e : edges) total += e.weight;
    return total;
}

class UnionFind {
public:
    explicit UnionFind(int n) : parent_(static_cast<std::size_t>(n)), size_(static_cast<std::size_t>(n), 1), sets_(n) {
        std::iota(parent_.begin(), parent_.end(), 0);
    }

    int find(int x) {
        while (parent_[x] != x) {
            parent_[x] = parent_[parent_[x]];  // compresión por mitades
            x = parent_[x];
        }
        return x;
    }

    bool unite(int a, int b) {
        a = find(a);
        b = find(b);
        if (a == b) return false;
        if (size_[a] < size_[b]) std::swap(a, b);
        parent_[b] = a;
        size_[a] += size_[b];
        sets_--;
        return true;
    }

    int sets() const { return sets_; }

private:
    std::vector<int> parent_;
    std::vector<int> size_;
    int sets_;
};

// Ordena por peso. Con pesos iguales respeta el orden de la lista.
inline std::vector<Edge> sorted_by_weight(std::vector<Edge> edges) {
    std::stable_sort(edges.begin(), edges.end(),
                     [](const Edge& a, const Edge& b) { return a.weight < b.weight; });
    return edges;
}

// Devuelve un bosque de expansión mínima: un árbol por cada componente del grafo.
// Si el grafo es conexo, tiene n - 1 aristas.
inline std::vector<Edge> kruskal(int n, const std::vector<Edge>& edges) {
    UnionFind sets(n);
    std::vector<Edge> forest;
    for (const Edge& e : sorted_by_weight(edges)) {
        // Invariante: forest está contenido en algún árbol de expansión mínima.
        if (sets.unite(e.u, e.v)) forest.push_back(e);
    }
    return forest;
}

struct Neighbor {
    int to;
    int weight;
};

inline std::vector<std::vector<Neighbor>> adjacency(int n, const std::vector<Edge>& edges) {
    std::vector<std::vector<Neighbor>> adj(static_cast<std::size_t>(n));
    for (const Edge& e : edges) {
        adj[e.u].push_back({e.v, e.weight});
        adj[e.v].push_back({e.u, e.weight});
    }
    return adj;
}

struct PrimResult {
    std::vector<Edge> tree;
    long long pushes = 0;      // entradas que entraron a la cola
    long long stale_pops = 0;  // entradas que salieron y se descartaron
};

// Crece desde source. std::priority_queue no puede bajar la clave de una entrada, así que se
// usa borrado perezoso: las entradas viejas se quedan y se descartan cuando salen.
inline PrimResult prim(int n, const std::vector<Edge>& edges, int source) {
    using Entry = std::tuple<int, int, int>;  // peso, nodo de afuera, nodo del árbol
    auto adj = adjacency(n, edges);
    std::vector<bool> in_tree(static_cast<std::size_t>(n), false);
    std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> heap;
    PrimResult result;

    auto add_node = [&](int u) {
        in_tree[u] = true;
        for (const Neighbor& nb : adj[u]) {
            if (!in_tree[nb.to]) {
                heap.push({nb.weight, nb.to, u});
                result.pushes++;
            }
        }
    };

    add_node(source);
    while (!heap.empty()) {
        auto [weight, v, from] = heap.top();
        heap.pop();
        if (in_tree[v]) {
            result.stale_pops++;  // sus dos extremos ya están adentro: no cruza el corte
            continue;
        }
        result.tree.push_back({from, v, weight});
        add_node(v);
    }
    return result;
}

// Distancias desde source con Dijkstra. También devuelve la arista con que se fijó cada nodo.
struct DijkstraResult {
    std::vector<long long> dist;
    std::vector<Edge> tree;
};

inline DijkstraResult dijkstra(int n, const std::vector<Edge>& edges, int source) {
    using Entry = std::tuple<long long, int, int, int>;  // distancia, nodo de afuera, nodo del árbol, peso
    auto adj = adjacency(n, edges);
    std::vector<bool> in_tree(static_cast<std::size_t>(n), false);
    std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> heap;
    DijkstraResult result;
    result.dist.assign(static_cast<std::size_t>(n), -1);

    auto add_node = [&](int u, long long du) {
        in_tree[u] = true;
        result.dist[u] = du;
        for (const Neighbor& nb : adj[u]) {
            if (!in_tree[nb.to]) heap.push({du + nb.weight, nb.to, u, nb.weight});
        }
    };

    add_node(source, 0);
    while (!heap.empty()) {
        auto [d, v, from, weight] = heap.top();
        heap.pop();
        if (in_tree[v]) continue;
        result.tree.push_back({from, v, weight});
        add_node(v, d);
    }
    return result;
}

// Idea que falla: las n - 1 aristas más baratas, sin mirar si forman ciclos.
inline std::vector<Edge> cheapest_edges_only(int n, const std::vector<Edge>& edges) {
    std::vector<Edge> sorted = sorted_by_weight(edges);
    std::size_t count = std::min(sorted.size(), static_cast<std::size_t>(n - 1));
    sorted.resize(count);
    return sorted;
}

// Idea que falla: cada nodo se queda con su arista más barata.
inline std::vector<Edge> cheapest_per_node(int n, const std::vector<Edge>& edges) {
    std::vector<int> best(static_cast<std::size_t>(n), -1);
    for (int i = 0; i < static_cast<int>(edges.size()); i++) {
        for (int end : {edges[i].u, edges[i].v}) {
            if (best[end] == -1 || edges[i].weight < edges[best[end]].weight) best[end] = i;
        }
    }
    std::vector<int> chosen;
    for (int i : best) {
        if (i != -1) chosen.push_back(i);
    }
    std::sort(chosen.begin(), chosen.end());
    chosen.erase(std::unique(chosen.begin(), chosen.end()), chosen.end());
    std::vector<Edge> result;
    for (int i : chosen) result.push_back(edges[i]);
    return result;
}

inline int count_components(int n, const std::vector<Edge>& edges) {
    UnionFind sets(n);
    for (const Edge& e : edges) sets.unite(e.u, e.v);
    return sets.sets();
}

// Un árbol de expansión de n nodos: n - 1 aristas que conectan todo.
inline bool is_spanning_tree(int n, const std::vector<Edge>& edges) {
    return static_cast<int>(edges.size()) == n - 1 && count_components(n, edges) == 1;
}

// Fuerza bruta: todos los árboles de expansión. Solo para grafos pequeños.
inline std::vector<std::vector<int>> all_spanning_trees(int n, const std::vector<Edge>& edges) {
    std::vector<std::vector<int>> trees;
    std::vector<int> chosen;
    std::function<void(int)> go = [&](int i) {
        if (static_cast<int>(chosen.size()) == n - 1) {
            std::vector<Edge> picked;
            for (int c : chosen) picked.push_back(edges[c]);
            if (count_components(n, picked) == 1) trees.push_back(chosen);
            return;
        }
        if (i == static_cast<int>(edges.size())) return;
        chosen.push_back(i);
        go(i + 1);
        chosen.pop_back();
        go(i + 1);
    };
    go(0);
    return trees;
}

// Fuerza bruta: los árboles de expansión de costo mínimo, como listas de índices de aristas.
inline std::vector<std::vector<int>> all_minimum_trees(int n, const std::vector<Edge>& edges) {
    std::vector<std::vector<int>> best;
    long long best_cost = std::numeric_limits<long long>::max();
    for (const std::vector<int>& tree : all_spanning_trees(n, edges)) {
        long long cost = 0;
        for (int c : tree) cost += edges[c].weight;
        if (cost < best_cost) {
            best_cost = cost;
            best.clear();
        }
        if (cost == best_cost) best.push_back(tree);
    }
    return best;
}

// Grupos de una lista de aristas: group[v] es el número del grupo de v, por orden de aparición.
inline std::vector<int> groups_of(int n, const std::vector<Edge>& edges) {
    UnionFind sets(n);
    for (const Edge& e : edges) sets.unite(e.u, e.v);
    std::vector<int> label(static_cast<std::size_t>(n), -1);
    std::vector<int> group(static_cast<std::size_t>(n));
    int next = 0;
    for (int v = 0; v < n; v++) {
        int root = sets.find(v);
        if (label[root] == -1) label[root] = next++;
        group[v] = label[root];
    }
    return group;
}

// Agrupar en k grupos: quitar las k - 1 aristas más caras de un árbol de expansión mínima.
inline std::vector<int> max_spacing_groups(int n, const std::vector<Edge>& edges, int k) {
    std::vector<Edge> kept = sorted_by_weight(kruskal(n, edges));
    kept.resize(kept.size() - static_cast<std::size_t>(k - 1));
    return groups_of(n, kept);
}

// Separación: la arista más barata entre dos grupos distintos. -1 si ninguna los une.
inline int spacing(const std::vector<Edge>& edges, const std::vector<int>& group) {
    int best = -1;
    for (const Edge& e : edges) {
        if (group[e.u] != group[e.v] && (best == -1 || e.weight < best)) best = e.weight;
    }
    return best;
}

// Fuerza bruta: la mayor separación entre todas las particiones en k grupos no vacíos.
// partitions, si no es nulo, recibe cuántas particiones se probaron.
inline int best_spacing_brute(int n, const std::vector<Edge>& edges, int k, long long* partitions = nullptr) {
    std::vector<int> group(static_cast<std::size_t>(n), 0);
    int best = -1;
    long long seen = 0;
    // Cada partición se genera una vez: el grupo de v es a lo sumo uno más que los ya usados.
    std::function<void(int, int)> go = [&](int v, int used) {
        if (v == n) {
            if (used == k) {
                seen++;
                best = std::max(best, spacing(edges, group));
            }
            return;
        }
        for (int g = 0; g <= used && g < k; g++) {
            group[v] = g;
            go(v + 1, std::max(used, g + 1));
        }
    };
    go(0, 0);
    if (partitions != nullptr) *partitions = seen;
    return best;
}
scc_traces.cppDescargar scc_traces.cpp
// Kosaraju-Sharir sobre el grafo de la clase, y lo que pasa si no se invierte.
#include <cstdio>
#include <iostream>
#include <set>
#include <string>
#include <utility>
#include <vector>

#include "digraph.h"

namespace {

std::string order_text(const std::vector<int>& order) {
    std::string text;
    for (int v : order) text += std::to_string(v) + " ";
    if (!text.empty()) text.pop_back();
    return text;
}

}  // namespace

int main() {
    Digraph g = make_digraph(8, {{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 5}, {5, 3}, {6, 5}, {6, 7}, {7, 6}});
    std::cout << "Grafo: 0->1 1->2 2->0 2->3 3->4 4->5 5->3 6->5 6->7 7->6\n\n";

    std::cout << "Paso 1, DFS sobre el grafo invertido:\n";
    Digraph r = reversed(g);
    DfsTimes t = dfs_iterative(r);
    std::cout << "  nodo  inicio  fin\n";
    for (int v = 0; v < static_cast<int>(g.size()); v++) {
        std::printf("  %4d  %6d  %3d\n", v, t.start[v], t.finish[v]);
    }
    std::cout << "  terminan: " << order_text(t.finish_order) << "\n";
    std::vector<int> order(t.finish_order.rbegin(), t.finish_order.rend());
    std::cout << "  orden decreciente de fin: " << order_text(order) << "\n\n";

    std::vector<int> comp = strong_components(g);
    std::cout << "Paso 2, recorrer el original en ese orden:\n";
    std::cout << "  " << groups_text(comp) << "\n";
    std::cout << "  por la definición: " << groups_text(components_brute(g)) << "\n\n";

    std::cout << "Grafo de componentes (numeradas como salieron):\n ";
    std::set<std::pair<int, int>> between;
    for (int u = 0; u < static_cast<int>(g.size()); u++) {
        for (int v : g[u]) {
            if (comp[u] != comp[v]) between.insert({comp[u], comp[v]});
        }
    }
    for (auto [a, b] : between) std::cout << " " << a << "->" << b;
    std::cout << "\n\n";

    std::cout << "Sin invertir:\n";
    std::cout << "  orden decreciente de fin en el original: " << order_text(reverse_finish_order(g)) << "\n";
    std::vector<int> wrong = components_without_reversing(g);
    std::cout << "  " << groups_text(wrong) << "\n";
    std::cout << "  " << (same_partition(wrong, comp) ? "coincide con las componentes" : "no son las componentes") << "\n";
}
ties_demo.cppDescargar ties_demo.cpp
// Pesos repetidos: varios árboles mínimos, y por qué el enunciado necesita el conjunto F.
#include <algorithm>
#include <iostream>
#include <string>
#include <vector>

#include "mst.h"

namespace {

std::string indices_text(const std::vector<Edge>& edges, const std::vector<int>& indices) {
    std::string text;
    for (int i : indices) {
        text += std::to_string(edges[i].u) + "-" + std::to_string(edges[i].v) + " ";
    }
    return text;
}

}  // namespace

int main() {
    std::vector<Edge> triangle = {{0, 1, 1}, {0, 2, 1}, {1, 2, 1}};
    std::cout << "Triángulo con tres pesos 1:\n";
    for (const std::vector<int>& tree : all_minimum_trees(3, triangle)) {
        std::cout << "  árbol mínimo: " << indices_text(triangle, tree) << "\n";
    }
    std::cout << "  cortes y una arista más barata de cada uno:\n";
    struct CutChoice {
        int side;
        const char* text;
        int edge;
    };
    for (const CutChoice& c : {CutChoice{0, "{0} | {1 2}", 0}, CutChoice{2, "{2} | {0 1}", 1}, CutChoice{1, "{1} | {0 2}", 2}}) {
        int lowest = 1000;
        for (const Edge& e : triangle) {
            if ((e.u == c.side) != (e.v == c.side)) lowest = std::min(lowest, e.weight);
        }
        const Edge& e = triangle[c.edge];
        bool crosses = (e.u == c.side) != (e.v == c.side);
        std::cout << "    " << c.text << ": " << e.u << "-" << e.v << " con peso " << e.weight
                  << (crosses && e.weight == lowest ? ", la más barata" : ", NO") << "\n";
    }
    std::cout << "  juntas forman " << count_components(3, triangle) << " grupo y "
              << (is_spanning_tree(3, triangle) ? "un árbol" : "un ciclo") << "\n";

    std::vector<Edge> ties = {{0, 1, 2}, {1, 2, 2}, {2, 3, 2}, {3, 0, 2}, {0, 2, 3}, {3, 4, 1}};
    std::cout << "\nCuadrado con pesos 2, diagonal 0-2:3 y 3-4:1:\n";
    for (const std::vector<int>& tree : all_minimum_trees(5, ties)) {
        long long cost = 0;
        for (int i : tree) cost += ties[i].weight;
        std::cout << "  árbol mínimo: " << indices_text(ties, tree) << " costo " << cost << "\n";
    }
    std::vector<Edge> reversed_list(ties.rbegin(), ties.rend());
    std::cout << "  Kruskal, con la lista como viene:  costo " << total_weight(kruskal(5, ties)) << "\n";
    std::cout << "  Kruskal, con la lista invertida:   costo " << total_weight(kruskal(5, reversed_list)) << "\n";
    std::cout << "  Prim desde 0:                      costo " << total_weight(prim(5, ties, 0).tree) << "\n";
    std::cout << "  Prim desde 4:                      costo " << total_weight(prim(5, ties, 4).tree) << "\n";
}

Pruebas

Comprueban lo que afirman los apuntes; se ejecutan con make test.

test_examples.cppDescargar test_examples.cpp
// Comprueba lo que afirman los apuntes. Se compila con sanitizers.
#include <algorithm>
#include <cassert>
#include <functional>
#include <iostream>
#include <map>
#include <random>
#include <set>
#include <vector>

#include "digraph.h"
#include "mst.h"

namespace {

std::vector<Edge> class_graph() {
    return {{0, 1, 7}, {0, 3, 5}, {1, 2, 8}, {1, 3, 9}, {1, 4, 6}, {2, 4, 4},
            {3, 4, 15}, {3, 5, 10}, {4, 5, 12}, {4, 6, 11}, {5, 6, 13}};
}

int random_int(std::mt19937& rng, int low, int high) {
    return low + static_cast<int>(rng() % static_cast<unsigned>(high - low + 1));
}

// Grafo conexo al azar: un árbol más algunas aristas. max_weight chico produce empates.
std::vector<Edge> random_connected(std::mt19937& rng, int n, int extra, int max_weight) {
    std::vector<Edge> edges;
    for (int v = 1; v < n; v++) edges.push_back({random_int(rng, 0, v - 1), v, random_int(rng, 1, max_weight)});
    for (int i = 0; i < extra; i++) {
        int a = random_int(rng, 0, n - 1);
        int b = random_int(rng, 0, n - 1);
        if (a != b) edges.push_back({a, b, random_int(rng, 1, max_weight)});
    }
    return edges;
}

long long best_cost_brute(int n, const std::vector<Edge>& edges) {
    auto trees = all_minimum_trees(n, edges);
    long long cost = 0;
    for (int c : trees.at(0)) cost += edges[c].weight;
    return cost;
}

// El grafo de la clase: Kruskal, Prim y la fuerza bruta dan 43, y las ideas falsas no.
void test_class_graph() {
    std::vector<Edge> edges = class_graph();
    assert(total_weight(kruskal(7, edges)) == 43);
    for (int source = 0; source < 7; source++) {
        PrimResult p = prim(7, edges, source);
        assert(is_spanning_tree(7, p.tree));
        assert(total_weight(p.tree) == 43);
        assert(p.pushes <= static_cast<long long>(edges.size()));  // borrado perezoso: a lo sumo E entradas
    }
    assert(best_cost_brute(7, edges) == 43);
    assert(all_minimum_trees(7, edges).size() == 1);  // pesos distintos: uno solo

    assert(!is_spanning_tree(7, cheapest_edges_only(7, edges)));
    assert(count_components(7, cheapest_per_node(7, edges)) > 1);
    std::vector<Edge> paths = dijkstra(7, edges, 0).tree;
    assert(is_spanning_tree(7, paths));
    assert(total_weight(paths) > 43);
}

// Propiedad del corte con pesos distintos: la más barata de cualquier corte está en el único árbol mínimo.
void test_cut_property_distinct() {
    std::mt19937 rng(61);
    for (int round = 0; round < 200; round++) {
        int n = random_int(rng, 2, 6);
        std::vector<Edge> edges = random_connected(rng, n, random_int(rng, 0, 6), 1000);
        // Pesos distintos.
        for (int i = 0; i < static_cast<int>(edges.size()); i++) edges[i].weight = edges[i].weight * 100 + i;
        auto minimum = all_minimum_trees(n, edges);
        assert(minimum.size() == 1);
        for (int mask = 1; mask + 1 < (1 << n); mask++) {
            int cheapest = -1;
            for (int i = 0; i < static_cast<int>(edges.size()); i++) {
                bool crosses = ((mask >> edges[i].u) & 1) != ((mask >> edges[i].v) & 1);
                if (crosses && (cheapest == -1 || edges[i].weight < edges[cheapest].weight)) cheapest = i;
            }
            assert(cheapest != -1);
            assert(std::count(minimum[0].begin(), minimum[0].end(), cheapest) == 1);
        }
    }
}

// Versión con empates: si F está en un árbol mínimo, ningún lado de F cruza el corte y e es una de
// las más baratas que lo cruzan, entonces F más e está en algún árbol mínimo.
void test_cut_property_ties() {
    std::mt19937 rng(62);
    long long checked = 0;
    for (int round = 0; round < 120; round++) {
        int n = random_int(rng, 2, 6);
        std::vector<Edge> edges = random_connected(rng, n, random_int(rng, 0, 6), 3);
        auto minimum = all_minimum_trees(n, edges);
        std::vector<std::set<int>> tree_sets;
        for (const auto& tree : minimum) tree_sets.emplace_back(tree.begin(), tree.end());

        // F: un subconjunto al azar de un árbol mínimo al azar.
        for (int attempt = 0; attempt < 8; attempt++) {
            const auto& base = minimum[static_cast<std::size_t>(random_int(rng, 0, static_cast<int>(minimum.size()) - 1))];
            std::set<int> forest;
            for (int c : base) {
                if (rng() % 2 == 0) forest.insert(c);
            }
            for (int mask = 1; mask + 1 < (1 << n); mask++) {
                auto crosses = [&](int i) { return ((mask >> edges[i].u) & 1) != ((mask >> edges[i].v) & 1); };
                bool respected = std::none_of(forest.begin(), forest.end(), crosses);
                if (!respected) continue;
                int low = -1;
                for (int i = 0; i < static_cast<int>(edges.size()); i++) {
                    if (crosses(i) && (low == -1 || edges[i].weight < low)) low = edges[i].weight;
                }
                for (int i = 0; i < static_cast<int>(edges.size()); i++) {
                    if (!crosses(i) || edges[i].weight != low) continue;
                    bool found = false;
                    for (const std::set<int>& tree : tree_sets) {
                        if (tree.count(i) == 0) continue;
                        found = std::includes(tree.begin(), tree.end(), forest.begin(), forest.end());
                        if (found) break;
                    }
                    assert(found);
                    checked++;
                }
            }
        }
    }
    assert(checked > 0);
}

// El triángulo con tres pesos iguales: tres árboles mínimos, y ninguna arista está en todos.
void test_triangle_ties() {
    std::vector<Edge> triangle = {{0, 1, 1}, {0, 2, 1}, {1, 2, 1}};
    auto minimum = all_minimum_trees(3, triangle);
    assert(minimum.size() == 3);
    for (int edge = 0; edge < 3; edge++) {
        int contained = 0;
        for (const auto& tree : minimum) contained += static_cast<int>(std::count(tree.begin(), tree.end(), edge));
        assert(contained == 2);
    }
    assert(!is_spanning_tree(3, triangle));  // las tres juntas no son un árbol
}

// Kruskal y Prim contra la fuerza bruta, con muchos empates y con pesos negativos.
void test_mst_algorithms_random() {
    std::mt19937 rng(63);
    for (int round = 0; round < 600; round++) {
        int n = random_int(rng, 1, 7);
        std::vector<Edge> edges = random_connected(rng, n, random_int(rng, 0, 7), random_int(rng, 1, 5));
        if (round % 3 == 0) {
            for (Edge& e : edges) e.weight -= 3;
        }
        long long best = n == 1 ? 0 : best_cost_brute(n, edges);
        std::vector<Edge> k = kruskal(n, edges);
        assert(is_spanning_tree(n, k) || n == 1);
        assert(total_weight(k) == best);
        std::vector<Edge> shuffled = edges;
        std::shuffle(shuffled.begin(), shuffled.end(), rng);
        assert(total_weight(kruskal(n, shuffled)) == best);
        for (int source = 0; source < n; source++) {
            assert(total_weight(prim(n, edges, source).tree) == best);
        }
    }
}

// Sobre un grafo desconexo, Kruskal da un bosque, con un árbol por componente.
void test_forest() {
    std::vector<Edge> edges = {{0, 1, 3}, {1, 2, 1}, {0, 2, 2}, {3, 4, 5}};
    std::vector<Edge> forest = kruskal(6, edges);
    assert(forest.size() == 3);
    assert(total_weight(forest) == 8);
    assert(count_components(6, forest) == 3);
}

// Triángulo: el árbol mínimo no contiene el camino mínimo, y el árbol de caminos no es mínimo.
void test_mst_is_not_paths() {
    std::vector<Edge> triangle = {{0, 1, 2}, {1, 2, 2}, {0, 2, 3}};
    std::vector<Edge> mst = kruskal(3, triangle);
    assert(total_weight(mst) == 4);
    assert(dijkstra(3, mst, 0).dist[2] == 4);
    assert(dijkstra(3, triangle, 0).dist[2] == 3);
    assert(total_weight(dijkstra(3, triangle, 0).tree) == 5);
}

// Sumar una constante: el árbol mínimo es el mismo y el camino mínimo cambia.
void test_add_constant() {
    std::vector<Edge> square = {{0, 1, 1}, {1, 2, 1}, {0, 2, 3}, {2, 3, 2}, {1, 3, 4}};
    assert(dijkstra(4, square, 0).dist[2] == 2);
    std::vector<Edge> shifted = square;
    for (Edge& e : shifted) e.weight += 2;
    assert(dijkstra(4, shifted, 0).dist[2] == 5);  // ahora la directa, con costo 3 + 2
    assert(total_weight(kruskal(4, shifted)) == total_weight(kruskal(4, square)) + 3 * 2);

    std::mt19937 rng(64);
    for (int round = 0; round < 200; round++) {
        int n = random_int(rng, 2, 6);
        std::vector<Edge> edges = random_connected(rng, n, 5, 1000);
        for (int i = 0; i < static_cast<int>(edges.size()); i++) edges[i].weight = edges[i].weight * 100 + i;
        auto before = all_minimum_trees(n, edges);
        std::vector<Edge> moved = edges;
        int c = random_int(rng, 1, 500);
        for (Edge& e : moved) e.weight += c;
        assert(all_minimum_trees(n, moved) == before);
    }
}

// Agrupar: quitar las k - 1 más caras alcanza la mayor separación entre todas las particiones.
void test_clustering() {
    std::mt19937 rng(65);
    for (int round = 0; round < 300; round++) {
        int n = random_int(rng, 2, 7);
        std::vector<Edge> edges = random_connected(rng, n, random_int(rng, 0, 10), random_int(rng, 2, 9));
        for (int k = 2; k <= n; k++) {
            std::vector<int> group = max_spacing_groups(n, edges, k);
            assert(*std::max_element(group.begin(), group.end()) == k - 1);  // exactamente k grupos
            assert(spacing(edges, group) == best_spacing_brute(n, edges, k));
        }
    }
}

// Tipos de arista: el ejemplo de la clase, y las condiciones con tiempos.
void test_dfs_times() {
    Digraph g = {{1, 2, 3}, {2}, {0}, {2}, {3, 5}, {4}};
    std::vector<ClassifiedEdge> kinds;
    DfsTimes t = dfs_recursive(g, &kinds);
    assert((t.start == std::vector<int>{1, 2, 3, 6, 9, 10}));
    assert((t.finish == std::vector<int>{8, 5, 4, 7, 12, 11}));
    std::map<std::pair<int, int>, EdgeKind> found;
    for (const ClassifiedEdge& e : kinds) found[{e.u, e.v}] = e.kind;
    assert((found[{0, 1}] == EdgeKind::tree));
    assert((found[{0, 2}] == EdgeKind::forward));
    assert((found[{2, 0}] == EdgeKind::back));
    assert((found[{3, 2}] == EdgeKind::cross));
    assert((found[{4, 3}] == EdgeKind::cross));
    assert((found[{5, 4}] == EdgeKind::back));
}

Digraph random_digraph(std::mt19937& rng, int n, int m) {
    Digraph g(static_cast<std::size_t>(n));
    for (int i = 0; i < m; i++) {
        g[static_cast<std::size_t>(random_int(rng, 0, n - 1))].push_back(random_int(rng, 0, n - 1));
    }
    return g;
}

Digraph random_dag(std::mt19937& rng, int n, int m) {
    std::vector<int> label(static_cast<std::size_t>(n));
    for (int i = 0; i < n; i++) label[i] = i;
    std::shuffle(label.begin(), label.end(), rng);
    Digraph g(static_cast<std::size_t>(n));
    if (n < 2) return g;
    for (int i = 0; i < m; i++) {
        int a = random_int(rng, 0, n - 2);
        int b = random_int(rng, a + 1, n - 1);
        g[label[a]].push_back(label[b]);
    }
    return g;
}

// En grafos al azar: los intervalos se anidan o no se tocan, la pila explícita da los mismos
// tiempos, y hay arista hacia atrás si y solo si hay un ciclo.
void test_dfs_random() {
    std::mt19937 rng(66);
    for (int round = 0; round < 1500; round++) {
        int n = random_int(rng, 1, 9);
        Digraph g = random_digraph(rng, n, random_int(rng, 0, 2 * n));
        std::vector<ClassifiedEdge> kinds;
        DfsTimes t = dfs_recursive(g, &kinds);
        DfsTimes it = dfs_iterative(g);
        assert(t.start == it.start);
        assert(t.finish == it.finish);
        assert(t.finish_order == it.finish_order);

        for (int u = 0; u < n; u++) {
            for (int v = 0; v < n; v++) {
                if (u == v) continue;
                bool disjoint = t.finish[u] < t.start[v] || t.finish[v] < t.start[u];
                bool u_in_v = t.start[v] < t.start[u] && t.finish[u] < t.finish[v];
                bool v_in_u = t.start[u] < t.start[v] && t.finish[v] < t.finish[u];
                assert(disjoint || u_in_v || v_in_u);
            }
        }

        bool has_back = false;
        for (const ClassifiedEdge& e : kinds) {
            has_back = has_back || e.kind == EdgeKind::back;
            switch (e.kind) {
                case EdgeKind::back:
                    assert(t.start[e.v] <= t.start[e.u] && t.finish[e.u] <= t.finish[e.v]);
                    break;
                case EdgeKind::tree:
                case EdgeKind::forward:
                    assert(t.start[e.u] < t.start[e.v] && t.finish[e.v] < t.finish[e.u]);
                    break;
                case EdgeKind::cross:
                    assert(t.finish[e.v] < t.start[e.u]);
                    break;
            }
        }
        auto reach = reach_matrix(g);
        bool has_cycle = false;
        for (int v = 0; v < n; v++) has_cycle = has_cycle || reach[v][v];
        assert(has_back == has_cycle);
    }
}

// En un DAG, toda arista u -> v tiene fin[v] < fin[u], y el orden decreciente de fin es topológico.
void test_topological() {
    std::mt19937 rng(67);
    for (int round = 0; round < 1000; round++) {
        int n = random_int(rng, 1, 10);
        Digraph g = random_dag(rng, n, random_int(rng, 0, 3 * n));
        DfsTimes t = dfs_recursive(g);
        for (int u = 0; u < n; u++) {
            for (int v : g[u]) assert(t.finish[v] < t.finish[u]);
        }
        assert(is_topological(g, reverse_finish_order(g)));
    }
    // Con un ciclo, el orden existe como lista, pero no es topológico.
    Digraph cycle = {{1}, {2}, {0, 3}, {}};
    assert(!is_topological(cycle, reverse_finish_order(cycle)));
}

// El atajo de meter todos los vecinos no da los tiempos de fin.
void test_stack_shortcut() {
    Digraph small = {{1, 2}, {2}, {}};
    std::vector<int> shortcut = stack_shortcut_order(small);
    assert((shortcut == std::vector<int>{0, 2, 1}));
    std::vector<int> back(shortcut.rbegin(), shortcut.rend());
    assert(!is_topological(small, shortcut));
    assert(!is_topological(small, back));
    assert(is_topological(small, reverse_finish_order(small)));
}

// Kosaraju-Sharir contra la definición, y en orden de sumidero a fuente.
void test_kosaraju() {
    std::mt19937 rng(68);
    int wrong_without_reversing = 0;
    for (int round = 0; round < 3000; round++) {
        int n = random_int(rng, 1, 9);
        Digraph g = random_digraph(rng, n, random_int(rng, 0, 2 * n));
        std::vector<int> comp = strong_components(g);
        assert(same_partition(comp, components_brute(g)));
        for (int u = 0; u < n; u++) {
            for (int v : g[u]) assert(comp[v] <= comp[u]);  // toda arista va a una componente ya hallada
        }
        // El mismo resultado sobre el grafo invertido, porque las componentes son las mismas.
        assert(same_partition(strong_components(reversed(g)), comp));
        // Lema: si hay arista de C a D en el original, en el invertido fin(D) > fin(C).
        DfsTimes t = dfs_recursive(reversed(g));
        std::map<int, int> top_finish;
        for (int v = 0; v < n; v++) top_finish[comp[v]] = std::max(top_finish[comp[v]], t.finish[v]);
        for (int u = 0; u < n; u++) {
            for (int v : g[u]) {
                if (comp[u] != comp[v]) assert(top_finish[comp[v]] > top_finish[comp[u]]);
            }
        }
        if (!same_partition(components_without_reversing(g), comp)) wrong_without_reversing++;
    }
    assert(wrong_without_reversing > 0);

    // El ejemplo de la clase.
    Digraph g = make_digraph(8, {{0, 1}, {1, 2}, {2, 0}, {2, 3}, {3, 4}, {4, 5}, {5, 3}, {6, 5}, {6, 7}, {7, 6}});
    assert(same_partition(strong_components(g), {0, 0, 0, 1, 1, 1, 2, 2}));
    assert(!same_partition(components_without_reversing(g), {0, 0, 0, 1, 1, 1, 2, 2}));
}

// Con pila explícita se puede recorrer un camino que agotaría la pila de llamadas.
void test_deep_iterative() {
    int n = 300000;
    Digraph path(static_cast<std::size_t>(n));
    for (int i = 0; i + 1 < n; i++) path[i].push_back(i + 1);
    DfsTimes t = dfs_iterative(path);
    assert(t.finish[0] == 2 * n);
    assert(t.finish[n - 1] == n + 1);
    std::vector<int> comp = strong_components(path);
    assert(comp[0] == n - 1);
}

}  // namespace

int main() {
    test_class_graph();
    test_cut_property_distinct();
    test_cut_property_ties();
    test_triangle_ties();
    test_mst_algorithms_random();
    test_forest();
    test_mst_is_not_paths();
    test_add_constant();
    test_clustering();
    test_dfs_times();
    test_dfs_random();
    test_topological();
    test_stack_shortcut();
    test_kosaraju();
    test_deep_iterative();
    std::cout << "Todas las pruebas de la semana 6 pasaron.\n";
}

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/curriculum/MakefileDescargar lab/curriculum/Makefile
TIEMPO = 3
include ../../../lab.mk
lab/curriculum/README.mdDescargar lab/curriculum/README.md
# Los prerrequisitos de una malla

La oficina académica revisa una malla curricular: una lista de cursos, cada uno con un código, y una lista de
prerrequisitos. Un prerrequisito `A B` significa que el curso A tiene que estar aprobado antes de llevar el curso B.

Quieren saber en cuántos semestres, como mínimo, se puede terminar la malla. Un curso se puede llevar en un semestre si
todos sus prerrequisitos se aprobaron en semestres anteriores. **No hay límite de cursos por semestre**, y se supone que
quien lleva un curso lo aprueba.

La malla también puede estar mal escrita. Un curso está **atrapado en un ciclo** si es prerrequisito de sí mismo, ya sea
directamente (`A A`) o a través de otros cursos (`A B`, `B C`, `C A`). Dicho de otra forma: siguiendo prerrequisitos desde
ese curso se puede volver a él. Un curso que solo depende de un curso atrapado no está atrapado: tampoco se puede llevar
nunca, pero corregir el ciclo lo arregla sin tocarlo. Cuando hay cursos atrapados, nadie termina la malla, y la oficina
quiere la lista de esos cursos, y solo de esos, para corregirla.

## Entrada

La primera línea tiene dos enteros: n, la cantidad de cursos, y m, la cantidad de prerrequisitos. La segunda parte son n
líneas con un código cada una, todos distintos. Luego vienen m líneas, cada una con dos códigos `A B`: A es prerrequisito
de B. Un mismo prerrequisito puede aparecer más de una vez. Todo código de un prerrequisito aparece en la lista de cursos.

## Salida

- Si ningún curso está atrapado, una línea con el mínimo de semestres.
- Si hay cursos atrapados, una línea `CICLO t`, donde t es cuántos son, y luego t líneas con sus códigos, **en el mismo
  orden en que aparecen en la lista de cursos de la entrada**.

## Límites

- 1 ≤ n ≤ 300 000
- 0 ≤ m ≤ 600 000
- Cada código tiene de 1 a 10 caracteres, entre letras sin tilde y dígitos.

## Ejemplo

Entrada:

```text
5 5
MAT1
PROG1
PROG2
ALGO
BD
MAT1 ALGO
PROG1 PROG2
PROG2 ALGO
PROG2 BD
MAT1 BD
```

Salida:

```text
3
```

PROG1 y MAT1 se llevan en el primer semestre, PROG2 en el segundo, y ALGO y BD en el tercero. No se puede en menos, porque
PROG1, PROG2 y ALGO tienen que ir en semestres distintos.

Otra entrada:

```text
5 5
SO
ARQ
REDES
TESIS
FIS
ARQ SO
SO ARQ
REDES REDES
SO TESIS
FIS SO
```

Salida:

```text
CICLO 3
SO
ARQ
REDES
```

SO y ARQ se piden entre sí, y REDES se pide a sí mismo. TESIS depende de SO, y FIS es prerrequisito de SO, pero ninguno de
los dos está en un ciclo.

## La malla de la carrera

Uno de los casos (`casos/12.in`) es la propuesta de malla de la carrera. Incluye solo los 27 cursos de Ciencia de la
Computación, con sus códigos, porque de los demás la propuesta no da códigos. Para escribirla en este formato se tomaron
estas decisiones, que son del curso y no de la propuesta:

- Los prerrequisitos de otros departamentos (Cálculo, Álgebra Lineal, Cálculo multivariable) no se cuentan.
- CS802 pide "6.º semestre aprobado". Se escribió como: todos los cursos de CS hasta el semestre 6 son prerrequisito de
  CS802. Basta listar seis, porque los demás ya son prerrequisito de alguno de ellos: CS501, CS504, CS601, CS602, CS603 y CS604.
- Los cuatro electivos (CS803, CS902, CS903 y CS1002) tienen prerrequisito "según línea", que depende de la línea que cada
  quien elija. Están en la lista, sin prerrequisitos.

Antes de correr su programa con este caso, adivinen la respuesta. ¿Coincide con los 10 semestres del plan? ¿Por qué?

## Tiempo

3 segundos por caso, compilado con `-O2`. Corran `make test` para probar con el juez.

Un programa que termina con `RE` en un caso grande sin error aparente quizá agotó la pila. En el juez, la pila es la que
tiene el contenedor por defecto (en bash, `ulimit -s` da 8192 KB).
lab/curriculum/solution.cppDescargar lab/curriculum/solution.cpp
// Los prerrequisitos de una malla: lee la malla e imprime 0. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n = 0;
    int m = 0;
    std::cin >> n >> m;
    std::vector<std::string> codes(static_cast<std::size_t>(n));
    for (std::string& code : codes) {
        std::cin >> code;
    }
    std::vector<std::pair<std::string, std::string>> prerequisites(static_cast<std::size_t>(m));
    for (auto& [before, after] : prerequisites) {
        std::cin >> before >> after;
    }

    // Por hacer: el mínimo de semestres, o la lista de cursos atrapados.
    std::cout << 0 << '\n';
}
lab/network_design/MakefileDescargar lab/network_design/Makefile
TIEMPO = 1
include ../../../lab.mk
lab/network_design/README.mdDescargar lab/network_design/README.md
# Diseñar una red

Una empresa tiene n sedes y quiere unirlas con fibra óptica, de modo que desde cualquier sede se pueda llegar a
cualquier otra, directamente o pasando por otras sedes. Un proveedor presentó una lista de m tramos que está dispuesto
a instalar. Cada tramo une dos sedes distintas, tiene un precio y sirve en los dos sentidos. El proveedor puede ofrecer
más de un tramo entre las mismas dos sedes, con precios distintos o iguales.

La empresa quiere saber cuánto es lo mínimo que tiene que pagar para que todas las sedes queden conectadas, o enterarse
de que con esa lista no se puede.

## Entrada

La primera línea tiene dos enteros: n, la cantidad de sedes, y m, la cantidad de tramos. Las sedes se numeran de 1 a n.
Cada una de las m líneas siguientes tiene tres enteros: a, b y c. Hay un tramo entre las sedes a y b que cuesta c.

## Salida

Una línea. Si se pueden conectar todas las sedes con tramos de la lista, el costo total mínimo. Si no se puede, la
palabra `IMPOSIBLE`. Con una sola sede no hace falta ningún tramo, y el costo es 0.

## Límites

- 1 ≤ n ≤ 200 000
- 0 ≤ m ≤ 500 000
- 1 ≤ a, b ≤ n, con a ≠ b
- 1 ≤ c ≤ 1 000 000 000

## Ejemplo

Entrada:

```text
5 7
1 2 4
1 3 6
2 3 5
2 4 9
3 4 7
3 5 8
4 5 3
```

Salida:

```text
19
```

Conviene contratar los tramos 4-5 (3), 1-2 (4), 2-3 (5) y 3-4 (7), que suman 19. El tramo 1-3 no hace falta aunque cueste
solo 6: 1 y 3 ya quedan unidos a través de 2.

Si la lista fuera solo `1 2 4` y `4 5 3`, la respuesta sería `IMPOSIBLE`.

## Tiempo

1 segundo por caso, compilado con `-O2`. Corran `make test` para probar con el juez.
lab/network_design/solution.cppDescargar lab/network_design/solution.cpp
// Diseñar una red: lee los tramos e imprime -1. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>

struct Link {
    int a;
    int b;
    int cost;
};

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n = 0;
    int m = 0;
    std::cin >> n >> m;
    std::vector<Link> links(static_cast<std::size_t>(m));
    for (Link& link : links) {
        std::cin >> link.a >> link.b >> link.cost;
    }

    // Por hacer: calcular el costo mínimo, o imprimir IMPOSIBLE.
    std::cout << -1 << '\n';
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 6: árboles de expansión mínima, DFS con tiempos y componentes fuertemente conexas.
# Se usa dentro del entorno del curso.
#
#   make             compila los ejemplos en build/
#   make test        corre las pruebas de los ejemplos, con sanitizers
#   make bench       mide Kruskal y Prim (no entra en make test)
#   make clean       borra lo generado
#
# Los problemas de lab/ tienen su propio Makefile y no entran aquí.

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
HEADERS = mst.h digraph.h
PROGRAMS = mst_traces mst_vs_paths ties_demo clustering_demo dfs_traces scc_traces test_examples
OPTIMIZED = mst_bench deep_recursion

.PHONY: all test bench clean

all: $(PROGRAMS:%=$(BUILD)/%) $(OPTIMIZED:%=$(BUILD)/%)

$(PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
	$(CXX) $(CXXFLAGS) $< -o $@

# Lo que mide o agota la pila va con -O2 y sin sanitizers, como en el juez.
$(OPTIMIZED:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
	$(CXX) $(BENCHFLAGS) $< -o $@

$(BUILD):
	mkdir -p $@

test: all
	$(BUILD)/test_examples

bench: $(BUILD)/mst_bench
	$(BUILD)/mst_bench

clean:
	rm -rf $(BUILD)