Semana 13 — P, NP y reducciones

Objetivos

  • Definir P y NP para problemas de decisión.
  • Escribir una reducción polinomial en la dirección correcta.
  • Reducir entre conjunto independiente, cobertura de vértices y clique.
  • Codificar un problema en CNF y resolverlo con un solucionador SAT.

Sesión A — P, NP y reducciones polinomiales

Lectura: Apuntes, semana 13.

Apoyo: Erickson, secs. 12.1 (A Game You Can’t Win), 12.2 (P versus NP) y 12.3 (NP-hard, NP-easy, and NP-complete). CLRS, capítulo NP-Completeness, 4.ª edición.

Temas:

  • Por qué existen problemas sin algoritmo rápido conocido. Problemas parecidos con destinos distintos: camino mínimo y camino más largo, 2-coloreo y 3-coloreo.
  • Problemas de decisión. Tamaño de la entrada en bits: vuelve la mochila de la semana 7.
  • P: se resuelve en tiempo polinomial. NP: un certificado se verifica en tiempo polinomial.
  • La pregunta P contra NP, sin resolver, y lo que se cree.
  • Reducción polinomial A ≤ₚ B: B es al menos tan difícil como A. Cómo usarla en cada sentido.
  • Error común: hacer la reducción al revés, del problema nuevo hacia uno conocido, lo cual solo dice que el nuevo no es más difícil.

Sesión B — Conjunto independiente, cobertura de vértices y SAT

Lectura: Apuntes, semana 13.

Apoyo: Erickson, secs. 12.5 (Reductions and Sat), 12.8 (The General Pattern) y 12.9 (Clique and Vertex Cover). CLRS, capítulo NP-Completeness, 4.ª edición.

Temas:

  • Conjunto independiente y cobertura de vértices: uno es el complemento del otro.
  • Conjunto independiente y clique: el grafo complementario.
  • El patrón de una reducción: transformar en tiempo polinomial y demostrar las dos direcciones.
  • SAT y CNF. Por qué un solucionador SAT es una herramienta práctica, aunque SAT sea difícil en teoría.
  • Codificar un problema: variables, y una cláusula por cada regla. Ejemplo: el coloreo de grafos. Formato DIMACS.
  • Error común: codificar “al menos uno” y olvidar “a lo sumo uno” cuando ninguna otra regla lo implica.

Laboratorio — Resolver sudokus con un solucionador SAT

  1. Escribir encode: lee un sudoku y escribe una fórmula CNF en formato DIMACS, con una variable por cada (fila, columna, dígito).
  2. Escribir decode: lee la respuesta de minisat e imprime el sudoku resuelto.
  3. Pasar los casos del juez, de hasta 25 × 25, incluido un sudoku sin solución.
  4. Comparar con su solucionador con backtracking de Introducción a la Programación en un sudoku difícil. Reportar tiempos, y cuántas variables y cláusulas tiene la fórmula.

Preguntas de repaso

  1. ¿Qué es un certificado para conjunto independiente? ¿Qué se verifica y cuánto cuesta?
  2. Si A ≤ₚ B y B está en P, ¿qué se sabe de A? ¿Y si A ≤ₚ B y A es difícil?
  3. Demuestren: S es un conjunto independiente si y solo si V − S es una cobertura de vértices.
  4. Escriban en CNF “exactamente uno de x, y, z es verdadero”.
  5. Si la mochila se resuelve en O(nW), ¿por qué eso no demuestra P = NP?

Código de la semana

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

Ejemplos (14)Pruebas (1)Laboratorio (5)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

backtrack_bench.cppDescargar backtrack_bench.cpp
// Backtracking por filas, para comparar con el solucionador SAT en el laboratorio. Lee un sudoku de 9 por 9 de un
// archivo, en el formato de Introducción a la Programación: 9 líneas de 9 dígitos, con 0 en las celdas vacías. Sin
// archivo, usa uno de 17 valores dados, difícil para este orden de búsqueda. Solo informa nodos y tiempo.
#include <array>
#include <chrono>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

namespace {

using Grid = std::array<int, 81>;

const char* const hard_puzzle[] = {"000000000", "000003085", "001020000", "000507000", "004000100",
                                   "090000000", "500000073", "002010000", "000040009"};

struct Counter {
    long long nodes = 0;
};

bool can_place(const Grid& grid, int position, int digit) {
    const int r = position / 9, c = position % 9;
    for (int i = 0; i < 9; i++) {
        if (grid[r * 9 + i] == digit || grid[i * 9 + c] == digit) return false;
        if (grid[(r / 3 * 3 + i / 3) * 9 + c / 3 * 3 + i % 3] == digit) return false;
    }
    return true;
}

bool solve(Grid& grid, int position, Counter& counter) {
    counter.nodes++;
    while (position < 81 && grid[position] != 0) position++;
    if (position == 81) return true;
    for (int digit = 1; digit <= 9; digit++) {
        if (can_place(grid, position, digit)) {
            grid[position] = digit;
            if (solve(grid, position + 1, counter)) return true;
            grid[position] = 0;
        }
    }
    return false;
}

}  // namespace

int main(int argc, char** argv) {
    std::vector<std::string> lines;
    if (argc > 1) {
        std::ifstream in(argv[1]);
        std::string line;
        while (in >> line) lines.push_back(line);
    } else {
        lines.assign(std::begin(hard_puzzle), std::end(hard_puzzle));
    }
    Grid grid{};
    int given = 0;
    for (int i = 0; i < 81; i++) {
        grid[i] = lines.at(i / 9).at(i % 9) - '0';
        given += grid[i] != 0 ? 1 : 0;
    }
    Counter counter;
    const auto start = std::chrono::steady_clock::now();
    const bool solved = solve(grid, 0, counter);
    const std::chrono::duration<double> elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "valores dados: " << given << "\n";
    std::cout << "resultado: " << (solved ? "tiene solución" : "sin solución") << "\n";
    std::cout << "nodos: " << counter.nodes << "\n";
    std::cout << "segundos: " << elapsed.count() << "\n";
}
cnf_demo.cppDescargar cnf_demo.cpp
// CNF a mano: "exactamente uno de x, y, z" con su tabla de verdad, y un DPLL seguido paso a paso.
#include <iostream>

#include "cnf.h"

int main() {
    Cnf one;
    one.variables = 3;
    add_exactly_one(one, {1, 2, 3});
    std::cout << "Exactamente uno de x, y, z. Las variables son 1, 2 y 3:\n" << one.to_dimacs();
    std::cout << "\nx y z  fórmula  verdaderas\n";
    for (int mask = 0; mask < 8; mask++) {
        Assignment value{false, (mask & 4) != 0, (mask & 2) != 0, (mask & 1) != 0};
        std::cout << value[1] << ' ' << value[2] << ' ' << value[3] << "  " << satisfies(one, value) << "        "
                  << value[1] + value[2] + value[3] << '\n';
    }

    Cnf formula;
    formula.variables = 4;
    formula.add({1, 2});
    formula.add({-1, 3});
    formula.add({-1, -3});
    formula.add({-2, 4});
    std::cout << "\nDPLL con (1 ∨ 2) ∧ (¬1 ∨ 3) ∧ (¬1 ∨ ¬3) ∧ (¬2 ∨ 4):\n";
    Dpll solver(formula, &std::cout);
    bool sat = solver.solve();
    std::cout << (sat ? "satisfacible:" : "insatisfacible");
    if (sat) {
        for (int v = 1; v <= formula.variables; v++) std::cout << ' ' << (solver.value(v) ? v : -v);
    }
    std::cout << '\n';
}
cnf.hDescargar cnf.h
// Fórmulas en CNF. Las variables van de 1 a variables, y un literal negativo es la variable negada,
// igual que en DIMACS.
#ifndef CNF_H
#define CNF_H

#include <cstdlib>
#include <ostream>
#include <sstream>
#include <string>
#include <vector>

using Clause = std::vector<int>;

struct Cnf {
    int variables = 0;
    std::vector<Clause> clauses;

    void add(Clause clause) { clauses.push_back(std::move(clause)); }

    std::string to_dimacs() const {
        std::ostringstream out;
        out << "p cnf " << variables << ' ' << clauses.size() << '\n';
        for (const Clause& clause : clauses) {
            for (int literal : clause) out << literal << ' ';
            out << "0\n";
        }
        return out.str();
    }
};

// value[v] es el valor de la variable v; value[0] no se usa.
using Assignment = std::vector<bool>;

