15
Flujo e intratabilidad

Qué hacer con un problema difícil

Flujo e intratabilidad

Semana 15 — Qué hacer con un problema difícil

Objetivos

  • Elegir una estrategia para un problema NP-difícil.
  • Demostrar el factor de un algoritmo de aproximación.
  • Mejorar una búsqueda exacta con poda.
  • Comparar con datos un método exacto, uno aproximado y una heurística.

Sesión A — Qué hacer con un problema difícil: poda y aproximación

Lectura: Apuntes, semana 15.

Apoyo: las notas Approximation Algorithms de Erickson, aparte del libro, y sec. 2.4 (The General Pattern). CLRS, capítulo Approximation Algorithms, 4.ª edición.

Temas:

  • Las opciones: ¿la entrada es pequeña?, ¿tiene una estructura especial?, ¿sirve una respuesta cercana?, ¿un solucionador?
  • Casos especiales ya vistos: cobertura de vértices en árboles (semana 9), y números pequeños en la mochila (semana 7).
  • Backtracking con poda y ramificación y acotamiento: una cota que descarta ramas.
  • Aproximación: una garantía demostrada, sin conocer el óptimo. La clave es comparar contra una cota inferior.
  • Cobertura de vértices: un matching maximal da factor 2.
  • Viajero métrico: duplicar el árbol de expansión mínima da factor 2. Por qué sin desigualdad triangular no hay aproximación con factor constante, salvo que P = NP.
  • Heurísticas: mejora local con 2-opt. Funciona bien, pero no garantiza nada.
  • Error común: decir “aproximación” para una heurística que no tiene garantía demostrada.

Sesión B — ✂️ Programación lineal como idea · Repaso

Lectura: Apuntes, semana 15.

Apoyo: las notas Linear Programming de Erickson, aparte del libro. CLRS, capítulo Linear Programming, 4.ª edición.

Temas:

  • ✂️ Programación lineal: optimizar una función lineal con restricciones lineales. Se resuelve en tiempo polinomial, y hay solucionadores maduros.
  • ✂️ Flujo máximo escrito como programa lineal. Con variables enteras, el problema se vuelve NP-difícil.
  • Repaso: dado un problema nuevo, ¿qué técnica sirve? Cómo identificar greedy, divide y vencerás, programación dinámica, flujo o reducción.
  • Qué sigue: Teoría de la Computación, Probabilidad y algoritmos aleatorizados, y Algoritmos Avanzados.

Laboratorio — El viajero: exacto frente a heurística

  1. Con las sedes regionales del Perú y las distancias en línea recta entre ellas (coordenadas de GeoNames), implementar Held-Karp para hasta unas 20 ciudades.
  2. Implementar la aproximación con el árbol de expansión mínima, y mejorarla con 2-opt. Un verificador comprueba que el recorrido sea válido y su largo.
  3. Comparar en una tabla el largo y el tiempo de cada método con 10, 15 y 20 ciudades. Con todas las ciudades, comparar solo los métodos que terminan.
  4. Comprobar que la aproximación está dentro del factor demostrado, y ver a qué distancia del óptimo queda en la práctica.

Preguntas de repaso

  1. Demuestren que un matching maximal da una cobertura de vértices con factor 2.
  2. ¿Por qué el árbol de expansión mínima es una cota inferior para el viajero?
  3. Demuestren el factor 2 de duplicar el árbol en un caso métrico. ¿Dónde se usa la desigualdad triangular?
  4. ¿Cuál es la diferencia entre una aproximación y una heurística?
  5. Para cada problema, elijan una técnica y justifíquenla: asignar 50 médicos a turnos con cupos; visitar 12 clientes en orden óptimo; elegir la mayor cantidad de clases sin cruces en un aula, entre 10000; alinear dos cadenas de ADN.

Código de la semana

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

Ejemplos (15)Pruebas (1)Laboratorio (8)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

bound_bench.cppDescargar bound_bench.cpp
// Nodos y tiempo de la búsqueda en los casos grandes. Una corrida de cada una, con -O2.
#include <chrono>
#include <iomanip>
#include <iostream>
#include <string>

#include "bound.h"
#include "geo.h"

namespace {

void row(const bound::Matrix& d, int n, const std::string& name, bound::Pruning pruning) {
    const auto start = std::chrono::steady_clock::now();
    const auto result = bound::solve(d, pruning);
    const std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - start;
    std::cout << std::setw(2) << n << "  " << std::left << std::setw(20) << name << std::right << std::setw(12)
              << result.nodes << std::setw(10) << std::fixed << std::setprecision(1)
              << static_cast<double>(result.cost) / 1000.0 << " km" << std::setw(9) << std::setprecision(2)
              << elapsed.count() << " s\n";
}

}  // namespace

int main() {
    const auto all = geo::read_places("capitales.csv");
    std::cout << " n  poda                       nodos       costo     tiempo\n";
    const auto d15 = geo::distance_matrix(geo::first_places(all, 15));
    row(d15, 15, "lo recorrido", bound::Pruning::partial);
    row(d15, 15, "con cota de árbol", bound::Pruning::spanning);
    row(geo::distance_matrix(geo::first_places(all, 20)), 20, "con cota de árbol", bound::Pruning::spanning);
    row(geo::distance_matrix(all), 26, "con cota de árbol", bound::Pruning::spanning);
}
bound_demo.cppDescargar bound_demo.cpp
// Cuántos nodos del árbol de búsqueda visita cada forma de podar, en las primeras filas de
// capitales.csv. Las tres dan el mismo costo; lo que cambia es el trabajo.
#include <iostream>
#include <string>

#include "../week-10/held_karp.h"
#include "bound.h"
#include "geo.h"

namespace {

std::string pad(const std::string& text, std::size_t width) {
    return std::string(width > text.size() ? width - text.size() : 0, ' ') + text;
}

std::string cell(const bound::Matrix& d, bound::Pruning pruning, int n, int up_to) {
    if (n > up_to) return "-";
    return std::to_string(bound::solve(d, pruning).nodes);
}

}  // namespace

int main() {
    const auto all = geo::read_places("capitales.csv");
    std::cout << " n  sin poda      lo recorrido  con cota de árbol  costo (m)\n";
    for (int n : {6, 8, 10, 11, 12, 13}) {
        const auto d = geo::distance_matrix(geo::first_places(all, n));
        std::cout << pad(std::to_string(n), 2) << pad(cell(d, bound::Pruning::none, n, 11), 10)
                  << pad(cell(d, bound::Pruning::partial, n, 13), 14)
                  << pad(cell(d, bound::Pruning::spanning, n, 13), 19)
                  << pad(std::to_string(bound::solve(d, bound::Pruning::spanning).cost), 11) << '\n';
    }
}
bound.hDescargar bound.h
// Búsqueda exacta del viajero por backtracking, con tres formas de podar. Solo devuelve el costo y la
// cantidad de nodos visitados del árbol de búsqueda. Un recorrido parcial sale de 0 y crece por el final.
#pragma once
#include <algorithm>
#include <cstddef>
#include <limits>
#include <vector>

namespace bound {

using Matrix = std::vector<std::vector<long long>>;

inline constexpr long long NO_TOUR = std::numeric_limits<long long>::max() / 4;

inline std::size_t at(int i) { return static_cast<std::size_t>(i); }

enum class Pruning {
    none,      // recorre todo el árbol
    partial,   // poda si lo ya recorrido alcanza al mejor conocido
    spanning,  // lo ya recorrido, más una cota inferior de lo que falta
};

struct Result {
    long long cost = NO_TOUR;
    long long nodes = 0;
    bool finished = true;
};

// El peso de un árbol de expansión mínima de los nodos marcados, con Prim en O(r²).
inline long long spanning_weight(const Matrix& d, const std::vector<int>& nodes) {
    if (nodes.size() <= 1) return 0;
    std::vector<long long> best(nodes.size(), NO_TOUR);
    std::vector<char> inside(nodes.size(), 0);
    best[0] = 0;
    long long weight = 0;
    for (std::size_t round = 0; round < nodes.size(); round++) {
        std::size_t v = nodes.size();
        for (std::size_t u = 0; u < nodes.size(); u++) {
            if (inside[u] == 0 && (v == nodes.size() || best[u] < best[v])) v = u;
        }
        inside[v] = 1;
        weight += best[v];
        for (std::size_t u = 0; u < nodes.size(); u++) {
            const long long edge = d[at(nodes[v])][at(nodes[u])];
            if (inside[u] == 0 && edge < best[u]) best[u] = edge;
        }
    }
    return weight;
}

// Una cota inferior para terminar un recorrido que está en last, con los nodos de rest sin visitar.
// Cualquier forma de terminar sale de last a un nodo de rest, recorre rest en un camino y vuelve de
// un nodo de rest a 0. Ese camino es un árbol de expansión de rest, así que pesa al menos el mínimo.
inline long long completion_bound(const Matrix& d, int last, const std::vector<int>& rest) {
    if (rest.empty()) return d[at(last)][0];
    long long out = NO_TOUR;
    long long back = NO_TOUR;
    for (int r : rest) {
        out = std::min(out, d[at(last)][at(r)]);
        back = std::min(back, d[at(r)][0]);
    }
    return out + spanning_weight(d, rest) + back;
}

struct Search {
    const Matrix& d;
    Pruning pruning;
    long long node_limit;
    Result result;

    void visit(int last, std::vector<int>& rest, long long so_far) {
        if (!result.finished) return;
        result.nodes++;
        if (result.nodes > node_limit) {
            result.finished = false;
            return;
        }
        if (rest.empty()) {
            result.cost = std::min(result.cost, so_far + d[at(last)][0]);
            return;
        }
        if (pruning == Pruning::partial && so_far >= result.cost) return;
        if (pruning == Pruning::spanning && so_far + completion_bound(d, last, rest) >= result.cost) return;
        // Probar primero lo más cercano encuentra pronto un buen recorrido, y eso hace podar más.
        std::vector<int> order = rest;
        std::sort(order.begin(), order.end(), [&](int a, int b) { return d[at(last)][at(a)] < d[at(last)][at(b)]; });
        for (int next : order) {
            std::vector<int> remaining;
            remaining.reserve(rest.size() - 1);
            for (int r : rest) {
                if (r != next) remaining.push_back(r);
            }
            visit(next, remaining, so_far + d[at(last)][at(next)]);
        }
    }
};

// node_limit corta la búsqueda: finished queda en false y el costo no vale como óptimo.
inline Result solve(const Matrix& d, Pruning pruning, long long node_limit = std::numeric_limits<long long>::max()) {
    const int n = static_cast<int>(d.size());
    Search search{d, pruning, node_limit, Result{}};
    if (n <= 1) {
        search.result.cost = 0;
        return search.result;
    }
    std::vector<int> rest;
    for (int v = 1; v < n; v++) rest.push_back(v);
    search.visit(0, rest, 0);
    return search.result;
}

}  // namespace bound
capitales.csvDescargar capitales.csv
capital,latitud,longitud
Abancay,-13.63426,-72.8838
Arequipa,-16.39899,-71.53747
Ayacucho,-13.1638,-74.22345
Cajamarca,-7.16378,-78.50027
Callao,-12.05162,-77.13452
Cerro de Pasco,-10.66577,-76.25309
Chachapoyas,-6.23169,-77.86903
Chiclayo,-6.77008,-79.85495
Cusco,-13.53188,-71.96701
Huacho,-11.11718,-77.60584
Huancavelica,-12.78691,-74.97298
Huancayo,-12.06866,-75.21027
Huaraz,-9.52614,-77.52869
Huánuco,-9.92882,-76.23989
Ica,-14.07538,-75.73422
Iquitos,-3.74814,-73.2529
Lima,-12.04318,-77.02824
Moquegua,-17.19747,-70.93587
Moyobamba,-6.03441,-76.97423
Piura,-5.18192,-80.65715
Pucallpa,-8.37915,-74.55387
Puerto Maldonado,-12.58946,-69.19948
Puno,-15.84003,-70.02198
Tacna,-18.01465,-70.25362
Trujillo,-8.11599,-79.02998
Tumbes,-3.55592,-80.44258
capitales.mdDescargar capitales.md
# Fuente de `capitales.csv`

