Semana 14 — NP-completitud
Objetivos
- Definir NP-completo y NP-difícil.
- Demostrar la NP-completitud de un problema con una reducción desde 3-SAT.
- Diseñar gadgets para una reducción.
- Reconocer qué cambios vuelven fácil o difícil un problema.
Sesión A — NP-completitud: Cook-Levin enunciado y reducciones desde 3-SAT
Lectura: Apuntes, semana 14.
Apoyo: Erickson, secs. 12.3 (NP-hard, NP-easy, and NP-complete) y 12.7 (Maximum Independent Set (from 3Sat)). CLRS, capítulo NP-Completeness, 4.ª edición.
Temas:
- NP-difícil y NP-completo. Si un solo problema NP-completo está en P, todos lo están.
- El teorema de Cook-Levin, enunciado: SAT es NP-completo. La demostración va en Teoría de la Computación.
- SAT se reduce a 3-SAT: partir las cláusulas largas.
- Receta para demostrar NP-completitud: está en NP, y elegir un problema difícil conocido, transformar, demostrar las dos direcciones y comprobar el tiempo polinomial.
- 3-SAT se reduce a conjunto independiente: un triángulo por cláusula y aristas entre literales opuestos. Con un ejemplo seguido en detalle.
- Error común: olvidar demostrar que el problema está en NP.
Sesión B — Más reducciones: coloreo y suma de subconjuntos
Lectura: Apuntes, semana 14.
Apoyo: Erickson, secs. 12.10 (Graph Coloring), 12.12 (Subset Sum) y 12.14 (Choosing the Right Problem). CLRS, capítulo NP-Completeness, 4.ª edición.
Temas:
- 3-SAT se reduce a 3-coloreo: gadgets de variable y de cláusula.
- Cobertura de vértices se reduce a suma de subconjuntos: números como dígitos de una tabla. Por qué la O(nW) de la semana 7 no contradice nada.
- Un mapa de problemas conocidos: a cuál reducir según el tipo de problema.
- Cerca de la frontera: 2-SAT frente a 3-SAT, 2-coloreo frente a 3-coloreo, ciclo euleriano frente a hamiltoniano, y camino más largo en un DAG frente a en un grafo general.
- Error común: decir “es NP-completo, así que no se puede resolver”. Sí se puede: el problema es garantizar que sea rápido en todos los casos.
Laboratorio — Práctica 4 · Opcional: ¿fácil o difícil?
Práctica 4 en la sesión de laboratorio.
Problema opcional: resolver 2-SAT en tiempo lineal con las componentes fuertemente conexas de la semana 6. Un verificador comprueba la asignación.
Entregas
- Práctica 4: Flujo, modelar con flujo y reducciones. Ver
practicas/README.md.
Preguntas de repaso
- ¿Qué dos cosas hay que demostrar para probar que un problema es NP-completo?
- Corran la reducción de 3-SAT a conjunto independiente con una fórmula de dos cláusulas.
- ¿Por qué la reducción a suma de subconjuntos necesita números con muchos dígitos?
- Clasifiquen como fácil o difícil: camino más largo en un DAG; 3-coloreo; ¿tiene el grafo un ciclo que use cada arista exactamente una vez?; 2-SAT.
- Si mañana alguien resuelve 3-SAT en O(n⁵), ¿qué pasa con la mochila?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (20)Pruebas (1)Laboratorio (4)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Fórmulas en CNF. Un literal es un entero: +v es la variable v, y -v es su negación. Las variables
// se numeran desde 1, como en DIMACS.
#pragma once
#include <cstddef>
#include <cstdlib>
#include <optional>
#include <string>
#include <vector>
using Clause = std::vector<int>;
struct Formula {
int variables = 0;
std::vector<Clause> clauses;
};
// Assignment[v] es el valor de la variable v. La posición 0 no se usa.
using Assignment = std::vector<bool>;
inline bool literal_true(const Assignment& a, int literal) {
bool value = a[static_cast<std::size_t>(std::abs(literal))];
return literal > 0 ? value : !value;
}
inline bool clause_true(const Assignment& a, const Clause& c) {
for (int literal : c) {
if (literal_true(a, literal)) return true;
}
return false;
}
// El verificador de SAT: el certificado es la asignación, y revisarla es lineal en la fórmula.
inline bool satisfies(const Formula& f, const Assignment& a) {
for (const Clause& c : f.clauses) {
if (!clause_true(a, c)) return false;
}
return true;
}
// Fuerza bruta: prueba las 2ⁿ asignaciones. Solo para fórmulas pequeñas.
inline std::optional<Assignment> brute_sat(const Formula& f) {
const std::size_t n = static_cast<std::size_t>(f.variables);
for (unsigned long long mask = 0; mask < (1ULL << n); mask++) {
Assignment a(n + 1, false);
for (std::size_t v = 1; v <= n; v++) a[v] = (mask >> (v - 1)) & 1;
if (satisfies(f, a)) return a;
}
return std::nullopt;
}
inline std::string literal_text(int literal) {
return (literal < 0 ? "¬x" : "x") + std::to_string(std::abs(literal));
}
inline std::string clause_text(const Clause& c) {
std::string text = "(";
for (std::size_t i = 0; i < c.size(); i++) text += (i ? " ∨ " : "") + literal_text(c[i]);
return text + ")";
}
inline std::string formula_text(const Formula& f) {
std::string text;
for (std::size_t i = 0; i < f.clauses.size(); i++) text += (i ? " ∧ " : "") + clause_text(f.clauses[i]);
return text;
}
inline std::string assignment_text(const Assignment& a) {
std::string text;
for (std::size_t v = 1; v < a.size(); v++) {
text += (v > 1 ? " " : "") + ("x" + std::to_string(v)) + "=" + (a[v] ? "V" : "F");
}
return text;
}
// Las ocho cláusulas posibles con tres variables: ninguna asignación las cumple todas.
inline Formula all_eight_clauses() {
Formula f{3, {}};
for (int mask = 0; mask < 8; mask++) {
f.clauses.push_back({mask & 1 ? 1 : -1, mask & 2 ? 2 : -2, mask & 4 ? 3 : -3});
}
return f;
}// La reducción de 3-SAT a 3-coloreo: la compuerta O, las 8 entradas de la cláusula y una fórmula completa.
#include <iostream>
#include <string>
#include "coloring.h"
namespace {
char color_name(int c) { return "TFB"[c]; }
std::string outputs_text(const std::array<bool, 3>& possible) {
std::string text;
for (int c = 0; c < 3; c++) {
if (possible[static_cast<std::size_t>(c)]) text += std::string(text.empty() ? "" : " ") + color_name(c);
}
return text.empty() ? "ninguno" : text;
}
void solve(const Formula& f) {
Graph g = reduce_to_coloring(f);
std::cout << formula_text(f) << '\n';
std::cout << " " << g.size() << " nodos, " << edge_count(g) << " aristas\n";
auto color = find_coloring(g, 3);
auto sat = brute_sat(f);
std::cout << " ¿3-coloreable? " << (color ? "sí" : "no") << ", fuerza bruta sobre la fórmula: "
<< (sat ? "satisfacible" : "insatisfacible") << '\n';
if (color) {
Assignment a = assignment_from_coloring(f, *color);
std::cout << " lectura del coloreo: " << assignment_text(a) << ", ¿cumple? " << (satisfies(f, a) ? "sí" : "no") << '\n';
}
}
} // namespace
int main() {
std::cout << "Una compuerta O: qué colores admite la salida r.\n";
std::cout << " a b | r\n";
for (int a : {COLOR_F, COLOR_T}) {
for (int b : {COLOR_F, COLOR_T}) {
std::cout << " " << color_name(a) << ' ' << color_name(b) << " | " << outputs_text(or_gate_outputs(a, b)) << '\n';
}
}
std::cout << "\nLa segunda compuerta puede recibir B, porque r no está unido a B.\n";
std::cout << " a b | r\n";
for (int b : {COLOR_F, COLOR_T}) {
std::cout << " B " << color_name(b) << " | " << outputs_text(or_gate_outputs(COLOR_B, b)) << '\n';
}
std::cout << "\nEl gadget de cláusula: coloreos de sus 6 nodos internos, de 3^6 = 729 posibles.\n";
std::cout << " a b c | coloreos\n";
for (int mask = 0; mask < 8; mask++) {
bool a = mask & 4;
bool b = mask & 2;
bool c = mask & 1;
std::cout << " " << (a ? 'T' : 'F') << ' ' << (b ? 'T' : 'F') << ' ' << (c ? 'T' : 'F') << " | "
<< clause_gadget_colorings(a, b, c) << '\n';
}
std::cout << "\nFórmulas completas:\n";
solve({3, {{1, -2, 3}, {-1, 2, -3}}});
solve({3, {{1, 2, 3}, {-1, -2, 3}, {1, -2, -3}}});
solve(all_eight_clauses());
std::cout << "\nSi faltan aristas, en la fórmula de ocho cláusulas, que no tiene solución:\n";
Formula eight = all_eight_clauses();
GadgetOptions no_var;
no_var.variable_to_b = false;
std::cout << " variables sin arista a B: "
<< (find_coloring(reduce_to_coloring(eight, no_var), 3) ? "3-coloreable" : "no 3-coloreable") << '\n';
GadgetOptions no_f;
no_f.output_to_f = false;
std::cout << " salida sin arista a F: "
<< (find_coloring(reduce_to_coloring(eight, no_f), 3) ? "3-coloreable" : "no 3-coloreable") << '\n';
GadgetOptions no_b;
no_b.output_to_b = false;
std::cout << " salida sin arista a B: "
<< (find_coloring(reduce_to_coloring(eight, no_b), 3) ? "3-coloreable" : "no 3-coloreable") << '\n';
}// La reducción de 3-SAT a 3-coloreo. La paleta es un triángulo T, F, B. Cada variable es un par de nodos
// unidos entre sí y con B. Cada cláusula es una cadena de dos compuertas O, cuya salida se une a F y a B.
#pragma once
#include <array>
#include <cstddef>
#include <cstdlib>
#include <vector>
#include "cnf.h"
#include "graph.h"
constexpr int COLOR_T = 0;
constexpr int COLOR_F = 1;
constexpr int COLOR_B = 2;
// Los tres primeros nodos del grafo son la paleta, y luego vienen x1, ¬x1, x2, ¬x2, ...
constexpr int NODE_T = 0;
constexpr int NODE_F = 1;
constexpr int NODE_B = 2;
inline int literal_node(int literal) {
int v = std::abs(literal);
return 3 + 2 * (v - 1) + (literal > 0 ? 0 : 1);
}
// Qué aristas poner, para ver qué se rompe si falta alguna.
struct GadgetOptions {
bool variable_to_b = true;
bool output_to_f = true;
bool output_to_b = true;
};
// Compuerta O: agrega los nodos p, q, r con a-p, b-q, p-q, p-r, q-r. Devuelve r, la salida.
inline int add_or_gate(Graph& g, int a, int b) {
int p = static_cast<int>(g.size());
int q = p + 1;
int r = p + 2;
g.resize(g.size() + 3);
add_edge(g, a, p);
add_edge(g, b, q);
add_edge(g, p, q);
add_edge(g, p, r);
add_edge(g, q, r);
return r;
}
// Cláusula de tres literales: la salida de la segunda compuerta queda obligada a ser T.
inline void add_clause_gadget(Graph& g, int a, int b, int c, const GadgetOptions& options = {}) {
int first = add_or_gate(g, a, b);
int output = add_or_gate(g, first, c);
if (options.output_to_f) add_edge(g, output, NODE_F);
if (options.output_to_b) add_edge(g, output, NODE_B);
}
inline Graph reduce_to_coloring(const Formula& f, const GadgetOptions& options = {}) {
Graph g(static_cast<std::size_t>(3 + 2 * f.variables));
add_edge(g, NODE_T, NODE_F);
add_edge(g, NODE_F, NODE_B);
add_edge(g, NODE_T, NODE_B);
for (int v = 1; v <= f.variables; v++) {
add_edge(g, literal_node(v), literal_node(-v));
if (options.variable_to_b) {
add_edge(g, literal_node(v), NODE_B);
add_edge(g, literal_node(-v), NODE_B);
}
}
for (const Clause& c : f.clauses) {
add_clause_gadget(g, literal_node(c[0]), literal_node(c[1]), literal_node(c[2]), options);
}
return g;
}
// De un coloreo a una asignación. Los colores no traen nombre: T es el color que recibió el nodo T.
inline Assignment assignment_from_coloring(const Formula& f, const std::vector<int>& color) {
Assignment a(static_cast<std::size_t>(f.variables) + 1, false);
for (int v = 1; v <= f.variables; v++) {
a[static_cast<std::size_t>(v)] = color[static_cast<std::size_t>(literal_node(v))] == color[NODE_T];
}
return a;
}
// Colorea una compuerta dados los colores de sus entradas: T en la salida si alguna entrada es T, y si
// no, F. Es la construcción de la demostración.
inline int color_or_gate(std::vector<int>& color, std::size_t p, int a_color, int b_color) {
int q_color = COLOR_B;
int p_color = COLOR_B;
int r_color = COLOR_F;
if (a_color == COLOR_T) {
r_color = COLOR_T;
p_color = b_color == COLOR_T ? COLOR_B : COLOR_F;
q_color = b_color == COLOR_T ? COLOR_F : COLOR_B;
} else if (b_color == COLOR_T) {
r_color = COLOR_T;
p_color = COLOR_B;
q_color = COLOR_F;
} else {
p_color = COLOR_T;
q_color = COLOR_B;
}
color[p] = p_color;
color[p + 1] = q_color;
color[p + 2] = r_color;
return r_color;
}
// De una asignación que cumple la fórmula a un coloreo, sin buscar.
inline std::vector<int> coloring_from_assignment(const Formula& f, const Assignment& a, const Graph& g) {
std::vector<int> color(g.size(), -1);
color[NODE_T] = COLOR_T;
color[NODE_F] = COLOR_F;
color[NODE_B] = COLOR_B;
for (int v = 1; v <= f.variables; v++) {
bool value = a[static_cast<std::size_t>(v)];
color[static_cast<std::size_t>(literal_node(v))] = value ? COLOR_T : COLOR_F;
color[static_cast<std::size_t>(literal_node(-v))] = value ? COLOR_F : COLOR_T;
}
std::size_t next = static_cast<std::size_t>(3 + 2 * f.variables);
for (const Clause& c : f.clauses) {
int first = color_or_gate(color, next, literal_true(a, c[0]) ? COLOR_T : COLOR_F, literal_true(a, c[1]) ? COLOR_T : COLOR_F);
color_or_gate(color, next + 3, first, literal_true(a, c[2]) ? COLOR_T : COLOR_F);
next += 6;
}
return color;
}
// Una compuerta sola con la paleta y sus entradas ya coloreadas: qué colores admite la salida.
// Devuelve, para cada color posible de r, si existe un coloreo de p y q que lo permita.
inline std::array<bool, 3> or_gate_outputs(int a_color, int b_color) {
std::array<bool, 3> possible = {false, false, false};
for (int p = 0; p < 3; p++) {
for (int q = 0; q < 3; q++) {
for (int r = 0; r < 3; r++) {
if (p != a_color && q != b_color && p != q && p != r && q != r) possible[static_cast<std::size_t>(r)] = true;
}
}
}
return possible;
}
// Enumera los coloreos de los 6 nodos internos del gadget de cláusula, con T, F, B y las tres entradas
// fijas. Cuenta cuántos son propios. Son 3⁶ posibilidades para cada una de las 8 entradas.
inline int clause_gadget_colorings(bool a, bool b, bool c) {
Graph g(6);
add_edge(g, NODE_T, NODE_F);
add_edge(g, NODE_F, NODE_B);
add_edge(g, NODE_T, NODE_B);
add_edge(g, 3, NODE_B);
add_edge(g, 4, NODE_B);
add_edge(g, 5, NODE_B);
add_clause_gadget(g, 3, 4, 5);
std::vector<int> color(g.size(), 0);
color[NODE_T] = COLOR_T;
color[NODE_F] = COLOR_F;
color[NODE_B] = COLOR_B;
color[3] = a ? COLOR_T : COLOR_F;
color[4] = b ? COLOR_T : COLOR_F;
color[5] = c ? COLOR_T : COLOR_F;
int total = 0;
for (int mask = 0; mask < 729; mask++) {
int rest = mask;
for (std::size_t v = 6; v < 12; v++) {
color[v] = rest % 3;
rest /= 3;
}
if (is_proper_coloring(g, color, 3)) total++;
}
return total;
}// Pregunta de repaso 4, con un ejemplo de cada problema y la fuerza bruta al lado.
#include <iostream>
#include "frontier.h"
#include "two_sat.h"
int main() {
Dag dag = {{1, 2}, {3}, {3, 4}, {5}, {5}, {}};
std::cout << "Camino más largo en un DAG de 6 nodos: " << longest_path_dag(dag) << " aristas, fuerza bruta "
<< longest_path_dag_brute(dag) << '\n';
Graph triangle = make_graph(3, {{0, 1}, {1, 2}, {0, 2}});
Graph square = make_graph(4, {{0, 1}, {1, 2}, {2, 3}, {0, 3}});
std::cout << "2-coloreo: triángulo " << (two_coloring(triangle) ? "sí" : "no") << " (3 colores: "
<< (find_coloring(triangle, 3) ? "sí" : "no") << "), cuadrado " << (two_coloring(square) ? "sí" : "no") << '\n';
Graph bowtie = make_graph(5, {{0, 1}, {1, 2}, {0, 2}, {2, 3}, {3, 4}, {2, 4}});
Graph two_triangles = make_graph(6, {{0, 1}, {1, 2}, {0, 2}, {3, 4}, {4, 5}, {3, 5}});
Graph tail = make_graph(4, {{0, 1}, {1, 2}, {0, 2}, {2, 3}});
auto euler_line = [](const char* name, const Graph& g) {
std::cout << " " << name << ": grados y conexión " << (has_euler_cycle(g) ? "sí" : "no") << ", fuerza bruta "
<< (euler_cycle_brute(g) ? "sí" : "no") << '\n';
};
std::cout << "¿Ciclo que use cada arista exactamente una vez?\n";
euler_line("dos triángulos con un nodo común", bowtie);
euler_line("dos triángulos separados ", two_triangles);
euler_line("triángulo con una cola ", tail);
std::cout << "Camino simple más largo, y camino hamiltoniano:\n";
Graph star = make_graph(4, {{0, 1}, {0, 2}, {0, 3}});
std::cout << " cuadrado: " << longest_simple_path(square) << " aristas, de V − 1 = 3: hamiltoniano\n";
std::cout << " estrella: " << longest_simple_path(star) << " aristas, de V − 1 = 3: no hamiltoniano\n";
std::vector<TwoClause> clauses = {{1, 2}, {-1, 3}, {-2, -3}};
auto a = solve_two_sat(3, clauses);
std::cout << "2-SAT: " << formula_text(two_clauses_as_formula(3, clauses)) << ": " << (a ? assignment_text(*a) : "insatisfacible") << '\n';
}// Los problemas fáciles que están al lado de los difíciles, y fuerzas brutas para comprobarlos.
#pragma once
#include <algorithm>
#include <cstddef>
#include <numeric>
#include <optional>
#include <queue>
#include <utility>
#include <vector>
#include "graph.h"
// 2-coloreo con BFS: cada nodo recibe el color contrario al de quien lo descubrió, y una arista entre
// dos nodos del mismo color es un ciclo impar.
inline std::optional<std::vector<int>> two_coloring(const Graph& g) {
std::vector<int> color(g.size(), -1);
std::queue<int> pending;
for (std::size_t s = 0; s < g.size(); s++) {
if (color[s] != -1) continue;
color[s] = 0;
pending.push(static_cast<int>(s));
while (!pending.empty()) {
std::size_t u = static_cast<std::size_t>(pending.front());
pending.pop();
for (int w : g[u]) {
std::size_t v = static_cast<std::size_t>(w);
if (color[v] == -1) {
color[v] = 1 - color[u];
pending.push(w);
} else if (color[v] == color[u]) {
return std::nullopt;
}
}
}
}
return color;
}
// Euler: todo grado par, y todas las aristas en una sola componente. Los nodos aislados no estorban.
inline bool has_euler_cycle(const Graph& g) {
std::vector<char> seen(g.size(), 0);
std::size_t start = g.size();
for (std::size_t v = 0; v < g.size(); v++) {
if (g[v].size() % 2 != 0) return false;
if (start == g.size() && !g[v].empty()) start = v;
}
if (start == g.size()) return true;
std::vector<std::size_t> stack = {start};
seen[start] = 1;
while (!stack.empty()) {
std::size_t u = stack.back();
stack.pop_back();
for (int w : g[u]) {
if (!seen[static_cast<std::size_t>(w)]) {
seen[static_cast<std::size_t>(w)] = 1;
stack.push_back(static_cast<std::size_t>(w));
}
}
}
for (std::size_t v = 0; v < g.size(); v++) {
if (!g[v].empty() && !seen[v]) return false;
}
return true;
}
// Fuerza bruta: busca un ciclo que use cada arista exactamente una vez, probando continuaciones.
inline bool euler_search(const std::vector<std::pair<int, int>>& edges, std::vector<char>& used, int at, int start, std::size_t count) {
if (count == edges.size()) return at == start;
for (std::size_t e = 0; e < edges.size(); e++) {
if (used[e]) continue;
auto [u, v] = edges[e];
if (u != at && v != at) continue;
used[e] = 1;
if (euler_search(edges, used, u == at ? v : u, start, count + 1)) {
used[e] = 0;
return true;
}
used[e] = 0;
}
return false;
}
inline bool euler_cycle_brute(const Graph& g) {
std::vector<std::pair<int, int>> edges = edge_list(g);
if (edges.empty()) return true;
std::vector<char> used(edges.size(), 0);
return euler_search(edges, used, edges[0].first, edges[0].first, 0);
}
// Hay camino hamiltoniano si el camino simple más largo tiene V − 1 aristas. Fuerza bruta, grafo no dirigido.
inline int longest_from(const Graph& g, std::size_t u, std::vector<char>& used) {
int best = 0;
used[u] = 1;
for (int w : g[u]) {
if (!used[static_cast<std::size_t>(w)]) best = std::max(best, 1 + longest_from(g, static_cast<std::size_t>(w), used));
}
used[u] = 0;
return best;
}
inline int longest_simple_path(const Graph& g) {
int best = 0;
std::vector<char> used(g.size(), 0);
for (std::size_t s = 0; s < g.size(); s++) best = std::max(best, longest_from(g, s, used));
return best;
}
// Grafos dirigidos para el camino más largo en un DAG, medido en aristas.
using Dag = std::vector<std::vector<int>>;
// L[u] es el mayor número de aristas de un camino que sale de u. Los sucesores se calculan primero.
inline int longest_path_dag(const Dag& g) {
std::vector<int> pending(g.size(), 0);
Dag inverse(g.size());
for (std::size_t u = 0; u < g.size(); u++) {
pending[u] = static_cast<int>(g[u].size());
for (int v : g[u]) inverse[static_cast<std::size_t>(v)].push_back(static_cast<int>(u));
}
std::vector<int> ready;
for (std::size_t u = 0; u < g.size(); u++) {
if (pending[u] == 0) ready.push_back(static_cast<int>(u));
}
std::vector<int> length(g.size(), 0);
int best = 0;
while (!ready.empty()) {
std::size_t u = static_cast<std::size_t>(ready.back());
ready.pop_back();
best = std::max(best, length[u]);
for (int w : inverse[u]) {
std::size_t p = static_cast<std::size_t>(w);
length[p] = std::max(length[p], length[u] + 1);
if (--pending[p] == 0) ready.push_back(w);
}
}
return best;
}
inline int longest_directed_from(const Dag& g, std::size_t u) {
int best = 0;
for (int v : g[u]) best = std::max(best, 1 + longest_directed_from(g, static_cast<std::size_t>(v)));
return best;
}
inline int longest_path_dag_brute(const Dag& g) {
int best = 0;
for (std::size_t u = 0; u < g.size(); u++) best = std::max(best, longest_directed_from(g, u));
return best;
}// Grafos no dirigidos simples y las fuerzas brutas que sirven de referencia en esta semana.
#pragma once
#include <algorithm>
#include <cstddef>
#include <optional>
#include <utility>
#include <vector>
using Graph = std::vector<std::vector<int>>;
inline void add_edge(Graph& g, int u, int v) {
g[static_cast<std::size_t>(u)].push_back(v);
g[static_cast<std::size_t>(v)].push_back(u);
}
inline Graph make_graph(int n, const std::vector<std::pair<int, int>>& edges) {
Graph g(static_cast<std::size_t>(n));
for (auto [u, v] : edges) add_edge(g, u, v);
return g;
}
inline std::vector<std::pair<int, int>> edge_list(const Graph& g) {
std::vector<std::pair<int, int>> edges;
for (std::size_t u = 0; u < g.size(); u++) {
for (int v : g[u]) {
if (static_cast<int>(u) < v) edges.push_back({static_cast<int>(u), v});
}
}
return edges;
}
inline std::size_t edge_count(const Graph& g) {
std::size_t total = 0;
for (const auto& list : g) total += list.size();
return total / 2;
}
// Verificadores: el certificado es el conjunto, y se revisa en tiempo polinomial.
inline bool is_independent(const Graph& g, const std::vector<int>& nodes) {
std::vector<char> inside(g.size(), 0);
for (int v : nodes) {
if (inside[static_cast<std::size_t>(v)]) return false;
inside[static_cast<std::size_t>(v)] = 1;
}
for (int v : nodes) {
for (int w : g[static_cast<std::size_t>(v)]) {
if (inside[static_cast<std::size_t>(w)]) return false;
}
}
return true;
}
inline bool is_cover(const Graph& g, const std::vector<int>& nodes) {
std::vector<char> inside(g.size(), 0);
for (int v : nodes) inside[static_cast<std::size_t>(v)] = 1;
for (auto [u, v] : edge_list(g)) {
if (!inside[static_cast<std::size_t>(u)] && !inside[static_cast<std::size_t>(v)]) return false;
}
return true;
}
inline bool is_proper_coloring(const Graph& g, const std::vector<int>& color, int colors) {
for (std::size_t u = 0; u < g.size(); u++) {
if (color[u] < 0 || color[u] >= colors) return false;
for (int v : g[u]) {
if (color[u] == color[static_cast<std::size_t>(v)]) return false;
}
}
return true;
}
// Conjunto independiente máximo: en cada nodo, tomarlo o no. Exponencial; solo para grafos pequeños.
inline void independent_search(const Graph& g, std::size_t v, std::vector<int>& blocked, std::vector<int>& current,
std::vector<int>& best) {
if (current.size() + (g.size() - v) <= best.size()) return;
if (v == g.size()) {
best = current;
return;
}
if (blocked[v] == 0) {
for (int w : g[v]) blocked[static_cast<std::size_t>(w)]++;
current.push_back(static_cast<int>(v));
independent_search(g, v + 1, blocked, current, best);
current.pop_back();
for (int w : g[v]) blocked[static_cast<std::size_t>(w)]--;
}
independent_search(g, v + 1, blocked, current, best);
}
inline std::vector<int> max_independent_set(const Graph& g) {
std::vector<int> blocked(g.size(), 0);
std::vector<int> current;
std::vector<int> best;
independent_search(g, 0, blocked, current, best);
return best;
}
// Cobertura mínima, probando los 2^V subconjuntos.
inline std::vector<int> min_vertex_cover(const Graph& g) {
const std::size_t n = g.size();
std::vector<int> best;
for (std::size_t i = 0; i < n; i++) best.push_back(static_cast<int>(i));
for (unsigned long long mask = 0; mask < (1ULL << n); mask++) {
std::vector<int> nodes;
for (std::size_t i = 0; i < n; i++) {
if ((mask >> i) & 1) nodes.push_back(static_cast<int>(i));
}
if (nodes.size() < best.size() && is_cover(g, nodes)) best = nodes;
}
return best;
}
// Backtracking para colorear con k colores, nodo por nodo.
inline bool color_search(const Graph& g, std::size_t v, int k, std::vector<int>& color) {
if (v == g.size()) return true;
for (int c = 0; c < k; c++) {
bool free = true;
for (int w : g[v]) {
if (color[static_cast<std::size_t>(w)] == c) free = false;
}
if (!free) continue;
color[v] = c;
if (color_search(g, v + 1, k, color)) return true;
color[v] = -1;
}
return false;
}
inline std::optional<std::vector<int>> find_coloring(const Graph& g, int k) {
std::vector<int> color(g.size(), -1);
if (color_search(g, 0, k, color)) return color;
return std::nullopt;
}// La reducción de 3-SAT a conjunto independiente: un triángulo por cláusula y una arista entre cada
// par de literales opuestos. La fórmula se cumple si y solo si hay un conjunto independiente de tamaño k,
// con k igual al número de cláusulas.
#pragma once
#include <cstddef>
#include <vector>
#include "cnf.h"
#include "graph.h"
struct IndependentSetInstance {
Graph graph;
int k = 0;
std::vector<int> literal; // literal[nodo] es el literal que representa ese nodo
};
// Las dos banderas existen para ver qué pasa si falta cada clase de aristas.
inline IndependentSetInstance reduce_to_independent_set(const Formula& f, bool triangles = true, bool conflicts = true) {
IndependentSetInstance out;
out.k = static_cast<int>(f.clauses.size());
for (const Clause& c : f.clauses) out.literal.insert(out.literal.end(), c.begin(), c.end());
out.graph.assign(out.literal.size(), {});
int base = 0;
for (const Clause& c : f.clauses) {
int size = static_cast<int>(c.size());
for (int i = 0; triangles && i < size; i++) {
for (int j = i + 1; j < size; j++) add_edge(out.graph, base + i, base + j);
}
base += size;
}
int nodes = static_cast<int>(out.literal.size());
for (int u = 0; conflicts && u < nodes; u++) {
for (int v = u + 1; v < nodes; v++) {
if (out.literal[static_cast<std::size_t>(u)] == -out.literal[static_cast<std::size_t>(v)]) add_edge(out.graph, u, v);
}
}
return out;
}
// De una asignación a un conjunto: el primer literal verdadero de cada cláusula.
inline std::vector<int> set_from_assignment(const Formula& f, const Assignment& a) {
std::vector<int> nodes;
int base = 0;
for (const Clause& c : f.clauses) {
for (std::size_t i = 0; i < c.size(); i++) {
if (literal_true(a, c[i])) {
nodes.push_back(base + static_cast<int>(i));
break;
}
}
base += static_cast<int>(c.size());
}
return nodes;
}
// De un conjunto a una asignación: lo elegido es verdadero. El resto de variables da igual: quedan en F.
inline Assignment assignment_from_set(const Formula& f, const IndependentSetInstance& instance, const std::vector<int>& nodes) {
Assignment a(static_cast<std::size_t>(f.variables) + 1, false);
for (int v : nodes) {
int literal = instance.literal[static_cast<std::size_t>(v)];
a[static_cast<std::size_t>(std::abs(literal))] = literal > 0;
}
return a;
}// La reducción de 3-SAT a conjunto independiente: el grafo, la respuesta y lo que pasa si faltan aristas.
#include <iostream>
#include <string>
#include "independent_set.h"
namespace {
std::string node_name(const IndependentSetInstance& s, int v) {
return std::to_string(v) + ":" + literal_text(s.literal[static_cast<std::size_t>(v)]);
}
void show_graph(const IndependentSetInstance& s) {
std::cout << " nodos:";
for (int v = 0; v < static_cast<int>(s.literal.size()); v++) std::cout << ' ' << node_name(s, v);
std::cout << "\n aristas de literales opuestos:";
for (auto [u, v] : edge_list(s.graph)) {
if (s.literal[static_cast<std::size_t>(u)] == -s.literal[static_cast<std::size_t>(v)]) std::cout << ' ' << u << '-' << v;
}
std::cout << '\n';
}
void solve(const Formula& f) {
IndependentSetInstance s = reduce_to_independent_set(f);
std::cout << formula_text(f) << '\n';
show_graph(s);
std::vector<int> best = max_independent_set(s.graph);
std::cout << " k = " << s.k << ", conjunto independiente máximo: " << best.size() << " (";
for (std::size_t i = 0; i < best.size(); i++) std::cout << (i ? ", " : "") << node_name(s, best[i]);
std::cout << ")\n";
auto sat = brute_sat(f);
std::cout << " fuerza bruta sobre la fórmula: " << (sat ? "satisfacible, " + assignment_text(*sat) : "insatisfacible") << '\n';
if (static_cast<int>(best.size()) >= s.k) {
Assignment a = assignment_from_set(f, s, best);
std::cout << " lectura del conjunto: " << assignment_text(a) << ", ¿cumple? " << (satisfies(f, a) ? "sí" : "no") << '\n';
}
std::cout << '\n';
}
} // namespace
int main() {
std::cout << "Dos cláusulas (pregunta de repaso 2):\n";
solve({3, {{1, -2, 3}, {-1, 2, -3}}});
std::cout << "El ejemplo del texto:\n";
solve({3, {{1, 2, 3}, {-1, -2, 3}, {1, -2, -3}}});
std::cout << "Sin solución: las ocho cláusulas sobre tres variables.\n";
Formula eight = all_eight_clauses();
IndependentSetInstance s = reduce_to_independent_set(eight);
std::cout << " " << s.literal.size() << " nodos, " << edge_count(s.graph) << " aristas, k = " << s.k
<< ", máximo: " << max_independent_set(s.graph).size() << "\n\n";
std::cout << "Si faltan aristas, en esa misma fórmula insatisfacible:\n";
IndependentSetInstance no_conflicts = reduce_to_independent_set(eight, true, false);
std::cout << " sin literales opuestos: máximo " << max_independent_set(no_conflicts.graph).size() << ", k = " << s.k << '\n';
IndependentSetInstance no_triangles = reduce_to_independent_set(eight, false, true);
std::cout << " sin triángulos: máximo " << max_independent_set(no_triangles.graph).size() << ", k = " << s.k << '\n';
}// De la decisión a la optimización, usando la decisión como una caja negra.
#include <iostream>
#include "knapsack_search.h"
int main() {
std::vector<Item> items = {{4, 10}, {3, 7}, {2, 4}, {5, 12}, {1, 1}};
const Number capacity = 9;
int calls = 0;
Number total = 0;
for (const Item& item : items) total += item.value;
Number best = knapsack_optimum_by_search(items, capacity, calls);
std::cout << "objetos (peso, valor):";
for (const Item& item : items) std::cout << " (" << item.weight << ", " << item.value << ")";
std::cout << "\ncapacidad " << capacity << ", suma de valores " << total << '\n';
std::cout << "óptimo por búsqueda binaria: " << best << ", con " << calls << " llamadas a la decisión\n";
std::cout << "óptimo por la tabla de la semana 7: " << knapsack_best(items, capacity) << '\n';
std::cout << "objetos elegidos, con otra llamada por objeto:";
for (std::size_t i : knapsack_items_by_search(items, capacity)) std::cout << ' ' << i;
std::cout << '\n';
std::vector<Number> numbers = {3, 5, 9};
for (Number target : {8ULL, 10ULL}) {
KnapsackDecision d = knapsack_from_subset_sum(numbers, target);
std::cout << "suma de subconjuntos {3, 5, 9} con objetivo " << target << ": mochila dice "
<< (knapsack_decide(d) ? "sí" : "no") << ", fuerza bruta dice "
<< (brute_subset_sum(numbers, target) ? "sí" : "no") << '\n';
}
}// La mochila en su versión de decisión: ¿hay objetos con peso a lo sumo W y valor al menos V? Suma de
// subconjuntos se reduce a ella, y con una caja negra para la decisión se recupera la de optimización.
#pragma once
#include <cstddef>
#include <vector>
#include "subset_sum.h"
struct Item {
Number weight;
Number value;
};
struct KnapsackDecision {
std::vector<Item> items;
Number capacity;
Number goal;
};
// Peso y valor iguales al número, y capacidad y meta iguales al objetivo.
inline KnapsackDecision knapsack_from_subset_sum(const std::vector<Number>& numbers, Number target) {
KnapsackDecision out{{}, target, target};
for (Number x : numbers) out.items.push_back({x, x});
return out;
}
// La mejor ganancia con capacidad W, en O(nW), como en la semana 7.
inline Number knapsack_best(const std::vector<Item>& items, Number capacity) {
std::vector<Number> best(static_cast<std::size_t>(capacity) + 1, 0);
for (const Item& item : items) {
for (std::size_t c = static_cast<std::size_t>(capacity) + 1; c-- > static_cast<std::size_t>(item.weight);) {
best[c] = std::max(best[c], best[c - static_cast<std::size_t>(item.weight)] + item.value);
}
}
return best[static_cast<std::size_t>(capacity)];
}
inline bool knapsack_decide(const KnapsackDecision& d) {
return knapsack_best(d.items, d.capacity) >= d.goal;
}
// Búsqueda binaria sobre el valor, llamando solo a la decisión. Cuenta las llamadas.
inline Number knapsack_optimum_by_search(const std::vector<Item>& items, Number capacity, int& calls) {
Number low = 0;
Number high = 0;
for (const Item& item : items) high += item.value;
calls = 0;
// Invariante: se puede alcanzar low, y no se puede alcanzar nada mayor que high.
while (low < high) {
Number middle = low + (high - low + 1) / 2;
calls++;
if (knapsack_decide({items, capacity, middle})) low = middle;
else high = middle - 1;
}
return low;
}
// Qué objetos llevar, también solo con la decisión: un objeto sobra si sin él se llega al mismo valor.
inline std::vector<std::size_t> knapsack_items_by_search(const std::vector<Item>& items, Number capacity) {
int calls = 0;
const Number goal = knapsack_optimum_by_search(items, capacity, calls);
std::vector<Item> kept = items;
std::vector<std::size_t> chosen;
Number used_value = 0;
Number room = capacity;
for (std::size_t i = 0; i < items.size(); i++) {
Item current = kept[i];
kept[i] = {0, 0};
if (!knapsack_decide({kept, room, goal - used_value})) {
chosen.push_back(i);
used_value += current.value;
room -= current.weight;
}
}
return chosen;
}// Escribe en DIMACS el principio del palomar: pigeonhole HUECOS. Hay una paloma más que huecos, cada paloma
// va en algún hueco y dos palomas no comparten hueco. Se sabe a simple vista que no tiene solución.
#include <cstdlib>
#include <iostream>
int main(int argc, char* argv[]) {
if (argc != 2) {
std::cerr << "uso: pigeonhole HUECOS\n";
return 1;
}
const int holes = std::atoi(argv[1]);
const int pigeons = holes + 1;
auto variable = [&](int pigeon, int hole) { return pigeon * holes + hole + 1; };
const int clauses = pigeons + holes * pigeons * (pigeons - 1) / 2;
std::cout << "p cnf " << pigeons * holes << ' ' << clauses << '\n';
for (int p = 0; p < pigeons; p++) {
for (int h = 0; h < holes; h++) std::cout << variable(p, h) << ' ';
std::cout << "0\n";
}
for (int h = 0; h < holes; h++) {
for (int p = 0; p < pigeons; p++) {
for (int q = p + 1; q < pigeons; q++) std::cout << -variable(p, h) << ' ' << -variable(q, h) << " 0\n";
}
}
}// Escribe en DIMACS una fórmula 3-SAT al azar: random_3sat VARIABLES CLAUSULAS SEMILLA. Cada cláusula tiene
// tres variables distintas y signos al azar. Usa std::mt19937 directamente, y sale igual en toda máquina.
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <random>
int main(int argc, char* argv[]) {
if (argc != 4) {
std::cerr << "uso: random_3sat VARIABLES CLAUSULAS SEMILLA\n";
return 1;
}
const std::uint32_t n = static_cast<std::uint32_t>(std::atoi(argv[1]));
const std::uint32_t m = static_cast<std::uint32_t>(std::atoi(argv[2]));
std::mt19937 random(static_cast<std::uint32_t>(std::atoi(argv[3])));
std::cout << "p cnf " << n << ' ' << m << '\n';
for (std::uint32_t c = 0; c < m; c++) {
std::uint32_t a = 1 + random() % n;
std::uint32_t b = a;
std::uint32_t d = a;
while (b == a) b = 1 + random() % n;
while (d == a || d == b) d = 1 + random() % n;
for (std::uint32_t v : {a, b, d}) std::cout << (random() % 2 ? "-" : "") << v << ' ';
std::cout << "0\n";
}
}#!/bin/bash
# Pasa fórmulas 3-SAT al azar por minisat, con cerca de 4,26 cláusulas por variable. Se corre con `make solver`,
# dentro del entorno del curso. Los tiempos son los que reporta minisat, y dependen de la máquina.
set -u
mkdir -p build/solver
printf '%10s %10s %8s %16s %12s\n' variables cláusulas semilla resultado segundos
for n in 100 150 200 250 300; do
m=$((n * 426 / 100))
for seed in 1 2 3; do
cnf=build/solver/f.cnf
build/random_3sat "$n" "$m" "$seed" > "$cnf"
log=$(timeout 30 minisat "$cnf" build/solver/f.out 2>&1)
code=$?
case "$code" in
10) result=satisfacible ;;
20) result=insatisfacible ;;
124) result="más de 30 s" ;;
*) result="código $code" ;;
esac
cpu=$(printf '%s\n' "$log" | grep 'CPU time' | sed 's/.*: *//; s/ s$//')
printf '%10s %10s %8s %16s %12s\n' "$n" "$m" "$seed" "$result" "${cpu:--}"
done
done
echo
echo "El principio del palomar: una paloma más que huecos."
printf '%10s %10s %10s %16s %12s\n' huecos variables cláusulas resultado segundos
for holes in 7 8 9 10; do
cnf=build/solver/p.cnf
build/pigeonhole "$holes" > "$cnf"
header=$(head -n 1 "$cnf")
log=$(timeout 30 minisat "$cnf" build/solver/p.out 2>&1)
code=$?
case "$code" in
10) result=satisfacible ;;
20) result=insatisfacible ;;
124) result="más de 30 s" ;;
*) result="código $code" ;;
esac
cpu=$(printf '%s\n' "$log" | grep 'CPU time' | sed 's/.*: *//; s/ s$//')
printf '%10s %10s %10s %16s %12s\n' "$holes" "$(echo "$header" | cut -d' ' -f3)" "$(echo "$header" | cut -d' ' -f4)" "$result" "${cpu:--}"
done// Costo de la programación dinámica O(nW) en instancias que salen de la reducción. Cada arista agrega un
// dígito en base 4, y el objetivo se multiplica por 4. Se compila con -O2, sin sanitizers.
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include "subset_sum.h"
namespace {
int bit_length(Number x) {
int bits = 0;
while (x > 0) {
bits++;
x >>= 1;
}
return bits;
}
} // namespace
int main() {
std::cout << " aristas n bits del objetivo objetivo W n·W entrada en bits respuesta segundos\n";
for (int edges = 2; edges <= 13; edges++) {
// Un ciclo con tantas aristas como nodos. La cobertura mínima de un ciclo es ⌈E / 2⌉.
Graph g(static_cast<std::size_t>(edges < 3 ? 3 : edges));
for (int i = 0; i < edges; i++) {
if (edges < 3) add_edge(g, i, i + 1);
else add_edge(g, i, (i + 1) % edges);
}
int k = edges < 3 ? 1 : (edges + 1) / 2;
SubsetSumInstance s = reduce_to_subset_sum(g, k);
std::size_t input_bits = 0;
for (Number x : s.numbers) input_bits += static_cast<std::size_t>(bit_length(x));
input_bits += static_cast<std::size_t>(bit_length(s.target));
auto start = std::chrono::steady_clock::now();
bool answer = subset_sum_dp(s.numbers, s.target);
std::chrono::duration<double> seconds = std::chrono::steady_clock::now() - start;
std::cout << std::setw(8) << edges << std::setw(4) << s.numbers.size() << std::setw(20) << bit_length(s.target)
<< std::setw(14) << s.target << std::setw(16) << static_cast<Number>(s.numbers.size()) * s.target
<< std::setw(18) << input_bits << std::setw(12) << (answer ? "sí" : "no") << std::setw(11)
<< std::fixed << std::setprecision(3) << seconds.count() << '\n';
}
}// La reducción de cobertura de vértices a suma de subconjuntos: la tabla, las respuestas y otras bases.
#include <iomanip>
#include <iostream>
#include <random>
#include "subset_sum.h"
namespace {
void show_table(const SubsetSumInstance& s) {
std::cout << " " << std::left << std::setw(12) << "número" << "arriba";
for (std::size_t j = s.edges.size(); j-- > 0;) std::cout << " e" << j;
std::cout << " decimal\n";
for (std::size_t i = 0; i < s.numbers.size(); i++) {
std::string label;
if (i < static_cast<std::size_t>(s.vertices)) label = "nodo " + std::to_string(i);
else label = "holgura e" + std::to_string(i - static_cast<std::size_t>(s.vertices));
// setw cuenta bytes: estas etiquetas son solo ASCII.
std::cout << " " << std::setw(12) << label;
std::cout << std::setw(6) << s.numbers[i] / power_of(s.base, s.edges.size());
for (std::size_t j = s.edges.size(); j-- > 0;) std::cout << " " << (s.numbers[i] / power_of(s.base, j)) % s.base;
std::cout << " " << s.numbers[i] << '\n';
}
std::cout << " " << std::setw(12) << "objetivo" << std::setw(6) << s.target / power_of(s.base, s.edges.size());
for (std::size_t j = s.edges.size(); j-- > 0;) std::cout << " " << (s.target / power_of(s.base, j)) % s.base;
std::cout << " " << s.target << '\n';
}
void show_answer(const Graph& g, int k) {
SubsetSumInstance s = reduce_to_subset_sum(g, k);
auto chosen = brute_subset_sum(s.numbers, s.target);
std::cout << " k = " << k << ": cobertura mínima de tamaño " << min_vertex_cover(g).size() << ", suma: ";
if (!chosen) {
std::cout << "ninguna elección llega al objetivo " << s.target << '\n';
return;
}
std::cout << "se llega con";
for (std::size_t i : *chosen) std::cout << ' ' << s.numbers[i];
std::cout << "\n nodos elegidos:";
for (int v : cover_from_subset(s, *chosen)) std::cout << ' ' << v;
std::cout << '\n';
}
// Busca un grafo pequeño en el que otra base dé una respuesta falsa.
void search_other_base(Number base) {
std::mt19937 random(14);
const int rounds = 4000;
for (int round = 0; round < rounds; round++) {
int n = 2 + static_cast<int>(random() % 4);
Graph g(static_cast<std::size_t>(n));
for (int u = 0; u < n; u++) {
for (int v = u + 1; v < n; v++) {
if (random() % 2) add_edge(g, u, v);
}
}
int cover = static_cast<int>(min_vertex_cover(g).size());
for (int k = 0; k <= n; k++) {
SubsetSumInstance s = reduce_to_subset_sum(g, k, base);
if (brute_subset_sum(s.numbers, s.target).has_value() != (cover <= k)) {
std::cout << " base " << base << ": hay una respuesta falsa\n";
return;
}
}
}
std::cout << " base " << base << ": ninguna respuesta falsa en " << rounds << " grafos al azar\n";
}
// Dos aristas separadas, en base 2: el objetivo ya no dice "2 en cada dígito".
void show_base_two() {
Graph g = make_graph(4, {{0, 1}, {2, 3}});
SubsetSumInstance s = reduce_to_subset_sum(g, 0, 2);
std::cout << " aristas 0-1 y 2-3, k = 0, cobertura mínima " << min_vertex_cover(g).size() << '\n';
std::cout << " números:";
for (Number x : s.numbers) std::cout << ' ' << x;
std::cout << ", objetivo " << s.target << '\n';
auto chosen = brute_subset_sum(s.numbers, s.target);
std::cout << " la suma dice " << (chosen ? "sí" : "no") << ", con";
for (std::size_t i : *chosen) std::cout << " el número " << s.numbers[i] << " (posición " << i << ")";
std::cout << '\n';
}
} // namespace
int main() {
Graph g = make_graph(4, {{0, 1}, {0, 2}, {1, 2}, {2, 3}});
std::cout << "Grafo de 4 nodos con aristas";
std::vector<std::pair<int, int>> edges = edge_list(g);
for (std::size_t j = 0; j < edges.size(); j++) std::cout << " e" << j << "=" << edges[j].first << "-" << edges[j].second;
std::cout << "\n\nLa tabla con k = 2, en base 4:\n";
show_table(reduce_to_subset_sum(g, 2));
std::cout << "\nRespuestas:\n";
for (int k = 0; k <= 5; k++) show_answer(g, k);
std::cout << "\nEn base 2 los dígitos se pisan:\n";
show_base_two();
std::cout << "\nBúsqueda de respuestas falsas en otras bases:\n";
search_other_base(2);
search_other_base(3);
search_other_base(4);
}// La reducción de cobertura de vértices a suma de subconjuntos, con un dígito en base 4 por arista.
// Los números son unsigned long long, así que esto solo corre con pocas aristas. Una reducción de verdad
// escribiría números de cualquier largo: esa longitud es el tema de la sección.
#pragma once
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <optional>
#include <string>
#include <vector>
#include "graph.h"
using Number = unsigned long long;
struct SubsetSumInstance {
std::vector<Number> numbers; // primero uno por nodo, después uno de holgura por arista
Number target = 0;
int vertices = 0;
std::vector<std::pair<int, int>> edges;
Number base = 4;
};
inline Number power_of(Number base, std::size_t exponent) {
Number value = 1;
for (std::size_t i = 0; i < exponent; i++) value *= base;
return value;
}
// base = 4 es la reducción correcta. Otras bases solo sirven para ver cómo se rompe.
inline SubsetSumInstance reduce_to_subset_sum(const Graph& g, int k, Number base = 4) {
SubsetSumInstance out;
out.base = base;
out.vertices = static_cast<int>(g.size());
out.edges = edge_list(g);
const std::size_t e = out.edges.size();
assert(e <= 28);
// Con k mayor que V, la respuesta no cambia si se toma k = V: V nodos ya cubren todo.
k = std::min(k, out.vertices);
const Number top = power_of(base, e);
out.numbers.assign(g.size(), top);
for (std::size_t j = 0; j < e; j++) {
out.numbers[static_cast<std::size_t>(out.edges[j].first)] += power_of(base, j);
out.numbers[static_cast<std::size_t>(out.edges[j].second)] += power_of(base, j);
}
for (std::size_t j = 0; j < e; j++) out.numbers.push_back(power_of(base, j));
out.target = static_cast<Number>(k) * top;
for (std::size_t j = 0; j < e; j++) out.target += 2 * power_of(base, j);
return out;
}
// Los dígitos de un número en la base de la instancia: el más alto primero, sin partir el de arriba.
inline std::string digits_text(Number value, Number base, std::size_t edges) {
std::string text = std::to_string(value / power_of(base, edges));
for (std::size_t j = edges; j-- > 0;) {
text += " " + std::to_string((value / power_of(base, j)) % base);
}
return text;
}
// El verificador: el certificado es la lista de posiciones elegidas.
inline bool sums_to_target(const SubsetSumInstance& s, const std::vector<std::size_t>& chosen) {
Number total = 0;
for (std::size_t i : chosen) total += s.numbers[i];
return total == s.target;
}
// Fuerza bruta: 2ⁿ subconjuntos. Devuelve las posiciones de uno que sume el objetivo.
inline std::optional<std::vector<std::size_t>> brute_subset_sum(const std::vector<Number>& numbers, Number target) {
const std::size_t n = numbers.size();
assert(n < 40);
for (unsigned long long mask = 0; mask < (1ULL << n); mask++) {
Number total = 0;
for (std::size_t i = 0; i < n; i++) {
if ((mask >> i) & 1) total += numbers[i];
}
if (total == target) {
std::vector<std::size_t> chosen;
for (std::size_t i = 0; i < n; i++) {
if ((mask >> i) & 1) chosen.push_back(i);
}
return chosen;
}
}
return std::nullopt;
}
// Programación dinámica de la semana 7, en O(nW): reachable[c] dice si alguna elección suma c.
// Los números tienen que ser positivos.
inline bool subset_sum_dp(const std::vector<Number>& numbers, Number target) {
std::vector<char> reachable(static_cast<std::size_t>(target) + 1, 0);
reachable[0] = 1;
for (Number x : numbers) {
if (x > target) continue;
for (std::size_t c = static_cast<std::size_t>(target); c >= static_cast<std::size_t>(x); c--) {
if (reachable[c - static_cast<std::size_t>(x)]) reachable[c] = 1;
}
}
return reachable[static_cast<std::size_t>(target)] != 0;
}
// De una cobertura de tamaño a lo sumo k a una elección que suma el objetivo: se completa hasta tener
// exactamente k nodos, y cada arista con un solo extremo elegido toma su holgura.
inline std::vector<std::size_t> subset_from_cover(const SubsetSumInstance& s, const std::vector<int>& cover, int k) {
std::vector<char> inside(static_cast<std::size_t>(s.vertices), 0);
for (int v : cover) inside[static_cast<std::size_t>(v)] = 1;
int have = static_cast<int>(cover.size());
k = std::min(k, s.vertices);
for (std::size_t v = 0; have < k; v++) {
if (!inside[v]) {
inside[v] = 1;
have++;
}
}
std::vector<std::size_t> chosen;
for (std::size_t v = 0; v < inside.size(); v++) {
if (inside[v]) chosen.push_back(v);
}
for (std::size_t j = 0; j < s.edges.size(); j++) {
int ends = inside[static_cast<std::size_t>(s.edges[j].first)] + inside[static_cast<std::size_t>(s.edges[j].second)];
if (ends == 1) chosen.push_back(static_cast<std::size_t>(s.vertices) + j);
}
return chosen;
}
// De una elección que suma el objetivo a una cobertura: los números de nodo elegidos.
inline std::vector<int> cover_from_subset(const SubsetSumInstance& s, const std::vector<std::size_t>& chosen) {
std::vector<int> cover;
for (std::size_t i : chosen) {
if (i < static_cast<std::size_t>(s.vertices)) cover.push_back(static_cast<int>(i));
}
return cover;
}// La reducción de SAT a 3-SAT, cláusula por cláusula, comparada con la fuerza bruta.
#include <iostream>
#include "three_sat.h"
namespace {
void show(const Formula& f) {
Formula out = to_three_sat(f);
std::cout << "entrada: " << formula_text(f) << '\n';
std::cout << "salida, con " << out.variables << " variables y " << out.clauses.size() << " cláusulas:\n";
for (const Clause& c : out.clauses) std::cout << " " << clause_text(c) << '\n';
if (out.clauses.empty()) std::cout << " (ninguna)\n";
std::cout << " fuerza bruta: entrada " << (brute_sat(f) ? "satisfacible" : "insatisfacible") << ", salida "
<< (brute_sat(out) ? "satisfacible" : "insatisfacible") << "\n\n";
}
} // namespace
int main() {
std::cout << "Una cláusula larga se parte en una cadena:\n";
show({6, {{1, 2, 3, 4, 5, 6}}});
std::cout << "Una cláusula corta se rellena con variables nuevas:\n";
show({1, {{1}}});
show({2, {{1, -2}}});
std::cout << "Repetidos y cláusulas que siempre se cumplen:\n";
show({2, {{1, 1, -2, 1}}});
show({2, {{1, -1, 2}}});
std::cout << "Una fórmula sin solución sigue sin solución:\n";
show({2, {{1}, {-1, 2}, {-2}}});
Formula long_clause{6, {{1, 2, 3, 4, 5, 6}}};
Formula out = to_three_sat(long_clause);
Assignment a(7, false);
a[4] = true;
Assignment extended = extend_assignment(long_clause, a, out.variables);
std::cout << "Extender x4 = V en la cadena: " << assignment_text(extended) << '\n';
std::cout << " (x7, x8, x9 son y1, y2, y3)\n";
std::cout << " ¿cumple la salida? " << (satisfies(out, extended) ? "sí" : "no") << '\n';
}// La reducción de SAT (en CNF) a 3-SAT. La salida tiene exactamente tres literales por cláusula, de
// tres variables distintas. Es equisatisfactible con la entrada, no equivalente: agrega variables.
#pragma once
#include <algorithm>
#include <cstddef>
#include <cstdlib>
#include <vector>
#include "cnf.h"
// Quita literales repetidos. Devuelve false si la cláusula tiene x y ¬x, y entonces siempre se cumple.
inline bool clean_clause(const Clause& input, Clause& output) {
output.clear();
for (int literal : input) {
if (std::find(output.begin(), output.end(), -literal) != output.end()) return false;
if (std::find(output.begin(), output.end(), literal) == output.end()) output.push_back(literal);
}
return true;
}
inline Formula to_three_sat(const Formula& f) {
Formula out{f.variables, {}};
Clause c;
for (const Clause& input : f.clauses) {
if (!clean_clause(input, c)) continue;
const std::size_t k = c.size();
if (k == 0) {
// La cláusula vacía nunca se cumple: las 8 cláusulas sobre tres variables nuevas tampoco.
int y = out.variables + 1;
out.variables += 3;
for (int mask = 0; mask < 8; mask++) {
out.clauses.push_back({mask & 1 ? y : -y, mask & 2 ? y + 1 : -(y + 1), mask & 4 ? y + 2 : -(y + 2)});
}
} else if (k == 1) {
int y = ++out.variables;
int z = ++out.variables;
out.clauses.push_back({c[0], y, z});
out.clauses.push_back({c[0], y, -z});
out.clauses.push_back({c[0], -y, z});
out.clauses.push_back({c[0], -y, -z});
} else if (k == 2) {
int y = ++out.variables;
out.clauses.push_back({c[0], c[1], y});
out.clauses.push_back({c[0], c[1], -y});
} else if (k == 3) {
out.clauses.push_back(c);
} else {
// Una cadena: yⱼ dice "el literal verdadero está más adelante".
int y = ++out.variables;
out.clauses.push_back({c[0], c[1], y});
for (std::size_t i = 2; i + 2 < k; i++) {
int next = ++out.variables;
out.clauses.push_back({-y, c[i], next});
y = next;
}
out.clauses.push_back({-y, c[k - 2], c[k - 1]});
}
}
return out;
}
// La dirección "de ida" de la demostración: extiende una asignación de f a las variables nuevas. Recorre
// las cláusulas en el mismo orden que to_three_sat, así que asigna las mismas variables nuevas.
inline Assignment extend_assignment(const Formula& f, const Assignment& a, int total_variables) {
Assignment out(static_cast<std::size_t>(total_variables) + 1, false);
for (std::size_t v = 1; v < a.size(); v++) out[v] = a[v];
std::size_t next = a.size();
Clause c;
for (const Clause& input : f.clauses) {
if (!clean_clause(input, c)) continue;
const std::size_t k = c.size();
if (k == 0) {
next += 3;
} else if (k == 1) {
next += 2;
} else if (k == 2) {
next += 1;
} else if (k > 3) {
// i es la posición de un literal verdadero, contando desde 1. yⱼ es verdadera si i ≥ j + 2.
std::size_t i = 1;
while (i <= k && !literal_true(a, c[i - 1])) i++;
for (std::size_t j = 1; j + 3 <= k; j++) out[next++] = i >= j + 2;
}
}
return out;
}// 2-SAT: el grafo de implicaciones, las componentes y la asignación que sale de ellas.
#include <iostream>
#include "two_sat.h"
namespace {
void solve(int variables, const std::vector<TwoClause>& clauses) {
Formula f = two_clauses_as_formula(variables, clauses);
std::cout << formula_text(f) << '\n';
ImplicationGraph g = implication_graph(variables, clauses);
std::cout << " aristas:";
for (std::size_t u = 0; u < g.size(); u++) {
for (int v : g[u]) std::cout << ' ' << literal_text(two_sat_literal(u)) << "→" << literal_text(two_sat_literal(static_cast<std::size_t>(v)));
}
std::cout << '\n';
std::vector<int> comp = implication_components(g);
std::cout << " componentes, en el orden en que salen (de sumidero a fuente):";
int count = 0;
for (int c : comp) count = std::max(count, c + 1);
for (int c = 0; c < count; c++) {
std::cout << " {";
bool first = true;
for (std::size_t node = 0; node < comp.size(); node++) {
if (comp[node] == c) {
std::cout << (first ? "" : " ") << literal_text(two_sat_literal(node));
first = false;
}
}
std::cout << "}";
}
std::cout << '\n';
if (same_component_clash(comp)) {
for (std::size_t node = 0; node < comp.size(); node += 2) {
if (comp[node] == comp[node + 1]) std::cout << " " << literal_text(two_sat_literal(node)) << " y su negación en la misma componente\n";
}
std::cout << " insatisfacible; fuerza bruta: " << (brute_sat(f) ? "satisfacible" : "insatisfacible") << "\n\n";
return;
}
Assignment a = assignment_from_components(comp);
Assignment wrong = assignment_with_reversed_order(comp);
std::cout << " x verdadera si comp[x] < comp[¬x]: " << assignment_text(a) << ", ¿cumple? " << (satisfies(f, a) ? "sí" : "no") << '\n';
std::cout << " con el orden al revés: " << assignment_text(wrong) << ", ¿cumple? " << (satisfies(f, wrong) ? "sí" : "no") << "\n\n";
}
} // namespace
int main() {
solve(3, {{1, 2}, {-1, 3}, {-2, -3}, {-3, -1}});
solve(4, {{1, 2}, {-2, 3}, {-3, 4}});
solve(2, {{1, 2}, {1, -2}, {-1, 2}, {-1, -2}});
std::cout << "Cláusulas raras: (x1 ∨ x1) obliga a x1, y (x2 ∨ ¬x2) no dice nada.\n";
solve(2, {{1, 1}, {2, -2}});
std::vector<TwoClause> hard = {{1, 2}, {1, -2}, {-1, 2}, {-1, -2}};
ImplicationGraph half = implication_graph(2, hard, false);
std::cout << "Con una sola arista por cláusula, en la fórmula insatisfacible:\n";
std::cout << " ¿x y ¬x en la misma componente? " << (same_component_clash(implication_components(half)) ? "sí" : "no") << '\n';
}// 2-SAT con el grafo de implicaciones. Esta versión es recursiva y guarda todo en estructuras simples,
// porque aquí solo corre con fórmulas pequeñas. Con millones de nodos, la pila no alcanza: ahí va la
// pila explícita de la semana 6.
#pragma once
#include <cstddef>
#include <cstdlib>
#include <optional>
#include <utility>
#include <vector>
#include "cnf.h"
using TwoClause = std::pair<int, int>;
// El nodo de un literal: x_v es 2(v − 1), y ¬x_v es 2(v − 1) + 1.
inline std::size_t two_sat_node(int literal) {
std::size_t v = static_cast<std::size_t>(std::abs(literal) - 1);
return 2 * v + (literal > 0 ? 0 : 1);
}
inline int two_sat_literal(std::size_t node) {
int v = static_cast<int>(node / 2) + 1;
return node % 2 == 0 ? v : -v;
}
using ImplicationGraph = std::vector<std::vector<int>>;
// Cada cláusula (a ∨ b) da ¬a → b y ¬b → a. Con both = false sale la versión con una sola arista.
inline ImplicationGraph implication_graph(int variables, const std::vector<TwoClause>& clauses, bool both = true) {
ImplicationGraph g(2 * static_cast<std::size_t>(variables));
for (auto [a, b] : clauses) {
g[two_sat_node(-a)].push_back(static_cast<int>(two_sat_node(b)));
if (both) g[two_sat_node(-b)].push_back(static_cast<int>(two_sat_node(a)));
}
return g;
}
inline void finish_order_from(const ImplicationGraph& g, std::size_t u, std::vector<char>& seen, std::vector<std::size_t>& order) {
seen[u] = 1;
for (int v : g[u]) {
if (!seen[static_cast<std::size_t>(v)]) finish_order_from(g, static_cast<std::size_t>(v), seen, order);
}
order.push_back(u);
}
inline void label_from(const ImplicationGraph& g, std::size_t u, int label, std::vector<int>& comp) {
comp[u] = label;
for (int v : g[u]) {
if (comp[static_cast<std::size_t>(v)] == -1) label_from(g, static_cast<std::size_t>(v), label, comp);
}
}
// Kosaraju-Sharir como en la semana 6: las componentes salen de sumidero a fuente, numeradas desde 0.
// Por eso una arista u → v entre componentes distintas cumple comp[u] > comp[v].
inline std::vector<int> implication_components(const ImplicationGraph& g) {
ImplicationGraph inverted(g.size());
for (std::size_t u = 0; u < g.size(); u++) {
for (int v : g[u]) inverted[static_cast<std::size_t>(v)].push_back(static_cast<int>(u));
}
std::vector<char> seen(g.size(), 0);
std::vector<std::size_t> order;
for (std::size_t u = 0; u < g.size(); u++) {
if (!seen[u]) finish_order_from(inverted, u, seen, order);
}
std::vector<int> comp(g.size(), -1);
int label = 0;
for (std::size_t i = order.size(); i-- > 0;) {
if (comp[order[i]] == -1) label_from(g, order[i], label++, comp);
}
return comp;
}
inline bool same_component_clash(const std::vector<int>& comp) {
for (std::size_t node = 0; node < comp.size(); node += 2) {
if (comp[node] == comp[node + 1]) return true;
}
return false;
}
// x es verdadera si su componente salió antes que la de ¬x: está más cerca de los sumideros.
inline Assignment assignment_from_components(const std::vector<int>& comp) {
Assignment a(comp.size() / 2 + 1, false);
for (std::size_t v = 1; v < a.size(); v++) a[v] = comp[2 * (v - 1)] < comp[2 * (v - 1) + 1];
return a;
}
// El error: leer el orden al revés.
inline Assignment assignment_with_reversed_order(const std::vector<int>& comp) {
Assignment a(comp.size() / 2 + 1, false);
for (std::size_t v = 1; v < a.size(); v++) a[v] = comp[2 * (v - 1)] > comp[2 * (v - 1) + 1];
return a;
}
inline std::optional<Assignment> solve_two_sat(int variables, const std::vector<TwoClause>& clauses) {
std::vector<int> comp = implication_components(implication_graph(variables, clauses));
if (same_component_clash(comp)) return std::nullopt;
return assignment_from_components(comp);
}
inline Formula two_clauses_as_formula(int variables, const std::vector<TwoClause>& clauses) {
Formula f{variables, {}};
for (auto [a, b] : clauses) f.clauses.push_back({a, b});
return f;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Las pruebas de lo que afirman los apuntes de la semana 14.
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <random>
#include <vector>
#include "coloring.h"
#include "frontier.h"
#include "independent_set.h"
#include "knapsack_search.h"
#include "subset_sum.h"
#include "three_sat.h"
#include "two_sat.h"
namespace {
using Random = std::mt19937;
int below(Random& random, int limit) { return static_cast<int>(random() % static_cast<std::uint32_t>(limit)); }
// Fórmula en CNF con cláusulas de cualquier largo, con repetidos y con x ∨ ¬x.
Formula random_cnf(Random& random, int variables, int clauses, int longest) {
Formula f{variables, {}};
for (int c = 0; c < clauses; c++) {
Clause clause;
int size = 1 + below(random, longest);
for (int i = 0; i < size; i++) {
int v = 1 + below(random, variables);
clause.push_back(below(random, 2) ? v : -v);
}
f.clauses.push_back(clause);
}
return f;
}
// Fórmula 3-SAT: tres variables distintas por cláusula.
Formula random_three(Random& random, int variables, int clauses) {
Formula f{variables, {}};
for (int c = 0; c < clauses; c++) {
int a = 1 + below(random, variables);
int b = a;
int d = a;
while (b == a) b = 1 + below(random, variables);
while (d == a || d == b) d = 1 + below(random, variables);
f.clauses.push_back({below(random, 2) ? a : -a, below(random, 2) ? b : -b, below(random, 2) ? d : -d});
}
return f;
}
Graph random_graph(Random& random, int n, int percent) {
Graph g(static_cast<std::size_t>(n));
for (int u = 0; u < n; u++) {
for (int v = u + 1; v < n; v++) {
if (below(random, 100) < percent) add_edge(g, u, v);
}
}
return g;
}
void test_three_sat() {
Random random(1401);
for (int round = 0; round < 1500; round++) {
Formula f = random_cnf(random, 1 + below(random, 5), below(random, 5), 7);
Formula out = to_three_sat(f);
// Forma: exactamente tres literales, de tres variables distintas.
for (const Clause& c : out.clauses) {
assert(c.size() == 3);
assert(std::abs(c[0]) != std::abs(c[1]) && std::abs(c[0]) != std::abs(c[2]) && std::abs(c[1]) != std::abs(c[2]));
}
// Tamaño: k literales dan a lo sumo máx(k, 4) cláusulas nuevas y a lo sumo máx(k, 2) variables.
std::size_t clause_bound = 0;
int variable_bound = f.variables;
for (const Clause& c : f.clauses) {
clause_bound += std::max<std::size_t>(c.size(), 4);
variable_bound += std::max(static_cast<int>(c.size()), 2);
}
assert(out.clauses.size() <= clause_bound);
assert(out.variables <= variable_bound);
// Las dos direcciones. La de ida es constructiva, y la de vuelta se lee restringiendo.
auto sat = brute_sat(f);
if (out.variables <= 14) {
auto sat_out = brute_sat(out);
assert(sat.has_value() == sat_out.has_value());
if (sat_out) {
Assignment restricted(sat_out->begin(), sat_out->begin() + f.variables + 1);
assert(satisfies(f, restricted));
}
}
if (sat) assert(satisfies(out, extend_assignment(f, *sat, out.variables)));
}
// Una cláusula vacía nunca se cumple, y su reemplazo tampoco.
Formula empty{1, {{}}};
assert(!brute_sat(empty));
assert(!brute_sat(to_three_sat(empty)));
// Una cláusula con x y ¬x desaparece.
assert(to_three_sat({2, {{1, -1, 2}}}).clauses.empty());
}
void test_independent_set() {
Random random(1402);
for (int round = 0; round < 1500; round++) {
Formula f = random_three(random, 3 + below(random, 3), 1 + below(random, 8));
IndependentSetInstance s = reduce_to_independent_set(f);
std::vector<int> best = max_independent_set(s.graph);
auto sat = brute_sat(f);
assert(is_independent(s.graph, best));
assert(static_cast<int>(best.size()) <= s.k); // a lo sumo uno por triángulo
assert(sat.has_value() == (static_cast<int>(best.size()) == s.k));
if (sat) {
std::vector<int> nodes = set_from_assignment(f, *sat);
assert(static_cast<int>(nodes.size()) == s.k && is_independent(s.graph, nodes));
assert(satisfies(f, assignment_from_set(f, s, best)));
}
}
// Pregunta de repaso 2.
Formula two{3, {{1, -2, 3}, {-1, 2, -3}}};
assert(max_independent_set(reduce_to_independent_set(two).graph).size() == 2);
// Las ocho cláusulas: no hay solución, y falta justo uno. Sin alguna clase de aristas, la respuesta es falsa.
Formula eight = all_eight_clauses();
assert(!brute_sat(eight));
assert(max_independent_set(reduce_to_independent_set(eight).graph).size() == 7);
assert(max_independent_set(reduce_to_independent_set(eight, true, false).graph).size() == 8);
assert(max_independent_set(reduce_to_independent_set(eight, false, true).graph).size() >= 8);
}
void test_coloring() {
// Compuerta O: si las dos entradas son F, la salida es F; si alguna es T, la salida puede ser T.
assert((or_gate_outputs(COLOR_F, COLOR_F) == std::array<bool, 3>{false, true, false}));
for (int a : {COLOR_F, COLOR_T}) {
for (int b : {COLOR_F, COLOR_T}) {
if (a == COLOR_T || b == COLOR_T) assert(or_gate_outputs(a, b)[COLOR_T]);
}
}
// Las 8 entradas del gadget.
for (int mask = 0; mask < 8; mask++) {
int count = clause_gadget_colorings(mask & 1, mask & 2, mask & 4);
assert((count > 0) == (mask != 0));
}
// Tamaño y correspondencia con SAT.
Random random(1403);
for (int round = 0; round < 200; round++) {
Formula f = random_three(random, 3 + below(random, 2), 1 + below(random, 10));
Graph g = reduce_to_coloring(f);
assert(g.size() == static_cast<std::size_t>(3 + 2 * f.variables) + 6 * f.clauses.size());
assert(edge_count(g) == static_cast<std::size_t>(3 + 3 * f.variables) + 12 * f.clauses.size());
auto sat = brute_sat(f);
auto color = find_coloring(g, 3);
assert(sat.has_value() == color.has_value());
if (sat) assert(is_proper_coloring(g, coloring_from_assignment(f, *sat, g), 3));
if (color) {
// La lectura no depende del nombre de los colores.
std::vector<int> names = {0, 1, 2};
do {
std::vector<int> renamed = *color;
for (int& c : renamed) c = names[static_cast<std::size_t>(c)];
assert(satisfies(f, assignment_from_coloring(f, renamed)));
} while (std::next_permutation(names.begin(), names.end()));
}
}
// Qué arista hace falta, en la fórmula sin solución.
Formula eight = all_eight_clauses();
assert(!find_coloring(reduce_to_coloring(eight), 3));
GadgetOptions no_var;
no_var.variable_to_b = false;
assert(find_coloring(reduce_to_coloring(eight, no_var), 3));
GadgetOptions no_f;
no_f.output_to_f = false;
assert(find_coloring(reduce_to_coloring(eight, no_f), 3));
// La arista de la salida a B no es indispensable: con solo F la salida no puede ser F.
GadgetOptions no_b;
no_b.output_to_b = false;
for (int round = 0; round < 60; round++) {
Formula f = random_three(random, 3, 1 + below(random, 10));
assert(brute_sat(f).has_value() == find_coloring(reduce_to_coloring(f, no_b), 3).has_value());
}
assert(!find_coloring(reduce_to_coloring(eight, no_b), 3));
}
void test_subset_sum() {
Random random(1404);
for (int round = 0; round < 400; round++) {
int n = 1 + below(random, 6);
Graph g = random_graph(random, n, 45);
if (edge_count(g) > 9) continue;
int cover = static_cast<int>(min_vertex_cover(g).size());
for (int k = 0; k <= n + 2; k++) {
SubsetSumInstance s = reduce_to_subset_sum(g, k);
// Sin acarreos: en cada dígito de arista, todos los números juntos suman a lo sumo 3.
for (std::size_t j = 0; j < s.edges.size(); j++) {
Number digit_sum = 0;
for (Number x : s.numbers) digit_sum += (x / power_of(4, j)) % 4;
assert(digit_sum == 3);
}
auto chosen = brute_subset_sum(s.numbers, s.target);
assert(chosen.has_value() == (cover <= k));
// La base 3 también da bien: el ejercicio del capítulo pide demostrarlo.
SubsetSumInstance three = reduce_to_subset_sum(g, k, 3);
assert(brute_subset_sum(three.numbers, three.target).has_value() == (cover <= k));
assert(subset_sum_dp(s.numbers, s.target) == (cover <= k));
if (chosen) {
std::vector<int> nodes = cover_from_subset(s, *chosen);
assert(is_cover(g, nodes));
assert(static_cast<int>(nodes.size()) == std::min(k, n));
}
if (cover <= k) {
assert(sums_to_target(s, subset_from_cover(s, min_vertex_cover(g), k)));
}
}
}
// En base 2 hay una respuesta falsa: dos aristas separadas y k = 0.
Graph two_edges = make_graph(4, {{0, 1}, {2, 3}});
SubsetSumInstance base_two = reduce_to_subset_sum(two_edges, 0, 2);
assert(brute_subset_sum(base_two.numbers, base_two.target).has_value());
// Sin aristas, y con k = 0.
Graph empty(3);
assert(brute_subset_sum(reduce_to_subset_sum(empty, 0).numbers, reduce_to_subset_sum(empty, 0).target).has_value());
// Con una arista y k = 0 no hay solución: la holgura sola da 1, y hacen falta 2.
Graph edge = make_graph(2, {{0, 1}});
assert(!brute_subset_sum(reduce_to_subset_sum(edge, 0).numbers, reduce_to_subset_sum(edge, 0).target));
}
void test_knapsack() {
Random random(1405);
for (int round = 0; round < 1500; round++) {
std::vector<Item> items;
int n = 1 + below(random, 7);
for (int i = 0; i < n; i++) items.push_back({static_cast<Number>(1 + below(random, 12)), static_cast<Number>(below(random, 15))});
Number capacity = static_cast<Number>(below(random, 30));
int calls = 0;
Number best = knapsack_optimum_by_search(items, capacity, calls);
assert(best == knapsack_best(items, capacity));
// Número de llamadas: a lo sumo ⌈log₂(suma + 1)⌉.
Number total = 0;
for (const Item& item : items) total += item.value;
int bits = 0;
while ((1ULL << bits) < total + 1) bits++;
assert(calls <= bits);
std::vector<std::size_t> chosen = knapsack_items_by_search(items, capacity);
Number weight = 0;
Number value = 0;
for (std::size_t i : chosen) {
weight += items[i].weight;
value += items[i].value;
}
assert(weight <= capacity && value == best);
// Suma de subconjuntos se reduce a la decisión de la mochila.
std::vector<Number> numbers;
for (int i = 0; i < n; i++) numbers.push_back(static_cast<Number>(1 + below(random, 20)));
Number target = static_cast<Number>(below(random, 40));
assert(knapsack_decide(knapsack_from_subset_sum(numbers, target)) == brute_subset_sum(numbers, target).has_value());
}
}
void test_two_sat() {
Random random(1406);
int reversed_fails = 0;
int unsatisfiable = 0;
int half_wrong = 0;
for (int round = 0; round < 6000; round++) {
int n = 1 + below(random, 7);
int m = below(random, 2 * n + 2);
std::vector<TwoClause> clauses;
for (int c = 0; c < m; c++) {
int a = 1 + below(random, n);
int b = 1 + below(random, n);
// Incluye (x ∨ x) y (x ∨ ¬x), porque a y b pueden ser la misma variable.
clauses.push_back({below(random, 2) ? a : -a, below(random, 2) ? b : -b});
}
Formula f = two_clauses_as_formula(n, clauses);
auto brute = brute_sat(f);
std::vector<int> comp = implication_components(implication_graph(n, clauses));
// Una arista entre componentes va de la que salió más tarde a la que salió antes.
ImplicationGraph g = implication_graph(n, clauses);
for (std::size_t u = 0; u < g.size(); u++) {
for (int v : g[u]) assert(comp[u] >= comp[static_cast<std::size_t>(v)]);
}
assert(same_component_clash(comp) != brute.has_value());
auto solved = solve_two_sat(n, clauses);
assert(solved.has_value() == brute.has_value());
if (solved) {
assert(satisfies(f, *solved));
if (!satisfies(f, assignment_with_reversed_order(comp))) reversed_fails++;
} else {
unsatisfiable++;
}
if (same_component_clash(implication_components(implication_graph(n, clauses, false))) != !brute.has_value()) half_wrong++;
}
assert(reversed_fails > 0 && unsatisfiable > 0 && half_wrong > 0);
// Con una sola arista por cláusula, la fórmula insatisfacible parece satisfacible.
std::vector<TwoClause> hard = {{1, 2}, {1, -2}, {-1, 2}, {-1, -2}};
assert(same_component_clash(implication_components(implication_graph(2, hard))));
assert(!same_component_clash(implication_components(implication_graph(2, hard, false))));
// (x1 ∨ x1) obliga a x1, y con el orden al revés sale falsa.
std::vector<TwoClause> unit = {{1, 1}};
assert(*solve_two_sat(1, unit) == Assignment({false, true}));
assert(assignment_with_reversed_order(implication_components(implication_graph(1, unit))) == Assignment({false, false}));
}
void test_frontier() {
Random random(1407);
int odd_cycles = 0;
for (int round = 0; round < 3000; round++) {
int n = 1 + below(random, 7);
Graph g = random_graph(random, n, 35);
auto two = two_coloring(g);
assert(two.has_value() == find_coloring(g, 2).has_value());
if (two) assert(is_proper_coloring(g, *two, 2));
else odd_cycles++;
// Euler coincide con buscar el ciclo, y hace falta la conexión.
if (edge_count(g) <= 10) assert(has_euler_cycle(g) == euler_cycle_brute(g));
}
assert(odd_cycles > 0);
Graph two_triangles = make_graph(6, {{0, 1}, {1, 2}, {0, 2}, {3, 4}, {4, 5}, {3, 5}});
for (const auto& list : two_triangles) assert(list.size() % 2 == 0);
assert(!has_euler_cycle(two_triangles));
// Camino más largo en un DAG: los nodos están numerados en orden topológico.
for (int round = 0; round < 3000; round++) {
int n = 1 + below(random, 8);
Dag dag(static_cast<std::size_t>(n));
for (int u = 0; u < n; u++) {
for (int v = u + 1; v < n; v++) {
if (below(random, 100) < 40) dag[static_cast<std::size_t>(u)].push_back(v);
}
}
assert(longest_path_dag(dag) == longest_path_dag_brute(dag));
}
Graph square = make_graph(4, {{0, 1}, {1, 2}, {2, 3}, {0, 3}});
Graph star = make_graph(4, {{0, 1}, {0, 2}, {0, 3}});
assert(longest_simple_path(square) == 3);
assert(longest_simple_path(star) == 2);
}
void test_complement() {
// Lo que se usa de la semana 13: S es independiente si y solo si V − S es una cobertura.
Random random(1408);
for (int round = 0; round < 500; round++) {
int n = 1 + below(random, 8);
Graph g = random_graph(random, n, 40);
assert(max_independent_set(g).size() + min_vertex_cover(g).size() == static_cast<std::size_t>(n));
}
}
} // namespace
int main() {
test_three_sat();
test_independent_set();
test_coloring();
test_subset_sum();
test_knapsack();
test_two_sat();
test_frontier();
test_complement();
std::cout << "Todas las pruebas de la semana 14 pasaron.\n";
}Laboratorio
Archivos de partida del laboratorio de la semana.
TIEMPO = 2
VERIFICADOR = verificador.cpp
include ../../../lab.mk# Turnos sin conflicto
Un pabellón de laboratorios tiene n secciones, numeradas de 1 a n. Cada sección se dicta en un solo turno: mañana o tarde.
Docentes, estudiantes y la oficina de horarios mandan m pedidos, y hay que elegir el turno de cada sección para cumplirlos
todos, o decir que no se puede.
Cada pedido habla de dos **condiciones**. Una condición es un entero distinto de 0: el entero `s`, positivo, significa "la
sección s va de mañana", y el entero `-s` significa "la sección s va de tarde". Hay cuatro clases de pedido:
| Pedido | Qué exige |
|---|---|
| `O p q` | Se cumple p, o q, o las dos. |
| `N p q` | No se cumplen p y q a la vez. |
| `S p q` | Si se cumple p, también se cumple q. |
| `D p q` | Se cumple exactamente una de las dos. |
Las dos condiciones de un pedido pueden hablar de la misma sección. Por ejemplo, `O 3 3` exige que la sección 3 vaya de mañana,
`O 3 -3` no exige nada, y `D 3 3` no se puede cumplir de ninguna manera. Un mismo pedido puede repetirse. Una sección de la que
nadie habla también recibe un turno.
## Entrada
La primera línea tiene dos enteros, n y m. Luego vienen m líneas, cada una con una letra (`O`, `N`, `S` o `D`) y dos enteros p y q.
## Salida
Si se pueden cumplir todos los pedidos, escriban `SI` y, en la línea siguiente, una cadena de exactamente n letras. La letra número
s es `M` si la sección s va de mañana, y `T` si va de tarde. Si no se pueden cumplir todos, escriban solo `NO`.
Puede haber varios horarios correctos, y cualquiera vale: un verificador revisa su respuesta.
## Límites
- 1 ≤ n ≤ 500 000
- 0 ≤ m ≤ 500 000
- 1 ≤ |p|, |q| ≤ n
## Ejemplo
Entrada:
```text
4 5
O 1 2
S 1 -3
D 3 4
N 2 4
O 4 -1
```
Salida:
```text
SI
TMMT
```
Con la sección 1 de tarde, y 2 y 3 de mañana y 4 de tarde: `O 1 2` se cumple por la sección 2. `S 1 -3` se cumple porque la
sección 1 no va de mañana. `D 3 4` se cumple porque solo la condición 3 es cierta. `N 2 4` se cumple porque la sección 4 no va de
mañana, y `O 4 -1` porque la sección 1 va de tarde.
## Tiempo
2 segundos por caso, compilado con `-O2`. Corran `make test` para probar con el juez. Algunos casos no se guardan, por su tamaño:
los genera `casos/gen_NN.cpp`. La pila del juez es la que trae el contenedor por defecto: `ulimit -s` responde 8192, en KB.// Turnos sin conflicto: lee los pedidos y responde que no se pueden cumplir. Esa respuesta casi nunca
// es la correcta. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n = 0;
int m = 0;
std::cin >> n >> m;
std::vector<char> kind(static_cast<std::size_t>(m));
std::vector<int> p(static_cast<std::size_t>(m));
std::vector<int> q(static_cast<std::size_t>(m));
for (std::size_t i = 0; i < kind.size(); i++) std::cin >> kind[i] >> p[i] >> q[i];
// Por hacer: decidir si hay un horario, y escribirlo.
std::cout << "NO\n";
}// Verificador de "Turnos sin conflicto": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA está solo SI o NO. Si es NO, la salida tiene que ser exactamente NO. Si es SI, se lee el
// horario y se revisa cada pedido: no se le cree a nadie.
// 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>
namespace {
[[noreturn]] void reject(const std::string& reason) {
std::cout << reason << '\n';
std::exit(1);
}
bool holds(const std::string& schedule, int condition) {
const std::size_t s = static_cast<std::size_t>(condition < 0 ? -condition : condition) - 1;
return condition > 0 ? schedule[s] == 'M' : schedule[s] == 'T';
}
} // 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::string answer;
expected >> answer;
std::string word;
if (!(output >> word)) reject("la salida está vacía");
if (word != "SI" && word != "NO") reject("la primera palabra tiene que ser SI o NO");
if (word != answer) reject("se esperaba " + answer + ", salió " + word);
if (word == "NO") {
if (output >> word) reject("sobra texto después de NO");
return 0;
}
std::string schedule;
if (!(output >> schedule)) reject("falta el horario");
if (schedule.size() != n) reject("el horario tiene " + std::to_string(schedule.size()) + " letras");
for (char c : schedule) {
if (c != 'M' && c != 'T') reject("el horario solo puede tener M y T");
}
if (output >> word) reject("sobra texto después del horario");
for (std::size_t i = 1; i <= m; i++) {
char kind = 0;
int p = 0;
int q = 0;
input >> kind >> p >> q;
const bool a = holds(schedule, p);
const bool b = holds(schedule, q);
bool fine = false;
if (kind == 'O') fine = a || b;
else if (kind == 'N') fine = !(a && b);
else if (kind == 'S') fine = !a || b;
else if (kind == 'D') fine = a != b;
else reject("clase de pedido desconocida en la entrada");
if (!fine) reject("no se cumple el pedido " + std::to_string(i));
}
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 14: NP-completitud. Reducciones desde SAT y 3-SAT, gadgets, suma de subconjuntos y la
# frontera entre lo fácil y lo difícil. Se usa dentro del entorno del curso.
#
# make compila los ejemplos en build/
# make test corre las pruebas de los ejemplos, con sanitizers
# make bench mide la programación dinámica en instancias que salen de la reducción, con -O2
# make solver pasa fórmulas 3-SAT al azar y el principio del palomar por minisat
# 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 = cnf.h graph.h three_sat.h independent_set.h coloring.h subset_sum.h knapsack_search.h two_sat.h frontier.h
PROGRAMS = three_sat_demo is_demo coloring_demo subset_demo knapsack_demo two_sat_demo frontier_demo test_examples
BENCHES = subset_bench random_3sat pigeonhole
.PHONY: all test bench solver 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: $(BUILD)/subset_bench
$(BUILD)/subset_bench
solver: $(BUILD)/random_3sat $(BUILD)/pigeonhole
bash solver_runs.sh
clean:
rm -rf $(BUILD)