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

Preguntas de repaso

  1. ¿Qué dos cosas hay que demostrar para probar que un problema es NP-completo?
  2. Corran la reducción de 3-SAT a conjunto independiente con una fórmula de dos cláusulas.
  3. ¿Por qué la reducción a suma de subconjuntos necesita números con muchos dígitos?
  4. 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.
  5. 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.

cnf.hDescargar cnf.h
// 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;
}
coloring_demo.cppDescargar coloring_demo.cpp
// 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';
}
coloring.hDescargar coloring.h
// 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;
}
frontier_demo.cppDescargar frontier_demo.cpp
// 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';
}
frontier.hDescargar frontier.h
// 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;
}
graph.hDescargar graph.h
// 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;
}
independent_set.hDescargar independent_set.h
// 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;
}
is_demo.cppDescargar is_demo.cpp
// 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';
}
knapsack_demo.cppDescargar knapsack_demo.cpp
// 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';
    }
}
knapsack_search.hDescargar knapsack_search.h
// 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;
}
pigeonhole.cppDescargar pigeonhole.cpp
// 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";
        }
    }
}
random_3sat.cppDescargar random_3sat.cpp
// 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";
    }
}
solver_runs.shDescargar solver_runs.sh
#!/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
subset_bench.cppDescargar subset_bench.cpp
// 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';
    }
}
subset_demo.cppDescargar subset_demo.cpp
// 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);
}
subset_sum.hDescargar subset_sum.h
// 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;
}
three_sat_demo.cppDescargar three_sat_demo.cpp
// 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';
}
three_sat.hDescargar three_sat.h
// 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;
}
two_sat_demo.cppDescargar two_sat_demo.cpp
// 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';
}
two_sat.hDescargar two_sat.h
// 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.

test_examples.cppDescargar test_examples.cpp
// 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.

lab/two_sat/MakefileDescargar lab/two_sat/Makefile
TIEMPO = 2
VERIFICADOR = verificador.cpp
include ../../../lab.mk
lab/two_sat/README.mdDescargar lab/two_sat/README.md
# 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.
lab/two_sat/solution.cppDescargar lab/two_sat/solution.cpp
// 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";
}
lab/two_sat/verificador.cppDescargar lab/two_sat/verificador.cpp
// 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.

MakefileDescargar Makefile
# 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)