Coordenadas de [GeoNames](https://www.geonames.org/), archivo `PE.zip` de
<https://download.geonames.org/export/dump/>, descargado el 2026-11-04. Licencia CC BY 4.0.

Son los 26 lugares de Perú con código `PPLA` (sede de una división de primer nivel) o `PPLC`
(capital del país): las sedes de los gobiernos regionales, incluidas Huacho y Callao, y Lima.
Solo se conservaron nombre, latitud y longitud, sin otros cambios.
compare_bench.cppDescargar compare_bench.cpp
// La comparación de la semana: largo y tiempo de cada método con 10, 15, 20 y 26 ciudades. Cada
// tiempo es el menor de tres corridas, con -O2. El 2-opt de aquí es el lento de tour.h.
#include <algorithm>
#include <chrono>
#include <functional>
#include <iomanip>
#include <iostream>
#include <string>

#include "../week-10/held_karp.h"
#include "bound.h"
#include "geo.h"
#include "tour.h"

namespace {

struct Measured {
    long long length;
    double seconds;
};

Measured measure(int runs, const std::function<long long()>& method) {
    Measured best{0, 1e18};
    for (int r = 0; r < runs; r++) {
        const auto start = std::chrono::steady_clock::now();
        best.length = method();
        const std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - start;
        best.seconds = std::min(best.seconds, elapsed.count());
    }
    return best;
}

void row(const std::string& name, const Measured& m, long long reference) {
    std::cout << "  " << std::left << std::setw(26) << name << std::right << std::setw(9) << std::fixed
              << std::setprecision(1) << static_cast<double>(m.length) / 1000.0 << " km" << std::setw(9)
              << std::setprecision(3) << static_cast<double>(m.length) / static_cast<double>(reference)
              << std::setw(10) << m.seconds << " s\n";
}

}  // namespace

int main() {
    const auto all = geo::read_places("capitales.csv");
    for (int n : {10, 15, 20, 26}) {
        const auto d = geo::distance_matrix(geo::first_places(all, n));
        // El óptimo viene de la búsqueda con cota de árbol, que es exacta. Es el divisor de la razón.
        const auto exact = measure(n >= 20 ? 1 : 3, [&] { return bound::solve(d, bound::Pruning::spanning).cost; });
        std::cout << "n = " << n << "                       largo     / óptimo    tiempo\n";
        row("árbol de expansión mínima", measure(3, [&] { return tour::minimum_spanning_tree(d).weight; }), exact.length);
        row("árbol duplicado", measure(3, [&] { return tour::tour_length(d, tour::double_tree(d)); }), exact.length);
        row("duplicado + 2-opt", measure(3, [&] {
            auto t = tour::double_tree(d);
            tour::two_opt_plain(d, t);
            return tour::tour_length(d, t);
        }), exact.length);
        row("vecino más cercano", measure(3, [&] { return tour::tour_length(d, tour::nearest_neighbor(d)); }), exact.length);
        row("vecino + 2-opt", measure(3, [&] {
            auto t = tour::nearest_neighbor(d);
            tour::two_opt_plain(d, t);
            return tour::tour_length(d, t);
        }), exact.length);
        if (n <= 20) row("Held-Karp", measure(3, [&] { return tsp::held_karp(d); }), exact.length);
        row("búsqueda con cota", exact, exact.length);
        std::cout << '\n';
    }
}
cover_demo.cppDescargar cover_demo.cpp
// Tres reglas para la cobertura de vértices: la que falla, la que tiene factor 2 y la natural.
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <random>
#include <string>

#include "cover.h"

namespace {

std::string list(const cover::Nodes& nodes) {
    std::string text = "{";
    for (std::size_t i = 0; i < nodes.size(); i++) text += (i > 0 ? ", " : "") + std::to_string(nodes[i]);
    return text + "}";
}

void compare_on(const std::string& title, int n, const cover::Edges& edges) {
    std::cout << title << '\n';
    std::cout << "  una sola punta: " << list(cover::one_endpoint_cover(n, edges)) << '\n';
    const auto matched = cover::matching_cover(n, edges);
    std::cout << "  matching maximal, " << matched.matching.size() << " aristas: " << list(matched.cover) << '\n';
    std::cout << "  mayor grado: " << list(cover::greedy_degree_cover(n, edges)) << '\n';
    std::cout << "  óptimo por fuerza bruta: " << cover::minimum_cover_size(n, edges) << "\n\n";
}

}  // namespace

int main() {
    compare_on("Estrella con centro 0 y 6 hojas:", 7, {{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}});
    compare_on("Camino 0-1-2-3, primero la arista 0-1:", 4, {{0, 1}, {1, 2}, {2, 3}});
    compare_on("Camino 0-1-2-3, primero la arista 1-2:", 4, {{1, 2}, {0, 1}, {2, 3}});
    compare_on("Una sola arista:", 2, {{0, 1}});

    std::cout << "La trampa del mayor grado:\n";
    std::cout << "   k  nodos  aristas  mayor grado  matching  mayor grado / k\n";
    for (int k : {6, 12, 24, 48, 96, 192}) {
        const auto trap = cover::greedy_trap(k);
        const auto chosen = cover::greedy_degree_cover(trap.nodes, trap.edges);
        const auto matched = cover::matching_cover(trap.nodes, trap.edges);
        std::cout << std::string(k < 10 ? 3 : (k < 100 ? 2 : 1), ' ') << k << "  " << trap.nodes << "  "
                  << trap.edges.size() << "  " << chosen.size() << "  " << matched.cover.size() << "  "
                  << static_cast<double>(chosen.size()) / k << '\n';
    }

    std::cout << "\n300 grafos al azar, 16 nodos, cada arista con probabilidad 1/4:\n";
    std::mt19937 rng(15);
    long long sum_opt = 0;
    long long sum_matching = 0;
    long long sum_greedy = 0;
    double worst_matching = 0;
    double worst_greedy = 0;
    const int graphs = 300;
    for (int g = 0; g < graphs; g++) {
        cover::Edges edges;
        for (int u = 0; u < 16; u++) {
            for (int v = u + 1; v < 16; v++) {
                if (rng() % 4 == 0) edges.push_back({u, v});
            }
        }
        const int opt = cover::minimum_cover_size(16, edges);
        const auto matching = cover::matching_cover(16, edges).cover.size();
        const auto greedy = cover::greedy_degree_cover(16, edges).size();
        sum_opt += opt;
        sum_matching += static_cast<long long>(matching);
        sum_greedy += static_cast<long long>(greedy);
        if (opt > 0) {
            worst_matching = std::max(worst_matching, static_cast<double>(matching) / opt);
            worst_greedy = std::max(worst_greedy, static_cast<double>(greedy) / opt);
        }
    }
    std::cout << "  promedio del óptimo: " << static_cast<double>(sum_opt) / graphs << '\n';
    std::cout << "  promedio del matching: " << static_cast<double>(sum_matching) / graphs
              << ", peor razón " << worst_matching << '\n';
    std::cout << "  promedio del mayor grado: " << static_cast<double>(sum_greedy) / graphs
              << ", peor razón " << worst_greedy << '\n';
}
cover.hDescargar cover.h
// Cobertura de vértices: un método con factor 2 demostrado, dos reglas parecidas y un árbitro.
// Un grafo es una cantidad de nodos y una lista de aristas.
#pragma once
#include <cstddef>
#include <utility>
#include <vector>

namespace cover {

using Edge = std::pair<int, int>;
using Edges = std::vector<Edge>;
using Nodes = std::vector<int>;

inline std::size_t at(int i) { return static_cast<std::size_t>(i); }

inline bool is_cover(const Nodes& chosen, int n, const Edges& edges) {
    std::vector<char> in(at(n), 0);
    for (int v : chosen) in[at(v)] = 1;
    for (const auto& [u, v] : edges) {
        if (in[at(u)] == 0 && in[at(v)] == 0) return false;
    }
    return true;
}

struct Matched {
    Edges matching;
    Nodes cover;
};

// Revisa las aristas en orden. Si ninguna punta de la arista está elegida, entra al matching y se
// eligen las dos. Invariante: lo elegido son las puntas del matching, y las aristas ya revisadas
// tienen alguna punta elegida. Al final el matching es maximal: no le cabe ninguna arista más.
inline Matched matching_cover(int n, const Edges& edges) {
    std::vector<char> used(at(n), 0);
    Matched result;
    for (const auto& [u, v] : edges) {
        if (used[at(u)] == 0 && used[at(v)] == 0) {
            used[at(u)] = used[at(v)] = 1;
            result.matching.push_back({u, v});
            result.cover.push_back(u);
            result.cover.push_back(v);
        }
    }
    return result;
}

// Una idea parecida que falla: por cada arista sin cubrir, elegir solo una punta, la segunda.
inline Nodes one_endpoint_cover(int n, const Edges& edges) {
    std::vector<char> used(at(n), 0);
    Nodes chosen;
    for (const auto& [u, v] : edges) {
        if (used[at(u)] == 0 && used[at(v)] == 0) {
            used[at(v)] = 1;
            chosen.push_back(v);
        }
    }
    return chosen;
}

// La regla natural: repetir con el nodo que cubre más aristas sin cubrir. En un empate, el de menor número.
inline Nodes greedy_degree_cover(int n, const Edges& edges) {
    std::vector<char> alive(edges.size(), 1);
    Nodes chosen;
    while (true) {
        std::vector<int> degree(at(n), 0);
        for (std::size_t e = 0; e < edges.size(); e++) {
            if (alive[e] != 0) {
                degree[at(edges[e].first)]++;
                degree[at(edges[e].second)]++;
            }
        }
        int best = 0;
        for (int v = 1; v < n; v++) {
            if (degree[at(v)] > degree[at(best)]) best = v;
        }
        if (n == 0 || degree[at(best)] == 0) return chosen;
        chosen.push_back(best);
        for (std::size_t e = 0; e < edges.size(); e++) {
            if (edges[e].first == best || edges[e].second == best) alive[e] = 0;
        }
    }
}

// El árbitro: prueba los 2ⁿ subconjuntos. Solo sirve para grafos chicos.
inline int minimum_cover_size(int n, const Edges& edges) {
    int best = n;
    for (unsigned mask = 0; mask < (1U << n); mask++) {
        bool covers = true;
        for (const auto& [u, v] : edges) {
            if (((mask >> u) & 1U) == 0 && ((mask >> v) & 1U) == 0) {
                covers = false;
                break;
            }
        }
        if (covers) {
            int size = 0;
            for (int v = 0; v < n; v++) size += static_cast<int>((mask >> v) & 1U);
            if (size < best) best = size;
        }
    }
    return best;
}

// Una trampa para la regla del mayor grado. Hay k nodos a la izquierda, 0, ..., k - 1. Para cada i de
// 2 a k hay ⌊k/i⌋ nodos a la derecha, y cada uno se une a i nodos de la izquierda, sin repetirlos
// dentro del grupo. Los k nodos de la izquierda ya son una cobertura, así que el óptimo es a lo sumo k.
struct Trap {
    int nodes;
    Edges edges;
    int right_nodes;
};

inline Trap greedy_trap(int k) {
    Trap trap{k, {}, 0};
    for (int i = 2; i <= k; i++) {
        for (int group = 0; group < k / i; group++) {
            const int r = trap.nodes++;
            trap.right_nodes++;
            for (int offset = 0; offset < i; offset++) trap.edges.push_back({group * i + offset, r});
        }
    }
    return trap;
}

}  // namespace cover
double_tree_demo.cppDescargar double_tree_demo.cpp
// El árbol duplicado, paso a paso, con ocho capitales.
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

#include "../week-10/held_karp.h"
#include "geo.h"
#include "tour.h"

namespace {

using geo::at;

std::vector<geo::Place> chosen_places() {
    const auto all = geo::read_places("capitales.csv");
    std::vector<geo::Place> places;
    for (const std::string name : {"Lima", "Callao", "Huacho", "Huancayo", "Ica", "Ayacucho", "Cusco", "Arequipa"}) {
        for (const auto& place : all) {
            if (place.name == name) places.push_back(place);
        }
    }
    return places;
}

std::string km(long long metres) {
    const long long tenths = (metres + 50) / 100;
    return std::to_string(tenths / 10) + "," + std::to_string(tenths % 10) + " km";
}

}  // namespace

int main() {
    const auto places = chosen_places();
    const auto d = geo::distance_matrix(places);
    const int n = static_cast<int>(places.size());
    const auto tree = tour::minimum_spanning_tree(d);

    std::cout << "Aristas del árbol de expansión mínima, con raíz en " << places[0].name << ":\n";
    for (int v = 0; v < n; v++) {
        for (int child : tree.children[at(v)]) {
            std::cout << "  " << places[at(v)].name << " - " << places[at(child)].name << ": " << km(d[at(v)][at(child)]) << '\n';
        }
    }
    std::cout << "peso del árbol: " << km(tree.weight) << "\n\n";

    const auto walk = tour::euler_walk(tree);
    std::cout << "Caminata, cada arista dos veces:\n ";
    for (int v : walk) std::cout << ' ' << places[at(v)].name;
    std::cout << "\nlargo de la caminata: " << km(tour::walk_length(d, walk)) << "\n\n";

    const auto shortened = tour::shortcut(walk, n);
    std::cout << "Atajando las repeticiones:\n ";
    for (int v : shortened) std::cout << ' ' << places[at(v)].name;
    std::cout << " y de vuelta a " << places[at(shortened[0])].name << '\n';
    const long long length = tour::tour_length(d, shortened);
    const long long optimum = tsp::held_karp(d);
    std::cout << "largo del recorrido: " << km(length) << '\n';
    std::cout << "óptimo, con Held-Karp: " << km(optimum) << '\n';
    std::cout << "dos veces el árbol: " << km(2 * tree.weight) << "\n\n";
}
geo.hDescargar geo.h
// Distancias entre lugares de la Tierra. El gran círculo es el camino más corto por la superficie
// de una esfera de radio 6371 km: no es una carretera ni una ruta aérea real.
// Cada distancia se guarda en metros enteros, redondeada hacia arriba.
#pragma once
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <fstream>
#include <numbers>
#include <stdexcept>
#include <string>
#include <vector>

namespace geo {

inline constexpr double EARTH_RADIUS_M = 6371000.0;

struct Place {
    std::string name;
    double latitude;
    double longitude;
};

using Matrix = std::vector<std::vector<long long>>;

inline std::size_t at(int i) { return static_cast<std::size_t>(i); }

// La fórmula de haversine, en metros. Se porta bien con puntos muy cercanos.
inline double great_circle_m(const Place& a, const Place& b) {
    const double to_radians = std::numbers::pi / 180.0;
    const double phi1 = a.latitude * to_radians;
    const double phi2 = b.latitude * to_radians;
    const double half_dphi = (phi2 - phi1) / 2.0;
    const double half_dlambda = (b.longitude - a.longitude) * to_radians / 2.0;
    const double h = std::sin(half_dphi) * std::sin(half_dphi) +
                     std::cos(phi1) * std::cos(phi2) * std::sin(half_dlambda) * std::sin(half_dlambda);
    return 2.0 * EARTH_RADIUS_M * std::asin(std::sqrt(std::min(1.0, h)));
}

inline long long distance_m(const Place& a, const Place& b) {
    return static_cast<long long>(std::ceil(great_circle_m(a, b)));
}

// Lo que ocurre si se tratan los grados como un plano: una mala distancia, a propósito.
inline long long flat_m(const Place& a, const Place& b) {
    const double dlat = a.latitude - b.latitude;
    const double dlon = a.longitude - b.longitude;
    const double metres_per_degree = EARTH_RADIUS_M * std::numbers::pi / 180.0;
    return static_cast<long long>(std::ceil(std::sqrt(dlat * dlat + dlon * dlon) * metres_per_degree));
}

inline Matrix distance_matrix(const std::vector<Place>& places) {
    const int n = static_cast<int>(places.size());
    Matrix d(at(n), std::vector<long long>(at(n), 0));
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) {
            d[at(i)][at(j)] = d[at(j)][at(i)] = distance_m(places[at(i)], places[at(j)]);
        }
    }
    return d;
}

