Semana 11 — Backtracking
Objetivos
- Plantear un problema de búsqueda como elecciones que se prueban y se deshacen.
- Generar permutaciones y subconjuntos.
- Reducir la búsqueda con poda, y medir cuánto se gana.
Teoría — Backtracking: permutaciones, subconjuntos, N reinas y poda
Lectura: Apuntes, semana 11.
Temas:
- El esquema: elegir, explorar, deshacer. En pseudocódigo y en C.
- Subconjuntos y permutaciones: el árbol de decisiones de cada uno y cuántas hojas tiene.
- Encontrar una solución o todas: qué cambia en el retorno.
- N reinas: representar el estado y deshacer de forma correcta.
- Poda: dejar de explorar una rama que no puede funcionar. Contar nodos visitados con y sin poda.
Laboratorio — Resolver sudokus
Esta semana una sesión es para la Práctica 3.
Sesión 1:
- Escribir
subset_sum, que dice si algún subconjunto suma un valor, y contar los nodos visitados. - Escribir un solucionador de sudokus con backtracking, sobre el lector y las pruebas dados.
- Cambiar el orden de la búsqueda eligiendo primero la celda con menos opciones. Comparar nodos visitados con el otro método en los tableros dados, incluido uno donde el resultado sorprende.
Sesión 2: Práctica 3.
Entregas
- Práctica 3: Tipos y recursión. Ver
practicas/README.md.
Preguntas de repaso
- ¿Cuántas hojas tiene el árbol de permutaciones de 5 elementos? ¿Y el de subconjuntos?
- ¿Qué pasa si olvidan deshacer una elección?
- ¿Cómo cambia el código si solo quieren la primera solución?
- ¿La poda puede hacer que se pierda una solución? ¿Qué condición debe cumplir?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (8)Errores (5)Pruebas (3)Laboratorio (13)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Menú con precios en soles: ¿cuántas combinaciones de platos caben en el presupuesto?
#include <stdio.h>
#define COUNT_OF_PRICES 12
// Sin poda: recorre todo el árbol y solo revisa el total en las hojas.
static long count_all(const int *prices, int n, int index, int total, int budget, long *nodes) {
(*nodes)++;
if (index == n) {
return total <= budget;
}
return count_all(prices, n, index + 1, total + prices[index], budget, nodes)
+ count_all(prices, n, index + 1, total, budget, nodes);
}
// Con poda: si ya se pasó, ningún plato más lo arregla, porque los precios son positivos.
static long count_pruned(const int *prices, int n, int index, int total, int budget, long *nodes) {
(*nodes)++;
if (total > budget) {
return 0;
}
if (index == n) {
return 1;
}
return count_pruned(prices, n, index + 1, total + prices[index], budget, nodes)
+ count_pruned(prices, n, index + 1, total, budget, nodes);
}
int main(void) {
int prices[COUNT_OF_PRICES] = {12, 7, 18, 5, 9, 15, 4, 11, 6, 14, 8, 10};
int budget = 30;
long nodes_all = 0;
long nodes_pruned = 0;
long all = count_all(prices, COUNT_OF_PRICES, 0, 0, budget, &nodes_all);
long pruned = count_pruned(prices, COUNT_OF_PRICES, 0, 0, budget, &nodes_pruned);
printf("sin poda: %ld combinaciones, %ld nodos\n", all, nodes_all);
printf("con poda: %ld combinaciones, %ld nodos\n", pruned, nodes_pruned);
return 0;
}// Cuenta de verdad las hojas y los nodos de los dos árboles, sin escribir cada resultado.
#include <stdio.h>
#define MAX_ITEMS 16
typedef struct {
long leaves;
long nodes;
} Count;
static void subsets(int n, int index, Count *count) {
count->nodes++;
if (index == n) {
count->leaves++;
return;
}
subsets(n, index + 1, count);
subsets(n, index + 1, count);
}
static void permutations(int n, int *used, int depth, Count *count) {
count->nodes++;
if (depth == n) {
count->leaves++;
return;
}
for (int i = 0; i < n; i++) {
if (!used[i]) {
used[i] = 1;
permutations(n, used, depth + 1, count);
used[i] = 0;
}
}
}
int main(void) {
printf(" n | subconjuntos nodos | permutaciones nodos\n");
for (int n = 1; n <= 10; n++) {
Count sub = {0, 0};
Count perm = {0, 0};
int used[MAX_ITEMS] = {0};
subsets(n, 0, &sub);
permutations(n, used, 0, &perm);
printf("%2d | %12ld %8ld | %13ld %10ld\n", n, sub.leaves, sub.nodes, perm.leaves, perm.nodes);
}
return 0;
}// Permutaciones de "abc": en cada posición se elige un elemento que no se haya usado.
#include <stdio.h>
#include <string.h>
#define MAX_ITEMS 16
static void permutations(const char *items, int n, char *current, int *used, int depth,
long *nodes) {
(*nodes)++;
if (depth == n) {
current[n] = '\0';
printf("%s\n", current);
return;
}
for (int i = 0; i < n; i++) {
if (!used[i]) {
used[i] = 1; // elegir
current[depth] = items[i];
permutations(items, n, current, used, depth + 1, nodes); // explorar
used[i] = 0; // deshacer
}
}
}
int main(void) {
const char *items = "abc";
char current[MAX_ITEMS + 1];
int used[MAX_ITEMS] = {0};
long nodes = 0;
permutations(items, (int) strlen(items), current, used, 0, &nodes);
printf("nodos: %ld\n", nodes);
return 0;
}// N reinas, hasta la primera solución: el retorno avisa que ya no hay que seguir buscando.
#include <stdio.h>
#define MAX_N 16
typedef struct {
int n;
int column_of_row[MAX_N];
int column_used[MAX_N];
int down_used[2 * MAX_N - 1];
int up_used[2 * MAX_N - 1];
long nodes;
} Queens;
static int can_place(const Queens *q, int row, int col) {
return !q->column_used[col]
&& !q->down_used[row - col + q->n - 1]
&& !q->up_used[row + col];
}
static void mark(Queens *q, int row, int col, int value) {
q->column_used[col] = value;
q->down_used[row - col + q->n - 1] = value;
q->up_used[row + col] = value;
}
// Devuelve 1 si encontró una solución, y la deja en column_of_row.
static int search(Queens *q, int row) {
q->nodes++;
if (row == q->n) {
return 1;
}
for (int col = 0; col < q->n; col++) {
if (can_place(q, row, col)) {
mark(q, row, col, 1);
q->column_of_row[row] = col;
if (search(q, row + 1)) {
return 1;
}
mark(q, row, col, 0);
}
}
return 0;
}
static void print_board(const Queens *q) {
for (int row = 0; row < q->n; row++) {
for (int col = 0; col < q->n; col++) {
if (q->column_of_row[row] == col) {
printf("Q ");
} else {
printf(". ");
}
}
printf("\n");
}
}
int main(int argc, char *argv[]) {
int n;
if (argc != 2 || sscanf(argv[1], "%d", &n) != 1 || n < 1 || n > MAX_N) {
fprintf(stderr, "uso: %s n, con n entre 1 y %d\n", argv[0], MAX_N);
return 1;
}
Queens q = {.n = n};
if (search(&q, 0)) {
print_board(&q);
} else {
printf("no hay solución\n");
}
printf("n = %d, primera solución: %ld nodos\n", n, q.nodes);
return 0;
}// N reinas sin poda: pone una reina en cada fila, en cualquier columna, y solo revisa al final.
#include <stdio.h>
#include <stdlib.h>
#define MAX_N 9
// Revisa cada par de reinas: 1 si ninguna ataca a otra.
static int all_safe(const int *column_of_row, int n) {
for (int a = 0; a < n; a++) {
for (int b = a + 1; b < n; b++) {
int same_column = column_of_row[a] == column_of_row[b];
int same_diagonal = abs(column_of_row[a] - column_of_row[b]) == b - a;
if (same_column || same_diagonal) {
return 0;
}
}
}
return 1;
}
static long search(int *column_of_row, int n, int row, long *nodes) {
(*nodes)++;
if (row == n) {
return all_safe(column_of_row, n);
}
long solutions = 0;
for (int col = 0; col < n; col++) {
column_of_row[row] = col;
solutions += search(column_of_row, n, row + 1, nodes);
}
return solutions;
}
int main(int argc, char *argv[]) {
int n;
if (argc != 2 || sscanf(argv[1], "%d", &n) != 1 || n < 1 || n > MAX_N) {
fprintf(stderr, "uso: %s n, con n entre 1 y %d\n", argv[0], MAX_N);
return 1;
}
int column_of_row[MAX_N];
long nodes = 0;
long solutions = search(column_of_row, n, 0, &nodes);
printf("n = %d, sin poda: %ld soluciones, %ld nodos\n", n, solutions, nodes);
return 0;
}// Escribe el árbol de búsqueda de 4 reinas: cada línea es una reina puesta, con su sangría.
#include <stdio.h>
#define N 4
typedef struct {
int column_used[N];
int down_used[2 * N - 1];
int up_used[2 * N - 1];
long nodes;
} Queens;
static int can_place(const Queens *q, int row, int col) {
return !q->column_used[col] && !q->down_used[row - col + N - 1] && !q->up_used[row + col];
}
static void mark(Queens *q, int row, int col, int value) {
q->column_used[col] = value;
q->down_used[row - col + N - 1] = value;
q->up_used[row + col] = value;
}
static void print_indent(int row) {
for (int i = 0; i < row; i++) {
printf(" ");
}
}
static void search(Queens *q, int row) {
q->nodes++;
if (row == N) {
print_indent(row);
printf("solución\n");
return;
}
int tried = 0;
for (int col = 0; col < N; col++) {
if (can_place(q, row, col)) {
tried = 1;
print_indent(row);
printf("fila %d, columna %d\n", row, col);
mark(q, row, col, 1);
search(q, row + 1);
mark(q, row, col, 0);
}
}
if (!tried) {
print_indent(row);
printf("sin lugar en la fila %d: se vuelve\n", row);
}
}
int main(void) {
Queens q = {.nodes = 0};
search(&q, 0);
printf("nodos: %ld\n", q.nodes);
return 0;
}// N reinas con poda: una columna o diagonal ocupada corta la rama antes de seguir.
#include <stdio.h>
#define MAX_N 16
typedef struct {
int n;
int column_used[MAX_N];
int down_used[2 * MAX_N - 1]; // diagonal que baja: row - col + n - 1 es constante
int up_used[2 * MAX_N - 1]; // diagonal que sube: row + col es constante
long solutions;
long nodes;
} Queens;
static int can_place(const Queens *q, int row, int col) {
return !q->column_used[col]
&& !q->down_used[row - col + q->n - 1]
&& !q->up_used[row + col];
}
// Pone una reina con value = 1 y la quita con value = 0.
static void mark(Queens *q, int row, int col, int value) {
q->column_used[col] = value;
q->down_used[row - col + q->n - 1] = value;
q->up_used[row + col] = value;
}
static void search(Queens *q, int row) {
q->nodes++;
if (row == q->n) {
q->solutions++;
return;
}
for (int col = 0; col < q->n; col++) {
if (can_place(q, row, col)) {
mark(q, row, col, 1); // elegir
search(q, row + 1); // explorar
mark(q, row, col, 0); // deshacer
}
}
}
int main(int argc, char *argv[]) {
int n;
if (argc != 2 || sscanf(argv[1], "%d", &n) != 1 || n < 1 || n > MAX_N) {
fprintf(stderr, "uso: %s n, con n entre 1 y %d\n", argv[0], MAX_N);
return 1;
}
Queens q = {.n = n};
search(&q, 0);
printf("n = %d, con poda: %ld soluciones, %ld nodos\n", n, q.solutions, q.nodes);
return 0;
}// Subconjuntos de "abc": en cada elemento se decide si se toma o no.
#include <stdio.h>
#include <string.h>
#define MAX_ITEMS 16
static void print_subset(const char *items, const int *chosen, int n) {
printf("{");
for (int i = 0; i < n; i++) {
if (chosen[i]) {
printf(" %c", items[i]);
}
}
printf(" }\n");
}
static void subsets(const char *items, int *chosen, int n, int index, long *nodes) {
(*nodes)++;
if (index == n) {
print_subset(items, chosen, n);
return;
}
chosen[index] = 1; // elegir: se toma
subsets(items, chosen, n, index + 1, nodes); // explorar
chosen[index] = 0; // deshacer: no se toma
subsets(items, chosen, n, index + 1, nodes); // explorar
}
int main(void) {
const char *items = "abc";
int chosen[MAX_ITEMS] = {0};
long nodes = 0;
subsets(items, chosen, (int) strlen(items), 0, &nodes);
printf("nodos: %ld\n", nodes);
return 0;
}Errores
Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.
// ERROR A PROPÓSITO: la poda supone precios positivos, y aquí hay un vale de descuento.
#include <stdio.h>
#define COUNT_OF_PRICES 3
static long count_all(const int *prices, int n, int index, int total, int budget) {
if (index == n) {
return total <= budget;
}
return count_all(prices, n, index + 1, total + prices[index], budget)
+ count_all(prices, n, index + 1, total, budget);
}
static long count_pruned(const int *prices, int n, int index, int total, int budget) {
if (total > budget) {
return 0; // falso aquí: el vale todavía podría bajar el total
}
if (index == n) {
return 1;
}
return count_pruned(prices, n, index + 1, total + prices[index], budget)
+ count_pruned(prices, n, index + 1, total, budget);
}
int main(void) {
int prices[COUNT_OF_PRICES] = {8, 5, -5};
int budget = 6;
printf("sin poda: %ld combinaciones\n", count_all(prices, COUNT_OF_PRICES, 0, 0, budget));
printf("con poda: %ld combinaciones\n", count_pruned(prices, COUNT_OF_PRICES, 0, 0, budget));
return 0;
}// ERROR A PROPÓSITO: search avisa que encontró una solución, y quien la llama no hace caso.
#include <stdio.h>
#define MAX_N 16
typedef struct {
int n;
int column_of_row[MAX_N];
int column_used[MAX_N];
int down_used[2 * MAX_N - 1];
int up_used[2 * MAX_N - 1];
long nodes;
} Queens;
static int can_place(const Queens *q, int row, int col) {
return !q->column_used[col]
&& !q->down_used[row - col + q->n - 1]
&& !q->up_used[row + col];
}
static void mark(Queens *q, int row, int col, int value) {
q->column_used[col] = value;
q->down_used[row - col + q->n - 1] = value;
q->up_used[row + col] = value;
}
// Devuelve 1 si encontró una solución, y la deja en column_of_row.
static int search(Queens *q, int row) {
q->nodes++;
if (row == q->n) {
return 1;
}
for (int col = 0; col < q->n; col++) {
if (can_place(q, row, col)) {
mark(q, row, col, 1);
q->column_of_row[row] = col;
search(q, row + 1); // falta revisar lo que devuelve
mark(q, row, col, 0);
}
}
return 0;
}
static void print_board(const Queens *q) {
for (int row = 0; row < q->n; row++) {
for (int col = 0; col < q->n; col++) {
if (q->column_of_row[row] == col) {
printf("Q ");
} else {
printf(". ");
}
}
printf("\n");
}
}
int main(int argc, char *argv[]) {
int n;
if (argc != 2 || sscanf(argv[1], "%d", &n) != 1 || n < 1 || n > MAX_N) {
fprintf(stderr, "uso: %s n, con n entre 1 y %d\n", argv[0], MAX_N);
return 1;
}
Queens q = {.n = n};
if (search(&q, 0)) {
print_board(&q);
} else {
printf("no hay solución\n");
}
printf("n = %d, primera solución: %ld nodos\n", n, q.nodes);
return 0;
}// ERROR A PROPÓSITO: el elemento usado nunca vuelve a quedar libre.
#include <stdio.h>
#include <string.h>
#define MAX_ITEMS 16
static void permutations(const char *items, int n, char *current, int *used, int depth) {
if (depth == n) {
current[n] = '\0';
printf("%s\n", current);
return;
}
for (int i = 0; i < n; i++) {
if (!used[i]) {
used[i] = 1;
current[depth] = items[i];
permutations(items, n, current, used, depth + 1);
// falta used[i] = 0;
}
}
}
int main(void) {
const char *items = "abc";
char current[MAX_ITEMS + 1];
int used[MAX_ITEMS] = {0};
permutations(items, (int) strlen(items), current, used, 0);
return 0;
}// ERROR A PROPÓSITO: se quita la reina de la columna, pero sus diagonales quedan marcadas.
#include <stdio.h>
#define MAX_N 16
typedef struct {
int n;
int column_used[MAX_N];
int down_used[2 * MAX_N - 1]; // diagonal que baja: row - col + n - 1 es constante
int up_used[2 * MAX_N - 1]; // diagonal que sube: row + col es constante
long solutions;
long nodes;
} Queens;
static int can_place(const Queens *q, int row, int col) {
return !q->column_used[col]
&& !q->down_used[row - col + q->n - 1]
&& !q->up_used[row + col];
}
// Pone una reina con value = 1 y la quita con value = 0.
static void mark(Queens *q, int row, int col, int value) {
q->column_used[col] = value;
q->down_used[row - col + q->n - 1] = value;
q->up_used[row + col] = value;
}
static void search(Queens *q, int row) {
q->nodes++;
if (row == q->n) {
q->solutions++;
return;
}
for (int col = 0; col < q->n; col++) {
if (can_place(q, row, col)) {
mark(q, row, col, 1); // elegir
search(q, row + 1); // explorar
q->column_used[col] = 0; // deshace solo una parte
}
}
}
int main(int argc, char *argv[]) {
int n;
if (argc != 2 || sscanf(argv[1], "%d", &n) != 1 || n < 1 || n > MAX_N) {
fprintf(stderr, "uso: %s n, con n entre 1 y %d\n", argv[0], MAX_N);
return 1;
}
Queens q = {.n = n};
search(&q, 0);
printf("n = %d, con poda: %ld soluciones, %ld nodos\n", n, q.solutions, q.nodes);
return 0;
}// ERROR A PROPÓSITO: después de explorar con el elemento, no se deshace la elección.
#include <stdio.h>
#include <string.h>
#define MAX_ITEMS 16
static void print_subset(const char *items, const int *chosen, int n) {
printf("{");
for (int i = 0; i < n; i++) {
if (chosen[i]) {
printf(" %c", items[i]);
}
}
printf(" }\n");
}
static void subsets(const char *items, int *chosen, int n, int index) {
if (index == n) {
print_subset(items, chosen, n);
return;
}
chosen[index] = 1;
subsets(items, chosen, n, index + 1);
subsets(items, chosen, n, index + 1); // falta chosen[index] = 0 antes de esta línea
}
int main(void) {
const char *items = "abc";
int chosen[MAX_ITEMS] = {0};
subsets(items, chosen, (int) strlen(items), 0);
return 0;
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
"""Comprueba los programas de la clase y lo que los apuntes dicen de ellos."""
import math
import pathlib
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
BUILD = HERE / "build"
BASE = ["-Wall", "-Wextra", "-std=c17"]
FLAGS = [*BASE, "-g", "-fsanitize=address,undefined"]
PROGRAMS = ["subsets", "permutations", "count_trees", "queens_no_prune", "queens", "queens_first",
"queens_trace", "combos"]
WITH_ERRORS = ["subsets_no_undo", "permutations_no_undo", "queens_partial_undo", "first_ignored", "bad_prune"]
QUEENS_SOLUTIONS = {4: 2, 5: 10, 6: 4, 7: 40, 8: 92}
QUEENS_PRUNED_NODES = {4: 17, 5: 54, 6: 153, 7: 552, 8: 2057}
def run(program, *arguments):
done = subprocess.run(
[str(program), *arguments], cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return done.stdout, done.returncode
def run_error(name, *arguments):
with tempfile.TemporaryDirectory() as folder:
binary = pathlib.Path(folder) / name
subprocess.run(["gcc", *FLAGS, "-Werror", f"errors/{name}.c", "-o", str(binary)], cwd=HERE, check=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
return run(binary, *arguments)
class Programs(unittest.TestCase):
def test_every_program_compiles_without_warnings(self):
for name in PROGRAMS:
with self.subTest(program=name):
subprocess.run(["gcc", *FLAGS, "-Werror", f"{name}.c", "-o", "/dev/null"], cwd=HERE, check=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
def test_subsets_tree(self):
out, code = run(BUILD / "subsets")
self.assertEqual(code, 0, out)
lines = out.splitlines()
self.assertEqual(len(lines) - 1, 8)
self.assertEqual(len(set(lines)), 9)
self.assertEqual(lines[-1], "nodos: 15")
def test_permutations_tree(self):
out, code = run(BUILD / "permutations")
self.assertEqual(code, 0, out)
self.assertEqual(out.splitlines(), ["abc", "acb", "bac", "bca", "cab", "cba", "nodos: 16"])
def test_the_formulas_agree_with_the_counts(self):
out, _ = run(BUILD / "count_trees")
for line in out.splitlines()[1:]:
parts = line.replace("|", " ").split()
n, sub_leaves, sub_nodes, perm_leaves, perm_nodes = map(int, parts)
self.assertEqual(sub_leaves, 2 ** n)
self.assertEqual(sub_nodes, 2 ** (n + 1) - 1)
self.assertEqual(perm_leaves, math.factorial(n))
# Nodos del nivel k: n! / (n - k)!
self.assertEqual(perm_nodes, sum(math.factorial(n) // math.factorial(n - k) for k in range(n + 1)))
def test_review_question_about_five_elements(self):
out, _ = run(BUILD / "count_trees")
self.assertIn(" 5 | 32 63 | 120 326", out)
def test_pruning_keeps_the_same_solutions(self):
for n, solutions in QUEENS_SOLUTIONS.items():
with self.subTest(n=n):
plain, _ = run(BUILD / "queens_no_prune", str(n))
pruned, _ = run(BUILD / "queens", str(n))
self.assertIn(f"{solutions} soluciones", plain)
self.assertIn(f"{solutions} soluciones", pruned)
self.assertIn(f"{(n ** (n + 1) - 1) // (n - 1)} nodos", plain)
self.assertIn(f"{QUEENS_PRUNED_NODES[n]} nodos", pruned)
def test_numbers_quoted_in_the_notes(self):
self.assertEqual(math.comb(64, 8), 4426165368)
self.assertEqual(8 ** 8, 16777216)
self.assertEqual(len(str(9 ** 57)), 55)
self.assertEqual(len(str(math.factorial(20))), 19)
self.assertGreater((12 ** 13 - 1) // 11, 9 * 10 ** 12)
empty = (HERE / "lab" / "data" / "dificil.txt").read_text().count("0")
self.assertEqual(empty, 57)
def test_first_solution_stops_early_and_is_a_real_solution(self):
out, code = run(BUILD / "queens_first", "8")
self.assertEqual(code, 0, out)
rows = [line.split() for line in out.splitlines()[:8]]
columns = [row.index("Q") for row in rows]
self.assertEqual(sorted(columns), list(range(8)))
for a in range(8):
for b in range(a + 1, 8):
self.assertNotEqual(abs(columns[a] - columns[b]), b - a)
self.assertIn("114 nodos", out)
def test_programs_reject_bad_arguments(self):
for name in ["queens", "queens_no_prune", "queens_first"]:
for arguments in [(), ("x",), ("0",), ("99",)]:
with self.subTest(program=name, arguments=arguments):
out, code = run(BUILD / name, *arguments)
self.assertEqual(code, 1)
self.assertIn("uso:", out)
def test_pruning_saves_nodes_and_keeps_the_answer(self):
out, _ = run(BUILD / "combos")
self.assertEqual(out.splitlines(), ["sin poda: 249 combinaciones, 8191 nodos",
"con poda: 249 combinaciones, 1265 nodos"])
class ProgramsWithErrors(unittest.TestCase):
def test_they_compile_without_warnings(self):
# Por eso son bugs difíciles: ni gcc ni los sanitizers avisan. Solo la salida los delata.
for name in WITH_ERRORS:
with self.subTest(program=name):
subprocess.run(["gcc", *FLAGS, "-Werror", f"errors/{name}.c", "-o", "/dev/null"], cwd=HERE,
check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
def test_subsets_without_undo_repeat(self):
out, code = run_error("subsets_no_undo")
self.assertEqual(code, 0, out)
self.assertEqual(out.splitlines(), ["{ a b c }"] * 8)
def test_permutations_without_undo_find_one(self):
out, code = run_error("permutations_no_undo")
self.assertEqual(code, 0, out)
self.assertEqual(out.splitlines(), ["abc"])
def test_partial_undo_loses_the_solutions(self):
out, code = run_error("queens_partial_undo", "8")
self.assertEqual(code, 0, out)
self.assertIn("n = 8, con poda: 0 soluciones, 11 nodos", out)
def test_ignoring_the_return_loses_the_solution(self):
out, code = run_error("first_ignored", "8")
self.assertEqual(code, 0, out)
self.assertEqual(out.splitlines(), ["no hay solución", "n = 8, primera solución: 2057 nodos"])
def test_bad_prune_loses_a_combination(self):
out, code = run_error("bad_prune")
self.assertEqual(code, 0, out)
self.assertEqual(out.splitlines(), ["sin poda: 5 combinaciones", "con poda: 4 combinaciones"])
if __name__ == "__main__":
unittest.main()"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.
Cada prueba compila un main propio con board.c de lab y con subset_sum.c y solver.c de esa
carpeta. Corren con ASan y UBSan. Las cuentas de nodos exactas salen de las reglas de
lab/solver.h: si una no coincide y el tablero sí queda bien, revisen esas reglas.
"""
import os
import pathlib
import subprocess
import tempfile
import unittest
HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
SOURCE = HERE / LAB_DIR
FLAGS = ["-Wall", "-Wextra", "-std=c17", "-g", "-fsanitize=address,undefined"]
BOARD_C = HERE / "lab" / "board.c"
DATA = HERE / "lab" / "data"
# Tablero: nodos en orden de lectura y nodos con menos opciones, medidos con la solución.
BOARDS = {"facil.txt": (72, 38), "medio.txt": (4314, 169), "dificil.txt": (1745319, 1815),
"sorpresa.txt": (815, 1116)}
# medio.txt con un 9 de más en la primera celda: es legal, pero ya no tiene solución.
UNSOLVABLE = "930000080000030005004072006190000000302001400700203961000100000000700239000029614"
UNSOLVABLE_NODES = (2070, 119)
METHODS = {"solve_in_order": 0, "solve_fewest_options": 1}
def as_rows(digits):
return "".join(digits[i:i + 9] + "\n" for i in range(0, 81, 9))
def build_and_run(main_source, sources, files=None):
"""Devuelve (salida, código de salida) de un main propio, en una carpeta temporal."""
with tempfile.TemporaryDirectory() as name:
folder = pathlib.Path(name)
for file_name, content in (files or {}).items():
(folder / file_name).write_text(content, encoding="utf-8")
driver = folder / "driver.c"
driver.write_text(main_source, encoding="utf-8")
binary = folder / "driver"
built = subprocess.run(
["gcc", *FLAGS, f"-I{HERE / 'lab'}", str(driver), *map(str, sources), "-o", str(binary)],
text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
)
if built.returncode != 0:
raise AssertionError(f"No compila:\n{built.stdout}")
try:
done = subprocess.run(
[str(binary)], cwd=folder, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
except subprocess.TimeoutExpired:
raise AssertionError("El programa tardó más de 60 segundos: ¿una búsqueda sin fin?")
return done.stdout, done.returncode
def main_with(body):
return ('#include <stdio.h>\n#include "board.h"\n#include "solver.h"\n#include "subset_sum.h"\n'
"int main(void) {\n" + body + "\nreturn 0;\n}\n")
def run_board(body, files=None):
return build_and_run(main_with(body), [BOARD_C], files)
def run_solver(body, files=None):
return build_and_run(main_with(body), [BOARD_C, SOURCE / "solver.c"], files)
def run_subset(body):
return build_and_run(main_with(body), [SOURCE / "subset_sum.c"])
def print_cells(name="board"):
return ("for (int r = 0; r < BOARD_SIZE; r++) { for (int c = 0; c < BOARD_SIZE; c++) { "
f'printf("%d", {name}.cells[r][c]); }} }} printf("\\n");\n')
class LabCase(unittest.TestCase):
longMessage = False
def check(self, result, wanted, what):
out, code = result
if code != 0:
self.fail(f"{what}: el programa terminó con error.\n{out}")
if out.strip() != wanted:
self.fail(f"{what}\n se esperaba: {wanted}\n se obtuvo: {out.strip()}")
class Reader(LabCase):
"""El lector ya viene hecho: estas pruebas pasan desde el inicio."""
def load(self, content):
body = 'Board board; printf("%d\\n", board_load("in.txt", &board));'
return run_board(body, {"in.txt": content})
def test_reads_a_good_file(self):
body = 'Board board; if (!board_load("in.txt", &board)) { return 1; }\n' + print_cells()
self.check(run_board(body, {"in.txt": as_rows(UNSOLVABLE)}), UNSOLVABLE, "Lectura de un archivo correcto.")
def test_last_line_may_lack_the_newline(self):
self.check(self.load(as_rows(UNSOLVABLE)[:-1]), "1", "Última línea sin salto de línea.")
def test_rejects_bad_formats(self):
rows = as_rows(UNSOLVABLE)
lines = rows.splitlines()
bad = {
"una fila menos": "\n".join(lines[:8]) + "\n",
"una fila más": rows + lines[0] + "\n",
"una línea corta": rows.replace(lines[4], lines[4][:8]),
"una línea larga": rows.replace(lines[4], lines[4] + "1"),
"una letra": rows.replace(lines[4], "x" + lines[4][1:]),
"archivo vacío": "",
}
for what, content in bad.items():
with self.subTest(what):
self.check(self.load(content), "0", f"Debe rechazar: {what}.")
def test_missing_file(self):
body = 'Board board; printf("%d\\n", board_load("no_existe.txt", &board));'
self.check(run_board(body), "0", "Un archivo que no existe.")
def test_legal_detects_repeats(self):
empty = "0" * 81
cases = {"fila": (0, 8), "columna": (0, 72), "caja": (0, 20)}
for what, (a, b) in cases.items():
digits = list(empty)
digits[a] = digits[b] = "5"
body = ('Board board; if (!board_load("in.txt", &board)) { return 1; }\n'
'printf("%d\\n", board_is_legal(&board));')
with self.subTest(what):
self.check(run_board(body, {"in.txt": as_rows("".join(digits))}), "0", f"Repetido en la {what}.")
body = ('Board board; if (!board_load("in.txt", &board)) { return 1; }\n'
'printf("%d\\n", board_is_legal(&board));')
self.check(run_board(body, {"in.txt": as_rows(UNSOLVABLE)}), "1", "Un tablero sin repetidos.")
@unittest.skipUnless((SOURCE / "subset_sum.c").exists(), f"no está {LAB_DIR}/subset_sum.c")
class SubsetSum(LabCase):
def answers(self, values, targets):
items = ", ".join(map(str, values))
body = f"int values[] = {{{items}}}; long nodes = 0;\n"
for target in targets:
body += f'printf("%d ", subset_sum(values, {len(values)}, {target}, &nodes));\n'
return run_subset(body)
def test_finds_sums(self):
self.check(self.answers([3, 9, 5, 14], [8, 12, 17, 31, 3]), "1 1 1 1 1", "Sumas que sí existen.")
def test_rejects_sums(self):
self.check(self.answers([3, 9, 5, 14], [1, 2, 13, 30, 32, -1]), "0 0 0 0 0 0", "Sumas que no existen.")
def test_empty_subset_sums_zero(self):
body = 'int values[] = {7}; long nodes = 0;\nprintf("%d %d\\n", subset_sum(values, 0, 0, &nodes),'\
' subset_sum(values, 0, 7, &nodes));'
self.check(run_subset(body), "1 0", "Con 0 elementos solo se puede sumar 0.")
self.check(self.answers([4, 6], [0]), "1", "El subconjunto vacío suma 0.")
def test_negative_values_are_not_pruned_away(self):
# 8 ya se pasa de 3, pero 8 + (-5) = 3. Una poda por "ya me pasé" pierde esta solución.
self.check(self.answers([8, 5, -5], [3, 0, 13, 8, 6]), "1 1 1 1 0", "Con valores negativos.")
def test_no_solution_visits_the_whole_tree(self):
items = ", ".join(str(2 * i + 2) for i in range(10))
body = (f'int values[] = {{{items}}}; long nodes = 0;\n'
'int found = subset_sum(values, 10, 3, &nodes);\n'
'printf("%d %ld\\n", found, nodes);')
self.check(run_subset(body), "0 2047", "Los pares no suman 3, así que se visitan 2^11 - 1 nodos.")
def test_stops_at_the_first_solution(self):
items = ", ".join(str(i + 1) for i in range(16))
body = (f'int values[] = {{{items}}}; long nodes = 0;\n'
'int found = subset_sum(values, 16, 1, &nodes);\n'
'printf("%d %d\\n", found, nodes > 0 && nodes < 131071);')
self.check(run_subset(body), "1 1", "Debe contar nodos, y detenerse al encontrar una solución.")
@unittest.skipUnless((SOURCE / "solver.c").exists(), f"no está {LAB_DIR}/solver.c")
class Solver(LabCase):
def on_board(self, name, body):
code = (f'Board board; if (!board_load("{DATA / name}", &board)) {{ return 1; }}\n' + body)
return run_solver(code)
def test_can_place(self):
body = ('printf("%d %d %d %d %d\\n", can_place(&board, 0, 4, 3), can_place(&board, 3, 3, 4),'
" can_place(&board, 0, 4, 8), can_place(&board, 0, 4, 5), can_place(&board, 0, 4, 1));")
self.check(self.on_board("facil.txt", body), "0 0 0 1 1",
"can_place: 3 está en la fila, 4 en la columna, 8 en la caja; 5 y 1 están libres.")
def test_can_place_looks_at_the_whole_box(self):
# Un 5 en el centro: (3, 3), (5, 5) y (3, 5) lo ven solo por la caja, y (2, 2) no lo ve.
digits = ["0"] * 81
digits[40] = "5"
code = ('Board board; if (!board_load("in.txt", &board)) { return 1; }\n'
'printf("%d %d %d %d\\n", can_place(&board, 3, 3, 5), can_place(&board, 5, 5, 5),'
" can_place(&board, 3, 5, 5), can_place(&board, 2, 2, 5));")
self.check(run_solver(code, {"in.txt": as_rows("".join(digits))}), "0 0 0 1",
"can_place con un 5 en el centro del tablero.")
def test_count_options(self):
body = ('printf("%d %d %d\\n", count_options(&board, 0, 2), count_options(&board, 0, 4),'
" count_options(&board, 8, 0));")
self.check(self.on_board("facil.txt", body), "3 2 1", "count_options en tres celdas de facil.txt.")
def solved(self, method, name):
"""Comprueba el tablero con solo mirarlo: que respete las pistas, que esté lleno y sea legal."""
original = (DATA / name).read_text().replace("\n", "")
body = (f"long nodes = 0; int found = {method}(&board, &nodes);\n"
'printf("%d %ld %d\\n", found, nodes, board_is_legal(&board));\n' + print_cells())
out, code = self.on_board(name, body)
if code != 0:
self.fail(f"{method} con {name}: el programa terminó con error.\n{out}")
first, cells = out.split("\n")[:2]
found, nodes, legal = first.split()
if found != "1":
self.fail(f"{method} con {name}: dijo que no hay solución, y sí hay una.")
if "0" in cells:
self.fail(f"{method} con {name}: el tablero quedó con celdas vacías.\n{cells}")
if legal != "1":
self.fail(f"{method} con {name}: el tablero quedó con repetidos.\n{cells}")
if any(a != "0" and a != b for a, b in zip(original, cells)):
self.fail(f"{method} con {name}: cambió una celda que venía dada.")
return int(nodes)
def test_solves_every_board_with_the_expected_nodes(self):
for method, position in METHODS.items():
for name, wanted in BOARDS.items():
with self.subTest(method=method, board=name):
nodes = self.solved(method, name)
if nodes != wanted[position]:
self.fail(f"{method} con {name}: el tablero queda bien, pero visitó {nodes} nodos y "
f"se esperaban {wanted[position]}. Revisen las reglas de lab/solver.h.")
def test_unsolvable_board_is_left_as_it_was(self):
for method, position in METHODS.items():
code = ('Board board; if (!board_load("in.txt", &board)) { return 1; }\n'
f'long nodes = 0; int found = {method}(&board, &nodes); printf("%d %ld\\n", found, nodes);\n' + print_cells())
with self.subTest(method=method):
self.check(run_solver(code, {"in.txt": as_rows(UNSOLVABLE)}),
f"0 {UNSOLVABLE_NODES[position]}\n{UNSOLVABLE}",
f"{method} en un tablero sin solución: debe devolver 0, contar los nodos y dejar el "
"tablero igual. Si cambió, falta deshacer.")
def test_full_board_is_one_node(self):
for method in METHODS:
code = (f'Board board; if (!board_load("{DATA / "facil.txt"}", &board)) {{ return 1; }}\n'
f"long nodes = 0; if (!{method}(&board, &nodes)) {{ return 1; }}\n"
f'printf("%d ", {method}(&board, &nodes)); printf("%ld\\n", nodes);')
with self.subTest(method=method):
self.check(run_solver(code), "1 1", f"{method} en un tablero ya lleno: un nodo, y devuelve 1.")
def test_empty_board_has_a_solution(self):
for method in METHODS:
code = ('Board board; if (!board_load("in.txt", &board)) { return 1; }\n'
f"long nodes = 0; int found = {method}(&board, &nodes);\n"
"int empty = 0; for (int r = 0; r < BOARD_SIZE; r++) { for (int c = 0; c < BOARD_SIZE; c++) "
"{ if (board.cells[r][c] == 0) { empty++; } } }\n"
'printf("%d %d %d\\n", found, empty, board_is_legal(&board));')
with self.subTest(method=method):
self.check(run_solver(code, {"in.txt": as_rows("0" * 81)}), "1 0 1",
f"{method} en un tablero vacío: hay muchas soluciones, y basta la primera.")
def test_the_counter_starts_again_in_each_call(self):
for method, position in METHODS.items():
code = (f'Board a; Board b; if (!board_load("{DATA / "facil.txt"}", &a)) {{ return 1; }}\n'
"b = a; long first = 0; long second = 0;\n"
f"{method}(&a, &first); {method}(&b, &second);\n"
'printf("%ld %ld\\n", first, second);')
wanted = BOARDS["facil.txt"][position]
with self.subTest(method=method):
self.check(run_solver(code), f"{wanted} {wanted}",
"Dos llamadas seguidas deben contar lo mismo: el contador no es una variable static.")
@unittest.skipUnless((HERE / "solution" / "count_solutions.c").exists(), "falta la solución")
class GivenBoards(unittest.TestCase):
def test_every_given_board_has_exactly_one_solution(self):
paths = sorted(DATA.glob("*.txt"))
with tempfile.TemporaryDirectory() as name:
binary = pathlib.Path(name) / "count_solutions"
sources = [HERE / "solution" / "count_solutions.c", HERE / "solution" / "solver.c", BOARD_C]
subprocess.run(["gcc", *FLAGS, f"-I{HERE / 'lab'}", *map(str, sources), "-o", str(binary)], check=True)
done = subprocess.run([str(binary), *map(str, paths)], text=True, stdout=subprocess.PIPE, timeout=60)
self.assertEqual(done.returncode, 0)
self.assertEqual(done.stdout, "".join(f"{path}: 1\n" for path in paths))
if __name__ == "__main__":
unittest.main()"""Vuelve a correr los comandos que aparecen en los apuntes y en las diapositivas
y comprueba que el código citado existe de verdad en esta carpeta."""
import pathlib
import re
import subprocess
import unittest
HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-11.md"
SLIDES = ROOT / "slides" / "week-11.tex"
# Antes de un bloque cuya salida cambia con el sistema. No se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de la salida de lab/solve, que se compara con build/solution/solve.
SOLVE_OUTPUT = "<!-- salida de solve -->"
def normalize(text):
# Según el idioma del entorno, gcc usa comillas tipográficas o rectas.
return text.replace("‘", "'").replace("’", "'")
def run_shell(command):
done = subprocess.run(
["bash", "-c", command], cwd=HERE, text=True, encoding="utf-8", errors="replace",
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=60,
)
return normalize(done.stdout)
def clean_lines(text):
return [line.rstrip() for line in text.splitlines()]
def split_commands(lines):
"""Parte un bloque de terminal en pares (comando, líneas de salida)."""
pairs = []
for line in lines:
if line.startswith("$ "):
pairs.append((line[2:], []))
elif pairs:
pairs[-1][1].append(normalize(line.rstrip()))
return pairs
def note_blocks(language):
"""Devuelve (líneas, marca) de cada bloque de los apuntes."""
blocks, current, mark, previous = [], None, "", ""
for raw in NOTES.read_text(encoding="utf-8").splitlines():
line = re.sub(r"^> ?", "", raw)
if current is None:
if line.strip() == "```" + language:
current, mark = [], previous.strip()
elif line.strip():
previous = line
elif line.strip() == "```":
blocks.append((current, mark))
current = None
else:
current.append(line)
return blocks
def slide_blocks(environments):
text = SLIDES.read_text(encoding="utf-8")
pattern = r"\\begin\{(?:%s)\}[^\n]*\n(.*?)\\end\{(?:%s)\}" % (environments, environments)
return [match.group(1).splitlines() for match in re.finditer(pattern, text, re.S)]
def known_code_lines():
lines = set()
sources = list(HERE.glob("*.c")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.c"))
sources += list((HERE / "lab").glob("*.c")) + list((HERE / "lab").glob("*.h"))
for source in sources:
lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
return lines
class NotesAreTrue(unittest.TestCase):
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_terminal_blocks_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if not lines or not lines[0].startswith("$ ") or mark == SOLVE_OUTPUT:
continue
for command, expected in split_commands(lines):
actual = clean_lines(run_shell(command))
checked += 1
if mark != SYSTEM_DEPENDENT:
with self.subTest(command=command):
self.assertEqual(actual, expected)
self.assertGreater(checked, 10)
@unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
def test_code_blocks_in_notes_exist(self):
known = known_code_lines()
for lines, _ in note_blocks("c"):
for line in lines:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
@unittest.skipUnless(NOTES.exists() and (HERE / "solution").exists(), "falta la solución")
def test_solve_output_in_notes(self):
checked = 0
for lines, mark in note_blocks("text"):
if mark == SOLVE_OUTPUT:
for command, expected in split_commands(lines):
self.assertTrue(command.startswith("build/lab/solve "))
actual = clean_lines(run_shell(command.replace("build/lab/", "build/solution/")))
self.assertEqual(actual, expected)
checked += 1
self.assertGreater(checked, 0)
class SlidesAreTrue(unittest.TestCase):
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_terminal_blocks_in_slides(self):
for block in slide_blocks("terminal|salida|salidalarga"):
for command, expected in split_commands(block):
# La salida de lab/solve necesita el laboratorio terminado: se compara con la solución.
if command.startswith("build/lab/"):
if not (HERE / "solution").exists():
continue
command = command.replace("build/lab/", "build/solution/")
actual = clean_lines(run_shell(command))
position = 0
# Una diapositiva puede omitir líneas, pero no cambiarlas ni reordenarlas.
for line in expected:
line = line.strip()
if not line:
continue
with self.subTest(command=command, line=line):
found = [i for i in range(position, len(actual)) if actual[i].strip() == line]
self.assertTrue(found, f"no aparece en la salida real: {line}")
if found:
position = found[0] + 1
@unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
def test_code_blocks_in_slides_exist(self):
known = known_code_lines()
for block in slide_blocks("codigo|codigochico|codigomini"):
for line in block:
if line.strip() and line.strip() != "...":
with self.subTest(line=line):
self.assertIn(line.strip(), known)
if __name__ == "__main__":
unittest.main()Laboratorio
Archivos de partida del laboratorio de la semana.
#include "board.h"
#include <stdio.h>
#include <string.h>
// Alcanza para 9 dígitos, el salto de línea, el '\0', y uno más: así se nota una línea larga.
#define LINE_SIZE (BOARD_SIZE + 3)
// Pasa una línea a una fila del tablero. Devuelve 0 si no son exactamente 9 dígitos.
static int parse_row(char *line, int *row) {
size_t length = strlen(line);
if (length > 0 && line[length - 1] == '\n') {
line[length - 1] = '\0';
length--;
}
if (length != BOARD_SIZE) {
return 0;
}
for (int col = 0; col < BOARD_SIZE; col++) {
if (line[col] < '0' || line[col] > '9') {
return 0;
}
row[col] = line[col] - '0';
}
return 1;
}
static int read_rows(FILE *file, Board *board) {
char line[LINE_SIZE];
for (int row = 0; row < BOARD_SIZE; row++) {
if (fgets(line, sizeof line, file) == NULL || !parse_row(line, board->cells[row])) {
return 0;
}
}
// Después de la fila 9 no puede venir nada más.
return fgets(line, sizeof line, file) == NULL;
}
int board_load(const char *path, Board *board) {
FILE *file = fopen(path, "r");
if (file == NULL) {
return 0;
}
int ok = read_rows(file, board);
fclose(file);
return ok;
}
// ¿Otra celda de la misma fila, columna o caja tiene el dígito de (row, col)?
static int is_repeated(const Board *board, int row, int col) {
int digit = board->cells[row][col];
int box_row = row - row % BOX_SIZE;
int box_col = col - col % BOX_SIZE;
for (int i = 0; i < BOARD_SIZE; i++) {
if (i != col && board->cells[row][i] == digit) {
return 1;
}
if (i != row && board->cells[i][col] == digit) {
return 1;
}
int other_row = box_row + i / BOX_SIZE;
int other_col = box_col + i % BOX_SIZE;
if ((other_row != row || other_col != col) && board->cells[other_row][other_col] == digit) {
return 1;
}
}
return 0;
}
int board_is_legal(const Board *board) {
for (int row = 0; row < BOARD_SIZE; row++) {
for (int col = 0; col < BOARD_SIZE; col++) {
if (board->cells[row][col] != 0 && is_repeated(board, row, col)) {
return 0;
}
}
}
return 1;
}
void board_print(const Board *board) {
for (int row = 0; row < BOARD_SIZE; row++) {
if (row > 0 && row % BOX_SIZE == 0) {
printf("------+-------+------\n");
}
for (int col = 0; col < BOARD_SIZE; col++) {
if (col > 0 && col % BOX_SIZE == 0) {
printf("| ");
}
if (board->cells[row][col] == 0) {
printf(". ");
} else {
printf("%d ", board->cells[row][col]);
}
}
printf("\n");
}
}// Tablero de sudoku y su lectura desde un archivo.
#ifndef BOARD_H
#define BOARD_H
#define BOARD_SIZE 9
#define BOX_SIZE 3
// Cada celda tiene un dígito de 1 a 9, o 0 si está vacía.
typedef struct {
int cells[BOARD_SIZE][BOARD_SIZE];
} Board;
// Lee un archivo con 9 líneas de 9 dígitos, con 0 para una celda vacía.
// Devuelve 1 si lo leyó, y 0 si no se puede abrir o no tiene ese formato: en ese caso,
// no hay que usar *board. No revisa las reglas del sudoku: para eso está board_is_legal.
int board_load(const char *path, Board *board);
// Devuelve 1 si ningún dígito se repite en una fila, en una columna o en una caja de 3 x 3.
int board_is_legal(const Board *board);
// Escribe el tablero, con un punto en cada celda vacía.
void board_print(const Board *board);
#endif# Laboratorio de la semana 11: resolver sudokus
Esta semana hay una sola sesión de laboratorio, porque la otra es para la Práctica 3. Está pensada
para unos 90 minutos de trabajo. Los comandos se corren desde `code/week-11`. Las ideas están en los
apuntes, en la sección "El laboratorio, paso a paso".
| Archivo | Qué es | ¿Lo cambian? |
|---|---|---|
| `board.h`, `board.c` | El tablero y su lectura desde un archivo | No |
| `subset_sum.h` | Lo que promete `subset_sum` | No |
| `subset_sum.c` | `subset_sum` hecha, y su función recursiva por escribir | **Sí** |
| `solver.h` | Lo que promete cada función del solucionador, y cómo se cuentan los nodos | No |
| `solver.c` | Las funciones por escribir | **Sí** |
| `solver_tests.c` | Pruebas con `assert`. Agreguen las suyas | **Sí** |
| `solve.c` | El programa que resuelve archivos y compara los nodos | No |
| `data/` | `facil.txt`, `medio.txt`, `dificil.txt` y `sorpresa.txt`: cuatro tableros con una sola solución | No |
Un archivo de tablero tiene 9 líneas de 9 dígitos, con 0 para una celda vacía. El lector no acepta otra
cosa, y no revisa las reglas del sudoku: de eso se encarga `board_is_legal`, y `solve.c` la llama antes de
resolver.
```sh
make lab # compila solver_tests y solve en build/lab/
build/lab/solver_tests # corre sus pruebas
build/lab/solve lab/data/facil.txt
make test-lab # comprueba todo el laboratorio; falla hasta que terminen
```
## Parte 1: leer (unos 10 minutos)
Lean `board.h`, `solver.h` y `subset_sum.h` completos, y luego `board.c`. Respondan por escrito, sin
ejecutar nada.
1. En `board.c`, ¿qué línea rechaza una línea de 10 dígitos? ¿Y un archivo con una décima
línea?
2. En `is_repeated`, ¿qué celda revisa la vuelta `i = 5` si `row = 4` y `col = 7`?
3. `solver.h` promete que, si no hay solución, `*board` queda igual que al inicio. ¿Qué línea de su
búsqueda tendrá que cumplir esa promesa?
4. Dos cuentas de nodos solo se pueden comparar si significan lo mismo. Según `solver.h`, ¿qué es un nodo?
¿Cuenta la primera llamada?
## Parte 2: `subset_sum` (unos 20 minutos)
Escriban `search` en `subset_sum.c`. Por cada elemento hay dos ramas, con él y sin él: es el árbol de
subconjuntos de los apuntes, con dos diferencias. La hoja responde en vez de escribir, y el retorno se
revisa para detenerse en la primera solución.
Antes de ejecutar, calculen a mano cuántos nodos da `{3, 9, 5}` con un objetivo que no existe. La prueba en
`solver_tests.c` lo comprueba. No agreguen una poda por "ya me pasé": los valores pueden ser negativos, y
las pruebas lo revisan.
## Parte 3: el solucionador en orden (unos 35 minutos)
1. `can_place`: fila, columna y caja, en un solo ciclo o en tres.
2. `search_in_order`: cuenta el nodo, busca la primera celda vacía y prueba en ella los dígitos de 1 a 9. Si
no hay celdas vacías, ya terminó. Elegir, explorar, deshacer.
Cuando pasen las primeras pruebas, comparen `facil.txt` con `medio.txt`. Busquen la línea que deshace,
bórrenla, y vean qué dice cada tablero. Luego vuelvan a ponerla.
## Parte 4: elegir la celda con menos opciones (unos 25 minutos)
1. `count_options`: es un ciclo sobre `can_place`.
2. `search_fewest_options`: lo mismo, pero en vez de tomar la primera celda vacía, toma la de menos opciones.
Escriban una función aparte que la busque: necesita dos ciclos, y devolver una fila y una columna.
Con eso, comparen:
```sh
build/lab/solve lab/data/facil.txt lab/data/medio.txt lab/data/dificil.txt lab/data/sorpresa.txt
make test-lab
```
Escriban en una tabla los nodos de cada método, y respondan: ¿en qué tablero se gana más? ¿Hay alguno en el
que se pierde? ¿Cuánto cuesta cada nodo en cada método, en llamadas a `can_place` como máximo?
**Si no alcanza el tiempo:** lo que no terminen lo terminan antes de la próxima clase. El orden de arriba deja
lo más importante primero. La parte 4 es la que puede quedar para después de la sesión.
## Si sobra tiempo
1. Escriban `int count_solutions(Board *board, int limit)`, que cuenta soluciones y se detiene al llegar a
`limit`. Con ella pueden comprobar que un tablero suyo tiene una sola solución.
2. Inventen un tablero con una sola solución en el que `solve_in_order` visite más de un millón de nodos.
¿Qué tienen que dejar vacío al inicio?
3. Agreguen a `subset_sum` una poda que sea correcta también con valores negativos. Pista: antes de buscar,
calculen cuánto pueden sumar como máximo y como mínimo los elementos que faltan.// Resuelve los tableros de los archivos dados, con los dos métodos, y compara sus nodos.
// Con un solo archivo escribe además el tablero y su solución.
#include <stdio.h>
#include "board.h"
#include "solver.h"
static void print_result(const char *method, int solved, long nodes) {
if (solved) {
printf(" %s resuelto, %ld nodos\n", method, nodes);
} else {
printf(" %s sin solución, %ld nodos\n", method, nodes);
}
}
// Devuelve 0 si el archivo no se pudo usar.
static int solve_file(const char *path, int show_boards) {
Board original;
if (!board_load(path, &original)) {
fprintf(stderr, "%s: no se pudo leer un tablero de 9 líneas de 9 dígitos\n", path);
return 0;
}
if (!board_is_legal(&original)) {
fprintf(stderr, "%s: el tablero repite un dígito\n", path);
return 0;
}
Board in_order = original;
Board fewest = original;
long nodes_in_order = 0;
long nodes_fewest = 0;
int solved_in_order = solve_in_order(&in_order, &nodes_in_order);
int solved_fewest = solve_fewest_options(&fewest, &nodes_fewest);
printf("%s\n", path);
if (show_boards) {
board_print(&original);
}
print_result("orden de lectura:", solved_in_order, nodes_in_order);
print_result("menos opciones: ", solved_fewest, nodes_fewest);
if (show_boards && solved_in_order) {
board_print(&in_order);
}
return 1;
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "uso: %s archivo...\n", argv[0]);
return 1;
}
int status = 0;
for (int i = 1; i < argc; i++) {
if (!solve_file(argv[i], argc == 2)) {
status = 1;
}
}
return status;
}// Pruebas con assert. Se corren desde code/week-11, con build/lab/solver_tests.
// Agreguen las suyas: cada función nueva, con un caso que puedan calcular a mano.
#include <assert.h>
#include <stdio.h>
#include "board.h"
#include "solver.h"
#include "subset_sum.h"
static void test_subset_sum_small(void) {
int values[] = {3, 9, 5};
long nodes = 0;
assert(subset_sum(values, 3, 8, &nodes) == 1); // 3 + 5
assert(subset_sum(values, 3, 0, &nodes) == 1); // el subconjunto vacío
assert(subset_sum(values, 3, 7, &nodes) == 0);
assert(nodes == 15); // sin solución: 2 + 4 + 8 + 1
}
static void test_can_place(void) {
Board board;
assert(board_load("lab/data/facil.txt", &board));
// Esta celda de la fila 0 está vacía, y 3 ya está en esa fila.
assert(can_place(&board, 0, 4, 3) == 0);
// Esta otra está vacía, y 4 ya está en la columna 3.
assert(can_place(&board, 3, 3, 4) == 0);
// 8 ya está en la caja de (0, 4), en la fila 1 y la columna 5.
assert(can_place(&board, 0, 4, 8) == 0);
assert(can_place(&board, 0, 4, 5) == 1);
}
static void test_in_order_solves_the_easy_board(void) {
Board board;
long nodes = 0;
assert(board_load("lab/data/facil.txt", &board));
assert(solve_in_order(&board, &nodes) == 1);
assert(nodes == 72);
assert(board_is_legal(&board));
assert(board.cells[0][2] == 8);
}
static void test_fewest_options_solves_the_easy_board(void) {
Board board;
long nodes = 0;
assert(board_load("lab/data/facil.txt", &board));
assert(count_options(&board, 0, 2) == 3); // 5, 7 u 8
assert(count_options(&board, 0, 4) == 2); // 1 o 5
assert(solve_fewest_options(&board, &nodes) == 1);
assert(nodes == 38);
assert(board_is_legal(&board));
}
int main(void) {
test_subset_sum_small();
test_can_place();
test_in_order_solves_the_easy_board();
test_fewest_options_solves_the_easy_board();
printf("todas las pruebas pasaron\n");
return 0;
}#include "solver.h"
// --- Por escribir: parte 3. ---
int can_place(const Board *board, int row, int col, int digit) {
// La caja de (row, col) empieza en la fila row - row % BOX_SIZE y en la
// columna col - col % BOX_SIZE. board_is_legal, en board.c, hace una cuenta parecida.
(void) board;
(void) row;
(void) col;
(void) digit;
return 0;
}
// Por escribir: elegir, explorar, deshacer. Primero cuenta el nodo: (*nodes)++.
static int search_in_order(Board *board, long *nodes) {
(void) board;
(void) nodes;
return 0;
}
// --- Hecho: la llamada de afuera solo prepara el contador. ---
int solve_in_order(Board *board, long *nodes) {
*nodes = 0;
return search_in_order(board, nodes);
}
// --- Por escribir: parte 4. ---
int count_options(const Board *board, int row, int col) {
(void) board;
(void) row;
(void) col;
return 0;
}
// Por escribir: igual que search_in_order, pero elige la celda con menos opciones.
static int search_fewest_options(Board *board, long *nodes) {
(void) board;
(void) nodes;
return 0;
}
// --- Hecho. ---
int solve_fewest_options(Board *board, long *nodes) {
*nodes = 0;
return search_fewest_options(board, nodes);
}// Solucionador de sudokus con backtracking.
//
// Un NODO es una llamada a la función recursiva, y se cuenta al entrar, incluida la primera.
// Para que dos soluciones correctas cuenten lo mismo, las dos búsquedas siguen estas reglas:
// - Prueban los dígitos de 1 a 9, y hacen la llamada recursiva solo si can_place lo permite.
// - Se detienen en la primera solución.
#ifndef SOLVER_H
#define SOLVER_H
#include "board.h"
// Devuelve 1 si digit no está en la fila, en la columna ni en la caja de (row, col).
// La celda (row, col) debe estar vacía.
int can_place(const Board *board, int row, int col, int digit);
// Devuelve cuántos dígitos, de 1 a 9, se pueden poner en la celda vacía (row, col).
int count_options(const Board *board, int row, int col);
// Las dos búsquedas reciben un tablero sin repetidos (board_is_legal) y dejan en *nodes
// los nodos visitados. Devuelven 1 si hay solución, y la dejan en *board. Si no hay,
// devuelven 0 y *board queda igual que al inicio.
// Llena siempre la primera celda vacía en orden de lectura: fila por fila, de izquierda a derecha.
int solve_in_order(Board *board, long *nodes);
// Llena siempre la celda vacía con menos opciones. En un empate, la primera en orden de lectura.
int solve_fewest_options(Board *board, long *nodes);
#endif#include "subset_sum.h"
// Por escribir: parte 2. index es el próximo elemento por decidir, y remaining lo que falta sumar.
static int search(const int *values, int n, int index, int remaining, long *nodes) {
(void) values;
(void) n;
(void) index;
(void) remaining;
(void) nodes;
return 0;
}
// Hecho: la llamada de afuera solo prepara el contador.
int subset_sum(const int *values, int n, int target, long *nodes) {
*nodes = 0;
return search(values, n, 0, target, nodes);
}// ¿Algún subconjunto de un arreglo suma un valor dado?
#ifndef SUBSET_SUM_H
#define SUBSET_SUM_H
// Devuelve 1 si algún subconjunto de values[0..n-1] suma exactamente target. El subconjunto
// vacío suma 0. Los valores pueden ser negativos.
//
// Busca con un árbol de dos ramas por elemento: con él y sin él. Un nodo es una llamada a la
// función recursiva, y se cuenta al entrar, incluida la primera. El total queda en *nodes.
// Se detiene en cuanto encuentra un subconjunto, así que sin solución visita todo el árbol.
int subset_sum(const int *values, int n, int target, long *nodes);
#endif700200800
003090000
000004007
000000000
000513080
640902100
004000000
030060075
500000609430900600
602408005
901672403
529006300
304520708
000300002
260047100
840100006
003260874030000080
000030005
004072006
190000000
302001400
700203961
000100000
000700239
000029614010000006
200007050
000236007
001009060
040700289
000000500
000000005
080060030
304080090Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 11: backtracking. Se usa dentro del entorno del curso.
#
# make compila los programas de la clase en build/
# make lab compila el laboratorio en build/lab/
# make test corre las pruebas de los ejemplos y de los apuntes
# make test-lab prueba su laboratorio (lab/subset_sum.c y lab/solver.c); falla hasta que lo terminen
# make clean borra lo generado
#
# Los programas de errors/ tienen errores a propósito: compilan, pero dan resultados
# equivocados. No se compilan con make: los compilan las pruebas, y ustedes, con los
# comandos de los apuntes.
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g -fsanitize=address,undefined
BUILD = build
PROGRAMS = subsets permutations count_trees queens_no_prune queens queens_first queens_trace combos
LAB_HEADERS = lab/board.h lab/solver.h lab/subset_sum.h
.PHONY: all lab test test-lab clean
all: $(PROGRAMS:%=$(BUILD)/%)
$(BUILD)/%: %.c | $(BUILD)
$(CC) $(CFLAGS) $< -o $@
# Cada .c se compila por separado y luego se enlaza, como en la semana 6.
$(BUILD)/lab/%.o: lab/%.c $(LAB_HEADERS) | $(BUILD)/lab
$(CC) $(CFLAGS) -c $< -o $@
$(BUILD)/lab/solve: $(BUILD)/lab/solve.o $(BUILD)/lab/board.o $(BUILD)/lab/solver.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/lab/solver_tests: $(BUILD)/lab/solver_tests.o $(BUILD)/lab/board.o $(BUILD)/lab/solver.o $(BUILD)/lab/subset_sum.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/solution/%.o: solution/%.c $(LAB_HEADERS) | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab -c $< -o $@
$(BUILD)/solution/lab_%.o: lab/%.c $(LAB_HEADERS) | $(BUILD)/solution
$(CC) $(CFLAGS) -Ilab -c $< -o $@
$(BUILD)/solution/solve: $(BUILD)/solution/lab_solve.o $(BUILD)/solution/lab_board.o $(BUILD)/solution/solver.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD)/solution/count_solutions: $(BUILD)/solution/count_solutions.o $(BUILD)/solution/lab_board.o $(BUILD)/solution/solver.o
$(CC) $(CFLAGS) $^ -o $@
$(BUILD) $(BUILD)/lab $(BUILD)/solution:
mkdir -p $@
lab: $(BUILD)/lab/solve $(BUILD)/lab/solver_tests
# Si existe solution/, también se comprueba la solución del laboratorio.
test: all
@if [ -d solution ]; then $(MAKE) --no-print-directory \
$(BUILD)/solution/solve $(BUILD)/solution/count_solutions; fi
python3 -m unittest discover -p 'test_*.py'
test-lab: all lab
LAB_DIR=lab python3 -m unittest -v test_lab
clean:
rm -rf $(BUILD) __pycache__