inline bool literal_true(int literal, const Assignment& value) {
    return literal > 0 ? value[literal] : !value[-literal];
}

// El verificador de SAT: el certificado es la asignación, y se mira cada literal una vez.
inline bool satisfies(const Cnf& formula, const Assignment& value) {
    for (const Clause& clause : formula.clauses) {
        bool any = false;
        for (int literal : clause) any = any || literal_true(literal, value);
        if (!any) return false;
    }
    return true;
}

// Reglas sobre una lista de variables.
inline void add_at_least_one(Cnf& formula, const std::vector<int>& vars) { formula.add(vars); }

inline void add_at_most_one(Cnf& formula, const std::vector<int>& vars) {
    for (std::size_t i = 0; i < vars.size(); i++) {
        for (std::size_t j = i + 1; j < vars.size(); j++) formula.add({-vars[i], -vars[j]});
    }
}

inline void add_exactly_one(Cnf& formula, const std::vector<int>& vars) {
    add_at_least_one(formula, vars);
    add_at_most_one(formula, vars);
}

// Fuerza bruta: las 2^variables asignaciones. Llama a visit con cada una que satisface.
template <typename Visit>
long long for_each_model(const Cnf& formula, Visit visit) {
    long long models = 0;
    const long long total = 1LL << formula.variables;
    Assignment value(formula.variables + 1, false);
    for (long long mask = 0; mask < total; mask++) {
        for (int v = 1; v <= formula.variables; v++) value[v] = ((mask >> (v - 1)) & 1) != 0;
        if (satisfies(formula, value)) {
            models++;
            visit(value);
        }
    }
    return models;
}

inline long long count_models(const Cnf& formula) {
    return for_each_model(formula, [](const Assignment&) {});
}

// DPLL: propagar cláusulas unitarias y, si no hay ninguna, probar una variable con los dos valores.
// state[v]: 0 sin asignar, 1 verdadera, −1 falsa. Si se pasa trace, anota lo que va pasando.
class Dpll {
public:
    explicit Dpll(const Cnf& formula, std::ostream* trace = nullptr)
        : formula_(formula), trace_(trace), state_(formula.variables + 1, 0) {}

    bool solve() { return search(0); }

    bool value(int v) const { return state_[v] == 1; }
    long long decisions() const { return decisions_; }

private:
    const Cnf& formula_;
    std::ostream* trace_;
    std::vector<int> state_;
    long long decisions_ = 0;

    void note(int depth, const std::string& text) {
        if (trace_ != nullptr) *trace_ << std::string(2 * static_cast<std::size_t>(depth), ' ') << text << '\n';
    }

    // Devuelve false si alguna cláusula queda con todos sus literales falsos.
    bool propagate(int depth, std::vector<int>& assigned) {
        bool changed = true;
        while (changed) {
            changed = false;
            for (const Clause& clause : formula_.clauses) {
                int free_literal = 0, free_count = 0;
                bool is_true = false;
                for (int literal : clause) {
                    int s = state_[std::abs(literal)];
                    if (s == 0) {
                        free_literal = literal;
                        free_count++;
                    } else if ((s == 1) == (literal > 0)) {
                        is_true = true;
                    }
                }
                if (is_true) continue;
                if (free_count == 0) {
                    note(depth, "conflicto");
                    return false;
                }
                if (free_count == 1) {
                    state_[std::abs(free_literal)] = free_literal > 0 ? 1 : -1;
                    assigned.push_back(std::abs(free_literal));
                    note(depth, "unitaria: " + std::to_string(free_literal));
                    changed = true;
                }
            }
        }
        return true;
    }

    bool search(int depth) {
        std::vector<int> assigned;
        bool ok = propagate(depth, assigned);
        if (ok) {
            int v = 1;
            while (v <= formula_.variables && state_[v] != 0) v++;
            if (v > formula_.variables) return true;
            for (int s : {1, -1}) {
                decisions_++;
                note(depth, "decisión: " + std::to_string(s * v));
                state_[v] = s;
                if (search(depth + 1)) return true;
                state_[v] = 0;
            }
        }
        for (int v : assigned) state_[v] = 0;
        return false;
    }
};

#endif
coloring_demo.cppDescargar coloring_demo.cpp
// Coloreo con minisat: la fórmula en DIMACS, la respuesta tal cual y su lectura.
#include <iostream>

#include "coloring.h"
#include "minisat.h"

namespace {

void solve_and_print(const std::string& name, const Graph& g, int colors, bool at_most_one) {
    Cnf formula = encode_coloring(g, colors, at_most_one);
    SatResult result = run_minisat(formula);
    std::cout << name << " con " << colors << " colores: " << formula.variables << " variables, "
              << formula.clauses.size() << " cláusulas, código " << result.exit_code << ": ";
    if (!result.satisfiable) {
        std::cout << "no se puede\n";
        return;
    }
    std::vector<int> color = decode_coloring(g, colors, result.value);
    for (int c : color) std::cout << c << ' ';
    std::cout << (is_proper_coloring(g, color) ? "(propio)\n" : "(NO propio)\n");
}

}  // namespace

int main() {
    const Graph triangle{3, {{0, 1}, {1, 2}, {0, 2}}};
    Cnf formula = encode_coloring(triangle, 3, true);
    std::cout << "El triángulo con 3 colores. Variable v · 3 + c + 1 para el nodo v y el color c:\n";
    std::cout << formula.to_dimacs();
    SatResult result = run_minisat(formula);
    std::cout << "\nminisat terminó con el código " << result.exit_code << ". El archivo de respuesta dice:\n"
              << result.raw;

    SatResult no = run_minisat(encode_coloring(triangle, 2, true));
    std::cout << "\nCon 2 colores, minisat terminó con el código " << no.exit_code << ". El archivo dice:\n"
              << no.raw << '\n';

    const Graph pentagon{5, {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}}};
    const Graph k4{4, {{0, 1}, {0, 2}, {0, 3}, {1, 2}, {1, 3}, {2, 3}}};
    const Graph petersen{10, {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}, {5, 7}, {7, 9}, {9, 6},
                              {6, 8}, {8, 5}, {0, 5}, {1, 6}, {2, 7}, {3, 8}, {4, 9}}};
    solve_and_print("ciclo de 5", pentagon, 2, true);
    solve_and_print("ciclo de 5", pentagon, 3, true);
    solve_and_print("K4", k4, 3, true);
    solve_and_print("K4", k4, 4, true);
    solve_and_print("Petersen", petersen, 2, true);
    solve_and_print("Petersen", petersen, 3, true);

    std::cout << "\nSin \"a lo sumo un color por nodo\":\n";
    solve_and_print("Petersen", petersen, 3, false);
    const Graph edge{2, {{0, 1}}};
    Cnf loose = encode_coloring(edge, 3, false), strict = encode_coloring(edge, 3, true);
    std::cout << "Una arista con 3 colores tiene " << count_models(strict) << " modelos con la regla y "
              << count_models(loose) << " sin ella.\n";
    int shown = 0;
    long long proper = 0, total = 0;
    for_each_model(loose, [&](const Assignment& value) {
        total++;
        // Cualquier color verdadero sirve: se prueban todas las elecciones posibles.
        bool every_choice = true;
        for (int a = 0; a < 3; a++) {
            for (int b = 0; b < 3; b++) {
                if (value[color_variable(0, a, 3)] && value[color_variable(1, b, 3)] && a == b) every_choice = false;
            }
        }
        proper += every_choice;
        std::vector<int> count = true_colors(edge, 3, value);
        if (shown == 0 && (count[0] > 1 || count[1] > 1)) {
            shown++;
            std::cout << "Un modelo con dos colores en un nodo:";
            for (int v = 0; v < 2; v++) {
                std::cout << " nodo " << v << " = {";
                for (int c = 0; c < 3; c++) {
                    if (value[color_variable(v, c, 3)]) std::cout << ' ' << c;
                }
                std::cout << " }";
            }
            std::cout << '\n';
        }
    });
    std::cout << "De " << total << " modelos sin la regla, en " << proper << " cualquier elección da un coloreo propio.\n";
}
coloring.hDescargar coloring.h
// Coloreo de grafos como CNF: una variable por cada (nodo, color).
#ifndef COLORING_H
#define COLORING_H

#include <vector>

#include "cnf.h"
#include "graphs.h"

inline int color_variable(int node, int color, int colors) { return node * colors + color + 1; }