// Lee capitales.csv: una cabecera y luego nombre,latitud,longitud.
inline std::vector<Place> read_places(const std::string& path) {
    std::ifstream file(path);
    if (!file) throw std::runtime_error("no se pudo abrir " + path);
    std::vector<Place> places;
    std::string line;
    std::getline(file, line);
    while (std::getline(file, line)) {
        const std::size_t first = line.find(',');
        const std::size_t second = line.find(',', first + 1);
        places.push_back({line.substr(0, first), std::stod(line.substr(first + 1, second - first - 1)),
                          std::stod(line.substr(second + 1))});
    }
    return places;
}

// Las primeras n filas del archivo.
inline std::vector<Place> first_places(const std::vector<Place>& places, int n) {
    return {places.begin(), places.begin() + n};
}

}  // namespace geo
hamilton_demo.cppDescargar hamilton_demo.cpp
// La reducción que descarta una aproximación sin desigualdad triangular. Si un algoritmo tuviera
// factor alfa = 2, comparar su largo con alfa·n decidiría si hay ciclo hamiltoniano.
#include <cstddef>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

#include "../week-10/held_karp.h"
#include "tour.h"

namespace {

using tour::at;
using EdgeList = std::vector<std::pair<int, int>>;

tour::Matrix weights_from_graph(int n, const EdgeList& edges, long long heavy) {
    tour::Matrix w(at(n), std::vector<long long>(at(n), heavy));
    for (int v = 0; v < n; v++) w[at(v)][at(v)] = 0;
    for (const auto& [u, v] : edges) w[at(u)][at(v)] = w[at(v)][at(u)] = 1;
    return w;
}

void show(const std::string& title, int n, const EdgeList& edges) {
    const int alpha = 2;
    const long long heavy = alpha * n + 1;
    const auto w = weights_from_graph(n, edges, heavy);
    const long long optimum = tsp::held_karp(w);
    const auto doubled = tour::double_tree(w);
    std::cout << title << '\n';
    std::cout << "  n = " << n << ", W = " << heavy << ", alfa·n = " << alpha * n << '\n';
    std::cout << "  óptimo: " << optimum << (optimum <= alpha * n ? " <= " : " > ") << alpha * n << '\n';
    std::cout << "  árbol duplicado:";
    for (int v : doubled) std::cout << ' ' << v;
    std::cout << ", largo " << tour::tour_length(w, doubled) << "\n\n";
}

}  // namespace

int main() {
    show("Un ciclo de 6 nodos con la numeración desordenada: sí hay ciclo hamiltoniano", 6,
         {{0, 3}, {3, 1}, {1, 5}, {5, 2}, {2, 4}, {4, 0}});
    show("Dos nodos unidos a otros cuatro, K(2,4): no hay ciclo hamiltoniano", 6,
         {{0, 2}, {0, 3}, {0, 4}, {0, 5}, {1, 2}, {1, 3}, {1, 4}, {1, 5}});
}
memory_bench.cppDescargar memory_bench.cpp
// Cuánta memoria toma de verdad una tabla de Held-Karp. Reserva cada tabla, la llena y mira el
// máximo de memoria del proceso (ru_maxrss). Van de menor a mayor porque ese máximo nunca baja.
#include <sys/resource.h>

#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

namespace {

double peak_mb() {
    rusage usage{};
    getrusage(RUSAGE_SELF, &usage);
    return static_cast<double>(usage.ru_maxrss) / 1024.0;
}

template <typename Cell>
void flat_table(const std::string& name, std::size_t rows, std::size_t columns) {
    std::vector<Cell> table(rows * columns, Cell{1});
    std::cout << std::left << std::setw(46) << name << std::right << std::setw(9) << std::fixed
              << std::setprecision(1) << static_cast<double>(table.size() * sizeof(Cell)) / (1024.0 * 1024.0)
              << " MiB" << std::setw(9) << peak_mb() << " MiB  " << (table[rows / 2] == Cell{1} ? "" : "?") << '\n';
}

}  // namespace

