Semana 10 — Caminos mínimos con programación dinámica
Objetivos
- Derivar Bellman-Ford como programación dinámica y demostrarlo.
- Detectar un ciclo negativo.
- Calcular caminos mínimos entre todos los pares con Floyd-Warshall.
- Elegir un algoritmo de caminos mínimos según el grafo.
Sesión A — Bellman-Ford como programación dinámica
Lectura: Apuntes, semana 10.
Apoyo: Erickson, secs. 8.5 (Directed Acyclic Graphs: Depth-First Search) y 8.7 (Relax ALL the Edges: Bellman-Ford). CLRS, capítulo Single-Source Shortest Paths, 4.ª edición.
Temas:
- Por qué con aristas negativas hace falta otra idea.
- El subproblema: distancia usando a lo sumo i aristas. Costo O(VE).
- Sin ciclos negativos, un camino mínimo tiene a lo sumo V − 1 aristas.
- Detectar un ciclo negativo: si la vuelta V todavía mejora algo. Recuperar el ciclo con los predecesores.
- Por qué basta con una sola fila, y por qué eso no rompe la demostración.
- Dónde se usa: el vector de distancias de los routers, que verán en Redes de Computadoras.
- Error común: partir de un valor “infinito” y sumarle a un nodo no alcanzado, con overflow incluido.
Sesión B — Floyd-Warshall · ✂️ Programación dinámica con máscaras de bits
Lectura: Apuntes, semana 10.
Apoyo: Erickson, cap. 9. CLRS, capítulo All-Pairs Shortest Paths, 4.ª edición.
Temas:
- Todos los pares: correr Dijkstra o Bellman-Ford V veces, y cuándo conviene cada uno.
- Floyd-Warshall: el subproblema con nodos intermedios permitidos. Costo Θ(V³).
- Por qué el ciclo de k va por fuera.
- Cuadro de decisión: BFS, DAG, Dijkstra, Bellman-Ford o Floyd-Warshall.
- ✂️ Máscaras de bits: un subconjunto como un entero. Held-Karp para el problema del viajero, en O(n²·2ⁿ): exponencial, pero mucho mejor que n!.
- Error común: poner el ciclo de k por dentro, un programa que compila, da respuestas plausibles y está mal.
Laboratorio — Arbitraje con tipos de cambio
- Con una tabla de tipos de cambio entre monedas, decidir si existe una cadena de cambios que termine con más dinero del inicial, e imprimir el ciclo. Un verificador comprueba la ganancia.
- Reducir el problema: un producto mayor que 1 equivale a una suma negativa de −log(tasa).
- Implementar Bellman-Ford y recuperar el ciclo.
- Pensar en los errores de redondeo: ¿cuándo declara arbitraje una tabla que no lo tiene? Agregar la tolerancia que pide el enunciado.
Preguntas de repaso
- Corran Bellman-Ford a mano sobre un grafo de 4 nodos con una arista negativa.
- ¿Por qué un camino mínimo sin ciclos negativos tiene a lo sumo V − 1 aristas?
- ¿Qué algoritmo usarían en cada caso: grafo sin pesos; DAG con pesos negativos; red de carreteras; todos los pares en un grafo pequeño y denso?
- Escriban el subproblema de Floyd-Warshall. ¿Por qué el orden de los ciclos importa?
- ¿Cómo se detecta un ciclo negativo con la matriz de Floyd-Warshall?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (13)Pruebas (1)Laboratorio (4)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Mide las tres formas de calcular todos los pares. Con -O2, una corrida, en la máquina del momento.
#include <chrono>
#include <cstdio>
#include <functional>
#include <random>
#include <vector>
#include "all_pairs.h"
namespace {
double seconds_of(const std::function<void()>& work) {
const auto start = std::chrono::steady_clock::now();
work();
return std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
}
std::vector<Edge> random_sparse(int n, int per_node, std::mt19937& gen) {
std::vector<Edge> edges;
for (int i = 0; i < n * per_node; i++) {
const int a = static_cast<int>(gen() % static_cast<unsigned>(n));
const int b = static_cast<int>(gen() % static_cast<unsigned>(n));
if (a != b) edges.push_back({a, b, static_cast<long long>(gen() % 1000) + 1});
}
return edges;
}
std::vector<Edge> complete(int n, std::mt19937& gen) {
std::vector<Edge> edges;
for (int a = 0; a < n; a++) {
for (int b = 0; b < n; b++) {
if (a != b) edges.push_back({a, b, static_cast<long long>(gen() % 1000) + 1});
}
}
return edges;
}
// Una cadena con sus aristas al revés: el peor orden para una sola fila.
std::vector<Edge> reversed_chain(int n) {
std::vector<Edge> edges;
for (int a = n - 2; a >= 0; a--) edges.push_back({a, a + 1, 1});
return edges;
}
void measure(const char* family, int n, const std::vector<Edge>& edges) {
Matrix a;
Matrix b;
FloydResult c;
const double t_dijkstra = seconds_of([&] { a = all_pairs_dijkstra(n, edges); });
const double t_bellman = seconds_of([&] { b = all_pairs_bellman_ford(n, edges); });
const double t_floyd = seconds_of([&] { c = floyd_warshall(n, edges); });
std::printf("%-16s %5d %8zu %10.3f %10.3f %10.3f %s\n", family, n, edges.size(), t_dijkstra, t_bellman, t_floyd,
a == b && b == c.dist ? "iguales" : "DISTINTOS");
}
} // namespace
int main() {
std::mt19937 gen(2026);
std::printf("%-16s %5s %8s %10s %10s %10s\n", "grafo", "V", "E", "V Dijkstra", "V B-F", "F-W");
for (int n : {100, 200, 400}) measure("disperso, E=4V", n, random_sparse(n, 4, gen));
for (int n : {100, 200, 400}) measure("completo", n, complete(n, gen));
for (int n : {200, 400, 800}) measure("cadena al revés", n, reversed_chain(n));
}// Caminos mínimos entre todos los pares: Floyd-Warshall bien escrito, con el ciclo de k por dentro,
// y correr un algoritmo de un origen V veces.
#pragma once
#include <cstddef>
#include <vector>
#include "shortest_paths.h"
using Matrix = std::vector<std::vector<long long>>;
// El estado inicial, d_0: sin nodos intermedios.
inline Matrix initial_matrix(int n, const std::vector<Edge>& edges) {
Matrix d(idx(n), std::vector<long long>(idx(n), INF));
for (int v = 0; v < n; v++) d[idx(v)][idx(v)] = 0;
for (const Edge& e : edges) {
if (e.weight < d[idx(e.from)][idx(e.to)]) d[idx(e.from)][idx(e.to)] = e.weight;
}
return d;
}
struct FloydResult {
Matrix dist;
bool negative_cycle = false; // si es verdadero, dist quedó a medias y no significa nada
};
// Una vuelta: permite k como nodo intermedio. Convierte d_k en d_{k + 1}, en el mismo lugar.
inline void floyd_round(Matrix& d, int n, int k) {
for (int i = 0; i < n; i++) {
if (d[idx(i)][idx(k)] == INF) continue;
for (int j = 0; j < n; j++) {
if (d[idx(k)][idx(j)] == INF) continue;
const long long through = d[idx(i)][idx(k)] + d[idx(k)][idx(j)];
if (through < d[idx(i)][idx(j)]) d[idx(i)][idx(j)] = through;
}
}
}
// Antes de cada vuelta mira la diagonal: si d[k][k] < 0, hay un ciclo negativo y se detiene.
// Por eso los valores no crecen sin control.
inline FloydResult floyd_warshall(int n, const std::vector<Edge>& edges) {
FloydResult result;
result.dist = initial_matrix(n, edges);
for (int k = 0; k < n; k++) {
if (result.dist[idx(k)][idx(k)] < 0) {
result.negative_cycle = true;
return result;
}
floyd_round(result.dist, n, k);
}
return result;
}
// El error de esta semana: compila, termina y da números creíbles.
inline Matrix floyd_warshall_k_inside(int n, const std::vector<Edge>& edges) {
Matrix d = initial_matrix(n, edges);
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
for (int k = 0; k < n; k++) {
if (d[idx(i)][idx(k)] == INF || d[idx(k)][idx(j)] == INF) continue;
const long long through = d[idx(i)][idx(k)] + d[idx(k)][idx(j)];
if (through < d[idx(i)][idx(j)]) d[idx(i)][idx(j)] = through;
}
}
}
return d;
}
// Todos los pares con V corridas de un algoritmo de un origen. El de Bellman-Ford supone que
// no hay ciclos negativos, o devuelve la fila tal como quedó.
inline Matrix all_pairs_dijkstra(int n, const std::vector<Edge>& edges) {
Matrix d;
for (int s = 0; s < n; s++) d.push_back(dijkstra(n, edges, s));
return d;
}
inline Matrix all_pairs_bellman_ford(int n, const std::vector<Edge>& edges) {
Matrix d;
for (int s = 0; s < n; s++) d.push_back(bellman_ford(n, edges, s).dist);
return d;
}// Bellman-Ford: el grafo donde Dijkstra falla, la tabla d_i(v), la versión de una sola fila y cuánto
// importa el orden de las aristas.
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "shortest_paths.h"
namespace {
std::string show(long long x) { return x >= INF ? "inf" : std::to_string(x); }
void print_row(const std::vector<long long>& row) {
for (long long x : row) std::cout << std::setw(5) << show(x);
std::cout << '\n';
}
// Una cadena 0 -> 1 -> ... -> n - 1, con sus aristas en el orden dado.
std::vector<Edge> chain(int n, bool forward) {
std::vector<Edge> edges;
for (int i = 0; i + 1 < n; i++) {
const int a = forward ? i : n - 2 - i;
edges.push_back({a, a + 1, 1});
}
return edges;
}
} // namespace
int main() {
const std::vector<Edge> edges = {{0, 1, 1}, {0, 2, 4}, {2, 1, -5}, {1, 3, 2}};
std::cout << "Aristas: 0->1 (1), 0->2 (4), 2->1 (-5), 1->3 (2)\n";
std::cout << "Dijkstra: ";
print_row(dijkstra(4, edges, 0));
std::cout << "\nLa tabla d_i(v):\n i 0 1 2 3\n";
const auto table = bellman_ford_table(4, edges, 0, 4);
for (std::size_t i = 0; i < table.size(); i++) {
std::cout << std::setw(3) << i;
for (long long x : table[i]) std::cout << std::setw(5) << show(x);
std::cout << '\n';
}
std::cout << "\nUna sola fila, con las aristas en el orden de arriba:\n";
std::cout << "vuelta 0 1 2 3\n";
bellman_ford(4, edges, 0, [](int round, const BellmanFordResult& r) {
std::cout << std::setw(3) << round << ' ';
print_row(r.dist);
});
const std::vector<Edge> shuffled = {{1, 3, 2}, {2, 1, -5}, {0, 2, 4}, {0, 1, 1}};
std::cout << "\nUna sola fila, con las aristas al revés:\n";
std::cout << "vuelta 0 1 2 3\n";
bellman_ford(4, shuffled, 0, [](int round, const BellmanFordResult& r) {
std::cout << std::setw(3) << round << ' ';
print_row(r.dist);
});
std::cout << "\nCuántas vueltas hacen falta, hasta una que no cambia nada, en una cadena de n nodos:\n";
std::cout << " n aristas en orden aristas al revés\n";
for (int n : {10, 100, 1000}) {
std::cout << std::setw(5) << n << std::setw(18) << bellman_ford(n, chain(n, true), 0).rounds << std::setw(18)
<< bellman_ford(n, chain(n, false), 0).rounds << '\n';
}
}// Detectar un ciclo negativo y recuperarlo: el nodo relajado en la vuelta V puede no estar en el ciclo.
#include <iostream>
#include <vector>
#include "shortest_paths.h"
namespace {
void print_values(const char* name, const std::vector<long long>& values) {
std::cout << name;
for (long long x : values) std::cout << ' ' << (x >= INF ? "inf" : std::to_string(x));
std::cout << '\n';
}
} // namespace
int main() {
// 0 -> 1, el ciclo 1 -> 2 -> 3 -> 1 de peso -1, y la cola 3 -> 4 -> 5.
const int n = 6;
const std::vector<Edge> edges = {{0, 1, 2}, {1, 2, 1}, {2, 3, 1}, {3, 1, -3}, {3, 4, 1}, {4, 5, 1}};
std::cout << "Aristas: 0->1 (2), 1->2 (1), 2->3 (1), 3->1 (-3), 3->4 (1), 4->5 (1)\n";
std::cout << "vuelta: d[0..5]\n";
const BellmanFordResult r = bellman_ford(n, edges, 0, [](int round, const BellmanFordResult& partial) {
std::cout << round << ":";
print_values("", partial.dist);
});
std::cout << "nodo relajado al final de la vuelta " << n << ": " << r.witness << '\n';
std::cout << "pred:";
for (int p : r.pred) std::cout << ' ' << p;
std::cout << '\n';
std::cout << "\nLeer directamente desde ese nodo, hasta volver a él (se corta a los 12 pasos):\n ";
int x = r.witness;
for (int step = 0; step < 12; step++) {
std::cout << ' ' << x;
x = r.pred[idx(x)];
if (x == r.witness) break;
}
std::cout << " ...\n";
std::cout << "\nRetroceder " << n << " veces y recién entonces leer:\n ";
x = r.witness;
for (int step = 0; step < n; step++) {
std::cout << ' ' << x;
x = r.pred[idx(x)];
}
std::cout << " -> " << x << '\n';
const std::vector<int> cycle = recover_cycle(n, r.pred, r.witness);
std::cout << "ciclo:";
for (int v : cycle) std::cout << ' ' << v;
std::cout << " -> " << cycle[0] << ", peso " << cycle_weight(edges, cycle) << '\n';
}// Cada fila del cuadro de decisión con el mismo grafo de prueba: el algoritmo rápido frente a
// Bellman-Ford, que sirve en todas pero cuesta más.
#include <iostream>
#include <vector>
#include "all_pairs.h"
namespace {
void print_row(const char* name, const std::vector<long long>& row) {
std::cout << " " << name;
for (long long x : row) std::cout << ' ' << (x >= INF ? "inf" : std::to_string(x));
std::cout << '\n';
}
} // namespace
int main() {
std::cout << "Sin pesos: 0->1, 0->2, 1->3, 2->3, 3->4\n";
std::vector<Edge> unweighted = {{0, 1, 1}, {0, 2, 1}, {1, 3, 1}, {2, 3, 1}, {3, 4, 1}};
print_row("BFS: ", bfs_distances(5, unweighted, 0));
print_row("Bellman-Ford:", bellman_ford(5, unweighted, 0).dist);
std::cout << "\nDAG con un peso negativo: 0->1 (1), 0->2 (4), 2->1 (-5), 1->3 (2)\n";
std::vector<Edge> dag = {{0, 1, 1}, {0, 2, 4}, {2, 1, -5}, {1, 3, 2}};
print_row("orden topológico:", dag_distances(4, dag, 0));
print_row("Bellman-Ford: ", bellman_ford(4, dag, 0).dist);
print_row("Dijkstra: ", dijkstra(4, dag, 0));
std::cout << "\nPesos no negativos: 0->1 (7), 0->2 (1), 2->1 (2), 1->3 (1), 2->3 (6)\n";
std::vector<Edge> roads = {{0, 1, 7}, {0, 2, 1}, {2, 1, 2}, {1, 3, 1}, {2, 3, 6}};
print_row("Dijkstra: ", dijkstra(4, roads, 0));
print_row("Bellman-Ford:", bellman_ford(4, roads, 0).dist);
std::cout << "\nCon un ciclo: el orden topológico no se aplica. Aristas 0->1 (1), 1->2 (1), 2->1 (1)\n";
const std::vector<Edge> cyclic = {{0, 1, 1}, {1, 2, 1}, {2, 1, 1}};
std::cout << " dag_distances devuelve " << dag_distances(3, cyclic, 0).size() << " valores\n";
std::cout << "\nTodos los pares con un negativo: 0->1 (3), 1->2 (-1), 2->0 (2), 2->1 (4)\n";
const std::vector<Edge> small = {{0, 1, 3}, {1, 2, -1}, {2, 0, 2}, {2, 1, 4}};
const Matrix floyd = floyd_warshall(3, small).dist;
const Matrix repeated = all_pairs_bellman_ford(3, small);
for (int s = 0; s < 3; s++) {
print_row("Floyd-Warshall:", floyd[idx(s)]);
print_row("V Bellman-Ford:", repeated[idx(s)]);
}
}// Floyd-Warshall: la matriz vuelta por vuelta, el ciclo de k por dentro, la diagonal y el crecimiento
// de los valores cuando hay un ciclo negativo y no se detiene.
#include <cstdio>
#include <limits>
#include <random>
#include <vector>
#include "all_pairs.h"
namespace {
void print_matrix(const Matrix& d) {
for (const auto& row : d) {
for (long long x : row) {
if (x >= INF) {
std::printf("%5s", "inf");
} else {
std::printf("%5lld", x);
}
}
std::printf("\n");
}
}
// Sin guardia, en double para ver el tamaño sin que haya overflow de enteros.
void growth_without_guard(int n) {
std::vector<std::vector<double>> d(idx(n), std::vector<double>(idx(n), -1));
for (int i = 0; i < n; i++) d[idx(i)][idx(i)] = 0;
const double limit = static_cast<double>(std::numeric_limits<long long>::max());
bool passed = false;
for (int k = 0; k < n; k++) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
const double through = d[idx(i)][idx(k)] + d[idx(k)][idx(j)];
if (through < d[idx(i)][idx(j)]) d[idx(i)][idx(j)] = through;
}
}
double lowest = 0;
for (const auto& row : d) {
for (double x : row) {
if (x < lowest) lowest = x;
}
}
if (k < 6 || (!passed && -lowest > limit)) {
std::printf(" tras la vuelta k = %2d: menor valor %.3e%s\n", k, lowest,
-lowest > limit ? " (ya no cabe en long long)" : "");
}
if (-lowest > limit) passed = true;
}
}
} // namespace
int main() {
const int n = 4;
const std::vector<Edge> edges = {{0, 1, 4}, {0, 3, 9}, {1, 2, -2}, {2, 3, 1}, {3, 1, 5}, {2, 0, 3}};
std::printf("Aristas: 0->1 (4), 0->3 (9), 1->2 (-2), 2->3 (1), 3->1 (5), 2->0 (3)\n");
Matrix d = initial_matrix(n, edges);
std::printf("d_0, sin nodos intermedios:\n");
print_matrix(d);
for (int k = 0; k < n; k++) {
floyd_round(d, n, k);
std::printf("d_%d, tras permitir el nodo %d:\n", k + 1, k);
print_matrix(d);
}
const std::vector<Edge> trap = {{1, 3, 5}, {3, 2, 5}, {2, 0, 9}, {1, 0, 30}};
std::printf("\nEl ciclo de k por dentro. Aristas: 1->3 (5), 3->2 (5), 2->0 (9), 1->0 (30)\n");
const Matrix right = floyd_warshall(n, trap).dist;
const Matrix wrong = floyd_warshall_k_inside(n, trap);
std::printf("k por fuera:\n");
print_matrix(right);
std::printf("k por dentro:\n");
print_matrix(wrong);
std::printf("\nGrafos al azar en los que k por dentro se equivoca (20000 por fila, 35 %% de aristas, pesos 1 a 9):\n");
std::mt19937 gen(10);
for (int size : {3, 4, 5, 8}) {
int bad = 0;
for (int t = 0; t < 20000; t++) {
std::vector<Edge> random_edges;
for (int a = 0; a < size; a++) {
for (int b = 0; b < size; b++) {
if (a != b && gen() % 100 < 35) random_edges.push_back({a, b, static_cast<long long>(gen() % 9) + 1});
}
}
if (floyd_warshall(size, random_edges).dist != floyd_warshall_k_inside(size, random_edges)) bad++;
}
std::printf(" n = %d: %d\n", size, bad);
}
std::printf("\nLa diagonal. Aristas: 0->1 (1), 1->2 (-3), 2->1 (2), 2->3 (1)\n");
const std::vector<Edge> negative = {{0, 1, 1}, {1, 2, -3}, {2, 1, 2}, {2, 3, 1}};
Matrix m = initial_matrix(n, negative);
for (int k = 0; k < n; k++) {
std::printf(" antes de k = %d, diagonal:", k);
for (int v = 0; v < n; v++) std::printf(" %lld", m[idx(v)][idx(v)]);
std::printf("\n");
if (m[idx(k)][idx(k)] < 0) {
std::printf(" d[%d][%d] < 0: hay un ciclo negativo, y se detiene\n", k, k);
break;
}
floyd_round(m, n, k);
}
std::printf("\nSin detenerse: 40 nodos, todas las aristas de peso -1.\n");
growth_without_guard(40);
}// Mide las permutaciones frente a Held-Karp. Con -O2, una corrida, en la máquina del momento.
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
#include "held_karp.h"
namespace {
template <typename Work>
double seconds_of(Work work) {
const auto start = std::chrono::steady_clock::now();
work();
return std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
}
} // namespace
int main() {
std::mt19937 gen(20);
std::printf("%3s %14s %14s %16s\n", "n", "permutaciones", "Held-Karp", "memoria de tabla");
for (int n = 8; n <= 20; n += (n < 12 ? 1 : 2)) {
tsp::Costs cost(tsp::idx(n), std::vector<long long>(tsp::idx(n), 0));
for (auto& row : cost) {
for (long long& c : row) c = static_cast<long long>(gen() % 100) + 1;
}
long long a = 0;
long long b = 0;
const double t_table = seconds_of([&] { b = tsp::held_karp(cost); });
const double megabytes = static_cast<double>((1ULL << (n - 1)) * static_cast<unsigned long long>(n) * 8) / 1e6;
if (n <= 12) {
const double t_brute = seconds_of([&] { a = tsp::brute_force(cost); });
std::printf("%3d %12.3f s %12.3f s %13.1f MB %s\n", n, t_brute, t_table, megabytes, a == b ? "iguales" : "DISTINTOS");
} else {
std::printf("%3d %14s %12.3f s %13.1f MB\n", n, "-", t_table, megabytes);
}
}
}// Held-Karp en cuatro ciudades, casilla por casilla, y cuánto trabajo ahorra frente a las permutaciones.
#include <iomanip>
#include <iostream>
#include <random>
#include <string>
#include <vector>
#include "held_karp.h"
namespace {
std::string subset_name(std::size_t mask, int n) {
std::string text = "{";
for (int v = 1; v < n; v++) {
if ((mask & (std::size_t{1} << (v - 1))) != 0) {
if (text.size() > 1) text += ",";
text += std::to_string(v);
}
}
return text + "}";
}
tsp::Costs random_costs(int n, std::mt19937& gen) {
tsp::Costs cost(tsp::idx(n), std::vector<long long>(tsp::idx(n), 0));
for (auto& row : cost) {
for (long long& c : row) c = static_cast<long long>(gen() % 100) + 1;
}
return cost;
}
} // namespace
int main() {
const tsp::Costs cost = {{0, 10, 15, 20}, {5, 0, 9, 10}, {6, 13, 0, 12}, {8, 8, 9, 0}};
std::cout << "Costos:\n";
for (const auto& row : cost) {
for (long long c : row) std::cout << std::setw(4) << c;
std::cout << '\n';
}
const auto table = tsp::held_karp_table(cost);
std::cout << "\nmáscara subconjunto termina en 1 en 2 en 3\n";
for (std::size_t mask = 1; mask < table.size(); mask++) {
std::cout << std::setw(7) << mask << " " << std::setw(11) << subset_name(mask, 4);
for (int v = 1; v <= 3; v++) {
const long long value = table[mask][tsp::idx(v)];
std::cout << std::setw(v == 1 ? 14 : 6) << (value == tsp::NONE ? std::string("-") : std::to_string(value));
}
std::cout << '\n';
}
std::cout << "\nmejor recorrido, Held-Karp: " << tsp::held_karp(cost) << '\n';
std::cout << "mejor recorrido, permutaciones: " << tsp::brute_force(cost) << '\n';
std::cout << "\n n recorridos (n-1)! transiciones n^2 2^n mismo costo\n";
std::mt19937 gen(15);
for (int n = 4; n <= 11; n++) {
const tsp::Costs random = random_costs(n, gen);
long long tours = 0;
long long transitions = 0;
const long long by_brute = tsp::brute_force(random, &tours);
const long long by_table = tsp::held_karp(random, &transitions);
std::cout << std::setw(2) << n << std::setw(19) << tours << std::setw(14) << transitions << std::setw(9)
<< n * n * (1LL << n) << std::setw(16) << (by_brute == by_table ? "sí" : "NO") << '\n';
}
}// El problema del viajero: fuerza bruta sobre las permutaciones, y Held-Karp sobre subconjuntos.
// Un subconjunto de nodos es un entero: el bit v vale 1 si v está en el subconjunto.
#pragma once
#include <algorithm>
#include <cstddef>
#include <limits>
#include <numeric>
#include <vector>
namespace tsp {
using Costs = std::vector<std::vector<long long>>;
// Un costo que ningún recorrido alcanza. Marca que no hay arista.
inline constexpr long long NONE = std::numeric_limits<long long>::max() / 4;
inline std::size_t idx(int v) { return static_cast<std::size_t>(v); }
// El costo mínimo de un recorrido que sale de 0, pasa una vez por cada nodo y vuelve a 0.
// Prueba las (n - 1)! órdenes. Devuelve NONE si no hay recorrido.
inline long long brute_force(const Costs& cost, long long* tours = nullptr) {
const int n = static_cast<int>(cost.size());
if (n <= 1) return 0;
std::vector<int> middle(idx(n - 1));
std::iota(middle.begin(), middle.end(), 1);
long long best = NONE;
long long count = 0;
do {
count++;
long long total = cost[0][idx(middle[0])];
for (std::size_t i = 0; i + 1 < middle.size() && total < NONE; i++) {
const long long step = cost[idx(middle[i])][idx(middle[i + 1])];
total = step == NONE ? NONE : total + step;
}
const long long back = cost[idx(middle.back())][0];
if (total < NONE && back < NONE) best = std::min(best, total + back);
} while (std::next_permutation(middle.begin(), middle.end()));
if (tours != nullptr) *tours = count;
return best;
}
// Held-Karp. table[mask][v] es el costo mínimo de un camino que sale de 0, visita exactamente los
// nodos de mask y termina en v, con v en mask. Los bits 1, ..., n - 1 son los nodos distintos de
// 0; el nodo 0 no se guarda en mask. Por eso el bit v - 1 representa al nodo v.
inline std::vector<std::vector<long long>> held_karp_table(const Costs& cost, long long* transitions = nullptr) {
const int n = static_cast<int>(cost.size());
const std::size_t masks = std::size_t{1} << (n - 1);
std::vector<std::vector<long long>> table(masks, std::vector<long long>(idx(n), NONE));
long long count = 0;
for (int v = 1; v < n; v++) table[std::size_t{1} << (v - 1)][idx(v)] = cost[0][idx(v)];
// Quitar un bit da un entero menor: al llegar a mask, sus subconjuntos ya están listos.
for (std::size_t mask = 1; mask < masks; mask++) {
for (int v = 1; v < n; v++) {
const std::size_t bit_v = std::size_t{1} << (v - 1);
if ((mask & bit_v) == 0 || mask == bit_v) continue;
const std::size_t without_v = mask ^ bit_v;
for (int u = 1; u < n; u++) {
if ((without_v & (std::size_t{1} << (u - 1))) == 0) continue;
count++;
if (table[without_v][idx(u)] == NONE || cost[idx(u)][idx(v)] == NONE) continue;
table[mask][idx(v)] = std::min(table[mask][idx(v)], table[without_v][idx(u)] + cost[idx(u)][idx(v)]);
}
}
}
if (transitions != nullptr) *transitions = count;
return table;
}
// El costo del mejor recorrido: se elige el último nodo antes de volver a 0.
inline long long held_karp(const Costs& cost, long long* transitions = nullptr) {
const int n = static_cast<int>(cost.size());
if (n <= 1) return 0;
const auto table = held_karp_table(cost, transitions);
const std::size_t masks = table.size();
long long best = NONE;
for (int v = 1; v < n; v++) {
if (table[masks - 1][idx(v)] != NONE && cost[idx(v)][0] != NONE) {
best = std::min(best, table[masks - 1][idx(v)] + cost[idx(v)][0]);
}
}
return best;
}
} // namespace tsp// Los dos errores de partir de un "infinito" y sumarle algo a un nodo no alcanzado.
// El primero es un overflow de int: es comportamiento indefinido, y aquí lo señala el sanitizer.
#include <climits>
#include <iostream>
#include <vector>
#include "shortest_paths.h"
namespace {
void print_row(const char* name, const std::vector<long long>& row) {
std::cout << name;
for (long long x : row) std::cout << ' ' << (x >= INF ? "inf" : std::to_string(x));
std::cout << '\n';
}
// Primer error: INT_MAX como infinito, sin mirar si el nodo fue alcanzado.
std::vector<int> relax_with_int_max(int n, const std::vector<Edge>& edges, int source) {
std::vector<int> d(idx(n), INT_MAX);
d[idx(source)] = 0;
for (int round = 1; round < n; round++) {
for (const Edge& e : edges) {
if (d[idx(e.from)] + static_cast<int>(e.weight) < d[idx(e.to)]) { // overflow si d[e.from] es INT_MAX
d[idx(e.to)] = d[idx(e.from)] + static_cast<int>(e.weight);
}
}
}
return d;
}
// Segundo error: un infinito sin overflow, pero tampoco se trata como especial.
std::vector<long long> relax_without_check(int n, const std::vector<Edge>& edges, int source, bool& changed_last) {
const long long big = 1'000'000'000'000;
std::vector<long long> d(idx(n), big);
d[idx(source)] = 0;
changed_last = false;
for (int round = 1; round <= n; round++) {
changed_last = false;
for (const Edge& e : edges) {
if (d[idx(e.from)] + e.weight < d[idx(e.to)]) {
d[idx(e.to)] = d[idx(e.from)] + e.weight;
changed_last = true;
}
}
}
return d;
}
} // namespace
int main() {
// El origen es 0, que solo llega a 1. Los nodos 2 y 3 no se alcanzan.
const std::vector<Edge> positive = {{2, 3, 5}, {0, 1, 7}};
std::cout << "Primer error: INT_MAX como infinito. Aristas 2->3 (5), 0->1 (7).\n";
std::vector<int> wrong = relax_with_int_max(4, positive, 0);
std::cout << "d con INT_MAX:";
for (int x : wrong) std::cout << ' ' << x;
std::cout << '\n';
print_row("correcto: ", bellman_ford(4, positive, 0).dist);
std::cout << "\nSegundo error: infinito + un negativo. Aristas 0->1 (7), 2->3 (-5).\n";
const std::vector<Edge> negative = {{0, 1, 7}, {2, 3, -5}};
bool changed = false;
print_row("sin revisar: ", relax_without_check(4, negative, 0, changed));
print_row("revisando: ", bellman_ford(4, negative, 0).dist);
std::cout << "\nY un falso ciclo: 0->1 (7), y el ciclo 2->3 (-5), 3->2 (1), que el origen no alcanza.\n";
const std::vector<Edge> unreachable = {{0, 1, 7}, {2, 3, -5}, {3, 2, 1}};
relax_without_check(4, unreachable, 0, changed);
std::cout << "sin revisar: la vuelta 4 cambió algo: " << (changed ? "sí" : "no") << '\n';
const BellmanFordResult r = bellman_ford(4, unreachable, 0);
std::cout << "revisando: la vuelta 4 cambió algo: " << (r.witness != -1 ? "sí" : "no") << '\n';
}// De productos a sumas, y lo que el redondeo le hace a esa cuenta.
#include <cmath>
#include <cstdio>
#include <vector>
#include "rates.h"
int main() {
std::printf("Producto real y suma de -ln(tasa) en double:\n");
std::printf("%-22s %10s %12s\n", "tasas", "producto", "suma");
const std::vector<std::pair<const char*, std::vector<double>>> chains = {
{"2 y 0,5", {2, 0.5}},
{"1,25 y 0,8", {1.25, 0.8}},
{"1,6 y 0,625", {1.6, 0.625}},
{"2,5 y 0,4", {2.5, 0.4}},
{"1,6, 1,25 y 0,5", {1.6, 1.25, 0.5}},
{"1,25, 1,25 y 0,64", {1.25, 1.25, 0.64}},
};
for (const auto& [name, rates] : chains) std::printf("%-22s %10s %12.3e\n", name, "1 exacto", log_sum(rates));
std::printf("%-22s %10s %12.3e\n", "1,1, 1,1 y 1/1,21 (*)", "menor a 1", log_sum({1.1, 1.1, 0.826446280991735}));
std::printf("(*) 0,826446280991735 es 1/1,21 truncado a 15 decimales.\n");
std::printf("\nCon pesos -ln(tasa) + epsilon, ¿la vuelta n relaja algo?\n");
std::printf("%10s %28s %28s\n", "epsilon", "tabla exacta (sin arbitraje)", "ciclo de 400, 1,000001");
for (double epsilon : {0.0, 1e-15, 1e-12, 1e-9, 1e-8, 1e-6}) {
const bool table = round_n_relaxes(5, consistent_table(), epsilon);
const bool cycle = round_n_relaxes(400, long_cycle(400), epsilon);
std::printf("%10.0e %28s %28s\n", epsilon, table ? "sí (falso arbitraje)" : "no", cycle ? "sí" : "no (se le escapa)");
}
}// Tipos de cambio: un producto mayor que 1 es una suma negativa de -ln(tasa). Estas funciones solo
// responden si, desde la moneda 0, se detecta un ciclo. Con ellas se mide qué hace el redondeo.
#pragma once
#include <cmath>
#include <cstddef>
#include <limits>
#include <vector>
struct Rate {
int from;
int to;
double rate; // una unidad de from compra rate unidades de to
};
// La suma de -ln(tasa) a lo largo de una cadena de tasas.
inline double log_sum(const std::vector<double>& rates) {
double total = 0;
for (double r : rates) total += -std::log(r);
return total;
}
// Bellman-Ford desde la moneda 0, con pesos -ln(tasa) + epsilon. Dice si la vuelta n relajó algo.
inline bool round_n_relaxes(int n, const std::vector<Rate>& rates, double epsilon) {
const double unreached = std::numeric_limits<double>::infinity();
std::vector<double> d(static_cast<std::size_t>(n), unreached);
d[0] = 0;
bool relaxed = false;
for (int round = 1; round <= n; round++) {
relaxed = false;
for (const Rate& e : rates) {
const auto u = static_cast<std::size_t>(e.from);
const auto v = static_cast<std::size_t>(e.to);
if (d[u] == unreached) continue;
const double through = d[u] + (-std::log(e.rate) + epsilon);
if (through < d[v]) {
d[v] = through;
relaxed = true;
}
}
if (!relaxed) return false;
}
return relaxed;
}
// Cinco monedas con valores 1, 1,25, 1,6, 2,5 y 0,4, y la tasa exacta entre cada par. Todos los
// productos alrededor de un ciclo valen exactamente 1, así que no hay arbitraje.
inline std::vector<Rate> consistent_table() {
return {{0, 1, 1.25}, {0, 2, 1.6}, {0, 3, 2.5}, {0, 4, 0.4}, {1, 0, 0.8}, {1, 2, 1.28}, {1, 3, 2},
{1, 4, 0.32}, {2, 0, 0.625}, {2, 1, 0.78125}, {2, 3, 1.5625}, {2, 4, 0.25}, {3, 0, 0.4}, {3, 1, 0.5},
{3, 2, 0.64}, {3, 4, 0.16}, {4, 0, 2.5}, {4, 1, 3.125}, {4, 2, 4}, {4, 3, 6.25}};
}
// Un ciclo de largo L, todas las tasas 1 menos una: su producto es exactamente 1,000001.
inline std::vector<Rate> long_cycle(int length) {
std::vector<Rate> rates;
for (int i = 0; i < length; i++) rates.push_back({i, (i + 1) % length, i == 0 ? 1.000001 : 1.0});
return rates;
}// Caminos mínimos desde un origen: Bellman-Ford en sus dos formas, la recuperación de un ciclo
// negativo, y los algoritmos con los que se compara (BFS, DAG y Dijkstra).
#pragma once
#include <cstddef>
#include <functional>
#include <limits>
#include <queue>
#include <utility>
#include <vector>
struct Edge {
int from;
int to;
long long weight;
};
// Un valor que ningún camino alcanza, y lejos del límite de long long.
inline constexpr long long INF = std::numeric_limits<long long>::max() / 4;
inline std::size_t idx(int v) { return static_cast<std::size_t>(v); }
// La tabla completa: table[i][v] es d_i(v), el menor largo de un camino de source a v con a lo
// sumo i aristas. Tiene las filas 0, ..., rows.
inline std::vector<std::vector<long long>> bellman_ford_table(int n, const std::vector<Edge>& edges, int source,
int rows) {
std::vector<std::vector<long long>> table(idx(rows) + 1, std::vector<long long>(idx(n), INF));
table[0][idx(source)] = 0;
for (int i = 1; i <= rows; i++) {
table[idx(i)] = table[idx(i - 1)]; // el caso de a lo sumo i - 1 aristas
for (const Edge& e : edges) { // el caso de exactamente i aristas, con e al final
if (table[idx(i - 1)][idx(e.from)] == INF) continue;
const long long through = table[idx(i - 1)][idx(e.from)] + e.weight;
if (through < table[idx(i)][idx(e.to)]) table[idx(i)][idx(e.to)] = through;
}
}
return table;
}
struct BellmanFordResult {
std::vector<long long> dist;
std::vector<int> pred; // pred[v] es el nodo desde el que se relajó v por última vez, o -1
int rounds = 0; // vueltas que se hicieron
int witness = -1; // un nodo relajado en la vuelta n, o -1 si no hubo ninguno
};
// Una sola fila, hasta n vueltas, y se detiene si una vuelta no cambia nada.
// Invariante: tras la vuelta i, d[v] es el largo de algún camino de source a v (o INF), y d[v] <= d_i(v).
// after_round, si se pasa, se llama al final de cada vuelta.
inline BellmanFordResult bellman_ford(int n, const std::vector<Edge>& edges, int source,
const std::function<void(int, const BellmanFordResult&)>& after_round = {}) {
BellmanFordResult result;
result.dist.assign(idx(n), INF);
result.pred.assign(idx(n), -1);
result.dist[idx(source)] = 0;
for (int round = 1; round <= n; round++) {
result.rounds = round;
int last_relaxed = -1;
for (const Edge& e : edges) {
if (result.dist[idx(e.from)] == INF) continue; // no alcanzado: no hay nada que propagar
if (result.dist[idx(e.from)] + e.weight < result.dist[idx(e.to)]) {
result.dist[idx(e.to)] = result.dist[idx(e.from)] + e.weight;
result.pred[idx(e.to)] = e.from;
last_relaxed = e.to;
}
}
if (after_round) after_round(round, result);
if (last_relaxed == -1) break;
if (round == n) result.witness = last_relaxed;
}
return result;
}
// Dado un nodo relajado en la vuelta n, devuelve un ciclo negativo: c[0], c[1], ..., con
// c[i] -> c[i + 1] aristas del grafo y c.back() -> c[0] la que lo cierra.
inline std::vector<int> recover_cycle(int n, const std::vector<int>& pred, int witness) {
int x = witness;
for (int step = 0; step < n; step++) x = pred[idx(x)]; // ahora x está en el ciclo
std::vector<int> cycle;
int y = x;
do {
cycle.push_back(y);
y = pred[idx(y)];
} while (y != x);
// Se leyó siguiendo a los predecesores, así que está al revés.
std::vector<int> forward(cycle.rbegin(), cycle.rend());
return forward;
}
// El menor peso de una arista de a hacia b. Sirve para sumar un ciclo o un camino ya recuperado.
inline long long edge_weight(const std::vector<Edge>& edges, int a, int b) {
long long best = INF;
for (const Edge& e : edges) {
if (e.from == a && e.to == b && e.weight < best) best = e.weight;
}
return best;
}
inline long long cycle_weight(const std::vector<Edge>& edges, const std::vector<int>& cycle) {
long long total = 0;
for (std::size_t i = 0; i < cycle.size(); i++) {
total += edge_weight(edges, cycle[i], cycle[(i + 1) % cycle.size()]);
}
return total;
}
inline std::vector<std::vector<std::pair<int, long long>>> adjacency(int n, const std::vector<Edge>& edges) {
std::vector<std::vector<std::pair<int, long long>>> adj(idx(n));
for (const Edge& e : edges) adj[idx(e.from)].push_back({e.to, e.weight});
return adj;
}
// BFS: sirve si todas las aristas pesan lo mismo. Cuenta aristas.
inline std::vector<long long> bfs_distances(int n, const std::vector<Edge>& edges, int source) {
const auto adj = adjacency(n, edges);
std::vector<long long> dist(idx(n), INF);
std::queue<int> pending;
dist[idx(source)] = 0;
pending.push(source);
while (!pending.empty()) {
const int u = pending.front();
pending.pop();
for (const auto& [v, weight] : adj[idx(u)]) {
(void)weight;
if (dist[idx(v)] == INF) {
dist[idx(v)] = dist[idx(u)] + 1;
pending.push(v);
}
}
}
return dist;
}
// DAG: procesa los nodos en orden topológico, con pesos de cualquier signo. Devuelve una lista
// vacía si el grafo tiene un ciclo.
inline std::vector<long long> dag_distances(int n, const std::vector<Edge>& edges, int source) {
const auto adj = adjacency(n, edges);
std::vector<int> in_degree(idx(n), 0);
for (const Edge& e : edges) in_degree[idx(e.to)]++;
std::vector<int> order;
for (int v = 0; v < n; v++) {
if (in_degree[idx(v)] == 0) order.push_back(v);
}
std::vector<long long> dist(idx(n), INF);
dist[idx(source)] = 0;
for (std::size_t next = 0; next < order.size(); next++) {
const int u = order[next];
for (const auto& [v, weight] : adj[idx(u)]) {
if (dist[idx(u)] != INF && dist[idx(u)] + weight < dist[idx(v)]) dist[idx(v)] = dist[idx(u)] + weight;
if (--in_degree[idx(v)] == 0) order.push_back(v);
}
}
if (order.size() != idx(n)) return {};
return dist;
}
// Dijkstra con heap y borrado perezoso, de la semana 5. Solo es correcto con pesos no negativos: si
// hay negativos, un nodo fijado puede bajar después, pero nadie vuelve a leer sus aristas.
inline std::vector<long long> dijkstra(int n, const std::vector<Edge>& edges, int source) {
const auto adj = adjacency(n, edges);
std::vector<long long> dist(idx(n), INF);
using Entry = std::pair<long long, int>;
std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> heap;
std::vector<bool> settled(idx(n), false);
dist[idx(source)] = 0;
heap.push({0, source});
while (!heap.empty()) {
const int u = heap.top().second;
heap.pop();
if (settled[idx(u)]) continue;
settled[idx(u)] = true;
for (const auto& [v, weight] : adj[idx(u)]) {
if (dist[idx(u)] + weight < dist[idx(v)]) {
dist[idx(v)] = dist[idx(u)] + weight;
heap.push({dist[idx(v)], v});
}
}
}
return dist;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Comprueba lo que afirman los apuntes. Se compila con sanitizers.
#include <algorithm>
#include <cassert>
#include <functional>
#include <iostream>
#include <random>
#include <vector>
#include "all_pairs.h"
#include "held_karp.h"
#include "rates.h"
#include "shortest_paths.h"
namespace {
std::vector<Edge> random_graph(int n, int percent, int low, int high, std::mt19937& gen) {
std::vector<Edge> edges;
for (int a = 0; a < n; a++) {
for (int b = 0; b < n; b++) {
if (a != b && static_cast<int>(gen() % 100) < percent) {
edges.push_back({a, b, low + static_cast<long long>(gen() % static_cast<unsigned>(high - low + 1))});
}
}
}
std::shuffle(edges.begin(), edges.end(), gen);
return edges;
}
// Árbitro: el menor largo de un camino de source a cada nodo con a lo sumo limit aristas, probándolos todos.
std::vector<long long> walks_up_to(int n, const std::vector<Edge>& edges, int source, int limit) {
std::vector<long long> best(idx(n), INF);
const std::function<void(int, long long, int)> go = [&](int u, long long length, int used) {
best[idx(u)] = std::min(best[idx(u)], length);
if (used == limit) return;
for (const Edge& e : edges) {
if (e.from == u) go(e.to, length + e.weight, used + 1);
}
};
go(source, 0, 0);
return best;
}
// Árbitro: recorre los ciclos simples entre los nodos permitidos y dice si alguno pesa menos que cero.
// Cada ciclo se ve una vez, desde su nodo menor.
bool has_negative_cycle(int n, const std::vector<Edge>& edges, const std::vector<bool>& allowed) {
std::vector<bool> used(idx(n), false);
bool found = false;
const std::function<void(int, int, long long)> go = [&](int start, int u, long long length) {
used[idx(u)] = true;
for (const Edge& e : edges) {
if (e.from != u || e.to < start || !allowed[idx(e.to)]) continue;
if (e.to == start) {
if (length + e.weight < 0) found = true;
} else if (!used[idx(e.to)]) {
go(start, e.to, length + e.weight);
}
}
used[idx(u)] = false;
};
for (int s = 0; s < n; s++) {
if (allowed[idx(s)]) go(s, s, 0);
}
return found;
}
std::vector<bool> reachable_from(int n, const std::vector<Edge>& edges, int source) {
const std::vector<long long> hops = bfs_distances(n, edges, source);
std::vector<bool> reached(idx(n));
for (int v = 0; v < n; v++) reached[idx(v)] = hops[idx(v)] != INF;
return reached;
}
void test_recurrence_and_one_row() {
std::mt19937 gen(1);
int with_negative = 0;
for (int t = 0; t < 1500; t++) {
const int n = 2 + static_cast<int>(gen() % 4);
const std::vector<Edge> edges = random_graph(n, 45, -3, 8, gen);
// La tabla es la definición, con cualquier signo y con ciclos negativos también.
const auto table = bellman_ford_table(n, edges, 0, 4);
for (int i = 0; i <= 4; i++) assert(table[idx(i)] == walks_up_to(n, edges, 0, i));
const std::vector<bool> reached = reachable_from(n, edges, 0);
const bool negative = has_negative_cycle(n, edges, reached);
if (negative) with_negative++;
const auto full = bellman_ford_table(n, edges, 0, n);
std::vector<long long> previous;
const BellmanFordResult r = bellman_ford(n, edges, 0, [&](int round, const BellmanFordResult& partial) {
for (int v = 0; v < n; v++) {
assert(partial.dist[idx(v)] <= full[idx(round)][idx(v)]); // nunca por encima de la tabla
if (!negative) assert(partial.dist[idx(v)] >= full[idx(n)][idx(v)]); // ni por debajo de la verdad
if (!previous.empty()) assert(partial.dist[idx(v)] <= previous[idx(v)]); // solo bajan
}
previous = partial.dist;
});
if (!negative) {
// El lema de V - 1 aristas: la tabla ya no cambia, y una fila llega a lo mismo.
assert(full[idx(n - 1)] == full[idx(n)]);
assert(r.dist == full[idx(n - 1)]);
assert(r.witness == -1);
} else {
assert(r.witness != -1);
}
// d[v] >= d[pred[v]] + w, salvo en el origen sin relajar.
for (int v = 0; v < n; v++) {
if (r.pred[idx(v)] != -1) {
assert(r.dist[idx(v)] >= r.dist[idx(r.pred[idx(v)])] + edge_weight(edges, r.pred[idx(v)], v));
}
}
}
assert(with_negative > 100); // la prueba sí ejercita los dos lados
}
void test_cycle_recovery() {
std::mt19937 gen(2);
int seen = 0;
int off_cycle = 0;
for (int t = 0; t < 4000; t++) {
const int n = 2 + static_cast<int>(gen() % 7);
const std::vector<Edge> edges = random_graph(n, 30, -4, 9, gen);
const BellmanFordResult r = bellman_ford(n, edges, 0);
const bool expected = has_negative_cycle(n, edges, reachable_from(n, edges, 0));
assert((r.witness != -1) == expected);
if (r.witness == -1) continue;
seen++;
const std::vector<int> cycle = recover_cycle(n, r.pred, r.witness);
std::vector<int> sorted = cycle;
std::sort(sorted.begin(), sorted.end());
assert(std::adjacent_find(sorted.begin(), sorted.end()) == sorted.end()); // ciclo simple
for (std::size_t i = 0; i < cycle.size(); i++) {
assert(edge_weight(edges, cycle[i], cycle[(i + 1) % cycle.size()]) != INF);
}
assert(cycle_weight(edges, cycle) < 0);
if (std::find(cycle.begin(), cycle.end(), r.witness) == cycle.end()) off_cycle++;
}
assert(seen > 300);
assert(off_cycle > 0); // el testigo no siempre está en el ciclo
// El ejemplo de los apuntes: el nodo 5 cuelga del ciclo.
const std::vector<Edge> tail = {{0, 1, 2}, {1, 2, 1}, {2, 3, 1}, {3, 1, -3}, {3, 4, 1}, {4, 5, 1}};
const BellmanFordResult r = bellman_ford(6, tail, 0);
assert(r.witness == 5);
assert(cycle_weight(tail, recover_cycle(6, r.pred, r.witness)) == -1);
}
void test_unreached_nodes() {
const std::vector<Edge> unreachable = {{0, 1, 7}, {2, 3, -5}, {3, 2, 1}};
const BellmanFordResult r = bellman_ford(4, unreachable, 0);
assert(r.witness == -1);
assert(r.dist[2] == INF && r.dist[3] == INF);
assert(r.dist[1] == 7);
// n = 1, sin aristas.
assert(bellman_ford(1, {}, 0).dist[0] == 0);
}
void test_week5_graph() {
const std::vector<Edge> edges = {{0, 1, 1}, {0, 2, 4}, {2, 1, -5}, {1, 3, 2}};
assert((dijkstra(4, edges, 0) == std::vector<long long>{0, -1, 4, 3}));
assert((bellman_ford(4, edges, 0).dist == std::vector<long long>{0, -1, 4, 1}));
}
void test_decision_table() {
std::mt19937 gen(3);
for (int t = 0; t < 1500; t++) {
const int n = 2 + static_cast<int>(gen() % 8);
const std::vector<Edge> unit = random_graph(n, 30, 1, 1, gen);
assert(bfs_distances(n, unit, 0) == bellman_ford(n, unit, 0).dist);
const std::vector<Edge> positive = random_graph(n, 40, 0, 20, gen);
assert(dijkstra(n, positive, 0) == bellman_ford(n, positive, 0).dist);
std::vector<Edge> dag;
for (const Edge& e : random_graph(n, 50, -9, 9, gen)) {
if (e.from < e.to) dag.push_back(e);
}
assert(dag_distances(n, dag, 0) == bellman_ford(n, dag, 0).dist);
}
}
void test_floyd_warshall() {
std::mt19937 gen(4);
int negative_graphs = 0;
for (int t = 0; t < 3000; t++) {
const int n = 2 + static_cast<int>(gen() % 5);
const std::vector<Edge> edges = random_graph(n, 40, -3, 9, gen);
const bool negative = has_negative_cycle(n, edges, std::vector<bool>(idx(n), true));
const FloydResult f = floyd_warshall(n, edges);
assert(f.negative_cycle == negative); // la guardia ve todo ciclo negativo, y solo esos
if (negative) {
negative_graphs++;
// Con la guardia, los valores siguen en la escala de un camino simple.
for (const auto& row : f.dist) {
for (long long x : row) assert(x == INF || (x >= -6 * n * 3 && x <= 6 * n * 9));
}
// Sin ella, también queda negativa la diagonal al final.
Matrix m = initial_matrix(n, edges);
for (int k = 0; k < n; k++) floyd_round(m, n, k);
bool diagonal = false;
for (int v = 0; v < n; v++) diagonal = diagonal || m[idx(v)][idx(v)] < 0;
assert(diagonal);
continue;
}
assert(f.dist == all_pairs_bellman_ford(n, edges));
// d_k: comparar con la definición, y ver que la fila y la columna k no cambian en la vuelta k.
Matrix m = initial_matrix(n, edges);
for (int k = 0; k < n; k++) {
const Matrix before = m;
floyd_round(m, n, k);
for (int v = 0; v < n; v++) {
assert(m[idx(k)][idx(v)] == before[idx(k)][idx(v)]);
assert(m[idx(v)][idx(k)] == before[idx(v)][idx(k)]);
}
// Dos filas, como en la definición: da lo mismo que en el mismo lugar.
Matrix two_rows = before;
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (before[idx(i)][idx(k)] == INF || before[idx(k)][idx(j)] == INF) continue;
two_rows[idx(i)][idx(j)] =
std::min(two_rows[idx(i)][idx(j)], before[idx(i)][idx(k)] + before[idx(k)][idx(j)]);
}
}
assert(two_rows == m);
}
}
assert(negative_graphs > 100);
const std::vector<Edge> trap = {{1, 3, 5}, {3, 2, 5}, {2, 0, 9}, {1, 0, 30}};
assert(floyd_warshall(4, trap).dist[1][0] == 19);
assert(floyd_warshall_k_inside(4, trap)[1][0] == 30);
}
void test_rates() {
assert(log_sum({1.25, 0.8}) < 0); // el producto real es 1, y la suma sale negativa
assert(log_sum({2, 0.5}) == 0);
assert(round_n_relaxes(5, consistent_table(), 0)); // falso arbitraje sin tolerancia
assert(!round_n_relaxes(5, consistent_table(), 1e-9));
assert(round_n_relaxes(400, long_cycle(400), 1e-9)); // 400 * 1e-9 < ln(1,000001)
assert(!round_n_relaxes(400, long_cycle(400), 1e-8)); // 400 * 1e-8 > ln(1,000001)
}
void test_held_karp() {
std::mt19937 gen(5);
for (int t = 0; t < 400; t++) {
const int n = 1 + static_cast<int>(gen() % 8);
tsp::Costs cost(tsp::idx(n), std::vector<long long>(tsp::idx(n), 0));
for (auto& row : cost) {
for (long long& c : row) c = gen() % 5 == 0 ? tsp::NONE : static_cast<long long>(gen() % 50) + 1;
}
assert(tsp::held_karp(cost) == tsp::brute_force(cost));
}
for (int n = 3; n <= 9; n++) {
tsp::Costs cost(tsp::idx(n), std::vector<long long>(tsp::idx(n), 1));
long long tours = 0;
long long transitions = 0;
assert(tsp::brute_force(cost, &tours) == n);
assert(tsp::held_karp(cost, &transitions) == n);
long long factorial = 1;
for (int i = 2; i < n; i++) factorial *= i;
assert(tours == factorial);
assert(transitions == (n - 1) * (n - 2) * (1LL << (n - 3)));
}
}
} // namespace
int main() {
test_recurrence_and_one_row();
test_cycle_recovery();
test_unreached_nodes();
test_week5_graph();
test_decision_table();
test_floyd_warshall();
test_rates();
test_held_karp();
std::cout << "Todas las pruebas de la semana 10 pasaron.\n";
}Laboratorio
Archivos de partida del laboratorio de la semana.
TIEMPO = 1
VERIFICADOR = verificador.cpp
include ../../../lab.mk# Arbitraje entre monedas
Una casa de cambio publica una tabla de tipos de cambio. Cada fila dice cuántas unidades de una moneda
se reciben por una unidad de otra. A veces la tabla está mal armada: cambiando en cadena, de moneda en
moneda, se vuelve a la moneda inicial con más dinero que al empezar, sin riesgo. Eso es un **arbitraje**.
La casa de cambio quiere revisar su tabla antes de publicarla. Ustedes reciben la tabla, dicen si hay
arbitraje y, si lo hay, muestran una cadena que lo pruebe.
Una **cadena** es una lista de monedas c₀, c₁, …, c_k, con k ≥ 2, en la que c₀ = c_k, las monedas c₀, …,
c_{k−1} son todas distintas, y la tabla tiene una tasa de cada moneda de la lista a la siguiente. Su
**producto** es la multiplicación de esas k tasas: lo que se tiene al final por cada unidad inicial.
Las tablas de este problema cumplen una **promesa**: el producto de toda cadena es a lo sumo 1, o es por
lo menos 1,000001. Nunca cae entre esos dos valores. Por lo tanto hay arbitraje exactamente cuando
alguna cadena tiene producto mayor que 1, y entonces ese producto es por lo menos 1,000001.
Una cadena con producto exactamente 1 no es arbitraje. Muchas tablas tienen tasas exactas entre todas sus
monedas, y en ellas todas las cadenas valen 1: la respuesta correcta es que no hay arbitraje, aunque los
números de una computadora, que se redondean, digan otra cosa.
## Entrada
La primera línea tiene dos enteros: n, la cantidad de monedas, y m, la cantidad de tasas. Las monedas se
numeran de 0 a n − 1.
Luego vienen m líneas, cada una con `u v r`: una unidad de la moneda u compra r unidades de la moneda v.
r se escribe con punto decimal, por ejemplo `0.8` o `1.000001`.
## Salida
Si no hay arbitraje, una línea con `NO`.
Si lo hay, tres líneas:
- `SI`
- el entero k, la cantidad de cambios de la cadena
- las k + 1 monedas c₀, c₁, …, c_k, separadas por espacios
Puede haber varias cadenas, y cualquiera vale: un verificador recalcula el producto con las tasas de la
entrada, y exige que la cadena cumpla la definición y que su producto supere 1,0000005.
## Límites
- 1 ≤ n ≤ 400
- 0 ≤ m ≤ n(n − 1)
- u ≠ v, y hay a lo sumo una tasa por cada par ordenado (u, v)
- 0,000001 ≤ r ≤ 1000000, con a lo sumo 15 decimales
- Se cumple la promesa de arriba
## Ejemplo
Entrada:
```text
4 6
0 1 0.3
1 2 0.9
2 0 4
0 3 2
3 1 0.1
2 3 1
```
Salida:
```text
SI
3
0 1 2 0
```
La cadena 0, 1, 2, 0 tiene producto 0,3 · 0,9 · 4 = 1,08. También sería correcto escribirla como 1, 2, 0, 1
o como 2, 0, 1, 2, con el mismo k. Las otras dos cadenas de la tabla no ganan: 0, 3, 1, 2, 0 tiene
producto 0,72 y 1, 2, 3, 1 tiene producto 0,09.
## Tiempo
1 segundo por caso, compilado con `-O2`. Corran `make test` para probar con el juez.// Arbitraje entre monedas: lee la tabla y siempre dice que no hay arbitraje. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>
struct ExchangeRate {
int from;
int to;
double rate;
};
int main() {
int n = 0;
int m = 0;
std::cin >> n >> m;
std::vector<ExchangeRate> rates(static_cast<std::size_t>(m));
for (ExchangeRate& e : rates) std::cin >> e.from >> e.to >> e.rate;
// Por hacer: buscar una cadena con producto mayor que 1, e imprimirla como pide el enunciado.
std::cout << "NO\n";
}// Verificador de "Arbitraje entre monedas": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA importa solo la primera palabra, SI o NO. Si hay arbitraje, se comprueba la cadena y se
// recalcula su producto con las tasas de la entrada. Termina con 0 si todo está bien, y con 1 y un
// mensaje si no.
#include <cstddef>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace {
[[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 9.
bool parse_number(const std::string& word, std::size_t& value) {
if (word.empty() || word.size() > 9) return false;
value = 0;
for (char c : word) {
if (c < '0' || c > '9') return false;
value = value * 10 + static_cast<std::size_t>(c - '0');
}
return true;
}
} // 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;
std::size_t m = 0;
input >> n >> m;
std::vector<std::vector<long double>> rate(n, std::vector<long double>(n, 0));
for (std::size_t e = 0; e < m; e++) {
std::size_t u = 0;
std::size_t v = 0;
long double r = 0;
input >> u >> v >> r;
rate[u][v] = r;
}
std::string want;
expected >> want;
std::string got;
if (!(output >> got)) reject("la salida está vacía");
if (got != "SI" && got != "NO") reject("se esperaba SI o NO, salió " + got);
if (got != want) reject("se esperaba " + want + ", salió " + got);
std::string word;
if (got == "NO") {
if (output >> word) reject("sobra texto después de NO: " + word);
return 0;
}
std::size_t k = 0;
if (!(output >> word) || !parse_number(word, k)) reject("k no es un número válido");
if (k < 2 || k > n) reject("k fuera de rango: " + word);
std::vector<std::size_t> chain(k + 1);
for (std::size_t& c : chain) {
if (!(output >> word) || !parse_number(word, c)) reject("falta una moneda, o no es un número");
if (c >= n) reject("moneda fuera de rango: " + word);
}
if (output >> word) reject("sobra texto después de la cadena: " + word);
if (chain[0] != chain[k]) reject("la cadena no vuelve a la moneda inicial");
std::vector<bool> seen(n, false);
long double product = 1;
for (std::size_t i = 0; i < k; i++) {
if (seen[chain[i]]) reject("la moneda " + std::to_string(chain[i]) + " se repite");
seen[chain[i]] = true;
const long double r = rate[chain[i]][chain[i + 1]];
if (r == 0) reject("no hay tasa de " + std::to_string(chain[i]) + " a " + std::to_string(chain[i + 1]));
product *= r;
}
if (product <= 1.0000005L) reject("el producto de la cadena no supera 1,0000005");
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 10: caminos mínimos con programación dinámica. Bellman-Ford, ciclos negativos,
# Floyd-Warshall y Held-Karp. 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, con -O2
# make clean borra lo generado
#
# El problema de lab/ tiene su propio Makefile y no entra aquí.
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
HEADERS = shortest_paths.h all_pairs.h held_karp.h rates.h
PROGRAMS = bf_demo cycle_demo infinity_demo rates_demo fw_demo decision_demo held_karp_demo test_examples
BENCHES = all_pairs_bench held_karp_bench
.PHONY: all test bench clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BENCHES:%=$(BUILD)/%)
$(PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
$(CXX) $(CXXFLAGS) $< -o $@
# Lo que mide tiempos va con -O2 y sin sanitizers.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
$(CXX) $(BENCHFLAGS) $< -o $@
$(BUILD):
mkdir -p $@
test: $(BUILD)/test_examples
$(BUILD)/test_examples
bench: $(BENCHES:%=$(BUILD)/%)
$(BUILD)/all_pairs_bench
$(BUILD)/held_karp_bench
clean:
rm -rf $(BUILD)