// Cada nodo tiene al menos un color, y dos vecinos no comparten ninguno. "A lo sumo un color por nodo" es opcional.
inline Cnf encode_coloring(const Graph& g, int colors, bool at_most_one) {
    Cnf formula;
    formula.variables = g.n * colors;
    std::vector<int> vars(colors);
    for (int v = 0; v < g.n; v++) {
        for (int c = 0; c < colors; c++) vars[c] = color_variable(v, c, colors);
        add_at_least_one(formula, vars);
        if (at_most_one) add_at_most_one(formula, vars);
    }
    for (auto [u, v] : g.edges) {
        for (int c = 0; c < colors; c++) formula.add({-color_variable(u, c, colors), -color_variable(v, c, colors)});
    }
    return formula;
}

// Lee el primer color verdadero de cada nodo.
inline std::vector<int> decode_coloring(const Graph& g, int colors, const Assignment& value) {
    std::vector<int> color(g.n, -1);
    for (int v = 0; v < g.n; v++) {
        for (int c = colors - 1; c >= 0; c--) {
            if (value[color_variable(v, c, colors)]) color[v] = c;
        }
    }
    return color;
}

// Cuántos colores verdaderos tiene cada nodo.
inline std::vector<int> true_colors(const Graph& g, int colors, const Assignment& value) {
    std::vector<int> count(g.n, 0);
    for (int v = 0; v < g.n; v++) {
        for (int c = 0; c < colors; c++) count[v] += value[color_variable(v, c, colors)] ? 1 : 0;
    }
    return count;
}

#endif
graphs.hDescargar graphs.h
// Grafos pequeños y las fuerzas brutas de la semana. Un conjunto de nodos es una máscara de bits.
#ifndef GRAPHS_H
#define GRAPHS_H

#include <bit>
#include <queue>
#include <string>
#include <utility>
#include <vector>

// Grafo no dirigido y simple: sin lazos y sin aristas repetidas.
struct Graph {
    int n = 0;
    std::vector<std::pair<int, int>> edges;

    bool adjacent(int u, int v) const {
        for (auto [a, b] : edges) {
            if ((a == u && b == v) || (a == v && b == u)) return true;
        }
        return false;
    }
};

// Una instancia de un problema de decisión sobre grafos: ¿hay un conjunto de tamaño k?
struct Instance {
    Graph graph;
    int k = 0;
};

inline bool contains(unsigned set, int v) { return ((set >> v) & 1u) != 0; }

// Verificadores: reciben el certificado, y solo miran cada arista una vez. Cuestan O(V + E).
inline bool is_independent_set(const Graph& g, unsigned set) {
    for (auto [u, v] : g.edges) {
        if (contains(set, u) && contains(set, v)) return false;
    }
    return true;
}

inline bool is_vertex_cover(const Graph& g, unsigned set) {
    for (auto [u, v] : g.edges) {
        if (!contains(set, u) && !contains(set, v)) return false;
    }
    return true;
}

// Mira cada par de nodos: O(V² · E) con esta representación tan simple, y O(V²) con una matriz.
inline bool is_clique(const Graph& g, unsigned set) {
    for (int u = 0; u < g.n; u++) {
        for (int v = u + 1; v < g.n; v++) {
            if (contains(set, u) && contains(set, v) && !g.adjacent(u, v)) return false;
        }
    }
    return true;
}

inline bool is_proper_coloring(const Graph& g, const std::vector<int>& color) {
    for (auto [u, v] : g.edges) {
        if (color[u] == color[v]) return false;
    }
    return true;
}

// Fuerza bruta: las 2ⁿ máscaras.
template <typename Check, typename Size>
bool any_set(const Graph& g, Check check, Size size_ok) {
    const unsigned total = 1u << g.n;
    for (unsigned set = 0; set < total; set++) {
        if (size_ok(std::popcount(set)) && check(g, set)) return true;
    }
    return false;
}

// ¿Hay un conjunto independiente de tamaño al menos k?
inline bool has_independent_set(const Instance& in) {
    return any_set(in.graph, is_independent_set, [&](int size) { return size >= in.k; });
}

// ¿Hay una cobertura de vértices de tamaño a lo sumo k?
inline bool has_vertex_cover(const Instance& in) {
    return any_set(in.graph, is_vertex_cover, [&](int size) { return size <= in.k; });
}

// ¿Hay un clique de tamaño al menos k?
inline bool has_clique(const Instance& in) {
    return any_set(in.graph, is_clique, [&](int size) { return size >= in.k; });
}

inline Graph complement(const Graph& g) {
    Graph h{g.n, {}};
    for (int u = 0; u < g.n; u++) {
        for (int v = u + 1; v < g.n; v++) {
            if (!g.adjacent(u, v)) h.edges.emplace_back(u, v);
        }
    }
    return h;
}

// Las reducciones de los apuntes. Ninguna mira si la respuesta es sí o no.
inline Instance independent_set_to_vertex_cover(const Instance& in) { return {in.graph, in.graph.n - in.k}; }
inline Instance independent_set_to_clique(const Instance& in) { return {complement(in.graph), in.k}; }
inline Instance vertex_cover_to_clique(const Instance& in) {
    return independent_set_to_clique(independent_set_to_vertex_cover(in));
}

// El grafo con un nodo nuevo, vecino de todos.
inline Graph add_apex(const Graph& g) {
    Graph h = g;
    for (int v = 0; v < g.n; v++) h.edges.emplace_back(v, g.n);
    h.n = g.n + 1;
    return h;
}

// 2-coloreo con BFS, en O(V + E) con listas de adyacencia: cada componente se pinta por capas.
inline bool two_colorable(const Graph& g) {
    std::vector<std::vector<int>> next(g.n);
    for (auto [u, v] : g.edges) {
        next[u].push_back(v);
        next[v].push_back(u);
    }
    std::vector<int> color(g.n, -1);
    for (int start = 0; start < g.n; start++) {
        if (color[start] != -1) continue;
        color[start] = 0;
        std::queue<int> pending;
        pending.push(start);
        while (!pending.empty()) {
            int u = pending.front();
            pending.pop();
            for (int v : next[u]) {
                if (color[v] == -1) {
                    color[v] = 1 - color[u];
                    pending.push(v);
                } else if (color[v] == color[u]) {
                    return false;
                }
            }
        }
    }
    return true;
}

// Fuerza bruta: prueba las colorsⁿ formas de pintar.
inline bool colorable_brute_force(const Graph& g, int colors) {
    std::vector<int> color(g.n, 0);
    while (true) {
        if (is_proper_coloring(g, color)) return true;
        int v = 0;
        while (v < g.n && ++color[v] == colors) color[v++] = 0;
        if (v == g.n) return false;
    }
}

inline std::string set_to_string(unsigned set, int n) {
    std::string text = "{";
    for (int v = 0; v < n; v++) {
        if (contains(set, v)) text += (text.size() > 1 ? ", " : "") + std::to_string(v);
    }
    return text + "}";
}

#endif
latin_demo.cppDescargar latin_demo.cpp
// "Al menos uno" sin "a lo sumo uno", en cuadrados latinos. Se muestran los tres casos: la regla es redundante,
// o su falta rompe la respuesta.
#include <iostream>

#include "latin.h"
#include "minisat.h"

namespace {

void print_table(const std::vector<std::vector<int>>& table) {
    for (const auto& row : table) {
        std::cout << "  ";
        for (int s : row) std::cout << s + 1 << ' ';
        std::cout << '\n';
    }
}

void solve_and_print(const std::string& name, int n, const std::vector<Given>& givens, const LatinRules& rules) {
    Cnf formula = encode_latin(n, givens, rules);
    SatResult result = run_minisat(formula);
    std::cout << name << ": " << formula.variables << " variables, " << formula.clauses.size() << " cláusulas, minisat "
              << (result.satisfiable ? "dice SAT\n" : "dice UNSAT\n");
    if (!result.satisfiable) return;
    auto table = decode_latin(n, result.value);
    print_table(table);
    std::cout << (is_latin(table) ? "  es un cuadrado latino\n" : "  NO es un cuadrado latino\n");
}

}  // namespace