int main() {
    const std::size_t half = std::size_t{1} << 19;
    const std::size_t full = std::size_t{1} << 20;
    std::cout << std::left << std::setw(46) << "tabla para n = 20" << std::right << std::setw(13) << "la cuenta"
              << std::setw(13) << "pico" << '\n';
    flat_table<std::int32_t>("2^19 x 20, int", half, 20);
    flat_table<std::int64_t>("2^19 x 20, long long", half, 20);
    flat_table<std::int32_t>("2^20 x 20, int", full, 20);
    flat_table<std::int64_t>("2^20 x 20, long long", full, 20);
    {
        std::vector<std::vector<std::int64_t>> table(full, std::vector<std::int64_t>(20, 1));
        std::cout << std::left << std::setw(46) << "2^20 filas de 20, vector de vectores, long long" << std::right
                  << std::setw(9) << static_cast<double>(full * 20 * 8) / (1024.0 * 1024.0) << " MiB" << std::setw(9)
                  << peak_mb() << " MiB  " << (table[7][3] == 1 ? "" : "?") << '\n';
    }
}
metric_demo.cppDescargar metric_demo.cpp
// Comprueba la desigualdad triangular en los 26 lugares de capitales.csv, con tres formas de guardar
// la distancia, y muestra por qué redondear al entero más cercano puede romperla.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <numbers>
#include <string>
#include <vector>

#include "geo.h"

namespace {

using geo::at;
using geo::Place;

struct Check {
    long long triples = 0;
    long long broken = 0;
    double worst = 0;  // cuánto le sobra a un lado, en metros, en el peor caso
};

template <typename Distance>
Check check_triples(const std::vector<Place>& places, Distance distance) {
    Check check;
    const int n = static_cast<int>(places.size());
    for (int a = 0; a < n; a++) {
        for (int b = 0; b < n; b++) {
            for (int c = 0; c < n; c++) {
                if (a == b || b == c || a == c) continue;
                check.triples++;
                const double excess = static_cast<double>(distance(places[at(a)], places[at(c)])) -
                                      static_cast<double>(distance(places[at(a)], places[at(b)])) -
                                      static_cast<double>(distance(places[at(b)], places[at(c)]));
                if (excess > 0) {
                    check.broken++;
                    check.worst = std::max(check.worst, excess);
                }
            }
        }
    }
    return check;
}

void show(const std::string& title, const Check& check) {
    std::cout << "  " << title << ": " << check.broken << " de " << check.triples << " ternas la rompen";
    if (check.broken > 0) std::cout << ", peor exceso " << check.worst << " m";
    std::cout << '\n';
}

}  // namespace

int main() {
    const auto places = geo::read_places("capitales.csv");
    const int n = static_cast<int>(places.size());
    std::cout << std::fixed << std::setprecision(3);

    double nearest = 1e12;
    double farthest = 0;
    int near_a = 0;
    int near_b = 0;
    int far_a = 0;
    int far_b = 0;
    double to_integer = 1;
    long double formula_gap = 0;
    for (int a = 0; a < n; a++) {
        for (int b = a + 1; b < n; b++) {
            const double value = geo::great_circle_m(places[at(a)], places[at(b)]);
            if (value < nearest) {
                nearest = value;
                near_a = a;
                near_b = b;
            }
            if (value > farthest) {
                farthest = value;
                far_a = a;
                far_b = b;
            }
            to_integer = std::min({to_integer, value - std::floor(value), std::ceil(value) - value});
            const double flat = static_cast<double>(geo::flat_m(places[at(a)], places[at(b)]));
            formula_gap = std::max<long double>(formula_gap, std::abs(flat - value) / value);
        }
    }
    std::cout << n << " lugares, " << n * (n - 1) / 2 << " pares\n";
    std::cout << "más cercanos: " << places[at(near_a)].name << " y " << places[at(near_b)].name << ", " << nearest << " m\n";
    std::cout << "más lejanos: " << places[at(far_a)].name << " y " << places[at(far_b)].name << ", " << farthest << " m\n";
    std::cout << std::setprecision(6) << "lo más cerca de un entero que queda una distancia: " << to_integer << " m\n"
              << std::setprecision(3);
    std::cout << "peor error relativo de tratar los grados como un plano: " << 100.0L * formula_gap << " %\n\n";

    std::cout << "Desigualdad triangular, d(a,c) <= d(a,b) + d(b,c), en todas las ternas:\n";
    show("double, en metros", check_triples(places, geo::great_circle_m));
    show("entero, redondeado hacia arriba", check_triples(places, geo::distance_m));
    show("entero, al más cercano", check_triples(places, [](const Place& a, const Place& b) {
        return std::llround(geo::great_circle_m(a, b));
    }));

    double tightest = 1e12;
    int ta = 0;
    int tb = 0;
    int tc = 0;
    for (int a = 0; a < n; a++) {
        for (int b = 0; b < n; b++) {
            for (int c = 0; c < n; c++) {
                if (a == b || b == c || a == c) continue;
                const double slack = geo::great_circle_m(places[at(a)], places[at(b)]) +
                                     geo::great_circle_m(places[at(b)], places[at(c)]) -
                                     geo::great_circle_m(places[at(a)], places[at(c)]);
                if (slack < tightest) {
                    tightest = slack;
                    ta = a;
                    tb = b;
                    tc = c;
                }
            }
        }
    }
    std::cout << "la terna más justa: " << places[at(ta)].name << ", " << places[at(tb)].name << ", "
              << places[at(tc)].name << ", le sobran " << tightest << " m\n\n";

    // Tres puntos sobre el ecuador, separados 1000,4 m: el redondeo al más cercano rompe la desigualdad.
    const double degrees = 1000.4 / (geo::EARTH_RADIUS_M * std::numbers::pi / 180.0);
    const Place a{"a", 0, 0};
    const Place b{"b", 0, degrees};
    const Place c{"c", 0, 2 * degrees};
    std::cout << "Tres puntos sobre el ecuador:\n";
    std::cout << "  reales: ab = " << geo::great_circle_m(a, b) << ", bc = " << geo::great_circle_m(b, c)
              << ", ac = " << geo::great_circle_m(a, c) << '\n';
    std::cout << "  al más cercano: " << std::llround(geo::great_circle_m(a, b)) << " + "
              << std::llround(geo::great_circle_m(b, c)) << " < " << std::llround(geo::great_circle_m(a, c)) << '\n';
    std::cout << "  hacia arriba: " << geo::distance_m(a, b) << " + " << geo::distance_m(b, c) << " >= "
              << geo::distance_m(a, c) << '\n';
}
tour.hDescargar tour.h
// Recorridos del viajero sobre una matriz de distancias simétrica: el árbol duplicado, el vecino
// más cercano y la mejora local. Un recorrido es una permutación de los nodos: del último se vuelve
// al primero. El 2-opt de aquí prefiere ser claro a ser rápido: mide el recorrido entero en cada intento.
#pragma once
#include <algorithm>
#include <cstddef>
#include <limits>
#include <numeric>
#include <vector>

namespace tour {

using Matrix = std::vector<std::vector<long long>>;
using Tour = std::vector<int>;

inline std::size_t at(int i) { return static_cast<std::size_t>(i); }

inline long long tour_length(const Matrix& d, const Tour& t) {
    long long total = 0;
    for (std::size_t i = 0; i < t.size(); i++) {
        total += d[at(t[i])][at(t[(i + 1) % t.size()])];
    }
    return total;
}

// El largo de una caminata abierta, en el orden dado, sin volver al inicio.
inline long long walk_length(const Matrix& d, const std::vector<int>& walk) {
    long long total = 0;
    for (std::size_t i = 0; i + 1 < walk.size(); i++) total += d[at(walk[i])][at(walk[i + 1])];
    return total;
}

// ¿Pasa exactamente una vez por cada uno de los n nodos?
inline bool is_tour(const Tour& t, int n) {
    if (t.size() != at(n)) return false;
    std::vector<char> seen(at(n), 0);
    for (int v : t) {
        if (v < 0 || v >= n || seen[at(v)] != 0) return false;
        seen[at(v)] = 1;
    }
    return true;
}

struct SpanningTree {
    std::vector<std::vector<int>> children;  // hijos de cada nodo, con la raíz en 0
    long long weight = 0;
};

// Prim en O(n²), que es lo que conviene en un grafo completo. Invariante: best[v] es la arista más
// barata desde el árbol hasta v, y from[v] su otro extremo.
inline SpanningTree minimum_spanning_tree(const Matrix& d) {
    const int n = static_cast<int>(d.size());
    const long long INF = std::numeric_limits<long long>::max();
    std::vector<long long> best(at(n), INF);
    std::vector<int> from(at(n), -1);
    std::vector<char> inside(at(n), 0);
    SpanningTree tree{std::vector<std::vector<int>>(at(n)), 0};
    if (n == 0) return tree;
    best[0] = 0;
    for (int round = 0; round < n; round++) {
        int v = -1;
        for (int u = 0; u < n; u++) {
            if (inside[at(u)] == 0 && (v == -1 || best[at(u)] < best[at(v)])) v = u;
        }
        inside[at(v)] = 1;
        tree.weight += best[at(v)];
        if (from[at(v)] != -1) tree.children[at(from[at(v)])].push_back(v);
        for (int u = 0; u < n; u++) {
            if (inside[at(u)] == 0 && d[at(v)][at(u)] < best[at(u)]) {
                best[at(u)] = d[at(v)][at(u)];
                from[at(u)] = v;
            }
        }
    }
    return tree;
}

// Recorre el árbol de ida y de vuelta: cada arista aparece dos veces, y los nodos se repiten.
inline void euler_walk_from(const SpanningTree& tree, int v, std::vector<int>& walk) {
    walk.push_back(v);
    for (int child : tree.children[at(v)]) {
        euler_walk_from(tree, child, walk);
        walk.push_back(v);
    }
}

inline std::vector<int> euler_walk(const SpanningTree& tree) {
    std::vector<int> walk;
    if (!tree.children.empty()) euler_walk_from(tree, 0, walk);
    return walk;
}

// Atajar: de cada nodo se conserva su primera aparición.
inline Tour shortcut(const std::vector<int>& walk, int n) {
    std::vector<char> seen(at(n), 0);
    Tour result;
    for (int v : walk) {
        if (seen[at(v)] == 0) {
            seen[at(v)] = 1;
            result.push_back(v);
        }
    }
    return result;
}

inline Tour double_tree(const Matrix& d) {
    const int n = static_cast<int>(d.size());
    return shortcut(euler_walk(minimum_spanning_tree(d)), n);
}

// Un greedy sin garantía de este curso: desde 0, ir siempre a la ciudad libre más cercana.
inline Tour nearest_neighbor(const Matrix& d) {
    const int n = static_cast<int>(d.size());
    std::vector<char> seen(at(n), 0);
    Tour t;
    if (n == 0) return t;
    int current = 0;
    seen[0] = 1;
    t.push_back(0);
    while (static_cast<int>(t.size()) < n) {
        int next = -1;
        for (int u = 0; u < n; u++) {
            if (seen[at(u)] == 0 && (next == -1 || d[at(current)][at(u)] < d[at(current)][at(next)])) next = u;
        }
        seen[at(next)] = 1;
        t.push_back(next);
        current = next;
    }
    return t;
}

// Un cambio de 2-opt ya aplicado: se invirtió el tramo de la posición i a la j.
struct Move {
    int i;
    int j;
    long long length_after;
};

// Mejora local hasta que ningún cambio de dos aristas acorte el recorrido. Invierte el tramo, mide
// todo otra vez y deshace si no mejoró: O(n) por intento. Devuelve los cambios aplicados.
inline std::vector<Move> two_opt_plain(const Matrix& d, Tour& t) {
    const int n = static_cast<int>(t.size());
    std::vector<Move> moves;
    long long current = tour_length(d, t);
    bool improved = true;
    while (improved) {
        improved = false;
        for (int i = 1; i + 1 < n; i++) {
            for (int j = i + 1; j < n; j++) {
                std::reverse(t.begin() + i, t.begin() + j + 1);
                const long long candidate = tour_length(d, t);
                if (candidate < current) {
                    current = candidate;
                    moves.push_back({i, j, candidate});
                    improved = true;
                } else {
                    std::reverse(t.begin() + i, t.begin() + j + 1);
                }
            }
        }
    }
    return moves;
}

// ¿Algún cambio de dos aristas acortaría el recorrido? Mira cada par de aristas sin nodos en común.
inline bool is_two_opt_local_optimum(const Matrix& d, const Tour& t) {
    const int n = static_cast<int>(t.size());
    for (int a = 0; a < n; a++) {
        for (int b = a + 2; b < n; b++) {
            if (a == 0 && b == n - 1) continue;  // las aristas comparten el nodo t[0]
            const long long now = d[at(t[at(a)])][at(t[at(a + 1)])] + d[at(t[at(b)])][at(t[at((b + 1) % n)])];
            const long long swapped = d[at(t[at(a)])][at(t[at(b)])] + d[at(t[at(a + 1)])][at(t[at((b + 1) % n)])];
            if (swapped < now) return false;
        }
    }
    return true;
}

}  // namespace tour
two_opt_demo.cppDescargar two_opt_demo.cpp
// Mejora local con 2-opt sobre las capitales: de dónde sale, a dónde llega y qué tanto depende de
// dónde empieza. Las cifras de distancias en kilómetros son los metros divididos entre 1000.
#include <algorithm>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>

