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
- 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.
- 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.
- 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.
- 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
- Demuestren que un matching maximal da una cobertura de vértices con factor 2.
- ¿Por qué el árbol de expansión mínima es una cota inferior para el viajero?
- Demuestren el factor 2 de duplicar el árbol en un caso métrico. ¿Dónde se usa la desigualdad triangular?
- ¿Cuál es la diferencia entre una aproximación y una heurística?
- 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.
// 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);
}// 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';
}
}// 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 boundcapital,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# 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.// 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';
}
}// 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';
}// 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// 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";
}// 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// 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}});
}// 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';
}
}// 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';
}// 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// 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.
// 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.
TIEMPO = 2
VERIFICADOR = verificador.cpp
include ../../../lab.mk# 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.// 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';
}// 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;
}TIEMPO = 2
VERIFICADOR = verificador.cpp
include ../../../lab.mk# 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.// 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';
}// 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.
# 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)