int main() {
    const LatinRules complete;
    LatinRules at_least_only;
    at_least_only.cell_at_most_one = false;
    at_least_only.line_at_most_one = false;
    LatinRules no_cell_rule;
    no_cell_rule.cell_at_most_one = false;
    no_cell_rule.line_at_least_one = false;

    std::cout << "Cuántas soluciones tiene el cuadrado de lado 2:\n";
    std::cout << "  reglas completas:                      " << count_models(encode_latin(2, {}, complete)) << '\n';
    std::cout << "  celda al menos uno, línea a lo sumo uno: " << count_models(encode_latin(2, {}, no_cell_rule)) << '\n';
    std::cout << "  solo reglas de al menos uno:           " << count_models(encode_latin(2, {}, at_least_only)) << '\n';

    std::cout << "\nEl argumento del palomar, en una fila de n celdas (todas las asignaciones):\n";
    for (int n = 2; n <= 4; n++) {
        Cnf row = one_row_formula(n);
        long long two_in_a_cell = 0;
        long long models = for_each_model(row, [&](const Assignment& value) {
            for (int cell = 0; cell < n; cell++) {
                int count = 0;
                for (int s = 0; s < n; s++) count += value[cell * n + s + 1] ? 1 : 0;
                if (count > 1) {
                    two_in_a_cell++;
                    return;
                }
            }
        });
        std::cout << "  n = " << n << ": " << models << " modelos, " << two_in_a_cell << " con dos símbolos en una celda\n";
    }

    std::cout << "\nUn cuadrado de lado 3, con 1 en la celda (0, 0) y 2 en la celda (1, 1):\n";
    const std::vector<Given> fine{{0, 0, 0}, {1, 1, 1}};
    solve_and_print("reglas completas", 3, fine, complete);
    solve_and_print("celda al menos uno, línea a lo sumo uno", 3, fine, no_cell_rule);
    solve_and_print("solo al menos uno", 3, fine, at_least_only);

    std::cout << "\nEl mismo cuadrado con 1 en (0, 0) y 1 en (0, 1), que no tiene solución:\n";
    const std::vector<Given> clash{{0, 0, 0}, {0, 1, 0}};
    solve_and_print("reglas completas", 3, clash, complete);
    solve_and_print("celda al menos uno, línea a lo sumo uno", 3, clash, no_cell_rule);
    solve_and_print("solo al menos uno", 3, clash, at_least_only);
}
latin.hDescargar latin.h
// Cuadrado latino de lado n: una tabla donde cada fila y cada columna tiene cada símbolo una vez.
// Una variable por cada (fila, columna, símbolo). Sirve para ver qué pasa con "al menos uno" y "a lo sumo uno".
#ifndef LATIN_H
#define LATIN_H

#include <vector>

#include "cnf.h"

struct LatinRules {
    bool cell_at_least_one = true;
    bool cell_at_most_one = true;
    bool line_at_least_one = true;   // cada símbolo sale en cada fila y en cada columna
    bool line_at_most_one = true;    // cada símbolo no se repite en una fila ni en una columna
};

inline int latin_variable(int n, int row, int column, int symbol) { return row * n * n + column * n + symbol + 1; }

struct Given {
    int row, column, symbol;
};

inline Cnf encode_latin(int n, const std::vector<Given>& givens, const LatinRules& rules) {
    Cnf formula;
    formula.variables = n * n * n;
    std::vector<int> vars(n);
    auto apply = [&](bool least, bool most) {
        if (least) add_at_least_one(formula, vars);
        if (most) add_at_most_one(formula, vars);
    };
    for (int a = 0; a < n; a++) {
        for (int b = 0; b < n; b++) {
            for (int x = 0; x < n; x++) vars[x] = latin_variable(n, a, b, x);
            apply(rules.cell_at_least_one, rules.cell_at_most_one);
            for (int x = 0; x < n; x++) vars[x] = latin_variable(n, a, x, b);
            apply(rules.line_at_least_one, rules.line_at_most_one);
            for (int x = 0; x < n; x++) vars[x] = latin_variable(n, x, a, b);
            apply(rules.line_at_least_one, rules.line_at_most_one);
        }
    }
    for (const Given& g : givens) formula.add({latin_variable(n, g.row, g.column, g.symbol)});
    return formula;
}

// Lee el primer símbolo verdadero de cada celda.
inline std::vector<std::vector<int>> decode_latin(int n, const Assignment& value) {
    std::vector<std::vector<int>> table(n, std::vector<int>(n, -1));
    for (int r = 0; r < n; r++) {
        for (int c = 0; c < n; c++) {
            for (int s = n - 1; s >= 0; s--) {
                if (value[latin_variable(n, r, c, s)]) table[r][c] = s;
            }
        }
    }
    return table;
}

inline bool is_latin(const std::vector<std::vector<int>>& table) {
    const int n = static_cast<int>(table.size());
    for (int a = 0; a < n; a++) {
        std::vector<int> in_row(n, 0), in_column(n, 0);
        for (int b = 0; b < n; b++) {
            if (table[a][b] < 0 || table[b][a] < 0) return false;
            if (in_row[table[a][b]]++ || in_column[table[b][a]]++) return false;
        }
    }
    return true;
}

// Una sola fila de n celdas: al menos un símbolo por celda, y cada símbolo a lo sumo una vez en la fila.
// Es la situación del argumento del palomar.
inline Cnf one_row_formula(int n) {
    Cnf formula;
    formula.variables = n * n;
    auto var = [&](int cell, int symbol) { return cell * n + symbol + 1; };
    std::vector<int> vars(n);
    for (int a = 0; a < n; a++) {
        for (int x = 0; x < n; x++) vars[x] = var(a, x);
        add_at_least_one(formula, vars);
        for (int x = 0; x < n; x++) vars[x] = var(x, a);
        add_at_most_one(formula, vars);
    }
    return formula;
}

#endif
minisat.hDescargar minisat.h
// Correr minisat desde C++: se escribe la fórmula en un archivo, se llama al programa y se lee su respuesta.
// mkdtemp y WEXITSTATUS no son C++ estándar: son de POSIX, y el entorno del curso los tiene.
#ifndef MINISAT_H
#define MINISAT_H

#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <vector>

#include <sys/wait.h>
#include <unistd.h>

#include "cnf.h"

struct SatResult {
    bool satisfiable = false;
    int exit_code = 0;
    Assignment value;       // si satisfiable; una variable que no sale en la respuesta queda falsa
    std::string raw;        // el archivo de respuesta, tal cual
    std::string statistics; // lo que minisat escribe en la terminal
};

inline SatResult run_minisat(const Cnf& formula, const std::string& options = "-verb=0") {
    std::string pattern = (std::filesystem::temp_directory_path() / "week13_XXXXXX").string();
    if (mkdtemp(pattern.data()) == nullptr) throw std::runtime_error("no se pudo crear la carpeta temporal");
    const std::filesystem::path folder = pattern;
    {
        std::ofstream cnf(folder / "formula.cnf");
        cnf << formula.to_dimacs();
    }
    const std::string command = "minisat " + options + " '" + (folder / "formula.cnf").string() + "' '" +
                                (folder / "result.txt").string() + "' > '" + (folder / "log.txt").string() + "' 2>&1";
    int status = std::system(command.c_str());

    SatResult result;
    result.exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
    auto slurp = [](const std::filesystem::path& path) {
        std::ifstream in(path);
        std::ostringstream all;
        all << in.rdbuf();
        return all.str();
    };
    result.raw = slurp(folder / "result.txt");
    result.statistics = slurp(folder / "log.txt");
    std::filesystem::remove_all(folder);
    if (result.exit_code != 10 && result.exit_code != 20) {
        throw std::runtime_error("minisat terminó con el código " + std::to_string(result.exit_code));
    }

    result.satisfiable = result.exit_code == 10;
    result.value.assign(formula.variables + 1, false);
    if (result.satisfiable) {
        std::istringstream in(result.raw);
        std::string verdict;
        in >> verdict;
        int literal;
        while (in >> literal && literal != 0) {
            if (literal > 0) result.value[literal] = true;
        }
    }
    return result;
}

#endif
reduction_demo.cppDescargar reduction_demo.cpp
// Las tres reducciones en un grafo pequeño, comparadas con fuerza bruta, y una reducción mal hecha.
#include <iostream>

#include "graphs.h"