#include "../week-10/held_karp.h"
#include "geo.h"
#include "tour.h"

namespace {

using geo::at;

void show_tour(const std::vector<geo::Place>& places, const tour::Tour& t) {
    std::cout << " ";
    for (int v : t) std::cout << ' ' << places[at(v)].name;
    std::cout << '\n';
}

double km(long long metres) { return static_cast<double>(metres) / 1000.0; }

}  // namespace

int main() {
    const auto all = geo::read_places("capitales.csv");
    const auto places = geo::first_places(all, 20);
    const auto d = geo::distance_matrix(places);
    std::cout << std::fixed << std::setprecision(1);

    auto t = tour::double_tree(d);
    std::cout << "Las primeras 20 filas. Árbol duplicado, " << km(tour::tour_length(d, t)) << " km:\n";
    show_tour(places, t);
    const auto moves = tour::two_opt_plain(d, t);
    for (const auto& move : moves) {
        std::cout << "  invertir de la posición " << move.i << " a la " << move.j << ": " << km(move.length_after) << " km\n";
    }
    std::cout << "Después de 2-opt:\n";
    show_tour(places, t);
    const long long optimum = tsp::held_karp(d);
    std::cout << "óptimo, con Held-Karp: " << km(optimum) << " km\n\n";

    std::cout << "2-opt desde 2000 recorridos al azar, las mismas 20 filas:\n";
    std::mt19937 rng(15);
    std::vector<long long> finals;
    for (int run = 0; run < 2000; run++) {
        tour::Tour start(places.size());
        for (std::size_t i = 0; i < start.size(); i++) start[i] = static_cast<int>(i);
        for (std::size_t i = start.size() - 1; i > 0; i--) std::swap(start[i], start[rng() % (i + 1)]);
        tour::two_opt_plain(d, start);
        finals.push_back(tour::tour_length(d, start));
    }
    std::sort(finals.begin(), finals.end());
    const auto count_best = std::count(finals.begin(), finals.end(), optimum);
    std::cout << "  mejor " << km(finals.front()) << " km, mediana " << km(finals[finals.size() / 2]) << " km, peor "
              << km(finals.back()) << " km\n";
    std::cout << "  llegaron al óptimo: " << count_best << " de " << finals.size() << '\n';
    std::cout << std::setprecision(3) << "  peor largo / óptimo: "
              << static_cast<double>(finals.back()) / static_cast<double>(optimum) << '\n';
}

Pruebas

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

test_examples.cppDescargar test_examples.cpp
// Las pruebas de la semana 15: comprueban lo que dicen los apuntes. Cada grupo es una función.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <iostream>
#include <numbers>
#include <numeric>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include "../week-10/held_karp.h"
#include "bound.h"
#include "cover.h"
#include "geo.h"
#include "tour.h"

namespace {

using geo::at;
using tour::Matrix;
using tour::Tour;

// Una instancia métrica al azar: puntos en un plano, distancia euclidiana redondeada hacia arriba.
Matrix random_metric(int n, std::mt19937& rng) {
    std::vector<std::pair<int, int>> p;
    for (int i = 0; i < n; i++) p.push_back({static_cast<int>(rng() % 1000), static_cast<int>(rng() % 1000)});
    Matrix d(at(n), std::vector<long long>(at(n), 0));
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            const double dx = p[at(i)].first - p[at(j)].first;
            const double dy = p[at(i)].second - p[at(j)].second;
            d[at(i)][at(j)] = static_cast<long long>(std::ceil(std::sqrt(dx * dx + dy * dy)));
        }
    }
    return d;
}

// Pesos simétricos sin ninguna relación entre sí: no cumplen la desigualdad triangular.
Matrix random_symmetric(int n, std::mt19937& rng) {
    Matrix d(at(n), std::vector<long long>(at(n), 0));
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) d[at(i)][at(j)] = d[at(j)][at(i)] = 1 + static_cast<long long>(rng() % 1000);
    }
    return d;
}

// El árbitro del árbol de expansión mínima: Kruskal con union-find.
long long kruskal_weight(const Matrix& d) {
    const int n = static_cast<int>(d.size());
    std::vector<std::pair<long long, std::pair<int, int>>> edges;
    for (int i = 0; i < n; i++) {
        for (int j = i + 1; j < n; j++) edges.push_back({d[at(i)][at(j)], {i, j}});
    }
    std::sort(edges.begin(), edges.end());
    std::vector<int> parent(at(n));
    std::iota(parent.begin(), parent.end(), 0);
    auto find = [&](int v) {
        while (parent[at(v)] != v) v = parent[at(v)] = parent[at(parent[at(v)])];
        return v;
    };
    long long total = 0;
    for (const auto& [w, e] : edges) {
        const int a = find(e.first);
        const int b = find(e.second);
        if (a != b) {
            parent[at(a)] = b;
            total += w;
        }
    }
    return total;
}

bool triangle_holds(const Matrix& d) {
    const std::size_t n = d.size();
    for (std::size_t a = 0; a < n; a++) {
        for (std::size_t b = 0; b < n; b++) {
            for (std::size_t c = 0; c < n; c++) {
                if (d[a][c] > d[a][b] + d[b][c]) return false;
            }
        }
    }
    return true;
}

void test_distances() {
    const auto all = geo::read_places("capitales.csv");
    assert(all.size() == 26);
    const auto d = geo::distance_matrix(all);
    assert(triangle_holds(d));
    for (std::size_t i = 0; i < all.size(); i++) {
        assert(d[i][i] == 0);
        for (std::size_t j = 0; j < all.size(); j++) {
            assert(d[i][j] == d[j][i]);
            assert(i == j || d[i][j] > 0);
        }
    }
    // Lima y Callao son el par más cercano; Tacna y Tumbes, el más lejano.
    const auto index_of = [&](const std::string& name) {
        for (int i = 0; i < 26; i++) {
            if (all[at(i)].name == name) return i;
        }
        return -1;
    };
    const int lima = index_of("Lima");
    const int callao = index_of("Callao");
    assert(d[at(lima)][at(callao)] == 11596);
    assert(d[at(index_of("Tacna"))][at(index_of("Tumbes"))] == 1953472);
    for (std::size_t i = 0; i < all.size(); i++) {
        for (std::size_t j = i + 1; j < all.size(); j++) assert(d[i][j] >= 11596 && d[i][j] <= 1953472);
    }
    // Redondear hacia arriba mantiene la desigualdad en puntos cualesquiera; al más cercano, no siempre.
    const double degrees = 1000.4 / (geo::EARTH_RADIUS_M * std::numbers::pi / 180.0);
    const geo::Place a{"a", 0, 0};
    const geo::Place b{"b", 0, degrees};
    const geo::Place c{"c", 0, 2 * degrees};
    assert(std::llround(geo::great_circle_m(a, b)) + std::llround(geo::great_circle_m(b, c)) <
           std::llround(geo::great_circle_m(a, c)));
    assert(geo::distance_m(a, b) + geo::distance_m(b, c) >= geo::distance_m(a, c));
    std::mt19937 rng(1);
    for (int round = 0; round < 3000; round++) {
        std::vector<geo::Place> p;
        for (int i = 0; i < 3; i++) {
            p.push_back({"", -90.0 + rng() % 18001 / 100.0, -180.0 + rng() % 36001 / 100.0});
        }
        assert(geo::distance_m(p[0], p[2]) <= geo::distance_m(p[0], p[1]) + geo::distance_m(p[1], p[2]));
    }
}

void test_spanning_tree() {
    std::mt19937 rng(2);
    for (int round = 0; round < 300; round++) {
        const int n = 1 + static_cast<int>(rng() % 25);
        const auto d = round % 2 == 0 ? random_metric(n, rng) : random_symmetric(n, rng);
        assert(tour::minimum_spanning_tree(d).weight == kruskal_weight(d));
    }
}

// Lo que dice la demostración del factor 2, paso por paso.
void check_double_tree(const Matrix& d, long long optimum) {
    const int n = static_cast<int>(d.size());
    const auto tree = tour::minimum_spanning_tree(d);
    const auto walk = tour::euler_walk(tree);
    assert(walk.size() == at(2 * n - 1));
    assert(tour::walk_length(d, walk) == 2 * tree.weight);
    const auto t = tour::shortcut(walk, n);
    assert(tour::is_tour(t, n));
    assert(tree.weight <= optimum);
    assert(tour::tour_length(d, t) <= 2 * tree.weight);
    assert(tour::tour_length(d, t) <= 2 * optimum);
    // 2-opt desde ahí nunca empeora, y mantiene el factor por haber empezado en el árbol.
    auto improved = t;
    const auto moves = tour::two_opt_plain(d, improved);
    assert(tour::is_tour(improved, n));
    assert(tour::is_two_opt_local_optimum(d, improved));
    assert(optimum <= tour::tour_length(d, improved));
    assert(tour::tour_length(d, improved) <= tour::tour_length(d, t));
    long long previous = tour::tour_length(d, t);
    for (const auto& move : moves) {
        assert(move.length_after < previous);
        previous = move.length_after;
    }
}

void test_factor_two() {
    const auto all = geo::read_places("capitales.csv");
    for (int n = 3; n <= 14; n++) {
        const auto d = geo::distance_matrix(geo::first_places(all, n));
        check_double_tree(d, tsp::held_karp(d));
    }
    std::mt19937 rng(3);
    for (int round = 0; round < 300; round++) {
        const int n = 3 + static_cast<int>(rng() % 9);
        const auto d = random_metric(n, rng);
        assert(triangle_holds(d));
        check_double_tree(d, tsp::held_karp(d));
    }
}

void test_no_guarantee_without_triangle() {
    // El ciclo desordenado del ejemplo: el árbol duplicado da 30 y el óptimo es 6, cinco veces más.
    const int n = 6;
    const long long heavy = 2 * n + 1;
    Matrix w(at(n), std::vector<long long>(at(n), heavy));
    for (int v = 0; v < n; v++) w[at(v)][at(v)] = 0;
    const std::vector<std::pair<int, int>> edges = {{0, 3}, {3, 1}, {1, 5}, {5, 2}, {2, 4}, {4, 0}};
    for (const auto& [u, v] : edges) w[at(u)][at(v)] = w[at(v)][at(u)] = 1;
    assert(!triangle_holds(w));
    assert(tsp::held_karp(w) == 6);
    assert(tour::tour_length(w, tour::double_tree(w)) == 30);
}

// ¿Hay ciclo hamiltoniano? Árbitro: prueba todos los órdenes.
bool has_hamiltonian_cycle(int n, const std::vector<std::vector<char>>& adjacent) {
    std::vector<int> order(at(n));
    std::iota(order.begin(), order.end(), 0);
    do {
        bool ok = true;
        for (int i = 0; i < n && ok; i++) ok = adjacent[at(order[at(i)])][at(order[at((i + 1) % n)])] != 0;
        if (ok) return true;
    } while (std::next_permutation(order.begin() + 1, order.end()));
    return false;
}

void test_hamilton_reduction() {
    std::mt19937 rng(4);
    int with_cycle = 0;
    int without_cycle = 0;
    for (int round = 0; round < 600; round++) {
        const int n = 3 + static_cast<int>(rng() % 5);
        const long long alpha = 1 + static_cast<long long>(rng() % 4);
        const long long heavy = alpha * n + 1;
        std::vector<std::vector<char>> adjacent(at(n), std::vector<char>(at(n), 0));
        Matrix w(at(n), std::vector<long long>(at(n), heavy));
        for (int u = 0; u < n; u++) {
            w[at(u)][at(u)] = 0;
            for (int v = u + 1; v < n; v++) {
                if (rng() % 2 == 0) {
                    adjacent[at(u)][at(v)] = adjacent[at(v)][at(u)] = 1;
                    w[at(u)][at(v)] = w[at(v)][at(u)] = 1;
                }
            }
        }
        const long long optimum = tsp::held_karp(w);
        if (has_hamiltonian_cycle(n, adjacent)) {
            with_cycle++;
            assert(optimum == n);
        } else {
            without_cycle++;
            assert(optimum >= heavy + n - 1);
            assert(optimum > alpha * n);
        }
    }
    assert(with_cycle > 50 && without_cycle > 50);
}

void test_two_opt_depends_on_start() {
    // Sin el árbol no hay garantía, y dos inicios pueden terminar en óptimos locales distintos.
    const auto d = geo::distance_matrix(geo::first_places(geo::read_places("capitales.csv"), 20));
    const long long optimum = tsp::held_karp(d);
    auto from_tree = tour::double_tree(d);
    tour::two_opt_plain(d, from_tree);
    assert(tour::tour_length(d, from_tree) == 4867292);
    assert(optimum == 4640216);
    auto from_neighbor = tour::nearest_neighbor(d);
    tour::two_opt_plain(d, from_neighbor);
    assert(tour::tour_length(d, from_neighbor) == 4827795);
}

void test_tour_helpers() {
    assert(tour::is_tour({2, 0, 1}, 3));
    assert(!tour::is_tour({2, 0, 0}, 3));
    assert(!tour::is_tour({2, 0}, 3));
    assert(!tour::is_tour({2, 0, 3}, 3));
}

// Lo que completion_bound promete: nunca pasa de la mejor forma de terminar.
void test_bound_is_sound() {
    std::mt19937 rng(5);
    for (int round = 0; round < 200; round++) {
        const int n = 3 + static_cast<int>(rng() % 6);
        const auto d = round % 2 == 0 ? random_metric(n, rng) : random_symmetric(n, rng);
        const int last = 1 + static_cast<int>(rng() % static_cast<unsigned>(n - 1));
        std::vector<int> rest;
        for (int v = 1; v < n; v++) {
            if (v != last && rng() % 2 == 0) rest.push_back(v);
        }
        std::sort(rest.begin(), rest.end());
        long long best = bound::NO_TOUR;
        auto order = rest;
        do {
            long long total = 0;
            int current = last;
            for (int v : order) {
                total += d[at(current)][at(v)];
                current = v;
            }
            best = std::min(best, total + d[at(current)][0]);
        } while (std::next_permutation(order.begin(), order.end()));
        assert(bound::completion_bound(d, last, rest) <= best);
    }
}

void test_search_is_exact() {
    std::mt19937 rng(6);
    for (int round = 0; round < 200; round++) {
        const int n = 1 + static_cast<int>(rng() % 9);
        const auto d = round % 2 == 0 ? random_metric(n, rng) : random_symmetric(n, rng);
        const long long optimum = tsp::held_karp(d);
        const auto none = bound::solve(d, bound::Pruning::none);
        const auto partial = bound::solve(d, bound::Pruning::partial);
        const auto spanning = bound::solve(d, bound::Pruning::spanning);
        assert(none.cost == optimum && partial.cost == optimum && spanning.cost == optimum);
        assert(partial.nodes <= none.nodes && spanning.nodes <= none.nodes);
    }
    const auto all = geo::read_places("capitales.csv");
    // Las cuentas de la tabla de los apuntes. El árbol sin podar tiene una fórmula.
    const auto d10 = geo::distance_matrix(geo::first_places(all, 10));
    long long expected = 0;
    long long level = 1;
    for (int k = 0; k <= 9; k++) {
        expected += level;
        level *= 9 - k;
    }
    assert(bound::solve(d10, bound::Pruning::none).nodes == expected);
    assert(expected == 986410);
    assert(bound::solve(d10, bound::Pruning::partial).nodes == 146810);
    assert(bound::solve(d10, bound::Pruning::spanning).nodes == 3157);
    assert(bound::solve(d10, bound::Pruning::spanning).cost == 3167199);
    // El corte por cantidad de nodos avisa que el costo no vale.
    assert(!bound::solve(d10, bound::Pruning::none, 100).finished);
    const auto d15 = geo::distance_matrix(geo::first_places(all, 15));
    assert(bound::solve(d15, bound::Pruning::spanning).cost == tsp::held_karp(d15));
}

void test_cover() {
    std::mt19937 rng(7);
    for (int round = 0; round < 1500; round++) {
        const int n = 1 + static_cast<int>(rng() % 11);
        cover::Edges edges;
        for (int u = 0; u < n; u++) {
            for (int v = u + 1; v < n; v++) {
                if (rng() % 3 == 0) edges.push_back({u, v});
            }
        }
        std::shuffle(edges.begin(), edges.end(), rng);
        const int optimum = cover::minimum_cover_size(n, edges);
        const auto matched = cover::matching_cover(n, edges);
        const int picked = static_cast<int>(matched.cover.size());
        assert(cover::is_cover(matched.cover, n, edges));
        assert(picked == 2 * static_cast<int>(matched.matching.size()));
        // Aristas sin nodos en común: el óptimo necesita un nodo distinto por cada una.
        std::vector<char> used(at(n), 0);
        for (const auto& [u, v] : matched.matching) {
            assert(used[at(u)] == 0 && used[at(v)] == 0);
            used[at(u)] = used[at(v)] = 1;
        }
        // Maximal: toda arista toca al matching.
        for (const auto& [u, v] : edges) assert(used[at(u)] != 0 || used[at(v)] != 0);
        assert(static_cast<int>(matched.matching.size()) <= optimum);
        assert(picked <= 2 * optimum);
        assert(cover::is_cover(cover::greedy_degree_cover(n, edges), n, edges));
    }
    // La estrella: una sola punta elige todas las hojas. Una arista: el factor 2 se alcanza.
    const cover::Edges star = {{0, 1}, {0, 2}, {0, 3}, {0, 4}, {0, 5}, {0, 6}};
    assert(cover::one_endpoint_cover(7, star).size() == 6);
    assert(cover::minimum_cover_size(7, star) == 1);
    assert(cover::matching_cover(2, {{0, 1}}).cover.size() == 2);
    assert(cover::minimum_cover_size(2, {{0, 1}}) == 1);
    // Maximal sin ser máximo: en el camino 0-1-2-3, la arista 1-2 ya no deja meter otra.
    assert(cover::matching_cover(4, {{1, 2}, {0, 1}, {2, 3}}).matching.size() == 1);
    // La trampa: el mayor grado elige toda la derecha, y el matching demuestra que el óptimo es k.
    for (int k : {6, 12, 24, 48, 96, 192}) {
        const auto trap = cover::greedy_trap(k);
        int count = 0;
        for (int i = 2; i <= k; i++) count += k / i;
        assert(trap.right_nodes == count);
        const auto chosen = cover::greedy_degree_cover(trap.nodes, trap.edges);
        assert(static_cast<int>(chosen.size()) == count);
        for (int v : chosen) assert(v >= k);
        assert(static_cast<int>(cover::matching_cover(trap.nodes, trap.edges).matching.size()) == k);
    }
    assert(cover::greedy_degree_cover(84, cover::greedy_trap(24).edges).size() == 60);
}