namespace {

Graph small_graph() {
    return {6, {{0, 1}, {0, 2}, {1, 2}, {2, 3}, {3, 4}, {4, 5}}};
}

void print_edges(const Graph& g) {
    for (auto [u, v] : g.edges) std::cout << ' ' << u << '-' << v;
    std::cout << '\n';
}

std::string yes_no(bool answer) { return answer ? "sí" : "no"; }

// Busca el conjunto más grande que cumple check, entre todas las máscaras.
template <typename Check>
unsigned best_set(const Graph& g, Check check, bool largest) {
    unsigned best = 0;
    bool found = false;
    for (unsigned set = 0; set < (1u << g.n); set++) {
        if (!check(g, set)) continue;
        int size = std::popcount(set), size_best = std::popcount(best);
        if (!found || (largest ? size > size_best : size < size_best)) best = set;
        found = true;
    }
    return best;
}

// Recorre los grafos de menos a más nodos, hasta que CI(G, k) y CV(G, k) no coinciden.
void print_counterexample() {
    for (int n = 1; n <= 4; n++) {
        for (unsigned edges = 0; edges < (1u << (n * (n - 1) / 2)); edges++) {
            Graph candidate{n, {}};
            int bit = 0;
            for (int u = 0; u < n; u++) {
                for (int v = u + 1; v < n; v++) {
                    if ((edges >> bit++) & 1u) candidate.edges.emplace_back(u, v);
                }
            }
            for (int k = 1; k <= n; k++) {
                Instance in{candidate, k};
                if (has_independent_set(in) != has_vertex_cover(in)) {
                    std::cout << "  el contraejemplo más chico: " << n << " nodos, aristas:";
                    print_edges(candidate);
                    std::cout << "  k = " << k << ": CI dice " << yes_no(has_independent_set(in)) << ", CV dice "
                              << yes_no(has_vertex_cover(in)) << '\n';
                    return;
                }
            }
        }
    }
}

}  // namespace

int main() {
    const Graph g = small_graph();
    std::cout << "G tiene " << g.n << " nodos y estas aristas:";
    print_edges(g);
    unsigned independent = best_set(g, is_independent_set, true);
    unsigned cover = best_set(g, is_vertex_cover, false);
    std::cout << "conjunto independiente máximo: " << set_to_string(independent, g.n) << ", tamaño "
              << std::popcount(independent) << '\n';
    std::cout << "cobertura mínima:              " << set_to_string(cover, g.n) << ", tamaño " << std::popcount(cover)
              << '\n';
    const unsigned rest = ((1u << g.n) - 1) & ~independent;
    std::cout << "V − S = " << set_to_string(rest, g.n) << ", ¿es cobertura? " << yes_no(is_vertex_cover(g, rest))
              << '\n';

    std::cout << "\nConjunto independiente a cobertura, con k → n − k:\n";
    std::cout << "  k  CI(G, k)  CV(G, n − k)\n";
    for (int k = 0; k <= g.n + 1; k++) {
        Instance in{g, k};
        std::cout << "  " << k << "  " << yes_no(has_independent_set(in)) << "        "
                  << yes_no(has_vertex_cover(independent_set_to_vertex_cover(in))) << '\n';
    }

    const Graph h = complement(g);
    std::cout << "\nEl complemento tiene estas aristas:";
    print_edges(h);
    unsigned clique = best_set(h, is_clique, true);
    std::cout << "clique máximo del complemento: " << set_to_string(clique, g.n) << ", tamaño "
              << std::popcount(clique) << '\n';
    std::cout << "  k  CI(G, k)  clique(complemento, k)\n";
    for (int k = 0; k <= g.n + 1; k++) {
        Instance in{g, k};
        std::cout << "  " << k << "  " << yes_no(has_independent_set(in)) << "        "
                  << yes_no(has_clique(independent_set_to_clique(in))) << '\n';
    }

    std::cout << "\nUna reducción mal hecha: conservar k, sin cambiarlo por n − k.\n";
    print_counterexample();

    std::cout << "\n2-coloreo a 3-coloreo: agregar un nodo vecino de todos.\n";
    const Graph pentagon{5, {{0, 1}, {1, 2}, {2, 3}, {3, 4}, {4, 0}}};
    const Graph square{4, {{0, 1}, {1, 2}, {2, 3}, {3, 0}}};
    for (const auto& [name, graph] : {std::pair{"ciclo de 4", square}, std::pair{"ciclo de 5", pentagon}}) {
        std::cout << "  " << name << ": 2 colores con BFS: " << yes_no(two_colorable(graph))
                  << "; 3 colores en el grafo con el nodo nuevo: " << yes_no(colorable_brute_force(add_apex(graph), 3))
                  << '\n';
    }
}
sat_bench.cppDescargar sat_bench.cpp
// Cuánto le cuesta a minisat el principio del palomar: n + 1 palomas en n huecos. No tiene solución, y para saberlo
// el solucionador acumula conflictos muy rápido. Tiempo: lo que informa minisat, con un límite de CPU por fórmula.
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

#include "minisat.h"

namespace {

Cnf pigeonhole(int holes) {
    const int pigeons = holes + 1;
    Cnf formula;
    formula.variables = pigeons * holes;
    auto var = [&](int pigeon, int hole) { return pigeon * holes + hole + 1; };
    std::vector<int> vars;
    for (int p = 0; p < pigeons; p++) {
        vars.clear();
        for (int h = 0; h < holes; h++) vars.push_back(var(p, h));
        add_at_least_one(formula, vars);
    }
    for (int h = 0; h < holes; h++) {
        vars.clear();
        for (int p = 0; p < pigeons; p++) vars.push_back(var(p, h));
        add_at_most_one(formula, vars);
    }
    return formula;
}

// Busca "palabra : valor" en lo que escribe minisat.
std::string statistic(const std::string& log, const std::string& word) {
    std::istringstream in(log);
    std::string line;
    while (std::getline(in, line)) {
        if (line.rfind(word, 0) == 0) {
            std::istringstream fields(line.substr(line.find(':') + 1));
            std::string value;
            fields >> value;
            return value;
        }
    }
    return "?";
}

}  // namespace

int main() {
    std::cout << "huecos  variables  cláusulas  respuesta  conflictos  CPU (s)\n";
    for (int holes = 5; holes <= 12; holes++) {
        Cnf formula = pigeonhole(holes);
        SatResult result;
        try {
            result = run_minisat(formula, "-verb=1 -cpu-lim=20");
        } catch (const std::exception&) {
            // minisat termina con 0 cuando se le acaba el límite: no sabe la respuesta.
            std::cout << std::setw(6) << holes << "  se acabó el límite de 20 s de CPU\n";
            break;
        }
        std::cout << std::setw(6) << holes << std::setw(11) << formula.variables << std::setw(11)
                  << formula.clauses.size() << std::setw(11) << (result.satisfiable ? "SAT" : "UNSAT") << std::setw(12)
                  << statistic(result.statistics, "conflicts") << std::setw(9)
                  << statistic(result.statistics, "CPU time") << '\n';
    }
}
ejemplos/bad_header.cnfDescargar ejemplos/bad_header.cnf
c el encabezado miente: dice 5 variables y 9 cláusulas
p cnf 5 9
1 -2 0
ejemplos/formula.cnfDescargar ejemplos/formula.cnf
c (x1 o x2) y (no x1 o x3) y (no x1 o no x3) y (no x2 o x4)
p cnf 4 4
1 2 0
-1 3 0
-1 -3 0
-2 4 0
ejemplos/unsat.cnfDescargar ejemplos/unsat.cnf
p cnf 1 2
1 0
-1 0

Pruebas

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

test_examples.cppDescargar test_examples.cpp
// Pruebas de lo que afirman los apuntes.
#include <cassert>
#include <iostream>
#include <random>

#include "coloring.h"
#include "graphs.h"
#include "latin.h"
#include "minisat.h"

namespace {

Graph graph_from_mask(int n, unsigned mask) {
    Graph g{n, {}};
    int bit = 0;
    for (int u = 0; u < n; u++) {
        for (int v = u + 1; v < n; v++) {
            if ((mask >> bit++) & 1u) g.edges.emplace_back(u, v);
        }
    }
    return g;
}

Graph random_graph(int n, std::mt19937& rng) {
    const int pairs = n * (n - 1) / 2;
    unsigned mask = pairs == 0 ? 0 : static_cast<unsigned>(rng()) & ((1u << pairs) - 1);
    return graph_from_mask(n, mask);
}

// S es independiente si y solo si V − S es cobertura, para cada subconjunto.
void test_complement_lemma(const Graph& g) {
    const unsigned all = (1u << g.n) - 1;
    for (unsigned set = 0; set <= all; set++) {
        assert(is_independent_set(g, set) == is_vertex_cover(g, all & ~set));
        assert(is_independent_set(g, set) == is_clique(complement(g), set));
    }
}

// Las reducciones conservan la respuesta, también con k fuera de rango.
void test_reductions(const Graph& g) {
    for (int k = -1; k <= g.n + 2; k++) {
        Instance independent{g, k};
        bool answer = has_independent_set(independent);
        assert(answer == has_vertex_cover(independent_set_to_vertex_cover(independent)));
        assert(answer == has_clique(independent_set_to_clique(independent)));
        Instance cover{g, k};
        assert(has_vertex_cover(cover) == has_clique(vertex_cover_to_clique(cover)));
    }
}

void test_graph_reductions() {
    for (int n = 0; n <= 5; n++) {
        for (unsigned mask = 0; mask < (1u << (n * (n - 1) / 2)); mask++) {
            Graph g = graph_from_mask(n, mask);
            test_complement_lemma(g);
            test_reductions(g);
            // 2-coloreo se reduce a 3-coloreo con el nodo nuevo, y BFS coincide con fuerza bruta.
            assert(two_colorable(g) == colorable_brute_force(g, 2));
            assert(two_colorable(g) == colorable_brute_force(add_apex(g), 3));
        }
    }
    std::mt19937 rng(13);
    for (int i = 0; i < 300; i++) {
        Graph g = random_graph(6 + i % 3, rng);
        test_reductions(g);
    }
}

// El contraejemplo de los apuntes para la reducción que conserva k: una sola arista, con k = 2.
void test_wrong_reduction() {
    const Graph edge{2, {{0, 1}}};
    assert(!has_independent_set({edge, 2}));
    assert(has_vertex_cover({edge, 2}));
}

void test_exactly_one() {
    Cnf formula;
    formula.variables = 3;
    add_exactly_one(formula, {1, 2, 3});
    assert(formula.clauses.size() == 4);
    for (int mask = 0; mask < 8; mask++) {
        Assignment value{false, (mask & 1) != 0, (mask & 2) != 0, (mask & 4) != 0};
        assert(satisfies(formula, value) == (value[1] + value[2] + value[3] == 1));
    }
}

// DPLL contra la fuerza bruta, y minisat contra los dos.
void test_solvers() {
    std::mt19937 rng(7);
    int disagreements = 0;
    for (int round = 0; round < 400; round++) {
        Cnf formula;
        formula.variables = 3 + static_cast<int>(rng() % 6);
        const int clauses = 1 + static_cast<int>(rng() % 24);
        for (int c = 0; c < clauses; c++) {
            Clause clause;
            const int size = 1 + static_cast<int>(rng() % 3);
            for (int i = 0; i < size; i++) {
                int v = 1 + static_cast<int>(rng() % static_cast<unsigned>(formula.variables));
                clause.push_back(rng() % 2 == 0 ? v : -v);
            }
            formula.add(clause);
        }
        const bool truth = count_models(formula) > 0;
        Dpll solver(formula);
        const bool answer = solver.solve();
        assert(answer == truth);
        if (answer) {
            Assignment value(formula.variables + 1, false);
            for (int v = 1; v <= formula.variables; v++) value[v] = solver.value(v);
            assert(satisfies(formula, value));
        }
        if (round % 4 == 0) {
            SatResult result = run_minisat(formula);
            if (result.satisfiable != truth) disagreements++;
            if (result.satisfiable) assert(satisfies(formula, result.value));
        }
    }
    assert(disagreements == 0);
}

void test_minisat_format() {
    Cnf sat;
    sat.variables = 2;
    sat.add({1});
    sat.add({-1, 2});
    SatResult yes = run_minisat(sat);
    assert(yes.exit_code == 10);
    assert(yes.raw.rfind("SAT\n", 0) == 0);
    assert(yes.value[1] && yes.value[2]);

    Cnf unsat;
    unsat.variables = 1;
    unsat.add({1});
    unsat.add({-1});
    SatResult no = run_minisat(unsat);
    assert(no.exit_code == 20);
    assert(no.raw.rfind("UNSAT", 0) == 0);
}

// El coloreo con minisat contra la fuerza bruta. Sin "a lo sumo uno", cualquier color verdadero sirve.
void test_coloring() {
    std::mt19937 rng(21);
    for (int round = 0; round < 150; round++) {
        Graph g = random_graph(1 + round % 7, rng);
        for (int colors = 1; colors <= 3; colors++) {
            const bool truth = colorable_brute_force(g, colors);
            for (bool at_most_one : {true, false}) {
                SatResult result = run_minisat(encode_coloring(g, colors, at_most_one));
                assert(result.satisfiable == truth);
                if (truth) assert(is_proper_coloring(g, decode_coloring(g, colors, result.value)));
            }
        }
    }
    // Exhaustivo: en todo modelo sin la regla, dos vecinos no comparten ningún color verdadero.
    for (int n = 1; n <= 4; n++) {
        for (unsigned mask = 0; mask < (1u << (n * (n - 1) / 2)); mask++) {
            Graph g = graph_from_mask(n, mask);
            for (int colors = 1; colors <= 3; colors++) {
                if (n * colors > 12) continue;
                Cnf loose = encode_coloring(g, colors, false);
                Cnf strict = encode_coloring(g, colors, true);
                assert((count_models(loose) > 0) == (count_models(strict) > 0));
                for_each_model(loose, [&](const Assignment& value) {
                    for (auto [u, v] : g.edges) {
                        for (int c = 0; c < colors; c++) {
                            assert(!(value[color_variable(u, c, colors)] && value[color_variable(v, c, colors)]));
                        }
                    }
                });
            }
        }
    }
    // Las cuentas de los apuntes.
    const Graph triangle{3, {{0, 1}, {1, 2}, {0, 2}}};
    assert(encode_coloring(triangle, 3, true).variables == 9);
    assert(encode_coloring(triangle, 3, true).clauses.size() == 21);
    assert(encode_coloring(triangle, 3, false).clauses.size() == 12);
}

// Palomar: en una fila, "al menos un símbolo por celda" y "a lo sumo una vez cada símbolo" obligan a uno por celda.
void test_pigeonhole() {
    for (int n = 2; n <= 4; n++) {
        for_each_model(one_row_formula(n), [&](const Assignment& value) {
            for (int cell = 0; cell < n; cell++) {
                int count = 0;
                for (int s = 0; s < n; s++) count += value[cell * n + s + 1] ? 1 : 0;
                assert(count == 1);
            }
        });
    }
}

void test_latin_rules() {
    const LatinRules complete;
    LatinRules at_least_only;
    at_least_only.cell_at_most_one = false;
    at_least_only.line_at_most_one = false;
    LatinRules no_cell_rule;
    no_cell_rule.cell_at_most_one = false;
    no_cell_rule.line_at_least_one = false;

    assert(count_models(encode_latin(2, {}, complete)) == 2);
    assert(count_models(encode_latin(2, {}, no_cell_rule)) == 2);
    assert(count_models(encode_latin(2, {}, at_least_only)) > 2);

    // Dos veces el mismo símbolo en una fila: con reglas de "a lo sumo", no hay solución; sin ellas, sí.
    const std::vector<Given> clash{{0, 0, 0}, {0, 1, 0}};
    assert(!run_minisat(encode_latin(3, clash, complete)).satisfiable);
    assert(!run_minisat(encode_latin(3, clash, no_cell_rule)).satisfiable);
    assert(run_minisat(encode_latin(3, clash, at_least_only)).satisfiable);
    auto wrong = decode_latin(3, run_minisat(encode_latin(3, clash, at_least_only)).value);
    assert(!is_latin(wrong));

    const std::vector<Given> fine{{0, 0, 0}, {1, 1, 1}};
    assert(is_latin(decode_latin(3, run_minisat(encode_latin(3, fine, complete)).value)));
    assert(is_latin(decode_latin(3, run_minisat(encode_latin(3, fine, no_cell_rule)).value)));
}

}  // namespace

int main() {
    test_graph_reductions();
    test_wrong_reduction();
    test_exactly_one();
    test_minisat_format();
    test_solvers();
    test_coloring();
    test_pigeonhole();
    test_latin_rules();
    std::cout << "todas las pruebas pasaron\n";
}

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/sudoku_sat/driver.hDescargar lab/sudoku_sat/driver.h
// Programa principal del laboratorio. No hace falta cambiar este archivo: ustedes escriben encode y
// decode en solution.cpp, y este archivo los conecta con minisat.
//
//   build/solution                  lee el sudoku de stdin, llama a encode, corre minisat, llama a decode
//   build/solution --encode         lee el sudoku de stdin y escribe la fórmula en stdout
//   build/solution --decode G R     lee el sudoku del archivo G y la respuesta de minisat del archivo R
//
// Antes de pasarle la fórmula a minisat, se revisa que sea DIMACS correcto. minisat solo avisa cuando
// el encabezado no coincide, y aquí eso es un error: el programa termina con código 2.
#ifndef DRIVER_H
#define DRIVER_H