// Programación lineal, solo con cuentas a mano: no hay un solucionador en el entorno.
void test_linear_programming_examples() {
    // máx 3x + 2y con x + y ≤ 4, x ≤ 3, y ≤ 3, x, y ≥ 0. Se mira cada esquina y una grilla fina.
    const auto feasible = [](double x, double y) { return x >= 0 && y >= 0 && x + y <= 4 && x <= 3 && y <= 3; };
    const std::vector<std::pair<double, double>> corners = {{0, 0}, {3, 0}, {3, 1}, {1, 3}, {0, 3}};
    double best = 0;
    for (const auto& [x, y] : corners) {
        assert(feasible(x, y));
        best = std::max(best, 3 * x + 2 * y);
    }
    assert(best == 11);
    for (int i = 0; i <= 400; i++) {
        for (int j = 0; j <= 400; j++) {
            const double x = i / 100.0;
            const double y = j / 100.0;
            if (feasible(x, y)) assert(3 * x + 2 * y <= best);
        }
    }
    // El triángulo: con ½ en cada nodo se cumple toda arista y la suma es 3/2. Con enteros, 2.
    const cover::Edges triangle = {{0, 1}, {1, 2}, {0, 2}};
    assert(0.5 + 0.5 >= 1 && 3 * 0.5 == 1.5);
    assert(cover::minimum_cover_size(3, triangle) == 2);
}

// Las cuentas de la pregunta 5 de repaso: 12 clientes.
void test_review_numbers() {
    long long transitions = 0;
    std::mt19937 rng(8);
    const auto d = random_metric(12, rng);
    static_cast<void>(tsp::held_karp(d, &transitions));
    assert(transitions == 56320);
    long long factorial = 1;
    for (int i = 2; i <= 11; i++) factorial *= i;
    assert(factorial == 39916800);
}

}  // namespace

int main() {
    test_distances();
    test_spanning_tree();
    test_factor_two();
    test_no_guarantee_without_triangle();
    test_hamilton_reduction();
    test_two_opt_depends_on_start();
    test_tour_helpers();
    test_bound_is_sound();
    test_search_is_exact();
    test_cover();
    test_linear_programming_examples();
    test_review_numbers();
    std::cout << "Todas las pruebas de la semana 15 pasaron.\n";
}

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/peru_exact/MakefileDescargar lab/peru_exact/Makefile
TIEMPO = 2
VERIFICADOR = verificador.cpp
include ../../../lab.mk
lab/peru_exact/README.mdDescargar lab/peru_exact/README.md
# La gira más corta

Un equipo de salud sale de una ciudad, visita una sola vez cada una de las otras y vuelve a la ciudad de partida. Tiene
pocas ciudades y va a pagar cada kilómetro durante años, así que quiere **el recorrido más corto que existe**. No uno
bueno: el mejor, y además saber cuál es.

Las coordenadas de las capitales regionales están en [`capitales.csv`](../../capitales.csv), tomadas de GeoNames, con
licencia CC BY 4.0. La fuente completa está en [`capitales.md`](../../capitales.md). Algunos casos usan esas ciudades, y
otros, puntos ficticios.

La distancia entre dos ciudades **no** es la de una carretera: es la del gran círculo, el camino más corto sobre una
esfera de radio R = 6371 km. Se mide así, con la fórmula de haversine, con φ y λ en radianes:

```text
h = sin²((φ₂ − φ₁) / 2) + cos φ₁ · cos φ₂ · sin²((λ₂ − λ₁) / 2)
d = ⌈2 · R · arcsen(√h)⌉          con R = 6371000 metros
```

Es decir, en metros, redondeada **hacia arriba** al entero. Calculen con `double`. Entre Lima y Callao, en este caso,
d = 11596 m. Como cada tramo es un entero, también lo es el largo de un recorrido, que es la suma de sus n tramos,
incluido el que vuelve al inicio. Con n = 2 se va y se vuelve por el mismo tramo, así que se cuenta dos veces.

## Entrada

La primera línea tiene n, la cantidad de ciudades. Siguen n líneas, una por ciudad, cada una con la latitud y la longitud
en grados, y después el nombre, que puede tener espacios. Las ciudades se numeran de 0 a n − 1, en el orden en que
aparecen. Dos ciudades pueden estar en el mismo punto, y entonces su distancia es 0.

## Salida

- Una línea con el largo mínimo, en metros.
- Una línea con los n números de las ciudades, en el orden en que se visitan. Se vuelve de la última a la primera sin
  escribirla otra vez.

Puede empezar en cualquier ciudad y puede ir en cualquier sentido, porque hay varios recorridos correctos: un verificador
revisa su respuesta. Comprueba que el largo sea el mínimo, que aparezca cada ciudad una vez y que el largo del recorrido
escrito, medido de nuevo con la fórmula, sea el que ustedes dicen.

## Límites

- 1 ≤ n ≤ 20
- −90 ≤ latitud ≤ 90, y −180 ≤ longitud ≤ 180, con 5 decimales a lo sumo
- Memoria: 256 MiB

## Ejemplo

Entrada:

```text
4
-12.04318 -77.02824 Lima
-12.05162 -77.13452 Callao
-12.06866 -75.21027 Huancayo
-14.07538 -75.73422 Ica
```

Salida:

```text
710919
1 3 2 0
```

El recorrido es Callao, Ica, Huancayo, Lima, y de vuelta a Callao. Medido en el otro sentido, o desde otra ciudad, también
vale.

## Tiempo

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

El juez no mide la memoria. Para comprobarla por su cuenta, en bash, desde esta carpeta y con los casos ya armados por
`make test` (`build/casos`):

```sh
bash -c 'ulimit -v 262144; build/solution < build/casos/13.in'
```

Ese límite cuenta todo lo que el programa pide, en KiB: 262144 son 256 MiB. Si no le alcanza, el programa termina con error.
lab/peru_exact/solution.cppDescargar lab/peru_exact/solution.cpp
// La gira más corta: lee las ciudades y responde con el orden de la entrada, sin medir nada.
// Casi nunca es la respuesta correcta. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

int main() {
    int n = 0;
    std::cin >> n;
    std::vector<double> latitude(static_cast<std::size_t>(n));
    std::vector<double> longitude(static_cast<std::size_t>(n));
    std::vector<std::string> name(static_cast<std::size_t>(n));
    for (std::size_t i = 0; i < name.size(); i++) {
        std::cin >> latitude[i] >> longitude[i];
        std::getline(std::cin, name[i]);  // el nombre puede tener espacios
    }

    // Por hacer: calcular las distancias y el mejor recorrido.
    std::cout << 0 << '\n';
    for (int v = 0; v < n; v++) std::cout << v << (v + 1 == n ? "" : " ");
    std::cout << '\n';
}
lab/peru_exact/verificador.cppDescargar lab/peru_exact/verificador.cpp
// Verificador de "La gira más corta": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA está solo el largo mínimo. Comprueba que salga ese largo, que el recorrido pase una
// vez por cada ciudad y que su largo, medido aquí con la fórmula del enunciado, sea el que se escribió.
// Termina con 0 si todo está bien, y con 1 y un mensaje si no.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <numbers>
#include <string>
#include <vector>

namespace {

struct Place {
    double latitude;
    double longitude;
};

[[noreturn]] void reject(const std::string& reason) {
    std::cout << reason << '\n';
    std::exit(1);
}

// Convierte una palabra en número sin riesgo de overflow: solo dígitos, y a lo sumo 18.
bool parse_number(const std::string& word, long long& value) {
    if (word.empty() || word.size() > 18) return false;
    value = 0;
    for (char c : word) {
        if (c < '0' || c > '9') return false;
        value = value * 10 + (c - '0');
    }
    return true;
}

long long distance_m(const Place& a, const Place& b) {
    const double to_radians = std::numbers::pi / 180.0;
    const double phi1 = a.latitude * to_radians;
    const double phi2 = b.latitude * to_radians;
    const double half_dphi = (phi2 - phi1) / 2.0;
    const double half_dlambda = (b.longitude - a.longitude) * to_radians / 2.0;
    const double h = std::sin(half_dphi) * std::sin(half_dphi) +
                     std::cos(phi1) * std::cos(phi2) * std::sin(half_dlambda) * std::sin(half_dlambda);
    return static_cast<long long>(std::ceil(2.0 * 6371000.0 * std::asin(std::sqrt(std::min(1.0, h)))));
}

}  // namespace

int main(int argc, char* argv[]) {
    if (argc != 4) reject("uso: verificador ENTRADA ESPERADA SALIDA");
    std::ifstream input(argv[1]);
    std::ifstream expected(argv[2]);
    std::ifstream output(argv[3]);

    std::size_t n = 0;
    input >> n;
    std::vector<Place> places(n);
    for (auto& place : places) {
        std::string name;
        input >> place.latitude >> place.longitude;
        std::getline(input, name);
    }
    long long minimum = 0;
    expected >> minimum;

    std::string word;
    long long length = 0;
    if (!(output >> word) || !parse_number(word, length)) reject("la primera palabra no es un largo válido");
    if (length != minimum) reject("largo mínimo esperado: " + std::to_string(minimum) + ", salió " + word);

    std::vector<std::size_t> tour;
    std::vector<char> seen(n, 0);
    for (std::size_t k = 0; k < n; k++) {
        long long city = 0;
        if (!(output >> word)) reject("faltan ciudades en el recorrido");
        if (!parse_number(word, city)) reject("número de ciudad inválido: " + word);
        if (city < 0 || city >= static_cast<long long>(n)) reject("ciudad fuera de rango: " + word);
        const std::size_t v = static_cast<std::size_t>(city);
        if (seen[v] != 0) reject("la ciudad " + word + " aparece dos veces");
        seen[v] = 1;
        tour.push_back(v);
    }
    if (output >> word) reject("sobra texto después del recorrido: " + word);

    long long measured = 0;
    for (std::size_t k = 0; k < n; k++) measured += distance_m(places[tour[k]], places[tour[(k + 1) % n]]);
    if (measured != length) reject("el recorrido mide " + std::to_string(measured) + ", no " + std::to_string(length));
    return 0;
}
lab/peru_tour/MakefileDescargar lab/peru_tour/Makefile
TIEMPO = 2
VERIFICADOR = verificador.cpp
include ../../../lab.mk
lab/peru_tour/README.mdDescargar lab/peru_tour/README.md
# Una gira para muchos lugares

Una empresa reparte vacunas por cientos de lugares. Sale de un lugar, pasa una sola vez por cada uno y vuelve al inicio.
Ya no le hace falta el recorrido más corto que existe, que nadie va a poder calcular con tantos lugares a tiempo. Le basta
un recorrido que cumpla dos condiciones, y que las pueda comprobar cualquiera:

1. **Que tenga un tope.** Sea A el menor largo total de un conjunto de tramos que deje conectados a todos los lugares, de
   modo que de cada uno se pueda llegar a cualquier otro pasando por esos tramos. El largo del recorrido no debe pasar de 2A.
2. **Que no se pueda arreglar a la vista.** Tomen dos tramos del recorrido que no tengan lugares en común, de a hacia b y de
   c hacia d, con el recorrido escrito en este orden: a, b, …, c, d, …. Si cambiamos esos dos tramos por los de a hacia c y
   de b hacia d, recorriendo lo de en medio al revés, sale otro recorrido. Ninguno de esos cambios debe acortarlo.

Hay casos con las capitales regionales, de [`capitales.csv`](../../capitales.csv), tomadas de GeoNames, con licencia CC BY
4.0, y la fuente completa en [`capitales.md`](../../capitales.md). Otros usan lugares ficticios.

La distancia entre dos lugares **no** es la de una carretera: es la del gran círculo, el camino más corto sobre una
esfera de radio R = 6371 km. Se mide así, con la fórmula de haversine, con φ y λ en radianes:

```text
h = sin²((φ₂ − φ₁) / 2) + cos φ₁ · cos φ₂ · sin²((λ₂ − λ₁) / 2)
d = ⌈2 · R · arcsen(√h)⌉          con R = 6371000 metros
```

Es decir, en metros, redondeada **hacia arriba** al entero. Calculen con `double`. Entre Lima y Callao, en este caso,
d = 11596 m. Como cada tramo es un entero, también lo es el largo de un recorrido, que es la suma de sus n tramos,
incluido el que vuelve al inicio.

## Entrada

La primera línea tiene n, la cantidad de lugares. Siguen n líneas, una por lugar, cada una con la latitud y la longitud en
grados, y después el nombre, que puede tener espacios. Los lugares se numeran de 0 a n − 1, en el orden en que aparecen.
Dos lugares pueden estar en el mismo punto, y entonces su distancia es 0.

## Salida

- Una línea con el largo del recorrido, en metros.
- Una línea con los n números de los lugares, en el orden en que se visitan. Se vuelve del último al primero sin
  escribirlo otra vez.

Puede haber muchos recorridos correctos: un verificador revisa su respuesta. Comprueba que aparezca cada lugar una vez, que
el largo, medido de nuevo con la fórmula, sea el que ustedes dicen, y las dos condiciones.

## Límites

- 1 ≤ n ≤ 1500
- −90 ≤ latitud ≤ 90, y −180 ≤ longitud ≤ 180, con 5 decimales a lo sumo
- Memoria: 256 MiB

## Ejemplo

Entrada:

```text
5
-12.04318 -77.02824 Lima
-12.05162 -77.13452 Callao
-12.06866 -75.21027 Huancayo
-13.53188 -71.96701 Cusco
-16.39899 -71.53747 Arequipa
```

Salida:

```text
1691829
0 1 4 3 2
```

El orden de la entrada, `0 1 2 3 4`, mide 1695072 m y no cumple la condición 2: cambiar los tramos Callao-Huancayo y
Arequipa-Lima por Callao-Arequipa y Huancayo-Lima lo acorta. En este ejemplo A = 918911 m, así que 2A = 1837822 m, y la salida no pasa de ahí.

## Tiempo

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

El juez no mide la memoria. Para comprobarla por su cuenta, en bash, desde esta carpeta y con los casos ya armados por
`make test` (`build/casos`):

```sh
bash -c 'ulimit -v 262144; build/solution < build/casos/13.in > /dev/null'
```

Ese límite cuenta todo lo que el programa pide, en KiB: 262144 son 256 MiB. Si no le alcanza, el programa termina con error.
lab/peru_tour/solution.cppDescargar lab/peru_tour/solution.cpp
// Una gira para muchos lugares: lee los lugares y responde con el orden de la entrada, sin medir
// nada. Casi nunca cumple lo que se pide. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <string>
#include <vector>

int main() {
    int n = 0;
    std::cin >> n;
    std::vector<double> latitude(static_cast<std::size_t>(n));
    std::vector<double> longitude(static_cast<std::size_t>(n));
    std::vector<std::string> name(static_cast<std::size_t>(n));
    for (std::size_t i = 0; i < name.size(); i++) {
        std::cin >> latitude[i] >> longitude[i];
        std::getline(std::cin, name[i]);  // el nombre puede tener espacios
    }

    // Por hacer: calcular las distancias y armar un recorrido que cumpla las dos condiciones.
    std::cout << 0 << '\n';
    for (int v = 0; v < n; v++) std::cout << v << (v + 1 == n ? "" : " ");
    std::cout << '\n';
}
lab/peru_tour/verificador.cppDescargar lab/peru_tour/verificador.cpp
// Verificador de "Una gira para muchos lugares": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA está A, el peso de la red más barata que conecta todos los lugares. El verificador lo
// recalcula. Comprueba que el recorrido pase una vez por cada lugar, que su largo, medido aquí, sea
// el que se escribió, que no pase de 2A y que ningún cambio de dos tramos lo acorte.
// Termina con 0 si todo está bien, con 1 y un mensaje si no, y con 2 si el caso está mal armado.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <limits>
#include <numbers>
#include <string>
#include <vector>

namespace {

struct Place {
    double latitude;
    double longitude;
};

[[noreturn]] void reject(const std::string& reason) {
    std::cout << reason << '\n';
    std::exit(1);
}

bool parse_number(const std::string& word, long long& value) {
    if (word.empty() || word.size() > 18) return false;
    value = 0;
    for (char c : word) {
        if (c < '0' || c > '9') return false;
        value = value * 10 + (c - '0');
    }
    return true;
}

long long distance_m(const Place& a, const Place& b) {
    const double to_radians = std::numbers::pi / 180.0;
    const double phi1 = a.latitude * to_radians;
    const double phi2 = b.latitude * to_radians;
    const double half_dphi = (phi2 - phi1) / 2.0;
    const double half_dlambda = (b.longitude - a.longitude) * to_radians / 2.0;
    const double h = std::sin(half_dphi) * std::sin(half_dphi) +
                     std::cos(phi1) * std::cos(phi2) * std::sin(half_dlambda) * std::sin(half_dlambda);
    return static_cast<long long>(std::ceil(2.0 * 6371000.0 * std::asin(std::sqrt(std::min(1.0, h)))));
}

long long spanning_weight(const std::vector<long long>& d, std::size_t n) {
    const long long INF = std::numeric_limits<long long>::max();
    std::vector<long long> best(n, INF);
    std::vector<char> inside(n, 0);
    if (n > 0) best[0] = 0;
    long long weight = 0;
    for (std::size_t round = 0; round < n; round++) {
        std::size_t v = n;
        for (std::size_t u = 0; u < n; u++) {
            if (inside[u] == 0 && (v == n || best[u] < best[v])) v = u;
        }
        inside[v] = 1;
        weight += best[v];
        for (std::size_t u = 0; u < n; u++) {
            if (inside[u] == 0) best[u] = std::min(best[u], d[v * n + u]);
        }
    }
    return weight;
}

}  // namespace

int main(int argc, char* argv[]) {
    if (argc != 4) reject("uso: verificador ENTRADA ESPERADA SALIDA");
    std::ifstream input(argv[1]);
    std::ifstream expected(argv[2]);
    std::ifstream output(argv[3]);

    std::size_t n = 0;
    input >> n;
    std::vector<Place> places(n);
    for (auto& place : places) {
        std::string name;
        input >> place.latitude >> place.longitude;
        std::getline(input, name);
    }
    std::vector<long long> d(n * n, 0);
    for (std::size_t i = 0; i < n; i++) {
        for (std::size_t j = i + 1; j < n; j++) d[i * n + j] = d[j * n + i] = distance_m(places[i], places[j]);
    }
    const long long network = spanning_weight(d, n);
    long long written = 0;
    expected >> written;
    if (written != network) {
        std::cout << "caso mal armado: el archivo dice " << written << " y el cálculo da " << network << '\n';
        return 2;
    }

    std::string word;
    long long length = 0;
    if (!(output >> word) || !parse_number(word, length)) reject("la primera palabra no es un largo válido");

    std::vector<std::size_t> tour;
    std::vector<char> seen(n, 0);
    for (std::size_t k = 0; k < n; k++) {
        long long place = 0;
        if (!(output >> word)) reject("faltan lugares en el recorrido");
        if (!parse_number(word, place)) reject("número de lugar inválido: " + word);
        if (place < 0 || place >= static_cast<long long>(n)) reject("lugar fuera de rango: " + word);
        const std::size_t v = static_cast<std::size_t>(place);
        if (seen[v] != 0) reject("el lugar " + word + " aparece dos veces");
        seen[v] = 1;
        tour.push_back(v);
    }
    if (output >> word) reject("sobra texto después del recorrido: " + word);

    long long measured = 0;
    for (std::size_t k = 0; k < n; k++) measured += d[tour[k] * n + tour[(k + 1) % n]];
    if (measured != length) reject("el recorrido mide " + std::to_string(measured) + ", no " + std::to_string(length));
    if (length > 2 * network) {
        reject("el largo " + std::to_string(length) + " pasa de 2A = " + std::to_string(2 * network));
    }

    // Cada par de tramos sin lugares en común: (a, a + 1) y (b, b + 1).
    for (std::size_t a = 0; a < n; a++) {
        for (std::size_t b = a + 2; b < n; b++) {
            if (a == 0 && b == n - 1) continue;
            const std::size_t p = tour[a];
            const std::size_t q = tour[a + 1];
            const std::size_t r = tour[b];
            const std::size_t s = tour[(b + 1) % n];
            const long long now = d[p * n + q] + d[r * n + s];
            const long long changed = d[p * n + r] + d[q * n + s];
            if (changed < now) {
                reject("se puede acortar: cambiar los tramos de las posiciones " + std::to_string(a) + " y " +
                       std::to_string(b) + " ahorra " + std::to_string(now - changed) + " m");
            }
        }
    }
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 15: qué hacer con un problema difícil. Poda, aproximación y heurísticas sobre las capitales
# regionales del Perú. 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  corre las mediciones de tiempo y de memoria, con -O2
#   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 = geo.h tour.h bound.h cover.h ../week-10/held_karp.h

PROGRAMS = metric_demo cover_demo double_tree_demo hamilton_demo test_examples
# Estos hacen muchas cuentas: van con -O2 aunque no midan tiempos.
FAST = two_opt_demo bound_demo compare_bench bound_bench memory_bench

.PHONY: all test bench clean

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

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

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

$(BUILD):
	mkdir -p $@

test: $(BUILD)/test_examples
	$(BUILD)/test_examples

bench: $(BUILD)/compare_bench $(BUILD)/bound_bench $(BUILD)/memory_bench
	$(BUILD)/compare_bench
	$(BUILD)/bound_bench
	$(BUILD)/memory_bench

clean:
	rm -rf $(BUILD)