#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

#include <sys/wait.h>
#include <unistd.h>

struct Sudoku {
    int k = 0;                            // lado de una caja
    int n = 0;                            // lado del tablero: n = k · k
    std::vector<std::vector<int>> cells;  // n filas de n valores; 0 es una celda vacía
};

using Encoder = void (*)(const Sudoku&, std::ostream&);
using Decoder = void (*)(const Sudoku&, std::istream&, std::ostream&);

inline Sudoku read_sudoku(std::istream& in) {
    Sudoku s;
    if (!(in >> s.k) || s.k < 1) {
        std::cerr << "entrada: falta k\n";
        std::exit(2);
    }
    s.n = s.k * s.k;
    s.cells.assign(s.n, std::vector<int>(s.n, 0));
    for (auto& row : s.cells) {
        for (int& value : row) {
            if (!(in >> value) || value < 0 || value > s.n) {
                std::cerr << "entrada: valor fuera de rango\n";
                std::exit(2);
            }
        }
    }
    return s;
}

// Devuelve "" si el texto es una fórmula DIMACS con exactamente n³ variables y con tantas cláusulas
// como dice su encabezado. Si no, devuelve qué está mal.
inline std::string dimacs_error(const std::string& text, long long variables) {
    std::size_t i = 0;
    const std::size_t size = text.size();
    auto next_line = [&] {
        while (i < size && text[i] != '\n') i++;
        if (i < size) i++;
    };
    auto skip_space = [&] {
        while (i < size && (text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r')) i++;
    };
    skip_space();
    while (i < size && text[i] == 'c') {
        next_line();
        skip_space();
    }
    std::size_t end = text.find('\n', i);
    std::istringstream header(text.substr(i, end == std::string::npos ? end : end - i));
    std::string p, cnf;
    long long declared_vars = 0, declared_clauses = 0;
    if (!(header >> p >> cnf >> declared_vars >> declared_clauses) || p != "p" || cnf != "cnf") {
        return "falta el encabezado: p cnf <variables> <cláusulas>";
    }
    if (declared_vars != variables) {
        return "el encabezado declara " + std::to_string(declared_vars) + " variables, y deben ser n³ = " +
               std::to_string(variables);
    }
    i = end == std::string::npos ? size : end;

    long long clauses = 0;
    bool open_clause = false;
    while (true) {
        skip_space();
        if (i >= size) break;
        bool negative = text[i] == '-';
        if (negative) i++;
        if (i >= size || text[i] < '0' || text[i] > '9') return "se esperaba un literal, y hay otra cosa";
        long long value = 0;
        while (i < size && text[i] >= '0' && text[i] <= '9') {
            value = value * 10 + (text[i] - '0');
            if (value > variables) return "un literal usa una variable mayor que " + std::to_string(variables);
            i++;
        }
        if (value == 0) {
            clauses++;
            open_clause = false;
        } else {
            open_clause = true;
        }
    }
    if (open_clause) return "la última cláusula no termina en 0";
    if (clauses != declared_clauses) {
        return "el encabezado declara " + std::to_string(declared_clauses) + " cláusulas, y hay " +
               std::to_string(clauses);
    }
    return "";
}

inline std::string encode_checked(const Sudoku& s, Encoder encode) {
    std::ostringstream formula;
    encode(s, formula);
    std::string text = formula.str();
    std::string error = dimacs_error(text, 1LL * s.n * s.n * s.n);
    if (!error.empty()) {
        std::cerr << "encode: " << error << "\n";
        std::exit(2);
    }
    return text;
}

inline int run_pipeline(Encoder encode, Decoder decode) {
    Sudoku s = read_sudoku(std::cin);
    std::string text = encode_checked(s, encode);

    // Archivos temporales propios: pueden correr varios programas a la vez.
    std::string pattern = (std::filesystem::temp_directory_path() / "sudoku_sat_XXXXXX").string();
    if (mkdtemp(pattern.data()) == nullptr) {
        std::cerr << "no se pudo crear la carpeta temporal\n";
        return 3;
    }
    const std::filesystem::path folder = pattern;
    const std::filesystem::path cnf_path = folder / "formula.cnf";
    const std::filesystem::path result_path = folder / "result.txt";
    {
        std::ofstream cnf(cnf_path);
        cnf << text;
    }
    std::string command = "minisat -verb=0 '" + cnf_path.string() + "' '" + result_path.string() + "' > /dev/null 2>&1";
    int status = std::system(command.c_str());
    // minisat termina con 10 si la fórmula es satisfacible y con 20 si no lo es.
    int code = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
    if (code != 10 && code != 20) {
        std::cerr << "minisat terminó con el código " << code << "\n";
        std::filesystem::remove_all(folder);
        return 3;
    }
    {
        std::ifstream result(result_path);
        decode(s, result, std::cout);
    }
    std::filesystem::remove_all(folder);
    return 0;
}

inline int run_sudoku(int argc, char** argv, Encoder encode, Decoder decode) {
    const std::string mode = argc > 1 ? argv[1] : "";
    if (argc == 1) return run_pipeline(encode, decode);
    if (mode == "--encode" && argc == 2) {
        std::cout << encode_checked(read_sudoku(std::cin), encode);
        return 0;
    }
    if (mode == "--decode" && argc == 4) {
        std::ifstream grid(argv[2]), result(argv[3]);
        if (!grid || !result) {
            std::cerr << "no se pudo abrir un archivo\n";
            return 2;
        }
        decode(read_sudoku(grid), result, std::cout);
        return 0;
    }
    std::cerr << "uso: solution | solution --encode | solution --decode <sudoku> <respuesta>\n";
    return 2;
}

#endif
lab/sudoku_sat/MakefileDescargar lab/sudoku_sat/Makefile
TIEMPO = 3
VERIFICADOR = verificador.cpp
include ../../../lab.mk

# solution.cpp incluye driver.h, que está en esta carpeta.
CXXFLAGS += -I.
$(BUILD)/solution: driver.h

# Para mirar cada mitad por separado, con un caso:
#   make formula CASO=01    escribe build/01.cnf
#   make respuesta CASO=01  corre minisat con esa fórmula y escribe build/01.res
#   make decode CASO=01     llama a decode con esa respuesta
CASO ?= 01
.PHONY: formula respuesta decode

formula: $(BUILD)/solution
	$(BUILD)/solution --encode < casos/$(CASO).in > $(BUILD)/$(CASO).cnf
	@head -n 1 $(BUILD)/$(CASO).cnf

# minisat termina con 10 o 20 aunque todo haya salido bien, y make lo tomaría como un error.
respuesta: formula
	@minisat -verb=0 $(BUILD)/$(CASO).cnf $(BUILD)/$(CASO).res > /dev/null 2>&1; echo "minisat terminó con el código $$?"
	@head -c 100 $(BUILD)/$(CASO).res; echo

decode: respuesta
	$(BUILD)/solution --decode casos/$(CASO).in $(BUILD)/$(CASO).res
lab/sudoku_sat/README.mdDescargar lab/sudoku_sat/README.md
# Resolver sudokus con un solucionador SAT

La revista de pasatiempos de la universidad publica sudokus de 9 por 9, y este año también de 16 por 16 y de 25 por 25.
Les piden comprobar cada tablero antes de imprimirlo: encontrar una solución, o avisar que no hay ninguna, porque de vez en
cuando un tablero llega con un error de digitación.

Un sudoku de orden k es un tablero de n por n celdas, con n = k · k, partido en cajas de k por k. Se llena con los números de 1 a
n, de modo que cada número aparezca exactamente una vez en cada fila, en cada columna y en cada caja. Los valores que ya
vienen en el tablero no se pueden cambiar.

Esta vez no van a escribir una búsqueda. Van a escribir dos funciones, en `solution.cpp`:

- `encode` recibe un sudoku y escribe una fórmula CNF, en formato DIMACS, con **una variable por cada terna (fila, columna, dígito)**.
- `decode` recibe la respuesta de `minisat` y escribe el tablero resuelto.

El archivo `driver.h` trae el `main`: lee el sudoku, llama a `encode`, revisa la fórmula, corre `minisat` y llama a `decode`. Así
que el juez corre un solo programa, que lee de la entrada y escribe en la salida como siempre. No hace falta cambiar `driver.h`,
y conviene leerlo. Si quieren, pueden agregar funciones auxiliares en `solution.cpp`.

## Entrada

La primera línea tiene k. Luego vienen n líneas con n enteros cada una, separados por espacios: los valores del tablero, con 0 en
las celdas vacías.

## Salida

- Si el sudoku tiene solución, n líneas con n enteros cada una, separados por espacios. Puede tener varias soluciones, y cualquiera
  vale: un verificador revisa las reglas y que los valores dados sigan en su lugar.
- Si no tiene solución, una línea con la palabra `imposible`. También es el caso de un tablero que ya trae un valor repetido.

## Lo que se revisa de la fórmula

El programa termina con un error, que el juez muestra como `RE`, y explica la causa en la terminal, si la fórmula de `encode`:

- no empieza con una línea `p cnf V C`, después de comentarios opcionales que empiezan con `c`;
- no tiene V = n³;
- no tiene exactamente C cláusulas, cada una terminada en 0;
- usa un literal mayor que V.

`minisat` solo daría un aviso con estos errores, y seguiría. Aquí no pasan.

## Límites

- 1 ≤ k ≤ 5, así que n es 1, 4, 9, 16 o 25.
- Cada valor está entre 0 y n.

## Ejemplo

Entrada:

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

Salida:

```text
1 3 2 4
4 2 3 1
2 4 1 3
3 1 4 2
```

## Tiempo

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

## Mirar cada parte

El programa también corre cada mitad por separado, y el `Makefile` trae tres blancos para hacerlo con un caso:

```sh
make formula CASO=01      # encode: escribe build/01.cnf y muestra su encabezado
make respuesta CASO=01    # lo anterior, y corre minisat: escribe build/01.res y muestra el código con que terminó
make decode CASO=01       # lo anterior, y llama a decode con esa respuesta
```

`minisat` termina con el código 10 si la fórmula es satisfacible, y con 20 si no lo es. Su archivo de respuesta tiene la palabra
`SAT` y una línea de literales que termina en 0, o la palabra `UNSAT`. Una variable que no sale en ninguna cláusula tampoco sale en
la respuesta.

## Comparar

1. Anoten las variables y las cláusulas que tiene su fórmula en `casos/14.in`, de 9 por 9, y en `casos/17.in`, de 25 por 25. El
   encabezado lo dice. Revisen esos números con la cuenta a mano.
2. Midan su programa en esos dos casos. En bash, `time ./build/solution < casos/14.in`.
3. Midan un backtracking en `casos/14.in`, que es un sudoku difícil para buscar por filas. Si conservan su solucionador de sudokus de
   Introducción a la Programación, úsenlo: lee 9 líneas de 9 dígitos, sin espacios y sin la primera línea con k. Si no, el curso
   trae uno, que ya tiene ese sudoku adentro. Dentro de `code/week-13`, corran `make` y luego `build/backtrack_bench`. Informa los nodos y el tiempo. Acepta un
   archivo con ese mismo formato.
4. Prueben quitar de `encode` cada regla de "a lo sumo uno", una por una. ¿Cuáles cambian el veredicto del juez, y por qué?

Escriban los tiempos con la máquina en la que midieron y cómo lo hicieron: dependen de los dos.
lab/sudoku_sat/solution.cppDescargar lab/sudoku_sat/solution.cpp
// Sudoku con un solucionador SAT. Ustedes completan encode y decode. El resto lo hace driver.h.
#include <string>

#include "driver.h"

namespace {

// Escribe en out la fórmula del sudoku s, en formato DIMACS, con una variable por cada (fila, columna, dígito).
// s.k es el lado de una caja, s.n = s.k · s.k es el lado del tablero, y s.cells[fila][columna] vale 0 si está vacía.
void encode(const Sudoku& s, std::ostream& out) {
    // Por ahora, una fórmula sin ninguna cláusula: el encabezado es correcto, y no dice nada del sudoku.
    const long long variables = 1LL * s.n * s.n * s.n;
    out << "p cnf " << variables << " 0\n";
}

// Lee la respuesta de minisat de result y escribe en out el tablero resuelto, o la palabra imposible.
void decode(const Sudoku& s, std::istream& result, std::ostream& out) {
    std::string verdict;
    result >> verdict;
    (void)s;
    out << "pendiente\n";
}

}  // namespace

int main(int argc, char** argv) {
    return run_sudoku(argc, argv, encode, decode);
}
lab/sudoku_sat/verificador.cppDescargar lab/sudoku_sat/verificador.cpp
// Verificador: se llama con `verificador NN.in NN.out salida`, y termina con 0 si la salida es correcta.
// Hay sudokus con varias soluciones, así que se revisa la salida, sin compararla con una respuesta guardada.
// NN.out solo dice si existe solución: empieza con "imposible" cuando no existe.
#include <fstream>
#include <iostream>
#include <string>
#include <vector>

namespace {

int reject(const std::string& reason) {
    std::cout << reason << "\n";
    return 1;
}

}  // namespace

int main(int argc, char** argv) {
    if (argc != 4) return reject("uso: verificador NN.in NN.out salida");
    std::ifstream input(argv[1]), expected(argv[2]), output(argv[3]);

    int k = 0;
    input >> k;
    const int n = k * k;
    std::vector<std::vector<int>> given(n, std::vector<int>(n));
    for (auto& row : given) {
        for (int& value : row) input >> value;
    }

    std::string first_expected, first_output;
    expected >> first_expected;
    output >> first_output;
    const bool impossible = first_expected == "imposible";
    if (first_output.empty()) return reject("salida vacía");
    if (impossible) {
        if (first_output != "imposible") return reject("este sudoku no tiene solución, y la salida dice otra cosa");
        std::string extra;
        if (output >> extra) return reject("hay texto después de imposible");
        return 0;
    }
    if (first_output == "imposible") return reject("este sudoku sí tiene solución");

    std::vector<std::vector<int>> grid(n, std::vector<int>(n));
    for (int r = 0; r < n; r++) {
        for (int c = 0; c < n; c++) {
            if (r == 0 && c == 0) {
                if (first_output.size() > 3 || first_output.find_first_not_of("0123456789") != std::string::npos) {
                    return reject("se esperaba un número");
                }
                grid[0][0] = std::stoi(first_output);
            } else if (!(output >> grid[r][c])) {
                return reject("faltan valores en la salida");
            }
            if (grid[r][c] < 1 || grid[r][c] > n) return reject("un valor está fuera de rango");
            if (given[r][c] != 0 && given[r][c] != grid[r][c]) {
                return reject("cambió un valor dado en la fila " + std::to_string(r + 1));
            }
        }
    }
    std::string extra;
    if (output >> extra) return reject("sobran valores en la salida");

    for (int a = 0; a < n; a++) {
        std::vector<char> row(n + 1, 0), column(n + 1, 0), box(n + 1, 0);
        for (int b = 0; b < n; b++) {
            int in_row = grid[a][b];
            int in_column = grid[b][a];
            int in_box = grid[(a / k) * k + b / k][(a % k) * k + b % k];
            if (row[in_row]++) return reject("un valor se repite en la fila " + std::to_string(a + 1));
            if (column[in_column]++) return reject("un valor se repite en la columna " + std::to_string(a + 1));
            if (box[in_box]++) return reject("un valor se repite en la caja " + std::to_string(a + 1));
        }
    }
    return 0;
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 13: P, NP y reducciones. Conjunto independiente, cobertura de vértices, clique, CNF y coloreo con minisat.
# Se usa dentro del entorno del curso.
#
#   make        compila los ejemplos en build/
#   make test   corre las pruebas de los ejemplos, con sanitizers
#   make bench  corre las mediciones, con -O2
#   make clean  borra lo generado
#
# Los problemas de lab/ tienen su propio Makefile y no entran aquí.

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
HEADERS = graphs.h cnf.h minisat.h coloring.h latin.h
PROGRAMS = reduction_demo cnf_demo coloring_demo latin_demo test_examples
BENCHES = sat_bench backtrack_bench

.PHONY: all test bench clean

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

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

# Lo que mide tiempos va con -O2 y sin sanitizers.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
	$(CXX) $(BENCHFLAGS) $< -o $@

$(BUILD):
	mkdir -p $@

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

bench: $(BENCHES:%=$(BUILD)/%)
	$(BUILD)/sat_bench
	$(BUILD)/backtrack_bench

clean:
	rm -rf $(BUILD)