Semana 13 — Heaps y colas de prioridad

Objetivos

  • Implementar un heap binario sobre un arreglo.
  • Explicar por qué construir un heap cuesta Θ(n), y no Θ(n log n).
  • Usar una cola de prioridad para resolver problemas: los k mayores y la compresión de Huffman.

Teoría — Heaps y colas de prioridad · Heapsort

Lectura: Apuntes, semana 13.

Apoyo: Open Data Structures, secs. 10.1 (BinaryHeap) y 11.1.3 (Heap-sort). CLRS, capítulo Heapsort.

Temas:

  • El tipo abstracto cola de prioridad: insertar y sacar el mínimo. Tres implementaciones ingenuas y su costo.
  • La invariante del heap: cada padre es menor o igual que sus hijos. Es más débil que el orden de un árbol de búsqueda.
  • Un árbol completo sin punteros: en un arreglo, el padre de i está en (i − 1)/2, y los hijos en 2i + 1 y 2i + 2.
  • Insertar y subir; sacar la raíz y bajar. Costo O(log n).
  • Construir de abajo hacia arriba: la mayoría de los nodos baja poco, y el total es Θ(n). Con una suma por niveles.
  • Heapsort: en el lugar y O(n log n), pero no es estable y salta por toda la memoria.
  • Los k mayores de una secuencia con un heap de tamaño k. std::priority_queue.
  • Huffman: códigos sin prefijos comunes, y unir repetidamente los dos árboles de menor frecuencia. Por qué es óptimo queda para Algoritmos.
  • Error común: buscar en un heap como si fuera un árbol de búsqueda.

Laboratorio — Compresión de Huffman

Sesión 1:

  1. Escribir MinHeap<T> con comparador, con push, pop, top y construcción desde un arreglo, con check_invariants y pruebas al azar contra std::priority_queue.
  2. Medir la construcción de abajo hacia arriba frente a n inserciones.
  3. Escribir un programa que imprime las k palabras más frecuentes de un texto, y medirlo contra ordenar todo.

Sesión 2: 4. Con las frecuencias de bytes de un archivo, que se dan contadas, construir el árbol de Huffman con su heap. 5. Escribir la compresión y la descompresión: cabecera con lo necesario para volver a armar el árbol, y codificar y decodificar el contenido. Se dan hechas la tabla de códigos y la escritura y lectura de bits. 6. Comprobar con cmp que comprimir y descomprimir devuelve el archivo original, incluso con archivos binarios, vacíos o de un solo símbolo. Medir cuánto comprime cada tipo de archivo.

Preguntas de repaso

  1. Inserten 5, 3, 8, 1 y 4 en un min-heap vacío. Dibujen el arreglo después de cada inserción.
  2. Saquen el mínimo de ese heap. ¿Cómo queda?
  3. ¿Por qué el mínimo está en la raíz y el máximo no tiene un lugar fijo? ¿Dónde puede estar el máximo?
  4. Con frecuencias a:5, b:2, c:1 y d:1, construyan el árbol de Huffman y den los códigos. ¿Cuántos bits usa el texto completo?
  5. ¿Por qué ningún código de Huffman es prefijo de otro? ¿Qué permite eso al descomprimir?

Código de la semana

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

Ejemplos (13)Errores (6)Pruebas (4)Laboratorio (21)Compilación (1)

Ejemplos

Los programas que usan los apuntes y la clase.

bench.hDescargar bench.h
// Medidor de tiempos del curso. Compilar con -O2 y sin sanitizers.
#ifndef BENCH_H
#define BENCH_H

#include <algorithm>
#include <chrono>
#include <vector>

// Corre fn `reps` veces y devuelve la mediana en segundos.
template <typename Fn>
double median_seconds(int reps, Fn fn) {
    std::vector<double> times;
    times.reserve(reps);
    for (int i = 0; i < reps; i++) {
        auto start = std::chrono::steady_clock::now();
        fn();
        auto stop = std::chrono::steady_clock::now();
        times.push_back(std::chrono::duration<double>(stop - start).count());
    }
    std::sort(times.begin(), times.end());
    return times[times.size() / 2];
}

// Evita que el compilador borre un cálculo cuyo resultado no se usa.
template <typename T>
void keep(const T& value) {
    volatile T sink = value;
    (void)sink;
}

#endif
build_counts.cppDescargar build_counts.cpp
// Construir un min-heap de dos formas, contando comparaciones. Los conteos no dependen de la máquina.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>

#include "heapsort.h"
#include "int_heap.h"

namespace {

std::vector<int> make_input(const std::string& order, int n) {
    std::vector<int> items(n);
    std::iota(items.begin(), items.end(), 0);
    if (order == "desc") {
        std::reverse(items.begin(), items.end());
    } else if (order == "azar") {
        std::mt19937 generator(13);
        std::shuffle(items.begin(), items.end(), generator);
    }
    return items;
}

long count_pushes(const std::vector<int>& items) {
    IntHeap heap;
    for (int item : items) {
        heap.push(item);
    }
    heap.check_invariants();
    return heap.comparisons();
}

// build_max_heap con el orden al revés da un min-heap: la raíz es la menor.
long count_bottom_up(std::vector<int> items, std::size_t* levels = nullptr) {
    long comparisons = 0;
    std::size_t total = build_max_heap(items, [&](int a, int b) {
        comparisons++;
        return a > b;
    });
    if (levels != nullptr) {
        *levels = total;
    }
    return comparisons;
}

// Cuántos nodos de un árbol perfecto tienen altura h, y cuánto pueden bajar en total.
void print_levels(int k) {
    int n = (1 << (k + 1)) - 1;
    std::cout << "\nArbol perfecto con n = " << n << ", entrada descendente\n";
    std::cout << " altura  nodos  bajan como maximo\n";
    long total = 0;
    for (int h = 0; h <= k; h++) {
        long nodes = 1L << (k - h);
        total += nodes * h;
        std::cout << std::setw(7) << h << std::setw(7) << nodes << std::setw(19) << nodes * h << "\n";
    }
    std::size_t real = 0;
    count_bottom_up(make_input("desc", n), &real);
    std::cout << "suma: " << total << ", niveles que bajaron de verdad: " << real << "\n";
}

}  // namespace

int main() {
    std::cout << "        n  orden  abajo-arriba  /n    n pushes  /n  log2n\n";
    for (int n : {1023, 16383, 262143, 1048575}) {
        for (const std::string order : {"asc", "azar", "desc"}) {
            std::vector<int> items = make_input(order, n);
            long bottom_up = count_bottom_up(items);
            long pushes = count_pushes(items);
            std::cout << std::setw(9) << n << std::setw(7) << order << std::setw(14) << bottom_up
                      << std::fixed << std::setprecision(2) << std::setw(6) << double(bottom_up) / n
                      << std::setw(10) << pushes << std::setw(6) << double(pushes) / n
                      << std::setprecision(1) << std::setw(5) << std::log2(n) << "\n";
        }
    }
    print_levels(9);
    return 0;
}
heap_demo.cppDescargar heap_demo.cpp
// Sigue un min-heap paso a paso: el arreglo después de cada operación.
#include <iostream>

#include "int_heap.h"

namespace {

void print(const char* label, int value, const IntHeap& heap) {
    std::cout << label << " " << value << ": [";
    const char* separator = "";
    for (int item : heap.items()) {
        std::cout << separator << item;
        separator = " ";
    }
    std::cout << "]\n";
}

}  // namespace

int main() {
    IntHeap heap;
    for (int value : {5, 3, 8, 1, 4}) {
        heap.push(value);
        heap.check_invariants();
        print("push", value, heap);
    }
    int least = heap.top();
    heap.pop();
    heap.check_invariants();
    print("pop ", least, heap);
    return 0;
}
heapsort_cache.cppDescargar heapsort_cache.cpp
// ¿Cuánto salta cada algoritmo? Anota cada lectura y escritura del arreglo, y simula con ellas una
// cache pequeña: 32 KB, líneas de 64 bytes, cada línea en un único lugar posible (correspondencia directa).
// Es un conteo sobre un modelo, no una medición de la cache de esta máquina.
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>

namespace {

class CacheModel {
public:
    CacheModel() : tags_(lines_in_cache, -1) {}

    // position es el número del int dentro de la memoria simulada.
    void touch(std::size_t position) {
        std::int64_t line = static_cast<std::int64_t>(position / ints_per_line);
        std::int64_t& tag = tags_[static_cast<std::size_t>(line) % lines_in_cache];
        accesses_++;
        if (tag != line) {
            misses_++;
            tag = line;
        }
    }
    long accesses() const { return accesses_; }
    long misses() const { return misses_; }

private:
    static constexpr std::size_t ints_per_line = 64 / sizeof(int);
    static constexpr std::size_t lines_in_cache = 32 * 1024 / 64;
    std::vector<std::int64_t> tags_;
    long accesses_ = 0;
    long misses_ = 0;
};

void sift_down(std::vector<int>& items, std::size_t i, std::size_t n, CacheModel& cache) {
    while (true) {
        std::size_t left = 2 * i + 1;
        std::size_t right = 2 * i + 2;
        std::size_t largest = i;
        cache.touch(i);
        if (left < n) {
            cache.touch(left);
            if (items[largest] < items[left]) {
                largest = left;
            }
        }
        if (right < n) {
            cache.touch(right);
            if (items[largest] < items[right]) {
                largest = right;
            }
        }
        if (largest == i) {
            return;
        }
        std::swap(items[i], items[largest]);
        i = largest;
    }
}

void modeled_heapsort(std::vector<int>& items, CacheModel& cache) {
    for (std::size_t i = items.size() / 2; i > 0; i--) {
        sift_down(items, i - 1, items.size(), cache);
    }
    for (std::size_t end = items.size(); end > 1; end--) {
        cache.touch(0);
        cache.touch(end - 1);
        std::swap(items[0], items[end - 1]);
        sift_down(items, 0, end - 1, cache);
    }
}

// Mergesort de abajo hacia arriba. El arreglo auxiliar va después del original en la memoria simulada.
void modeled_mergesort(std::vector<int>& items, CacheModel& cache) {
    const std::size_t n = items.size();
    std::vector<int> buffer(n);
    std::size_t source_at = 0;
    std::size_t target_at = n;
    for (std::size_t width = 1; width < n; width *= 2) {
        for (std::size_t low = 0; low < n; low += 2 * width) {
            std::size_t mid = std::min(low + width, n);
            std::size_t high = std::min(low + 2 * width, n);
            std::size_t a = low;
            std::size_t b = mid;
            std::size_t out = low;
            while (a < mid || b < high) {
                bool take_b;
                if (a < mid && b < high) {
                    cache.touch(source_at + a);
                    cache.touch(source_at + b);
                    take_b = items[b] < items[a];
                } else {
                    take_b = a == mid;
                    cache.touch(source_at + (take_b ? b : a));
                }
                cache.touch(target_at + out);
                buffer[out++] = take_b ? items[b++] : items[a++];
            }
        }
        items.swap(buffer);
        std::swap(source_at, target_at);
    }
}

}  // namespace

int main() {
    std::cout << "        n  algoritmo     accesos       fallos  fallos/n\n";
    for (int n : {1000, 10000, 100000, 1000000, 4000000}) {
        std::vector<int> input(n);
        std::iota(input.begin(), input.end(), 0);
        std::mt19937 generator(13);
        std::shuffle(input.begin(), input.end(), generator);

        std::vector<int> items = input;
        CacheModel heap_cache;
        modeled_heapsort(items, heap_cache);
        if (!std::is_sorted(items.begin(), items.end())) {
            return 1;
        }
        items = input;
        CacheModel merge_cache;
        modeled_mergesort(items, merge_cache);
        if (!std::is_sorted(items.begin(), items.end())) {
            return 1;
        }
        std::cout << std::fixed << std::setprecision(2);
        std::cout << std::setw(9) << n << "   heapsort" << std::setw(12) << heap_cache.accesses() << std::setw(13)
                  << heap_cache.misses() << std::setw(10) << double(heap_cache.misses()) / n << "\n";
        std::cout << std::setw(9) << n << "  mergesort" << std::setw(12) << merge_cache.accesses() << std::setw(13)
                  << merge_cache.misses() << std::setw(10) << double(merge_cache.misses()) / n << "\n";
    }
    return 0;
}
heapsort_counts.cppDescargar heapsort_counts.cpp
// Comparaciones de heapsort según la entrada. Los conteos no dependen de la máquina.
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>

#include "heapsort.h"

namespace {

std::vector<int> make_input(const std::string& kind, int n) {
    std::vector<int> items(n, 7);
    if (kind == "iguales") {
        return items;
    }
    std::iota(items.begin(), items.end(), 0);
    if (kind == "desc") {
        std::reverse(items.begin(), items.end());
    } else if (kind == "azar") {
        std::mt19937 generator(13);
        std::shuffle(items.begin(), items.end(), generator);
    }
    return items;
}

}  // namespace

int main() {
    std::cout << "        n  entrada  comparaciones   /n  /(n log2 n)\n";
    for (int n : {1000, 10000, 100000, 1000000}) {
        for (const std::string kind : {"azar", "asc", "desc", "iguales"}) {
            std::vector<int> items = make_input(kind, n);
            long comparisons = 0;
            heapsort(items, [&](int a, int b) {
                comparisons++;
                return a < b;
            });
            if (!std::is_sorted(items.begin(), items.end())) {
                std::cout << "no quedo ordenado\n";
                return 1;
            }
            std::cout << std::setw(9) << n << std::setw(9) << kind << std::setw(15) << comparisons
                      << std::fixed << std::setprecision(2) << std::setw(7) << double(comparisons) / n
                      << std::setw(13) << double(comparisons) / (n * std::log2(n)) << "\n";
        }
    }
    return 0;
}
heapsort_demo.cppDescargar heapsort_demo.cpp
// Heapsort paso a paso, y un caso que muestra que no es estable.
#include <cstddef>
#include <iostream>
#include <string>
#include <utility>
#include <vector>

#include "heapsort.h"

namespace {

using Item = std::pair<int, char>;

void print_range(const std::vector<int>& items, std::size_t end) {
    std::cout << "[";
    for (std::size_t i = 0; i < items.size(); i++) {
        if (i == end) {
            std::cout << " | ";
        } else if (i > 0) {
            std::cout << " ";
        }
        std::cout << items[i];
    }
    if (end == items.size()) {
        std::cout << " |";
    }
    std::cout << "]\n";
}

std::string text(const std::vector<Item>& items) {
    std::string out;
    for (const Item& item : items) {
        out += std::to_string(item.first) + std::string(1, item.second) + " ";
    }
    return out;
}

}  // namespace

int main() {
    auto ascending = [](int a, int b) { return a < b; };
    std::vector<int> items = {4, 1, 7, 3, 9, 2};
    std::cout << "entrada:   ";
    print_range(items, items.size());
    build_max_heap(items, ascending);
    std::cout << "heap:      ";
    print_range(items, items.size());
    for (std::size_t end = items.size(); end > 1; end--) {
        std::swap(items[0], items[end - 1]);
        sift_down_max(items, 0, end - 1, ascending);
        std::cout << "saca " << items[end - 1] << ":    ";
        print_range(items, end - 1);
    }

    // Tres claves: los dos 2 salen en el orden contrario al de entrada.
    std::vector<Item> pairs = {{2, 'a'}, {2, 'b'}, {1, 'c'}};
    std::cout << "\nentrada: " << text(pairs) << "\n";
    heapsort(pairs, [](const Item& a, const Item& b) { return a.first < b.first; });
    std::cout << "salida:  " << text(pairs) << "\n";
    return 0;
}
heapsort_times.cppDescargar heapsort_times.cpp
// Mide heapsort frente a las ordenaciones de la biblioteca. Compilar con -O2 y sin sanitizers.
// Los tiempos dependen de la máquina.
#include <algorithm>
#include <functional>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <vector>

#include "bench.h"
#include "heapsort.h"

namespace {

template <typename Sorter>
double time_sort(const std::vector<int>& input, Sorter sorter) {
    return median_seconds(5, [&] {
        std::vector<int> items = input;
        sorter(items);
        keep(items[items.size() / 2]);
    });
}

}  // namespace

int main() {
    std::cout << "        n   heapsort  std::sort  stable_sort  heap/sort\n";
    for (int n : {100000, 200000, 400000, 800000, 1600000, 3200000}) {
        std::vector<int> input(n);
        std::iota(input.begin(), input.end(), 0);
        std::mt19937 generator(13);
        std::shuffle(input.begin(), input.end(), generator);
        double heap = time_sort(input, [](std::vector<int>& items) { heapsort(items, std::less<int>()); });
        double quick = time_sort(input, [](std::vector<int>& items) { std::sort(items.begin(), items.end()); });
        double stable =
            time_sort(input, [](std::vector<int>& items) { std::stable_sort(items.begin(), items.end()); });
        std::cout << std::fixed << std::setprecision(4) << std::setw(9) << n << std::setw(11) << heap
                  << std::setw(11) << quick << std::setw(13) << stable << std::setprecision(2)
                  << std::setw(11) << heap / quick << "\n";
    }
    return 0;
}
heapsort.hDescargar heapsort.h
// Heapsort sobre un std::vector, en el lugar. Usa un heap con el mayor en la raíz.
#ifndef HEAPSORT_H
#define HEAPSORT_H

#include <cstddef>
#include <utility>
#include <vector>

// Baja el elemento de i dentro de items[0, n). less(a, b): a va antes que b en el orden final.
// Devuelve cuántos niveles bajó.
template <typename T, typename Less>
std::size_t sift_down_max(std::vector<T>& items, std::size_t i, std::size_t n, Less less) {
    std::size_t levels = 0;
    while (true) {
        std::size_t left = 2 * i + 1;
        std::size_t right = 2 * i + 2;
        std::size_t largest = i;
        if (left < n && less(items[largest], items[left])) {
            largest = left;
        }
        if (right < n && less(items[largest], items[right])) {
            largest = right;
        }
        if (largest == i) {
            return levels;
        }
        std::swap(items[i], items[largest]);
        i = largest;
        levels++;
    }
}

// Construye de abajo hacia arriba: las hojas ya son heaps, y se empieza por el último padre.
// El índice baja hasta 0 sin pasar por debajo: size_t no tiene negativos.
// Devuelve el total de niveles que bajaron los elementos.
template <typename T, typename Less>
std::size_t build_max_heap(std::vector<T>& items, Less less) {
    std::size_t total = 0;
    for (std::size_t i = items.size() / 2; i > 0; i--) {
        total += sift_down_max(items, i - 1, items.size(), less);
    }
    return total;
}

template <typename T, typename Less>
void heapsort(std::vector<T>& items, Less less) {
    build_max_heap(items, less);
    for (std::size_t end = items.size(); end > 1; end--) {
        // El mayor de items[0, end) pasa a su lugar definitivo.
        std::swap(items[0], items[end - 1]);
        sift_down_max(items, 0, end - 1, less);
    }
}

#endif
huffman_demo.cppDescargar huffman_demo.cpp
// Huffman sobre una tabla pequeña. Cada árbol se guarda como su lista de códigos: al unir dos árboles,
// los códigos del primero ganan un 0 adelante y los del segundo un 1.
#include <cstddef>
#include <cstdint>
#include <iostream>
#include <queue>
#include <string>
#include <utility>
#include <vector>

namespace {

struct Symbol {
    char letter;
    long frequency;
};

struct Partial {
    long weight;
    int order;  // desempata: primero el creado antes
    std::vector<std::pair<char, std::string>> codes;
    std::string name;
};

struct Heavier {
    bool operator()(const Partial& a, const Partial& b) const {
        if (a.weight != b.weight) {
            return a.weight > b.weight;
        }
        return a.order > b.order;
    }
};

void run(const std::vector<Symbol>& symbols) {
    std::priority_queue<Partial, std::vector<Partial>, Heavier> heap;
    int order = 0;
    long total_symbols = 0;
    for (const Symbol& symbol : symbols) {
        heap.push({symbol.frequency, order++, {{symbol.letter, ""}}, std::string(1, symbol.letter)});
        total_symbols += symbol.frequency;
    }
    long merged_total = 0;
    while (heap.size() > 1) {
        Partial first = heap.top();
        heap.pop();
        Partial second = heap.top();
        heap.pop();
        std::cout << "une " << first.name << ":" << first.weight << " con " << second.name << ":" << second.weight
                  << " -> " << first.weight + second.weight << "\n";
        Partial joined{first.weight + second.weight, order++, {}, "(" + first.name + second.name + ")"};
        for (auto& [letter, code] : first.codes) {
            joined.codes.push_back({letter, "0" + code});
        }
        for (auto& [letter, code] : second.codes) {
            joined.codes.push_back({letter, "1" + code});
        }
        merged_total += joined.weight;
        heap.push(joined);
    }
    long bits = 0;
    const Partial& root = heap.top();
    for (const Symbol& symbol : symbols) {
        for (const auto& [letter, code] : root.codes) {
            if (letter == symbol.letter) {
                std::cout << symbol.letter << " (" << symbol.frequency << "): " << code << "\n";
                bits += symbol.frequency * static_cast<long>(code.size());
            }
        }
    }
    std::size_t fixed_width = 0;
    while ((std::size_t{1} << fixed_width) < symbols.size()) {
        fixed_width++;
    }
    std::cout << "bits: " << bits << ", suma de los pesos unidos: " << merged_total
              << ", con codigo fijo de " << fixed_width << " bits: " << total_symbols * static_cast<long>(fixed_width)
              << "\n";
}

}  // namespace

int main() {
    run({{'a', 5}, {'b', 2}, {'c', 1}, {'d', 1}});
    std::cout << "\n";
    run({{'a', 45}, {'b', 13}, {'c', 12}, {'d', 16}, {'e', 9}, {'f', 5}});
    std::cout << "\n";
    run({{'a', 1}, {'b', 1}, {'c', 1}, {'d', 1}});
    return 0;
}
int_heap.hDescargar int_heap.h
// Min-heap de enteros sobre un arreglo. Es el heap de la clase: cuenta sus comparaciones.
#ifndef INT_HEAP_H
#define INT_HEAP_H

#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>

// Invariante: para todo i > 0, items_[(i - 1) / 2] <= items_[i].
// El árbol es completo porque el arreglo no tiene huecos: el nivel de abajo se llena de izquierda
// a derecha, y no hay punteros que guardar.
class IntHeap {
public:
    void push(int value) {
        items_.push_back(value);
        sift_up(items_.size() - 1);
    }

    // Requiere que no esté vacío.
    void pop() {
        assert(!items_.empty());
        items_[0] = items_.back();
        items_.pop_back();
        if (!items_.empty()) {
            sift_down(0);
        }
    }

    // Requiere que no esté vacío.
    int top() const {
        assert(!items_.empty());
        return items_[0];
    }

    bool empty() const { return items_.empty(); }
    std::size_t size() const { return items_.size(); }
    const std::vector<int>& items() const { return items_; }
    long comparisons() const { return comparisons_; }

    void check_invariants() const {
        for (std::size_t i = 1; i < items_.size(); i++) {
            assert(items_[(i - 1) / 2] <= items_[i]);
        }
    }

private:
    // Sube el elemento de i mientras sea menor que su padre.
    void sift_up(std::size_t i) {
        while (i > 0) {
            std::size_t parent = (i - 1) / 2;
            comparisons_++;
            if (items_[parent] <= items_[i]) {
                return;
            }
            std::swap(items_[parent], items_[i]);
            i = parent;
        }
    }

    // Baja el elemento de i mientras algún hijo sea menor. Se cambia con el menor de los dos.
    void sift_down(std::size_t i) {
        while (true) {
            std::size_t left = 2 * i + 1;
            std::size_t right = 2 * i + 2;
            std::size_t smallest = i;
            if (left < items_.size()) {
                comparisons_++;
                if (items_[left] < items_[smallest]) {
                    smallest = left;
                }
            }
            if (right < items_.size()) {
                comparisons_++;
                if (items_[right] < items_[smallest]) {
                    smallest = right;
                }
            }
            if (smallest == i) {
                return;
            }
            std::swap(items_[i], items_[smallest]);
            i = smallest;
        }
    }

    std::vector<int> items_;
    long comparisons_ = 0;
};

#endif
pq_demo.cppDescargar pq_demo.cpp
// std::priority_queue y los algoritmos de heap: el convenio de la biblioteca, y sus cotas contadas.
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <functional>
#include <iostream>
#include <queue>
#include <random>
#include <vector>

namespace {

template <typename Queue>
void drain(const char* label, Queue queue) {
    std::cout << label;
    while (!queue.empty()) {
        std::cout << " " << queue.top();
        queue.pop();
    }
    std::cout << "\n";
}

}  // namespace

int main() {
    std::vector<int> values = {5, 3, 8, 1, 4};

    // Por defecto usa std::less, y deja en top() al mayor.
    std::priority_queue<int> largest_first(std::less<int>(), values);
    drain("std::less:   ", largest_first);

    // Con std::greater, deja en top() al menor.
    std::priority_queue<int, std::vector<int>, std::greater<int>> smallest_first(std::greater<int>(), values);
    drain("std::greater:", smallest_first);

    // Se cuentan las comparaciones de los algoritmos sobre un arreglo.
    const int n = 100000;
    std::vector<int> items(n);
    std::mt19937 generator(13);
    for (int& item : items) {
        item = static_cast<int>(generator() >> 8);
    }
    long count = 0;
    auto counted = [&count](int a, int b) {
        count++;
        return a < b;
    };
    std::make_heap(items.begin(), items.end(), counted);
    std::cout << "make_heap: " << count << " comparaciones, cota 3n = " << 3 * n << "\n";

    int log_n = static_cast<int>(std::log2(n));
    int twice_log_n = static_cast<int>(2 * std::log2(n));
    long worst_pop = 0;
    long worst_push = 0;
    std::vector<int> heap = items;
    for (int i = 0; i < 1000; i++) {
        count = 0;
        std::pop_heap(heap.begin(), heap.end(), counted);
        worst_pop = std::max(worst_pop, count);
        // Un valor mayor que todos los demás sube hasta la raíz: el peor caso de push_heap.
        heap.back() = (1 << 24) + i;
        count = 0;
        std::push_heap(heap.begin(), heap.end(), counted);
        worst_push = std::max(worst_push, count);
    }
    std::cout << "pop_heap, peor de 1000: " << worst_pop << ", cota 2 log2 n = " << twice_log_n << "\n";
    std::cout << "push_heap, peor de 1000: " << worst_push << ", cota log2 n = " << log_n << "\n";
    return 0;
}
top_k_bench.cppDescargar top_k_bench.cpp
// Los k mayores de n números: heap de tamaño k frente a ordenar todo.
// Cuenta comparaciones y mide tiempos, con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <algorithm>
#include <cstddef>
#include <functional>
#include <iomanip>
#include <iostream>
#include <queue>
#include <random>
#include <vector>

#include "bench.h"

namespace {

// Un min-heap de tamaño k: en la raíz está el peor de los k mejores hasta ahora.
template <typename Greater>
std::vector<int> k_largest(const std::vector<int>& items, std::size_t k, Greater greater) {
    std::priority_queue<int, std::vector<int>, Greater> heap(greater);
    for (int item : items) {
        if (heap.size() < k) {
            heap.push(item);
        } else if (k > 0 && greater(item, heap.top())) {
            heap.pop();
            heap.push(item);
        }
    }
    std::vector<int> result;
    while (!heap.empty()) {
        result.push_back(heap.top());
        heap.pop();
    }
    std::reverse(result.begin(), result.end());
    return result;
}

template <typename Greater>
std::vector<int> by_sorting(std::vector<int> items, std::size_t k, Greater greater) {
    std::sort(items.begin(), items.end(), greater);
    items.resize(std::min(k, items.size()));
    return items;
}

}  // namespace

int main() {
    const std::size_t n = 2000000;
    std::vector<int> items(n);
    std::mt19937 generator(13);
    for (int& item : items) {
        item = static_cast<int>(generator() >> 1);
    }

    std::cout << "n = " << n << "\n";
    std::cout << "       k  comp. heap  comp. sort  t. heap  t. sort\n";
    for (std::size_t k : {std::size_t{10}, std::size_t{1000}, std::size_t{100000}, n}) {
        long heap_count = 0;
        long sort_count = 0;
        auto count_heap = [&](int a, int b) {
            heap_count++;
            return a > b;
        };
        auto count_sort = [&](int a, int b) {
            sort_count++;
            return a > b;
        };
        std::vector<int> from_heap = k_largest(items, k, count_heap);
        std::vector<int> from_sort = by_sorting(items, k, count_sort);
        if (from_heap != from_sort) {
            std::cout << "no coinciden\n";
            return 1;
        }
        double heap_time = median_seconds(3, [&] { keep(k_largest(items, k, std::greater<int>()).size()); });
        double sort_time = median_seconds(3, [&] { keep(by_sorting(items, k, std::greater<int>()).size()); });
        std::cout << std::setw(8) << k << std::setw(12) << heap_count << std::setw(12) << sort_count
                  << std::fixed << std::setprecision(4) << std::setw(9) << heap_time << std::setw(9)
                  << sort_time << "\n";
    }
    return 0;
}
where_is_max.cppDescargar where_is_max.cpp
// ¿Dónde puede estar el máximo de un min-heap? Revisa todas las permutaciones de 7 claves.
#include <algorithm>
#include <array>
#include <cstddef>
#include <iostream>
#include <numeric>
#include <vector>

namespace {

bool is_min_heap(const std::vector<int>& items) {
    for (std::size_t i = 1; i < items.size(); i++) {
        if (items[(i - 1) / 2] > items[i]) {
            return false;
        }
    }
    return true;
}

}  // namespace

int main() {
    std::vector<int> items(7);
    std::iota(items.begin(), items.end(), 1);
    std::array<int, 7> least_at{};
    std::array<int, 7> greatest_at{};
    int total = 0;
    int heaps = 0;
    do {
        total++;
        if (is_min_heap(items)) {
            heaps++;
            least_at[std::min_element(items.begin(), items.end()) - items.begin()]++;
            greatest_at[std::max_element(items.begin(), items.end()) - items.begin()]++;
        }
    } while (std::next_permutation(items.begin(), items.end()));

    std::cout << "permutaciones: " << total << ", son heaps: " << heaps << "\n";
    std::cout << "indice  minimo  maximo\n";
    for (std::size_t i = 0; i < items.size(); i++) {
        std::cout << "     " << i << "  " << least_at[i] << "      " << greatest_at[i] << "\n";
    }
    // Con claves repetidas, un nodo interno puede valer lo mismo que el máximo.
    std::vector<int> repeated = {1, 2, 2, 2};
    std::cout << "[1 2 2 2] es heap: " << (is_min_heap(repeated) ? "si" : "no")
              << ", y el indice 1 vale el maximo y tiene un hijo\n";
    return 0;
}

Errores

Ejemplos con errores a propósito: no compilan, fallan o dan un resultado incorrecto. Los apuntes explican cada uno.

errors/heap_as_bst.cppDescargar errors/heap_as_bst.cpp
// Error a propósito: buscar en un heap como si fuera un árbol de búsqueda.
#include <cstddef>
#include <iostream>
#include <vector>

namespace {

// Baja a la izquierda si la clave es menor, y a la derecha si es mayor. En un heap, eso no sirve.
bool looks_like_bst_search(const std::vector<int>& heap, int key) {
    std::size_t i = 0;
    while (i < heap.size()) {
        if (heap[i] == key) {
            return true;
        }
        i = key < heap[i] ? 2 * i + 1 : 2 * i + 2;
    }
    return false;
}

}  // namespace

int main() {
    std::vector<int> heap = {1, 3, 8, 5, 4};
    for (int key : heap) {
        std::cout << "busca " << key << ": " << (looks_like_bst_search(heap, key) ? "esta" : "NO ESTA") << "\n";
    }
    return 0;
}
errors/left_child_only.cppDescargar errors/left_child_only.cpp
// Error a propósito: al bajar, cambiar siempre con el hijo izquierdo, sin mirar cuál es el menor.
#include <cstddef>
#include <iostream>
#include <utility>
#include <vector>

namespace {

void sift_down_wrong(std::vector<int>& heap, std::size_t i) {
    while (2 * i + 1 < heap.size() && heap[2 * i + 1] < heap[i]) {
        std::swap(heap[i], heap[2 * i + 1]);
        i = 2 * i + 1;
    }
}

bool is_min_heap(const std::vector<int>& heap) {
    for (std::size_t i = 1; i < heap.size(); i++) {
        if (heap[(i - 1) / 2] > heap[i]) {
            return false;
        }
    }
    return true;
}

}  // namespace

int main() {
    std::vector<int> heap = {1, 4, 2, 5, 6, 3};
    std::cout << "antes de sacar: es heap: " << (is_min_heap(heap) ? "si" : "no") << "\n";
    heap[0] = heap.back();
    heap.pop_back();
    sift_down_wrong(heap, 0);
    std::cout << "arreglo:";
    for (int item : heap) {
        std::cout << " " << item;
    }
    std::cout << "\nraiz: " << heap[0] << ", es heap: " << (is_min_heap(heap) ? "si" : "no") << "\n";
    return 0;
}
errors/one_leaf.cppDescargar errors/one_leaf.cpp
// Error a propósito: un decodificador que lee un bit antes de mirar si ya está en una hoja.
// Con un árbol de una sola hoja, que es el de un archivo con un solo símbolo, baja a un hijo que no existe.
#include <iostream>
#include <vector>

namespace {

struct Node {
    int symbol;
    int left;
    int right;
};

char decode_one(const std::vector<Node>& nodes, int root, bool bit) {
    int node = root;
    do {
        node = bit ? nodes[node].right : nodes[node].left;
    } while (nodes[node].left >= 0);
    return static_cast<char>(nodes[node].symbol);
}

}  // namespace

int main() {
    std::vector<Node> nodes = {{'a', -1, -1}};
    std::cout << decode_one(nodes, 0, false) << "\n";
    return 0;
}
errors/padding_as_symbols.cppDescargar errors/padding_as_symbols.cpp
// Error a propósito: descomprimir hasta que se acaben los bits, sin mirar el tamaño original.
#include <iostream>
#include <string>

namespace {

// a = 1, b = 0: un árbol con dos hojas. Decodifica cada bit de una cadena.
std::string decode(const std::string& bits) {
    std::string out;
    for (char bit : bits) {
        out += bit == '1' ? 'a' : 'b';
    }
    return out;
}

}  // namespace

int main() {
    // El texto "aab" da los bits 110. El último byte se rellena con ceros hasta 8 bits.
    std::string bits = "110";
    std::string byte = bits + std::string(8 - bits.size(), '0');
    std::cout << "original: aab\n";
    std::cout << "bits en el archivo: " << byte << "\n";
    std::cout << "sin mirar el tamano: " << decode(byte) << "\n";
    std::cout << "con tamano 3: " << decode(byte.substr(0, 3)) << "\n";
    return 0;
}
errors/signed_byte.cppDescargar errors/signed_byte.cpp
// Error a propósito: usar un char como índice de la tabla de frecuencias.
// Se compila con -fsigned-char para que pase lo mismo en toda arquitectura.
#include <array>
#include <cstdio>
#include <string>

int main() {
    std::string text = "ñ";  // en UTF-8 son dos bytes: 0xC3 y 0xB1
    std::array<long, 256> counts{};
    for (char c : text) {
        counts.data()[c]++;
    }
    std::printf("contó %ld\n", counts[0xC3]);
    return 0;
}
errors/stop_at_ff.cppDescargar errors/stop_at_ff.cpp
// Error a propósito: dos formas de leer un archivo binario que pierden bytes.
// Se compila con -fsigned-char: con char sin signo, el primer ciclo no termina nunca.
#include <cstdio>
#include <fstream>
#include <iostream>
#include <string>

int main() {
    const std::string path = "build/stop_at_ff.bin";
    {
        std::ofstream out(path, std::ios::binary);
        out << 'a' << ' ' << 'b' << '\xFF' << 'c' << '\n' << 'd';
    }
    {
        std::ifstream in(path, std::ios::binary);
        int count = 0;
        char c;
        // get() devuelve int, para que EOF no se confunda con un byte. Guardarlo en char los confunde.
        while ((c = static_cast<char>(in.get())) != EOF) {
            count++;
        }
        std::cout << "con char y EOF: " << count << " bytes de 7\n";
    }
    {
        std::ifstream in(path, std::ios::binary);
        int count = 0;
        char c;
        // >> se salta los espacios en blanco, también en modo binario.
        while (in >> c) {
            count++;
        }
        std::cout << "con >>:         " << count << " bytes de 7\n";
    }
    {
        std::ifstream in(path, std::ios::binary);
        int count = 0;
        int c;
        while ((c = in.get()) != EOF) {
            count++;
        }
        std::cout << "con int y EOF:  " << count << " bytes de 7\n";
    }
    std::remove(path.c_str());
    return 0;
}

Pruebas

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

test_examples.pyDescargar test_examples.py
"""Corre los ejemplos de la semana y comprueba lo que se puede contar: nunca tiempos."""
import filecmp
import math
import pathlib
import subprocess
import unittest

HERE = pathlib.Path(__file__).parent


def run(*command):
    return subprocess.run(
        list(command), cwd=HERE, text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=300,
    )


class Examples(unittest.TestCase):
    def test_structure_tests_pass(self):
        done = run("build/test_structures")
        self.assertEqual(done.returncode, 0, done.stdout)

    def test_programs_exit_cleanly(self):
        for name in ["heap_demo", "where_is_max", "heapsort_demo", "pq_demo", "huffman_demo"]:
            with self.subTest(program=name):
                done = run(f"build/{name}")
                self.assertEqual(done.returncode, 0, done.stdout)

    def test_questions_one_and_two(self):
        lines = run("build/heap_demo").stdout.splitlines()
        self.assertEqual(lines[4], "push 4: [1 3 8 5 4]")
        self.assertEqual(lines[5], "pop  1: [3 4 8 5]")

    def test_question_four(self):
        lines = run("build/huffman_demo").stdout.splitlines()
        self.assertEqual(lines[3:8], [
            "a (5): 1", "b (2): 00", "c (1): 010", "d (1): 011",
            "bits: 15, suma de los pesos unidos: 15, con codigo fijo de 2 bits: 18",
        ])

    def test_greatest_is_always_a_leaf_with_distinct_keys(self):
        lines = run("build/where_is_max").stdout.splitlines()
        self.assertEqual(lines[0], "permutaciones: 5040, son heaps: 80")
        rows = [line.split() for line in lines[2:9]]
        self.assertEqual([int(row[1]) for row in rows], [80, 0, 0, 0, 0, 0, 0])
        self.assertEqual([int(row[2]) for row in rows[:3]], [0, 0, 0])
        self.assertEqual(sum(int(row[2]) for row in rows), 80)

    def test_build_counts(self):
        lines = run("build/build_counts").stdout.splitlines()
        for row in lines[1:13]:
            fields = row.split()
            n, order, bottom_up, pushes = int(fields[0]), fields[1], int(fields[2]), int(fields[4])
            self.assertLessEqual(bottom_up, 2 * n)
            if order == "asc":
                self.assertEqual(pushes, n - 1)
            if order == "azar":
                self.assertLess(pushes, 3 * n)
            if order == "desc" and n > 1000:
                self.assertGreater(pushes, 0.7 * n * math.log2(n))
        self.assertEqual(lines[-1], "suma: 1013, niveles que bajaron de verdad: 1013")

    def test_heapsort_counts(self):
        lines = run("build/heapsort_counts").stdout.splitlines()
        for row in lines[1:]:
            fields = row.split()
            n, kind, comparisons = int(fields[0]), fields[1], int(fields[2])
            self.assertLessEqual(comparisons, 2 * n * math.log2(n))
            if kind == "iguales":
                self.assertLessEqual(comparisons, 3 * n)

    def test_heapsort_is_not_stable(self):
        lines = run("build/heapsort_demo").stdout.splitlines()
        self.assertEqual(lines[-1].strip(), "salida:  1c 2b 2a".strip())

    def test_cache_model(self):
        rows = [line.split() for line in run("build/heapsort_cache").stdout.splitlines()[1:]]
        by_size = {(int(row[0]), row[1]): int(row[3]) for row in rows}
        self.assertGreater(by_size[(4000000, "heapsort")], 2 * by_size[(4000000, "mergesort")])

    def test_library_bounds(self):
        lines = run("build/pq_demo").stdout.splitlines()
        self.assertEqual(lines[0].split()[-5:], ["8", "5", "4", "3", "1"])
        self.assertEqual(lines[1].split()[-5:], ["1", "3", "4", "5", "8"])
        self.assertLessEqual(int(lines[2].split()[1]), 300000)
        self.assertLessEqual(int(lines[3].split()[4].rstrip(",")), 32)
        self.assertLessEqual(int(lines[4].split()[4].rstrip(",")), 16)

    def test_top_k_counts(self):
        rows = [line.split() for line in run("build/top_k_bench").stdout.splitlines()[2:]]
        self.assertEqual(len(rows), 4)
        n = 2000000
        for row in rows:
            k, heap_comparisons = int(row[0]), int(row[1])
            self.assertLessEqual(heap_comparisons, n * (1 + 3 * math.log2(k)))
        self.assertLess(int(rows[0][1]), int(rows[0][2]) / 10)


@unittest.skipUnless((HERE / "solution").is_dir(), "la solución no se reparte")
class Solution(unittest.TestCase):
    def test_given_programs_match(self):
        for name in ["compress.cpp", "decompress.cpp"]:
            with self.subTest(file=name):
                self.assertTrue(filecmp.cmp(HERE / "lab" / name, HERE / "solution" / name, shallow=False))

    def test_interface_of_week_15(self):
        """El comentario de semana 15 y las firmas no se pueden mover sin avisar."""
        for folder in ["lab", "solution"]:
            text = (HERE / folder / "min_heap.h").read_text(encoding="utf-8")
            for signature in [
                "template <typename T, typename Less = std::less<T>>",
                "MinHeap() = default;",
                "explicit MinHeap(Less less) : less_(std::move(less)) {}",
                "explicit MinHeap(std::vector<T> items, Less less = Less())",
                "void push(T value) {",
                "void pop() {",
                "const T& top() const {",
                "bool empty() const { return items_.empty(); }",
                "std::size_t size() const { return items_.size(); }",
                "void check_invariants() const",
            ]:
                with self.subTest(folder=folder, signature=signature):
                    self.assertIn(signature, text)


if __name__ == "__main__":
    unittest.main()
test_lab.pyDescargar test_lab.py
"""Pruebas del laboratorio. LAB_DIR dice dónde está el código: lab o solution.

Cada prueba corre en su propio proceso, con sanitizers: una fuga o un acceso inválido
la hace fallar aunque el resultado parezca correcto.
"""
import os
import pathlib
import re
import subprocess
import tempfile
import unittest

HERE = pathlib.Path(__file__).parent
LAB_DIR = os.environ.get("LAB_DIR", "solution")
DATA = os.environ.get("DATA", "../../data")
BUILD = HERE / "build" / LAB_DIR
FIXTURE = HERE / "lab" / "tests" / "fixture.txt"
SAMPLES = HERE / "build" / "samples"
# Un tamaño mal leído de la cabecera hace crecer la salida sin fin: se corta antes de agotar la memoria.
LIMIT_SECONDS = 30
SAMPLE_NAMES = ["empty", "one_byte", "single_symbol", "all_bytes", "random", "special"]
HEAP = [
    "new_is_empty", "guide_example", "pop_example", "pop_chooses_smaller_child", "pops_in_order",
    "one_element", "other_comparator", "comparator_with_capture", "only_uses_comparator",
    "pairs_for_dijkstra", "no_copies_inside", "copy_is_independent", "build_example",
    "build_small_sizes", "build_comparisons", "push_comparisons", "pop_comparisons",
    "random_against_priority_queue", "random_builds_and_strings",
]
BROKEN = ["broken_root", "broken_right_child", "broken_last_leaf"]
TOP = ["top_k_cases", "top_k_random", "top_k_comparisons", "top_k_keeps_few_objects"]
TREE = [
    "count_frequencies", "tree_guide_example", "tree_ties_follow_creation_order",
    "tree_weights_above_32_bits", "tree_empty", "tree_one_symbol", "tree_random_is_optimal",
    "codes_longer_than_a_word", "tree_write_guide_example", "tree_write_and_read_all_symbols",
    "tree_read_one_leaf",
]
COMPRESS = [
    "exact_bytes", "exact_size_in_header", "empty_input", "one_byte_input", "one_symbol_input",
    "two_symbols", "padding_is_not_data", "all_byte_values", "special_bytes", "random_round_trips",
    "reference_file", "rejects_bad_header", "rejects_truncated_file", "rejects_endless_tree",
    "rejects_too_many_leaves",
]


def short_report(text):
    """Deja lo útil de un informe: el tipo de error y las líneas de sus archivos."""
    if "ERROR:" not in text and "runtime error" not in text and "Assertion" not in text:
        return text.strip()
    useful = [line.strip() for line in text.splitlines()
              if "ERROR:" in line or "SUMMARY:" in line or "runtime error" in line or "Assertion" in line
              or "sin implementar" in line
              or re.search(r"(min_heap\.h|top_k\.h|huffman\.h|_tests\.cpp):\d+", line)]
    return "\n".join(useful[:12])


def build(target):
    done = subprocess.run(
        ["make", "--no-print-directory", f"LAB_DIR={LAB_DIR}", f"build/{LAB_DIR}/{target}"],
        cwd=HERE, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
    )
    return done.returncode, done.stdout


def run_case(binary, name):
    return subprocess.run(
        [str(BUILD / binary), name], cwd=HERE, text=True, errors="replace",
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=LIMIT_SECONDS,
    )


def add_cases(cls, binary, names):
    for name in names:
        def case(self, name=name):
            done = run_case(binary, name)
            self.assertEqual(done.returncode, 0, short_report(done.stdout))
        setattr(cls, f"test_{name}", case)
    return cls


def add_broken(cls, names):
    for name in names:
        def case(self, name=name):
            done = run_case("heap_tests", name)
            self.assertIn("sano", done.stdout, "check_invariants falló en un heap sano\n" + short_report(done.stdout))
            self.assertNotEqual(done.returncode, 0, "check_invariants no notó el daño")
            self.assertNotIn("sin implementar", done.stdout)
        setattr(cls, f"test_{name}", case)


class LabCase(unittest.TestCase):
    TARGETS = []

    @classmethod
    def setUpClass(cls):
        for target in cls.TARGETS:
            code, output = build(target)
            if code != 0:
                raise AssertionError(output)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class Builds(unittest.TestCase):
    def test_builds_without_warnings(self):
        for target in ["heap_tests", "huffman_tests", "top_words", "compress", "decompress"]:
            (BUILD / target).unlink(missing_ok=True)
            code, output = build(target)
            self.assertEqual(code, 0, output)
            # Solo las advertencias del compilador: make también avisa de otras cosas, como un reloj desfasado.
            self.assertNotRegex(output, r"\.(cpp|h):\d+:\d+: warning")


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestHeap(LabCase):
    """Sesión 1, paso 1: MinHeap con comparador, contra std::priority_queue."""
    TARGETS = ["heap_tests"]


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestTop(LabCase):
    """Sesión 1, paso 3: top_k y el programa top_words, sobre un texto pequeño."""
    TARGETS = ["heap_tests", "top_words"]

    def run_program(self, *args):
        return subprocess.run(
            [str(BUILD / "top_words"), *args], cwd=HERE, text=True, encoding="utf-8",
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=60,
        )

    def lines(self, *args):
        done = self.run_program(*args)
        self.assertEqual(done.returncode, 0, short_report(done.stderr))
        return done.stdout.splitlines()

    def test_top_three_with_ties_in_byte_order(self):
        self.assertEqual(self.lines(str(FIXTURE), "3"), ["4 árbol", "3 el", "3 y"])

    def test_all_words(self):
        self.assertEqual(self.lines(str(FIXTURE), "100"), [
            "4 árbol", "3 el", "3 y", "2 ana", "2 llegó", "2 zorra", "2 ñandú",
            "1 de", "1 después", "1 la", "1 no", "1 quién", "1 una", "1 zorras",
        ])

    def test_k_zero_prints_nothing(self):
        self.assertEqual(self.lines(str(FIXTURE), "0"), [])

    def test_bad_usage(self):
        self.assertNotEqual(self.run_program().returncode, 0)
        self.assertNotEqual(self.run_program("/no/existe.txt", "3").returncode, 0)


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestTree(LabCase):
    """Sesión 2, pasos 4 y 5: frecuencias, árbol, códigos, escribir y leer el árbol."""
    TARGETS = ["huffman_tests"]


@unittest.skipUnless((HERE / LAB_DIR).is_dir(), f"no existe la carpeta {LAB_DIR}")
class TestCompress(LabCase):
    """Sesión 2, pasos 5 y 6: comprimir, descomprimir y comparar archivos con cmp."""
    TARGETS = ["huffman_tests", "compress", "decompress"]

    def setUp(self):
        self.folder = tempfile.TemporaryDirectory()
        self.addCleanup(self.folder.cleanup)
        self.work = pathlib.Path(self.folder.name)

    def run_tool(self, tool, source, target):
        return subprocess.run(
            [str(BUILD / tool), str(source), str(target)], cwd=HERE, text=True, errors="replace",
            stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=LIMIT_SECONDS,
        )

    def assert_round_trip(self, source):
        packed = self.work / "packed"
        back = self.work / "back"
        done = self.run_tool("compress", source, packed)
        self.assertEqual(done.returncode, 0, short_report(done.stdout))
        done = self.run_tool("decompress", packed, back)
        self.assertEqual(done.returncode, 0, short_report(done.stdout))
        compared = subprocess.run(["cmp", str(source), str(back)], text=True, stdout=subprocess.PIPE,
                                  stderr=subprocess.STDOUT)
        self.assertEqual(compared.returncode, 0, compared.stdout)
        return packed

    def test_round_trip_fixture(self):
        self.assert_round_trip(FIXTURE)

    def test_round_trip_real_text(self):
        source = HERE / DATA / "tradiciones.txt"
        packed = self.assert_round_trip(source)
        self.assertLess(packed.stat().st_size, source.stat().st_size * 0.7)

    def test_empty_file_size(self):
        self.assertEqual(self.assert_round_trip(SAMPLES / "empty.bin").stat().st_size, 12)

    def test_single_symbol_size(self):
        self.assertEqual(self.assert_round_trip(SAMPLES / "single_symbol.bin").stat().st_size, 14)

    def test_garbage_fails_without_output(self):
        target = self.work / "out"
        done = self.run_tool("decompress", FIXTURE, target)
        self.assertEqual(done.returncode, 1, short_report(done.stdout))
        self.assertNotIn("ERROR:", done.stdout)
        self.assertFalse(target.exists())

    def test_truncated_file_fails_without_output(self):
        packed = self.assert_round_trip(FIXTURE)
        cut = self.work / "cut"
        cut.write_bytes(packed.read_bytes()[:-1])
        target = self.work / "out"
        done = self.run_tool("decompress", cut, target)
        self.assertEqual(done.returncode, 1, short_report(done.stdout))
        self.assertNotIn("ERROR:", done.stdout)
        self.assertFalse(target.exists())

    def test_missing_file(self):
        done = self.run_tool("compress", self.work / "no_existe", self.work / "out")
        self.assertEqual(done.returncode, 1)


for sample in SAMPLE_NAMES:
    def sample_case(self, sample=sample):
        self.assert_round_trip(SAMPLES / f"{sample}.bin")
    setattr(TestCompress, f"test_round_trip_{sample}", sample_case)

add_cases(TestHeap, "heap_tests", HEAP)
add_broken(TestHeap, BROKEN)
add_cases(TestTop, "heap_tests", TOP)
add_cases(TestTree, "huffman_tests", TREE)
add_cases(TestCompress, "huffman_tests", COMPRESS)


if __name__ == "__main__":
    unittest.main()
test_notes.pyDescargar test_notes.py
"""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 os
import pathlib
import platform
import re
import subprocess
import unittest

try:
    import pty
except ImportError:
    pty = None

HERE = pathlib.Path(__file__).parent
ROOT = HERE.parent.parent
NOTES = ROOT / "notes" / "week-13.md"
SLIDES = ROOT / "slides" / "week-13.tex"
# Antes de un bloque cuya salida cambia con el sistema. Se corre, pero no se compara.
SYSTEM_DEPENDENT = "<!-- depende del sistema -->"
# Antes de un informe de ASan. Puede omitir líneas, pero no cambiarlas ni reordenarlas.
# Las direcciones y los números de proceso no se comparan.
REPORT = "<!-- informe recortado -->"
# Los informes se capturaron en esta arquitectura: en otra cambian líneas de las bibliotecas.
REPORT_MACHINE = "aarch64"
# La solución del laboratorio no se reparte: sin esa carpeta, estos comandos no se pueden correr.
HAS_SOLUTION = (HERE / "solution").is_dir()


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",
        stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=120,
    )
    return normalize(done.stdout)


def run_in_terminal(command):
    """Corre el comando en una terminal de verdad: así se intercala la salida del programa
    con el informe, igual que cuando ustedes lo corren."""
    master, slave = pty.openpty()
    process = subprocess.Popen(
        ["bash", "-c", command], cwd=HERE, stdin=subprocess.DEVNULL, stdout=slave, stderr=slave,
    )
    os.close(slave)
    data = b""
    while True:
        try:
            chunk = os.read(master, 4096)
        except OSError:
            break
        if not chunk:
            break
        data += chunk
    os.close(master)
    process.wait(timeout=120)
    text = data.decode("utf-8", errors="replace").replace("\r", "")
    return normalize(re.sub(r"\x1b\[[0-9;]*[mK]", "", text))


def clean_lines(text):
    return [line.rstrip() for line in text.splitlines()]


def stable(line):
    """Quita de una línea de informe lo que cambia en cada corrida."""
    line = re.sub(r"0x[0-9a-f]+", "ADDR", line.strip())
    return re.sub(r"==\d+==", "==N==", line)


def split_commands(lines):
    """Parte un bloque de terminal en pares (comando, líneas de salida).
    Sin la carpeta solution/, omite el bloque entero si alguno de sus comandos la usa."""
    pairs = []
    for line in lines:
        if line.startswith("$ "):
            pairs.append((line[2:], []))
        elif pairs:
            pairs[-1][1].append(normalize(line.rstrip()))
    if not HAS_SOLUTION and any("solution/" in command for command, _ in pairs):
        return []
    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("*.cpp")) + list(HERE.glob("*.h")) + list((HERE / "errors").glob("*.cpp"))
    sources += list((HERE / "lab").glob("*.h")) + list((HERE / "lab").glob("*.cpp"))
    sources += list((HERE / "lab" / "tests").glob("*.cpp"))
    for source in sources:
        lines.update(line.strip() for line in source.read_text(encoding="utf-8").splitlines())
    return lines


class ReportCheck(unittest.TestCase):
    def assert_in_order(self, command, expected, actual):
        """Las líneas esperadas aparecen en la salida real, en orden. '...' marca un recorte."""
        wanted = [stable(line) for line in expected if line.strip() and line.strip() != "..."]
        real = [stable(line) for line in actual]
        position = 0
        for line in wanted:
            with self.subTest(command=command, line=line):
                found = [i for i in range(position, len(real)) if real[i] == line]
                self.assertTrue(found, f"no aparece en la salida real: {line}")
                if found:
                    position = found[0] + 1


class NotesAreTrue(ReportCheck):
    @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 == REPORT:
                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, 5)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    @unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
    def test_reports_in_notes(self):
        checked = 0
        for lines, mark in note_blocks("text"):
            if mark != REPORT:
                continue
            for command, expected in split_commands(lines):
                actual = clean_lines(run_in_terminal(command))
                self.assert_in_order(command, expected, actual)
                checked += 1
        self.assertGreater(checked, 0)

    @unittest.skipUnless(NOTES.exists(), "faltan los apuntes")
    def test_code_blocks_in_notes_exist(self):
        known = known_code_lines()
        for lines, _ in note_blocks("cpp"):
            for line in lines:
                if line.strip() and line.strip() != "...":
                    with self.subTest(line=line):
                        self.assertIn(line.strip(), known)


class SlidesAreTrue(ReportCheck):
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    def test_program_output_in_slides(self):
        for block in slide_blocks("terminal|salida"):
            for command, expected in split_commands(block):
                self.assert_in_order(command, expected, clean_lines(run_shell(command)))

    # Algunos informes usan programas que compiló la prueba de los apuntes, que corre antes.
    @unittest.skipUnless(SLIDES.exists(), "faltan las diapositivas")
    @unittest.skipUnless(pty and platform.machine() == REPORT_MACHINE, "los informes son de aarch64")
    def test_reports_in_slides(self):
        for block in slide_blocks("informe"):
            for command, expected in split_commands(block):
                self.assert_in_order(command, expected, clean_lines(run_in_terminal(command)))

    @unittest.skipUnless(NOTES.exists() and SLIDES.exists(), "faltan los apuntes o las diapositivas")
    def test_measurements_in_slides_come_from_notes(self):
        """Los tiempos dependen de la máquina: las diapositivas copian los de los apuntes."""
        notes = NOTES.read_text(encoding="utf-8").splitlines()
        blocks = slide_blocks("medicion")
        self.assertGreaterEqual(len(blocks), 2)
        for block in blocks:
            for line in block:
                with self.subTest(line=line):
                    self.assertIn(line, notes)

    @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()
test_structures.cppDescargar test_structures.cpp
// Pruebas de IntHeap y de heapsort: dirigidas y al azar, con semilla fija, contra la biblioteca.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <functional>
#include <iostream>
#include <queue>
#include <random>
#include <utility>
#include <vector>

#include "heapsort.h"
#include "int_heap.h"

namespace {

void test_guide_example() {
    IntHeap heap;
    for (int value : {5, 3, 8, 1, 4}) {
        heap.push(value);
        heap.check_invariants();
    }
    assert((heap.items() == std::vector<int>{1, 3, 8, 5, 4}));
    heap.pop();
    assert((heap.items() == std::vector<int>{3, 4, 8, 5}));
}

void test_random_against_priority_queue() {
    std::mt19937 generator(13);
    IntHeap heap;
    std::priority_queue<int, std::vector<int>, std::greater<int>> reference;
    for (int step = 0; step < 100000; step++) {
        if (reference.empty() || generator() % 3 != 0) {
            int value = static_cast<int>(generator() % 1000);
            heap.push(value);
            reference.push(value);
        } else {
            assert(heap.top() == reference.top());
            heap.pop();
            reference.pop();
        }
        assert(heap.size() == reference.size());
        if (step % 1000 == 0) {
            heap.check_invariants();
        }
    }
}

void test_operation_bounds() {
    IntHeap heap;
    for (int value = 100000; value > 0; value--) {
        long before = heap.comparisons();
        heap.push(value);
        assert(heap.comparisons() - before <= static_cast<long>(std::log2(double(heap.size()))) + 1);
    }
    while (!heap.empty()) {
        long before = heap.comparisons();
        std::size_t size = heap.size();
        heap.pop();
        assert(heap.comparisons() - before <= 2 * (static_cast<long>(std::log2(double(size))) + 1));
    }
}

void test_build_bound() {
    for (int n = 0; n < 2000; n += 37) {
        std::vector<int> items(n);
        for (int i = 0; i < n; i++) {
            items[i] = n - i;
        }
        long comparisons = 0;
        build_max_heap(items, [&](int a, int b) {
            comparisons++;
            return a > b;
        });
        assert(comparisons <= 2L * n);
        for (int i = 1; i < n; i++) {
            assert(items[(i - 1) / 2] <= items[i]);
        }
    }
}

void test_heapsort_against_sort() {
    std::mt19937 generator(13);
    for (int round = 0; round < 300; round++) {
        std::vector<int> items(generator() % 300);
        for (int& item : items) {
            item = static_cast<int>(generator() % 50);
        }
        std::vector<int> expected = items;
        std::sort(expected.begin(), expected.end());
        heapsort(items, std::less<int>());
        assert(items == expected);
    }
}

void test_heapsort_small_sizes() {
    for (int n = 0; n < 8; n++) {
        std::vector<int> items(n);
        for (int i = 0; i < n; i++) {
            items[i] = n - i;
        }
        heapsort(items, std::less<int>());
        assert(std::is_sorted(items.begin(), items.end()));
    }
}

}  // namespace

int main() {
    test_guide_example();
    test_random_against_priority_queue();
    test_operation_bounds();
    test_build_bound();
    test_heapsort_against_sort();
    test_heapsort_small_sizes();
    std::cout << "todas las pruebas pasaron\n";
    return 0;
}

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/bit_io.hDescargar lab/bit_io.h
// Escritura y lectura de bits sobre un arreglo de bytes. Viene hecho, con su orden fijo:
// dentro de cada byte se llena primero el bit más significativo.
#ifndef BIT_IO_H
#define BIT_IO_H

#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <vector>

using Bytes = std::vector<std::uint8_t>;

// Agrega bits al final de `out`. Llamen a finish() una vez, al terminar.
class BitWriter {
public:
    explicit BitWriter(Bytes& out) : out_(out) {}

    void put_bit(bool bit) {
        current_ = static_cast<std::uint8_t>((current_ << 1) | (bit ? 1 : 0));
        used_++;
        if (used_ == 8) {
            out_.push_back(current_);
            current_ = 0;
            used_ = 0;
        }
    }

    void put_byte(std::uint8_t byte) {
        for (int i = 7; i >= 0; i--) {
            put_bit(((byte >> i) & 1) != 0);
        }
    }

    // Completa el último byte con ceros, si quedó a medias.
    void finish() {
        while (used_ != 0) {
            put_bit(false);
        }
    }

private:
    Bytes& out_;
    std::uint8_t current_ = 0;
    int used_ = 0;
};

// Lee bits de `data`, desde el byte `start`. Si se acaban, lanza std::runtime_error.
class BitReader {
public:
    BitReader(const Bytes& data, std::size_t start) : data_(data), position_(start) {}

    bool get_bit() {
        if (position_ >= data_.size()) {
            throw std::runtime_error("archivo truncado");
        }
        bool bit = ((data_[position_] >> (7 - used_)) & 1) != 0;
        used_++;
        if (used_ == 8) {
            used_ = 0;
            position_++;
        }
        return bit;
    }

    std::uint8_t get_byte() {
        int byte = 0;
        for (int i = 0; i < 8; i++) {
            byte = (byte << 1) | (get_bit() ? 1 : 0);
        }
        return static_cast<std::uint8_t>(byte);
    }

private:
    const Bytes& data_;
    std::size_t position_;
    int used_ = 0;
};

#endif
lab/byte_file.hDescargar lab/byte_file.h
// Leer y escribir un archivo completo como bytes. Viene hecho: es fácil hacerlo mal.
#ifndef BYTE_FILE_H
#define BYTE_FILE_H

#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>

#include "bit_io.h"

// istreambuf_iterator entrega cada byte tal cual: no salta espacios, y no se detiene en 0x00 ni en 0xFF.
// El modo binario evita que el sistema traduzca fines de línea; en Linux no cambia nada, en Windows sí.
inline Bytes read_file(const std::string& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in) {
        throw std::runtime_error("no se pudo abrir " + path);
    }
    // Cada char pasa a uint8_t: 0xC3 vale 195 aquí, tenga o no signo el char.
    return Bytes(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}

inline void write_file(const std::string& path, const Bytes& bytes) {
    std::ofstream out(path, std::ios::binary);
    if (!out) {
        throw std::runtime_error("no se pudo crear " + path);
    }
    out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
    out.close();
    if (!out) {
        throw std::runtime_error("no se pudo escribir " + path);
    }
}

#endif
lab/compress.cppDescargar lab/compress.cpp
// Comprime un archivo: compress entrada salida. Viene hecho.
#include <exception>
#include <iostream>

#include "byte_file.h"
#include "huffman.h"

int main(int argc, char** argv) {
    if (argc != 3) {
        std::cerr << "uso: compress entrada salida\n";
        return 2;
    }
    try {
        write_file(argv[2], compress_bytes(read_file(argv[1])));
    } catch (const std::exception& error) {
        std::cerr << "compress: " << error.what() << "\n";
        return 1;
    }
    return 0;
}
lab/decompress.cppDescargar lab/decompress.cpp
// Descomprime un archivo: decompress entrada salida. Viene hecho.
// Solo crea la salida si todo salió bien.
#include <exception>
#include <iostream>

#include "byte_file.h"
#include "huffman.h"

int main(int argc, char** argv) {
    if (argc != 3) {
        std::cerr << "uso: decompress entrada salida\n";
        return 2;
    }
    try {
        write_file(argv[2], decompress_bytes(read_file(argv[1])));
    } catch (const std::exception& error) {
        std::cerr << "decompress: " << error.what() << "\n";
        return 1;
    }
    return 0;
}
lab/huffman.hDescargar lab/huffman.h
// Compresión de Huffman de un arreglo de bytes. Completen cada función marcada con not_implemented.
// Los tipos y el formato ya están decididos: las pruebas comparan archivos byte por byte.
#ifndef HUFFMAN_H
#define HUFFMAN_H

#include <array>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <vector>

#include "bit_io.h"
#include "min_heap.h"
#include "not_implemented.h"

// El formato del archivo comprimido:
//   bytes 0-3   "HUF1"
//   bytes 4-11  tamaño original en bytes, de 64 bits, primero el byte menos significativo
//   después, solo si el tamaño no es 0, una corriente de bits (ver bit_io.h):
//     el árbol en preorden: un nodo interno es un bit 0 seguido de sus hijos, izquierdo y derecho;
//       una hoja es un bit 1 seguido de los 8 bits de su símbolo
//     los códigos de los bytes del archivo, en orden
//     ceros de relleno hasta completar el último byte
// Un archivo con un solo símbolo tiene un árbol de una hoja, cuyo código tiene 0 bits.
inline constexpr std::size_t header_size = 12;
inline constexpr int max_tree_depth = 255;

using Frequencies = std::array<std::uint64_t, 256>;
using Codes = std::array<std::string, 256>;  // el código de cada byte, con caracteres '0' y '1'

// Los nodos viven en un arreglo y se señalan por índice. Así nadie tiene que hacer new ni delete.
struct HuffmanNode {
    std::uint64_t weight = 0;
    int symbol = -1;  // el byte, en una hoja
    int left = -1;    // hijo izquierdo: el bit 0
    int right = -1;   // hijo derecho: el bit 1
    bool is_leaf() const { return left < 0; }
};

struct HuffmanTree {
    std::vector<HuffmanNode> nodes;
    int root = -1;  // -1 si el árbol está vacío
};

// Lo que guarda el heap: un árbol parcial, con su peso.
struct HeapEntry {
    std::uint64_t weight;
    int node;
};

// El desempate es parte del formato: a igual peso, primero el nodo creado antes.
struct LighterEntry {
    bool operator()(const HeapEntry& a, const HeapEntry& b) const {
        if (a.weight != b.weight) {
            return a.weight < b.weight;
        }
        return a.node < b.node;
    }
};

// Viene hecho: cuántas veces aparece cada byte.
inline Frequencies count_frequencies(const Bytes& data) {
    Frequencies frequencies{};
    for (std::uint8_t byte : data) {
        frequencies[byte]++;
    }
    return frequencies;
}

// Paso 4: una hoja por cada byte con frecuencia mayor que 0, de menor a mayor símbolo. Luego, con
// MinHeap<HeapEntry, LighterEntry>, unan los dos más livianos hasta que quede uno: el primero que
// sale es el hijo izquierdo, y el nodo nuevo va al final de nodes. Sin símbolos, root queda en -1.
inline HuffmanTree build_tree(const Frequencies& frequencies) {
    (void)frequencies;
    not_implemented("build_tree");
}

// Viene hecho: el camino desde la raíz hasta cada hoja, 0 a la izquierda y 1 a la derecha. La recursión
// llega a 255 niveles como máximo. Una sola hoja tiene un código vacío. write_tree se escribe parecido.
inline void collect_codes(const HuffmanTree& tree, int node, std::string& path, Codes& codes) {
    const HuffmanNode& current = tree.nodes[node];
    if (current.is_leaf()) {
        codes[current.symbol] = path;
        return;
    }
    path.push_back('0');
    collect_codes(tree, current.left, path, codes);
    path.back() = '1';
    collect_codes(tree, current.right, path, codes);
    path.pop_back();
}

inline Codes make_codes(const HuffmanTree& tree) {
    Codes codes;
    if (tree.root >= 0) {
        std::string path;
        collect_codes(tree, tree.root, path, codes);
    }
    return codes;
}

// Paso 5: el árbol en preorden, como dice el formato. Requiere un árbol no vacío.
inline void write_tree(const HuffmanTree& tree, BitWriter& writer) {
    (void)tree;
    (void)writer;
    not_implemented("write_tree");
}

// Paso 5: lo contrario de write_tree. Los pesos quedan en 0. Un archivo dañado no debe agotar la
// pila ni colgar el programa: lancen std::runtime_error si la profundidad pasa de max_tree_depth
// o si hay más de 256 hojas. Si se acaban los bits, BitReader ya lanza su error.
// Cuidado: un push_back en nodes puede invalidar las referencias a sus elementos.
inline HuffmanTree read_tree(BitReader& reader) {
    (void)reader;
    not_implemented("read_tree");
}

// Paso 5: la cabecera, y si hay datos, el árbol y los códigos, con un BitWriter sobre el mismo
// arreglo. No olviden finish().
inline Bytes compress_bytes(const Bytes& data) {
    (void)data;
    not_implemented("compress_bytes");
}

// Paso 5: lanza std::runtime_error si no empieza con la cabecera correcta. Se detiene por el tamaño de
// la cabecera, y no porque se acaben los bits: el relleno no son símbolos. Antes de leer un bit,
// miren si ya están en una hoja.
inline Bytes decompress_bytes(const Bytes& compressed) {
    (void)compressed;
    not_implemented("decompress_bytes");
}

#endif
lab/min_heap.hDescargar lab/min_heap.h
// Cola de prioridad sobre un heap binario guardado en un arreglo.
// Completen cada método marcado con not_implemented. La semana 15 usará esta clase en Dijkstra:
// no cambien estas firmas.
#ifndef MIN_HEAP_H
#define MIN_HEAP_H

#include <cassert>
#include <cstddef>
#include <functional>
#include <utility>
#include <vector>

#include "not_implemented.h"

// less(a, b) dice si a tiene que estar más cerca de la raíz que b. Debe ser un orden estricto.
// Invariante: para todo i > 0, el hijo items_[i] no es menor que su padre items_[(i - 1) / 2].
// El árbol es completo porque el arreglo no tiene huecos.
// Ownership: el arreglo es un std::vector, así que copiar, mover y destruir ya funcionan.
// La referencia de top() vale hasta el próximo push o pop.
template <typename T, typename Less = std::less<T>>
class MinHeap {
    friend struct MinHeapTestAccess;

public:
    MinHeap() = default;

    explicit MinHeap(Less less) : less_(std::move(less)) {}

    // Paso 1: construye de abajo hacia arriba, desde el último padre hasta la raíz. Cuidado con los
    // tamaños 0 y 1: size_t no tiene negativos. Una prueba cuenta las comparaciones.
    explicit MinHeap(std::vector<T> items, Less less = Less()) : items_(std::move(items)), less_(std::move(less)) {
        not_implemented("MinHeap(items)");
    }

    // Paso 1: agrega al final y sube. Muevan el valor: una prueba cuenta las copias.
    void push(T value) {
        (void)value;
        not_implemented("push");
    }

    // Paso 1: la última hoja pasa a la raíz, y baja. Requiere que no esté vacío.
    void pop() {
        assert(!items_.empty());
        not_implemented("pop");
    }

    // Requiere que no esté vacío.
    const T& top() const {
        assert(!items_.empty());
        return items_.front();
    }

    bool empty() const { return items_.empty(); }
    std::size_t size() const { return items_.size(); }

    // Paso 1: detiene el programa, con assert, si algún hijo es menor que su padre.
    // Usen solo less_: T no tiene por qué tener operator<.
    void check_invariants() const { not_implemented("check_invariants"); }

private:
    // Sugerimos dos funciones privadas, sift_up y sift_down. Usen std::swap o std::move, sin copiar T.

    std::vector<T> items_;
    Less less_{};
};

#endif
lab/not_implemented.hDescargar lab/not_implemented.h
// Para los métodos que todavía no escriben: detiene el programa y dice cuál falta.
#ifndef NOT_IMPLEMENTED_H
#define NOT_IMPLEMENTED_H

#include <cstdio>
#include <cstdlib>

[[noreturn]] inline void not_implemented(const char* name) {
    std::fprintf(stderr, "sin implementar: %s\n", name);
    std::abort();
}

#endif
lab/report.mdDescargar lab/report.md
# Informe del laboratorio

Peguen aquí sus tablas y respondan con sus propios números. Digan qué midieron y qué suponen.

## Cómo midieron

Máquina, cómo compilaron, cuántas repeticiones y cuántas corridas.

## Paso 2: construir frente a insertar

Peguen la tabla de `make bench`.

¿Cuántas comparaciones por elemento hace cada forma, con claves al azar y con claves descendentes? ¿Cuál de las dos
columnas crece con n? ¿Se cumple la cota de 2n? ¿Qué tanto se parece la diferencia de tiempos a la de comparaciones?

## Paso 3: los k mayores

Peguen la tabla de `make bench-top`.

¿Para qué k conviene el heap, y desde qué k conviene ordenar todo? ¿Se explica con las comparaciones, o hay algo más?

## Paso 6: cuánto comprime

Peguen la tabla de `make bench-compress`, y anoten el tamaño de cada archivo con `gzip -9n`.

¿Cuál es el archivo que más comprime y cuál el que menos? ¿Por qué `random.bin` y `all_bytes.bin` crecen? ¿De dónde
sale exactamente lo que crecen? ¿Por qué `gzip` le gana a Huffman en los textos?
lab/top_k.hDescargar lab/top_k.h
// Los k mejores de una secuencia, con un heap de tamaño k.
#ifndef TOP_K_H
#define TOP_K_H

#include <algorithm>
#include <cstddef>
#include <vector>

#include "min_heap.h"
#include "not_implemented.h"

// Paso 3. better(a, b): a va antes que b. Devuelve los k mejores, del mejor al peor.
// Si hay menos de k, los devuelve todos. Debe costar O(n log k) comparaciones y guardar a lo
// sumo unos k elementos a la vez. Las pruebas cuentan las dos cosas.
// Para armar el heap, un comparador con captura sirve:
//     MinHeap<T, decltype(lambda)> heap(lambda);
template <typename T, typename Better>
std::vector<T> top_k(const std::vector<T>& items, std::size_t k, Better better) {
    (void)items;
    (void)k;
    (void)better;
    not_implemented("top_k");
}

#endif
lab/top_words.cppDescargar lab/top_words.cpp
// Imprime las k palabras más frecuentes de un texto: top_words archivo k
// Cada línea es "veces palabra", en el orden de MoreFrequent. Ver word_count.h.
#include <cstddef>
#include <exception>
#include <iostream>
#include <string>
#include <vector>

#include "top_k.h"
#include "word_count.h"

int main(int argc, char** argv) {
    if (argc != 3) {
        std::cerr << "uso: top_words archivo k\n";
        return 2;
    }
    try {
        std::size_t k = std::stoul(argv[2]);
        std::string text = read_text(argv[1]);
        // Paso 3: cuenten con count_words, elijan con top_k e impriman.
        (void)k;
        (void)text;
        not_implemented("top_words");
    } catch (const std::exception& error) {
        std::cerr << "top_words: " << error.what() << "\n";
        return 1;
    }
    return 0;
}
lab/word_count.hDescargar lab/word_count.h
// Contar las palabras de un texto. Viene hecho: el tema del paso 3 es elegir las k mayores.
#ifndef WORD_COUNT_H
#define WORD_COUNT_H

#include <cstddef>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <unordered_map>
#include <vector>

#include "words.h"

struct WordCount {
    std::string word;
    long count = 0;
};

// El orden de la salida: más veces primero y, si empatan, el orden de bytes de la palabra.
struct MoreFrequent {
    bool operator()(const WordCount& a, const WordCount& b) const {
        if (a.count != b.count) {
            return a.count > b.count;
        }
        return a.word < b.word;
    }
};

inline std::string read_text(const std::string& path) {
    std::ifstream in(path, std::ios::binary);
    if (!in) {
        throw std::runtime_error("no se pudo abrir " + path);
    }
    return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
}

// Una entrada por cada palabra distinta del texto, en cualquier orden.
inline std::vector<WordCount> count_words(const std::string& text) {
    std::unordered_map<std::string, long> counts;
    for (const std::string& word : split_words(text)) {
        counts[word]++;
    }
    std::vector<WordCount> result;
    result.reserve(counts.size());
    for (const auto& [word, count] : counts) {
        result.push_back({word, count});
    }
    return result;
}

#endif
lab/words.hDescargar lab/words.h
// Parte un texto en UTF-8 en palabras. Viene hecho: no es el tema del laboratorio.
#ifndef WORDS_H
#define WORDS_H

#include <cstddef>
#include <string>
#include <vector>

// Una palabra es una secuencia de letras: ASCII, o las de dos bytes que empiezan con 0xC3 (á, é, ñ,
// ü...), salvo × y ÷. Lo demás separa, incluidos ¿, ¡, « y la raya. Pasa a minúsculas también
// Á, É, Ñ: tolower de C trabaja por byte y no las cambiaría.
inline std::vector<std::string> split_words(const std::string& text) {
    std::vector<std::string> words;
    std::string current;
    std::size_t i = 0;
    while (i < text.size()) {
        unsigned char first = static_cast<unsigned char>(text[i]);
        if ((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z')) {
            current += static_cast<char>(first >= 'a' ? first : first + ('a' - 'A'));
            i++;
            continue;
        }
        if (first == 0xC3 && i + 1 < text.size()) {
            unsigned char second = static_cast<unsigned char>(text[i + 1]);
            if (second >= 0x80 && second != 0x97 && second != 0xB7) {
                // En este bloque, cada mayúscula está 0x20 antes que su minúscula.
                current += static_cast<char>(first);
                current += static_cast<char>(second < 0x9F ? second + 0x20 : second);
                i += 2;
                continue;
            }
        }
        if (!current.empty()) {
            words.push_back(current);
            current.clear();
        }
        // Saltar el carácter completo: los bytes 10xxxxxx continúan al anterior.
        i++;
        while (i < text.size() && (static_cast<unsigned char>(text[i]) & 0xC0) == 0x80) {
            i++;
        }
    }
    if (!current.empty()) {
        words.push_back(current);
    }
    return words;
}

#endif
lab/tests/bench_compress.cppDescargar lab/tests/bench_compress.cpp
// Paso 6: cuánto comprime cada archivo y cuánto tarda. Compilar con -O2 y sin sanitizers.
// Con --sizes solo imprime los tamaños, que no dependen de la máquina. Los tiempos sí dependen.
#include <cstring>
#include <exception>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

#include "bench.h"
#include "byte_file.h"
#include "huffman.h"

int main(int argc, char** argv) {
    bool sizes_only = argc > 1 && std::strcmp(argv[1], "--sizes") == 0;
    int first = sizes_only ? 2 : 1;
    if (argc <= first) {
        std::cerr << "uso: bench_compress [--sizes] archivo...\n";
        return 2;
    }
    std::cout << "archivo                original  comprimido       %";
    std::cout << (sizes_only ? "\n" : "   t.comp   t.desc\n");
    try {
        for (int i = first; i < argc; i++) {
            Bytes data = read_file(argv[i]);
            Bytes compressed = compress_bytes(data);
            if (decompress_bytes(compressed) != data) {
                std::cout << "no volvio igual: " << argv[i] << "\n";
                return 1;
            }
            std::string name = std::filesystem::path(argv[i]).filename().string();
            std::cout << std::left << std::setw(20) << name << std::right << std::setw(11) << data.size()
                      << std::setw(12) << compressed.size() << std::fixed << std::setprecision(1) << std::setw(8);
            if (data.empty()) {
                std::cout << "-";  // no se puede dividir entre 0
            } else {
                std::cout << 100.0 * double(compressed.size()) / double(data.size());
            }
            if (!sizes_only) {
                double compress_time = median_seconds(3, [&] { keep(compress_bytes(data).size()); });
                double decompress_time = median_seconds(3, [&] { keep(decompress_bytes(compressed).size()); });
                std::cout << std::setprecision(4) << std::setw(9) << compress_time << std::setw(9) << decompress_time;
            }
            std::cout << "\n";
        }
    } catch (const std::exception& error) {
        std::cerr << "bench_compress: " << error.what() << "\n";
        return 1;
    }
    return 0;
}
lab/tests/bench_heap.cppDescargar lab/tests/bench_heap.cpp
// Paso 2: construir de abajo hacia arriba frente a n inserciones.
// Cuenta comparaciones y mide tiempos, con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>

#include "bench.h"
#include "min_heap.h"

namespace {

struct CountingLess {
    long* count;
    bool operator()(int a, int b) const {
        (*count)++;
        return a < b;
    }
};

std::vector<int> make_input(const std::string& order, int n) {
    std::vector<int> items(n);
    std::iota(items.begin(), items.end(), 0);
    if (order == "desc") {
        std::reverse(items.begin(), items.end());
    } else if (order == "azar") {
        std::mt19937 generator(13);
        std::shuffle(items.begin(), items.end(), generator);
    }
    return items;
}

}  // namespace

int main() {
    std::cout << "       n orden  comp.build  /n  comp.push  /n  t.build  t.push\n";
    for (int n : {1 << 14, 1 << 16, 1 << 18, 1 << 20, 1 << 22}) {
        for (const std::string order : {"azar", "desc"}) {
            std::vector<int> items = make_input(order, n);
            long build_count = 0;
            {
                MinHeap<int, CountingLess> heap(items, CountingLess{&build_count});
                keep(heap.top());
            }
            long push_count = 0;
            {
                MinHeap<int, CountingLess> heap{CountingLess{&push_count}};
                for (int item : items) {
                    heap.push(item);
                }
                keep(heap.top());
            }
            double build_time = median_seconds(3, [&] {
                MinHeap<int> heap(items);
                keep(heap.top());
            });
            double push_time = median_seconds(3, [&] {
                MinHeap<int> heap;
                for (int item : items) {
                    heap.push(item);
                }
                keep(heap.top());
            });
            std::cout << std::setw(8) << n << std::setw(6) << order << std::setw(12) << build_count << std::fixed
                      << std::setprecision(2) << std::setw(5) << double(build_count) / n << std::setw(11)
                      << push_count << std::setw(6) << double(push_count) / n << std::setprecision(4) << std::setw(8)
                      << build_time << std::setw(8) << push_time << "\n";
        }
    }
    return 0;
}
lab/tests/bench_top.cppDescargar lab/tests/bench_top.cpp
// Paso 3: las k palabras más frecuentes con un heap, frente a ordenar todas.
// Cuenta comparaciones y mide tiempos, con -O2 y sin sanitizers. Los tiempos dependen de la máquina.
// Se mide solo la selección: contar las palabras cuesta lo mismo en los dos casos.
#include <algorithm>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>

#include "bench.h"
#include "top_k.h"
#include "word_count.h"

namespace {

std::vector<WordCount> by_sorting(std::vector<WordCount> counts, std::size_t k) {
    std::sort(counts.begin(), counts.end(), MoreFrequent());
    counts.resize(std::min(k, counts.size()));
    return counts;
}

bool same_words(const std::vector<WordCount>& a, const std::vector<WordCount>& b) {
    if (a.size() != b.size()) {
        return false;
    }
    for (std::size_t i = 0; i < a.size(); i++) {
        if (a[i].word != b[i].word || a[i].count != b[i].count) {
            return false;
        }
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: bench_top archivo\n";
        return 2;
    }
    std::vector<WordCount> counts = count_words(read_text(argv[1]));
    std::cout << "palabras distintas: " << counts.size() << "\n";
    std::cout << "       k  comp.heap  comp.sort    t.heap    t.sort\n";
    for (std::size_t k : {std::size_t{10}, std::size_t{100}, std::size_t{1000}, counts.size()}) {
        long heap_count = 0;
        long sort_count = 0;
        std::vector<WordCount> from_heap = top_k(counts, k, [&heap_count](const WordCount& a, const WordCount& b) {
            heap_count++;
            return MoreFrequent()(a, b);
        });
        std::vector<WordCount> copy = counts;
        std::sort(copy.begin(), copy.end(), [&sort_count](const WordCount& a, const WordCount& b) {
            sort_count++;
            return MoreFrequent()(a, b);
        });
        copy.resize(std::min(k, copy.size()));
        if (!same_words(from_heap, copy)) {
            std::cout << "no coinciden, con k = " << k << "\n";
            return 1;
        }
        double heap_time = median_seconds(5, [&] { keep(top_k(counts, k, MoreFrequent()).size()); });
        double sort_time = median_seconds(5, [&] { keep(by_sorting(counts, k).size()); });
        std::cout << std::setw(8) << k << std::setw(11) << heap_count << std::setw(11) << sort_count << std::fixed
                  << std::setprecision(5) << std::setw(10) << heap_time << std::setw(10) << sort_time << "\n";
    }
    return 0;
}
lab/tests/fixture.txtDescargar lab/tests/fixture.txt
—¿Quién llegó? ¡El Ñandú y el ñandú!
Árbol, árbol y ÁRBOL: el árbol de la zorra.
Una zorra, 12 zorras y Ana. «Ana» llegó después, ¿no?
lab/tests/heap_tests.cppDescargar lab/tests/heap_tests.cpp
// Pruebas públicas de los pasos 1 y 3. Se corre una por proceso: heap_tests nombre.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <cstring>
#include <functional>
#include <iostream>
#include <memory>
#include <queue>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include "min_heap.h"
#include "top_k.h"

// Permite a las pruebas mirar y dañar el arreglo.
struct MinHeapTestAccess {
    template <typename Heap>
    static auto& items(Heap& heap) {
        return heap.items_;
    }
};

namespace {

using IntHeap = MinHeap<int>;

std::vector<int> items_of(IntHeap& heap) { return MinHeapTestAccess::items(heap); }

// Comparador que cuenta. El contador vive afuera porque el heap guarda su propia copia.
struct CountingLess {
    long* count;
    bool operator()(int a, int b) const {
        (*count)++;
        return a < b;
    }
};

// Valor sin operator<: si check_invariants lo necesitara, la prueba no compilaría.
struct NoOrder {
    int value = 0;
};
struct NoOrderLess {
    bool operator()(const NoOrder& a, const NoOrder& b) const { return a.value < b.value; }
};

// Cuenta sus copias: bajar y subir no deben copiar nada.
struct CountedCopies {
    int value = 0;
    inline static long copies = 0;
    explicit CountedCopies(int v) : value(v) {}
    CountedCopies(const CountedCopies& other) : value(other.value) { copies++; }
    CountedCopies(CountedCopies&& other) noexcept : value(other.value) {}
    CountedCopies& operator=(const CountedCopies& other) {
        value = other.value;
        copies++;
        return *this;
    }
    CountedCopies& operator=(CountedCopies&& other) noexcept {
        value = other.value;
        return *this;
    }
    bool operator<(const CountedCopies& other) const { return value < other.value; }
};

// Cuenta cuántos objetos viven a la vez.
struct Live {
    int value = 0;
    inline static long count = 0;
    inline static long peak = 0;
    explicit Live(int v) : value(v) { born(); }
    Live(const Live& other) : value(other.value) { born(); }
    Live& operator=(const Live& other) = default;
    ~Live() { count--; }
    static void born() {
        count++;
        peak = std::max(peak, count);
    }
};

void test_new_is_empty() {
    IntHeap heap;
    assert(heap.empty());
    assert(heap.size() == 0);
    heap.check_invariants();
}

void test_guide_example() {
    IntHeap heap;
    const std::vector<std::vector<int>> expected = {{5}, {3, 5}, {3, 5, 8}, {1, 3, 8, 5}, {1, 3, 8, 5, 4}};
    const std::vector<int> values = {5, 3, 8, 1, 4};
    for (std::size_t i = 0; i < values.size(); i++) {
        heap.push(values[i]);
        heap.check_invariants();
        assert(items_of(heap) == expected[i]);
        assert(heap.size() == i + 1);
    }
    assert(heap.top() == 1);
}

void test_pop_example() {
    IntHeap heap;
    for (int value : {5, 3, 8, 1, 4}) {
        heap.push(value);
    }
    heap.pop();
    assert((items_of(heap) == std::vector<int>{3, 4, 8, 5}));
    heap.check_invariants();
}

void test_pop_chooses_smaller_child() {
    // Si baja siempre por la izquierda, la raíz queda mal.
    IntHeap heap(std::vector<int>{1, 4, 2, 5, 6, 3});
    assert((items_of(heap) == std::vector<int>{1, 4, 2, 5, 6, 3}));
    heap.pop();
    assert(heap.top() == 2);
    heap.check_invariants();
}

void test_pops_in_order() {
    IntHeap heap;
    for (int value : {9, 2, 7, 2, 8, 1, 5, 9, 0}) {
        heap.push(value);
    }
    std::vector<int> out;
    while (!heap.empty()) {
        out.push_back(heap.top());
        heap.pop();
        heap.check_invariants();
    }
    assert((out == std::vector<int>{0, 1, 2, 2, 5, 7, 8, 9, 9}));
}

void test_one_element() {
    MinHeap<std::string> heap;
    heap.push("solo");
    assert(heap.top() == "solo");
    heap.pop();
    assert(heap.empty());
    heap.push("otra vez");
    assert(heap.top() == "otra vez");
}

void test_other_comparator() {
    MinHeap<int, std::greater<int>> heap;
    for (int value : {5, 3, 8, 1, 4}) {
        heap.push(value);
    }
    heap.check_invariants();
    assert(heap.top() == 8);
    heap.pop();
    assert(heap.top() == 5);
}

void test_comparator_with_capture() {
    std::vector<int> distance = {30, 10, 20};
    auto closer = [&distance](int a, int b) { return distance[a] < distance[b]; };
    MinHeap<int, decltype(closer)> heap(closer);
    heap.push(0);
    heap.push(1);
    heap.push(2);
    assert(heap.top() == 1);
    MinHeap<int, decltype(closer)> built(std::vector<int>{0, 1, 2}, closer);
    assert(built.top() == 1);
}

void test_only_uses_comparator() {
    MinHeap<NoOrder, NoOrderLess> heap;
    for (int value : {4, 1, 3}) {
        heap.push(NoOrder{value});
    }
    heap.check_invariants();
    assert(heap.top().value == 1);
    MinHeap<NoOrder, NoOrderLess> built(std::vector<NoOrder>{{3}, {2}, {1}});
    built.check_invariants();
    assert(built.top().value == 1);
}

void test_pairs_for_dijkstra() {
    // La semana 15 guarda (distancia, nodo). std::pair ya compara así.
    MinHeap<std::pair<long, int>> heap;
    heap.push({30, 1});
    heap.push({10, 2});
    heap.push({10, 1});
    assert((heap.top() == std::pair<long, int>{10, 1}));
    heap.pop();
    assert((heap.top() == std::pair<long, int>{10, 2}));
}

void test_no_copies_inside() {
    std::vector<CountedCopies> start;
    start.reserve(2000);
    for (int i = 2000; i > 0; i--) {
        start.emplace_back(i);
    }
    CountedCopies::copies = 0;
    MinHeap<CountedCopies> heap(std::move(start));
    for (int i = 0; i < 2000; i++) {
        heap.push(CountedCopies(i));
    }
    while (!heap.empty()) {
        heap.pop();
    }
    assert(CountedCopies::copies == 0);
}

void test_copy_is_independent() {
    IntHeap heap(std::vector<int>{3, 1, 2});
    IntHeap copy = heap;
    copy.pop();
    assert(heap.size() == 3);
    assert(heap.top() == 1);
    assert(copy.top() == 2);
}

void test_build_example() {
    IntHeap heap(std::vector<int>{5, 3, 8, 1, 4});
    assert((items_of(heap) == std::vector<int>{1, 3, 8, 5, 4}));
    IntHeap larger(std::vector<int>{9, 8, 7, 6, 5, 4, 3, 2, 1});
    assert((items_of(larger) == std::vector<int>{1, 2, 3, 6, 5, 4, 7, 8, 9}));
}

void test_build_small_sizes() {
    for (int n = 0; n <= 70; n++) {
        std::vector<int> values(n);
        for (int i = 0; i < n; i++) {
            values[i] = n - i;
        }
        IntHeap heap(values);
        assert(heap.size() == static_cast<std::size_t>(n));
        heap.check_invariants();
        for (int i = 1; i <= n; i++) {
            assert(heap.top() == i);
            heap.pop();
        }
        assert(heap.empty());
    }
}

void test_build_comparisons() {
    // De abajo hacia arriba son unas 2n. La biblioteca promete 3n. n push sobre esto, unas 15n.
    const int n = 1 << 16;
    std::vector<int> descending(n);
    for (int i = 0; i < n; i++) {
        descending[i] = n - i;
    }
    long count = 0;
    MinHeap<int, CountingLess> heap(descending, CountingLess{&count});
    assert(count <= 3L * n);
    heap.check_invariants();
}

void test_push_comparisons() {
    const int n = 1 << 14;
    const long log_n = 14;
    long count = 0;
    MinHeap<int, CountingLess> descending_heap{CountingLess{&count}};
    for (int value = n; value > 0; value--) {
        long before = count;
        descending_heap.push(value);
        assert(count - before <= log_n);
    }
    // Un valor mayor que el de su padre no sube: una sola comparación.
    count = 0;
    MinHeap<int, CountingLess> ascending_heap{CountingLess{&count}};
    for (int value = 0; value < n; value++) {
        ascending_heap.push(value);
    }
    assert(count <= n);
}

void test_pop_comparisons() {
    const int n = 1 << 14;
    const long log_n = 14;
    long count = 0;
    std::vector<int> values(n);
    std::mt19937 generator(13);
    for (int& value : values) {
        value = static_cast<int>(generator() % 1000000);
    }
    MinHeap<int, CountingLess> heap(values, CountingLess{&count});
    while (!heap.empty()) {
        long before = count;
        heap.pop();
        assert(count - before <= 2 * log_n);
    }
}

void test_random_against_priority_queue() {
    std::mt19937 generator(13);
    IntHeap heap;
    std::priority_queue<int, std::vector<int>, std::greater<int>> reference;
    for (int step = 0; step < 200000; step++) {
        if (reference.empty() || generator() % 5 < 3) {
            int value = static_cast<int>(generator() % 1000);
            heap.push(value);
            reference.push(value);
        } else {
            assert(heap.top() == reference.top());
            heap.pop();
            reference.pop();
        }
        assert(heap.size() == reference.size());
        assert(heap.empty() == reference.empty());
        if (step % 5000 == 0) {
            heap.check_invariants();
        }
    }
    heap.check_invariants();
}

void test_random_builds_and_strings() {
    std::mt19937 generator(13);
    for (int round = 0; round < 200; round++) {
        std::vector<std::string> values(generator() % 200);
        for (std::string& value : values) {
            value = std::to_string(generator() % 500);
        }
        MinHeap<std::string> heap(values);
        heap.check_invariants();
        std::sort(values.begin(), values.end());
        for (const std::string& value : values) {
            assert(heap.top() == value);
            heap.pop();
        }
        assert(heap.empty());
    }
}

// Cada una daña un heap sano. El programa debe imprimir "sano" y luego detenerse.
template <typename Damage>
void broken(Damage damage) {
    IntHeap heap(std::vector<int>{10, 20, 30, 40, 50, 60, 70, 80, 90, 100});
    heap.check_invariants();
    std::cout << "sano" << std::endl;
    damage(MinHeapTestAccess::items(heap));
    heap.check_invariants();
}

void test_broken_root() {
    broken([](std::vector<int>& items) { items[0] = 25; });
}

void test_broken_right_child() {
    broken([](std::vector<int>& items) { items[2] = 5; });
}

void test_broken_last_leaf() {
    broken([](std::vector<int>& items) { items.back() = 45; });
}

// ---- Paso 3 ----

void test_top_k_cases() {
    std::vector<int> values = {5, 1, 9, 3, 7, 9};
    auto greater = std::greater<int>();
    assert(top_k(values, 0, greater).empty());
    assert((top_k(values, 1, greater) == std::vector<int>{9}));
    assert((top_k(values, 3, greater) == std::vector<int>{9, 9, 7}));
    assert((top_k(values, 6, greater) == std::vector<int>{9, 9, 7, 5, 3, 1}));
    assert((top_k(values, 100, greater) == std::vector<int>{9, 9, 7, 5, 3, 1}));
    assert(top_k(std::vector<int>{}, 3, greater).empty());
    assert((top_k(values, 2, std::less<int>()) == std::vector<int>{1, 3}));
}

void test_top_k_random() {
    std::mt19937 generator(13);
    for (int round = 0; round < 300; round++) {
        std::vector<int> values(generator() % 400);
        for (int& value : values) {
            value = static_cast<int>(generator() % 300);
        }
        std::size_t k = generator() % 450;
        std::vector<int> expected = values;
        std::sort(expected.begin(), expected.end(), std::greater<int>());
        expected.resize(std::min(k, expected.size()));
        assert(top_k(values, k, std::greater<int>()) == expected);
    }
}

void test_top_k_comparisons() {
    // Con valores crecientes, cada uno entra al heap. Ordenar todo usaría unas 17n.
    const int n = 100000;
    const std::size_t k = 8;
    std::vector<int> values(n);
    for (int i = 0; i < n; i++) {
        values[i] = i;
    }
    long count = 0;
    std::vector<int> result = top_k(values, k, [&count](int a, int b) {
        count++;
        return a > b;
    });
    assert(result.front() == n - 1);
    assert(result.size() == k);
    assert(count <= 10L * n);
}

void test_top_k_keeps_few_objects() {
    const int n = 20000;
    const std::size_t k = 10;
    std::vector<Live> values;
    values.reserve(n);
    for (int i = 0; i < n; i++) {
        values.emplace_back((i * 7919) % n);
    }
    Live::peak = Live::count;
    std::vector<Live> result = top_k(values, k, [](const Live& a, const Live& b) { return a.value > b.value; });
    assert(result.size() == k);
    assert(result[0].value == n - 1);
    // El heap, el resultado y algún temporal: de ninguna forma n copias.
    assert(Live::peak - n <= 3 * static_cast<long>(k) + 16);
}

struct Test {
    const char* name;
    void (*run)();
};

const Test tests[] = {
    {"new_is_empty", test_new_is_empty},
    {"guide_example", test_guide_example},
    {"pop_example", test_pop_example},
    {"pop_chooses_smaller_child", test_pop_chooses_smaller_child},
    {"pops_in_order", test_pops_in_order},
    {"one_element", test_one_element},
    {"other_comparator", test_other_comparator},
    {"comparator_with_capture", test_comparator_with_capture},
    {"only_uses_comparator", test_only_uses_comparator},
    {"pairs_for_dijkstra", test_pairs_for_dijkstra},
    {"no_copies_inside", test_no_copies_inside},
    {"copy_is_independent", test_copy_is_independent},
    {"build_example", test_build_example},
    {"build_small_sizes", test_build_small_sizes},
    {"build_comparisons", test_build_comparisons},
    {"push_comparisons", test_push_comparisons},
    {"pop_comparisons", test_pop_comparisons},
    {"random_against_priority_queue", test_random_against_priority_queue},
    {"random_builds_and_strings", test_random_builds_and_strings},
    {"broken_root", test_broken_root},
    {"broken_right_child", test_broken_right_child},
    {"broken_last_leaf", test_broken_last_leaf},
    {"top_k_cases", test_top_k_cases},
    {"top_k_random", test_top_k_random},
    {"top_k_comparisons", test_top_k_comparisons},
    {"top_k_keeps_few_objects", test_top_k_keeps_few_objects},
};

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: heap_tests nombre\n";
        return 2;
    }
    for (const Test& test : tests) {
        if (std::strcmp(test.name, argv[1]) == 0) {
            test.run();
            return 0;
        }
    }
    std::cerr << "no existe la prueba " << argv[1] << "\n";
    return 2;
}
lab/tests/huffman_tests.cppDescargar lab/tests/huffman_tests.cpp
// Pruebas públicas de los pasos 4 y 5. Se corre una por proceso: huffman_tests nombre.
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <fstream>
#include <functional>
#include <iostream>
#include <queue>
#include <random>
#include <stdexcept>
#include <string>
#include <vector>

#include "byte_file.h"
#include "huffman.h"

namespace {

Bytes bytes_of(const std::string& text) { return Bytes(text.begin(), text.end()); }

Frequencies frequencies_of(const std::vector<std::pair<int, std::uint64_t>>& pairs) {
    Frequencies frequencies{};
    for (const auto& [symbol, count] : pairs) {
        frequencies[symbol] = count;
    }
    return frequencies;
}

std::uint64_t total_bits(const Frequencies& frequencies, const Codes& codes) {
    std::uint64_t bits = 0;
    for (int symbol = 0; symbol < 256; symbol++) {
        bits += frequencies[symbol] * codes[symbol].size();
    }
    return bits;
}

// El costo óptimo: la suma de los pesos de cada unión, con la cola de la biblioteca.
std::uint64_t optimal_bits(const Frequencies& frequencies) {
    std::priority_queue<std::uint64_t, std::vector<std::uint64_t>, std::greater<std::uint64_t>> queue;
    for (std::uint64_t count : frequencies) {
        if (count > 0) {
            queue.push(count);
        }
    }
    std::uint64_t total = 0;
    while (queue.size() > 1) {
        std::uint64_t first = queue.top();
        queue.pop();
        std::uint64_t second = queue.top();
        queue.pop();
        total += first + second;
        queue.push(first + second);
    }
    return total;
}

void assert_prefix_free(const Frequencies& frequencies, const Codes& codes) {
    for (int a = 0; a < 256; a++) {
        for (int b = 0; b < 256; b++) {
            if (a != b && frequencies[a] > 0 && frequencies[b] > 0) {
                assert(codes[b].rfind(codes[a], 0) != 0);
            }
        }
    }
}

void assert_round_trip(const Bytes& data) {
    Bytes compressed = compress_bytes(data);
    assert(decompress_bytes(compressed) == data);
}

// Comprueba que descomprimir lance std::runtime_error, y no otra cosa.
void assert_rejected(const Bytes& compressed) {
    bool rejected = false;
    try {
        decompress_bytes(compressed);
    } catch (const std::runtime_error&) {
        rejected = true;
    }
    assert(rejected);
}

void test_count_frequencies() {
    Bytes data = {'a', 'b', 'a', 0x00, 0xFF, 0xFF, 0xC3, 0xFF};
    Frequencies frequencies = count_frequencies(data);
    assert(frequencies['a'] == 2);
    assert(frequencies['b'] == 1);
    assert(frequencies[0x00] == 1);
    assert(frequencies[0xFF] == 3);
    assert(frequencies[0xC3] == 1);
    assert(frequencies['c'] == 0);
    assert(count_frequencies(Bytes{})[0] == 0);
}

void test_tree_guide_example() {
    Frequencies frequencies = frequencies_of({{'a', 5}, {'b', 2}, {'c', 1}, {'d', 1}});
    HuffmanTree tree = build_tree(frequencies);
    assert(tree.nodes.size() == 7);
    assert(tree.root == 6);
    assert(tree.nodes[tree.root].weight == 9);
    Codes codes = make_codes(tree);
    assert(codes['a'] == "1");
    assert(codes['b'] == "00");
    assert(codes['c'] == "010");
    assert(codes['d'] == "011");
    assert(total_bits(frequencies, codes) == 15);
}

void test_tree_ties_follow_creation_order() {
    Frequencies frequencies = frequencies_of({{'a', 1}, {'b', 1}, {'c', 1}, {'d', 1}});
    Codes codes = make_codes(build_tree(frequencies));
    assert(codes['a'] == "00");
    assert(codes['b'] == "01");
    assert(codes['c'] == "10");
    assert(codes['d'] == "11");
}

void test_tree_weights_above_32_bits() {
    Frequencies frequencies = frequencies_of({{'a', 5000000000ULL}, {'b', 1}, {'c', 1}, {'d', 3000000000ULL}});
    Codes codes = make_codes(build_tree(frequencies));
    assert(codes['a'].size() == 1);
    assert(codes['d'].size() == 2);
    assert(codes['b'].size() == 3);
}

void test_tree_empty() {
    HuffmanTree tree = build_tree(Frequencies{});
    assert(tree.root == -1);
    assert(tree.nodes.empty());
    assert(make_codes(tree)[0].empty());
}

void test_tree_one_symbol() {
    Frequencies frequencies = frequencies_of({{0xFF, 42}});
    HuffmanTree tree = build_tree(frequencies);
    assert(tree.nodes.size() == 1);
    assert(tree.root == 0);
    assert(tree.nodes[0].is_leaf());
    assert(tree.nodes[0].symbol == 0xFF);
    assert(make_codes(tree)[0xFF].empty());
}

void test_tree_random_is_optimal() {
    std::mt19937 generator(13);
    for (int round = 0; round < 300; round++) {
        Frequencies frequencies{};
        int symbols = 2 + static_cast<int>(generator() % 255);
        for (int i = 0; i < symbols; i++) {
            frequencies[generator() % 256] = 1 + generator() % 1000;
        }
        HuffmanTree tree = build_tree(frequencies);
        Codes codes = make_codes(tree);
        assert_prefix_free(frequencies, codes);
        assert(total_bits(frequencies, codes) == optimal_bits(frequencies));
    }
}

void test_codes_longer_than_a_word() {
    // Frecuencias de Fibonacci: cada unión absorbe al árbol anterior, y los códigos se alargan.
    Frequencies frequencies{};
    std::uint64_t a = 1;
    std::uint64_t b = 1;
    for (int symbol = 0; symbol < 80; symbol++) {
        frequencies[symbol] = a;
        std::uint64_t next = a + b;
        a = b;
        b = next;
    }
    HuffmanTree tree = build_tree(frequencies);
    Codes codes = make_codes(tree);
    std::size_t longest = 0;
    for (const std::string& code : codes) {
        longest = std::max(longest, code.size());
    }
    assert(longest == 79);
    assert_prefix_free(frequencies, codes);
    // El árbol también viaja por el archivo.
    Bytes out;
    BitWriter writer(out);
    write_tree(tree, writer);
    writer.finish();
    BitReader reader(out, 0);
    assert(make_codes(read_tree(reader)) == codes);
}

void test_tree_write_guide_example() {
    Frequencies frequencies = frequencies_of({{'a', 2}, {'b', 1}});
    HuffmanTree tree = build_tree(frequencies);
    Bytes out;
    BitWriter writer(out);
    write_tree(tree, writer);
    writer.finish();
    // 0 | 1 01100010 | 1 01100001, con relleno
    assert((out == Bytes{0x58, 0xAC, 0x20}));
}

void test_tree_write_and_read_all_symbols() {
    Frequencies frequencies{};
    for (int symbol = 0; symbol < 256; symbol++) {
        frequencies[symbol] = 1 + symbol % 7;
    }
    HuffmanTree tree = build_tree(frequencies);
    Bytes out;
    BitWriter writer(out);
    write_tree(tree, writer);
    writer.finish();
    assert(out.size() == 320);  // 255 + 256 * 9 = 2559 bits
    BitReader reader(out, 0);
    HuffmanTree copy = read_tree(reader);
    assert(make_codes(copy) == make_codes(tree));
}

void test_tree_read_one_leaf() {
    Bytes bits = {0xFF, 0x80};  // 1, y el símbolo 11111111
    BitReader reader(bits, 0);
    HuffmanTree tree = read_tree(reader);
    assert(tree.nodes.size() == 1);
    assert(tree.root == 0);
    assert(tree.nodes[0].symbol == 0xFF);
}

void test_exact_bytes() {
    Bytes expected = {'H', 'U', 'F', '1', 3, 0, 0, 0, 0, 0, 0, 0, 0x58, 0xAC, 0x38};
    assert(compress_bytes(bytes_of("aab")) == expected);
}

void test_exact_size_in_header() {
    Bytes data(70000, 'x');
    Bytes compressed = compress_bytes(data);
    // 70000 = 0x11170: el byte menos significativo va primero.
    assert(compressed[4] == 0x70);
    assert(compressed[5] == 0x11);
    assert(compressed[6] == 0x01);
    assert(compressed[7] == 0x00);
}

void test_empty_input() {
    Bytes compressed = compress_bytes(Bytes{});
    assert((compressed == Bytes{'H', 'U', 'F', '1', 0, 0, 0, 0, 0, 0, 0, 0}));
    assert(decompress_bytes(compressed).empty());
}

void test_one_byte_input() {
    Bytes data = {0xFF};
    Bytes compressed = compress_bytes(data);
    assert(compressed.size() == header_size + 2);
    assert(decompress_bytes(compressed) == data);
}

void test_one_symbol_input() {
    Bytes data(100000, 'a');
    Bytes compressed = compress_bytes(data);
    // Una hoja son 9 bits, y sus códigos no ocupan nada.
    assert(compressed.size() == header_size + 2);
    assert(decompress_bytes(compressed) == data);
}

void test_two_symbols() {
    assert_round_trip(bytes_of("ab"));
    assert_round_trip(bytes_of("aaaaaaab"));
    Bytes data = compress_bytes(Bytes(16, 'a'));
    assert(decompress_bytes(data).size() == 16);
}

void test_padding_is_not_data() {
    // Cualquier relleno, de 0 a 7 bits, debe ignorarse: el tamaño manda.
    for (std::size_t n = 1; n <= 40; n++) {
        Bytes data(n, 'b');
        data[0] = 'a';
        assert_round_trip(data);
    }
}

void test_all_byte_values() {
    Bytes data;
    for (int round = 0; round < 5; round++) {
        for (int value = 0; value < 256; value++) {
            data.push_back(static_cast<std::uint8_t>(value));
        }
    }
    assert_round_trip(data);
}

void test_special_bytes() {
    assert_round_trip(Bytes{0x00, 0x00, 0xFF, 0x0A, 0x0D, 0x0A, 0x1A, 0x20, 0x00});
}

void test_random_round_trips() {
    std::mt19937 generator(13);
    for (int round = 0; round < 200; round++) {
        Bytes data(generator() % 2000);
        int alphabet = 1 + static_cast<int>(generator() % 256);
        for (std::uint8_t& byte : data) {
            byte = static_cast<std::uint8_t>(generator() % alphabet);
        }
        assert_round_trip(data);
    }
}

void test_reference_file() {
    // Lo comprimió la solución del curso. Si lee esto, su formato coincide.
    Bytes compressed = read_file("lab/tests/reference.huf");
    Bytes original = read_file("lab/tests/fixture.txt");
    assert(decompress_bytes(compressed) == original);
    assert(compress_bytes(original) == compressed);
}

void test_rejects_bad_header() {
    assert_rejected(Bytes{});
    assert_rejected(bytes_of("HUF"));
    assert_rejected(bytes_of("HUF1abc"));
    assert_rejected(bytes_of("esto no es un archivo comprimido"));
    // Un archivo correcto, con un solo byte cambiado en la marca.
    Bytes good = compress_bytes(bytes_of("aab"));
    for (std::size_t i = 0; i < 4; i++) {
        Bytes bad = good;
        bad[i] = 'x';
        assert_rejected(bad);
    }
}

void test_rejects_truncated_file() {
    Bytes compressed = compress_bytes(bytes_of("el ñandú llegó después, y la zorra no llegó"));
    for (std::size_t size = 0; size < compressed.size(); size++) {
        Bytes cut(compressed.begin(), compressed.begin() + static_cast<std::ptrdiff_t>(size));
        assert_rejected(cut);
    }
}

void test_rejects_endless_tree() {
    // Todo ceros: un nodo interno tras otro. Sin un límite, la recursión agota la pila.
    Bytes compressed = {'H', 'U', 'F', '1', 1, 0, 0, 0, 0, 0, 0, 0};
    compressed.resize(compressed.size() + 1000000, 0);
    assert_rejected(compressed);
}

// Escribe en preorden un árbol perfecto con 2^depth hojas.
void write_perfect_tree(BitWriter& writer, int depth) {
    if (depth == 0) {
        writer.put_bit(true);
        writer.put_byte('x');
        return;
    }
    writer.put_bit(false);
    write_perfect_tree(writer, depth - 1);
    write_perfect_tree(writer, depth - 1);
}

void test_rejects_too_many_leaves() {
    // 512 hojas, sin pasar del límite de profundidad. Le siguen bits de sobra para decodificar.
    Bytes out = {'H', 'U', 'F', '1', 1, 0, 0, 0, 0, 0, 0, 0};
    BitWriter writer(out);
    write_perfect_tree(writer, 9);
    for (int i = 0; i < 16; i++) {
        writer.put_bit(false);
    }
    writer.finish();
    assert_rejected(out);
}

struct Test {
    const char* name;
    void (*run)();
};

const Test tree_tests[] = {
    {"count_frequencies", test_count_frequencies},
    {"tree_guide_example", test_tree_guide_example},
    {"tree_ties_follow_creation_order", test_tree_ties_follow_creation_order},
    {"tree_weights_above_32_bits", test_tree_weights_above_32_bits},
    {"tree_empty", test_tree_empty},
    {"tree_one_symbol", test_tree_one_symbol},
    {"tree_random_is_optimal", test_tree_random_is_optimal},
    {"codes_longer_than_a_word", test_codes_longer_than_a_word},
    {"tree_write_guide_example", test_tree_write_guide_example},
    {"tree_write_and_read_all_symbols", test_tree_write_and_read_all_symbols},
    {"tree_read_one_leaf", test_tree_read_one_leaf},
    {"exact_bytes", test_exact_bytes},
    {"exact_size_in_header", test_exact_size_in_header},
    {"empty_input", test_empty_input},
    {"one_byte_input", test_one_byte_input},
    {"one_symbol_input", test_one_symbol_input},
    {"two_symbols", test_two_symbols},
    {"padding_is_not_data", test_padding_is_not_data},
    {"all_byte_values", test_all_byte_values},
    {"special_bytes", test_special_bytes},
    {"random_round_trips", test_random_round_trips},
    {"reference_file", test_reference_file},
    {"rejects_bad_header", test_rejects_bad_header},
    {"rejects_truncated_file", test_rejects_truncated_file},
    {"rejects_endless_tree", test_rejects_endless_tree},
    {"rejects_too_many_leaves", test_rejects_too_many_leaves},
};

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: huffman_tests nombre\n";
        return 2;
    }
    for (const Test& test : tree_tests) {
        if (std::strcmp(test.name, argv[1]) == 0) {
            test.run();
            return 0;
        }
    }
    std::cerr << "no existe la prueba " << argv[1] << "\n";
    return 2;
}
lab/tests/make_samples.cppDescargar lab/tests/make_samples.cpp
// Crea los archivos de prueba del paso 6 en la carpeta que se le indique. Con semilla fija.
#include <cstdint>
#include <fstream>
#include <iostream>
#include <random>
#include <string>
#include <vector>

namespace {

void save(const std::string& path, const std::vector<std::uint8_t>& bytes) {
    std::ofstream out(path, std::ios::binary);
    out.write(reinterpret_cast<const char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
}

}  // namespace

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "uso: make_samples carpeta\n";
        return 2;
    }
    const std::string folder = std::string(argv[1]) + "/";
    save(folder + "empty.bin", {});
    save(folder + "one_byte.bin", {0xFF});
    save(folder + "single_symbol.bin", std::vector<std::uint8_t>(100000, 'a'));

    std::vector<std::uint8_t> all_bytes;
    for (int round = 0; round < 1000; round++) {
        for (int value = 0; value < 256; value++) {
            all_bytes.push_back(static_cast<std::uint8_t>(value));
        }
    }
    save(folder + "all_bytes.bin", all_bytes);

    std::mt19937 generator(13);
    std::vector<std::uint8_t> random_bytes(1 << 20);
    for (std::uint8_t& byte : random_bytes) {
        byte = static_cast<std::uint8_t>(generator() >> 24);
    }
    save(folder + "random.bin", random_bytes);

    // Dos bytes difíciles y fines de línea, con pocos símbolos distintos.
    const std::vector<std::uint8_t> tricky = {0x00, 0xFF, 0x0A, 0x0D, 0x1A};
    std::vector<std::uint8_t> special(100000);
    for (std::uint8_t& byte : special) {
        byte = tricky[generator() % tricky.size()];
    }
    save(folder + "special.bin", special);
    return 0;
}
lab/tests/reference.hufDescargar lab/tests/reference.huf
HUF1�{Cd���g.��f��!��S�B����RUm!p�29CW�n�Vr�H
���b�s/7�sv,����߳9/O&�Jf�'�����j����6:	�裠���V�V�=��tG�/}�#��{�D?5*k�D?w�W܋��}����>�=�3N~�
lab/tests/roundtrip.shDescargar lab/tests/roundtrip.sh
#!/usr/bin/env bash
# Paso 6: comprime y descomprime cada archivo, y comprueba con cmp que vuelve igual.
# Uso: roundtrip.sh carpeta_de_binarios archivo...
set -u
bin="$1"
shift
status=0
work="$(mktemp -d)"
for file in "$@"; do
    if "$bin/compress" "$file" "$work/packed" && "$bin/decompress" "$work/packed" "$work/back" \
        && cmp "$file" "$work/back"; then
        echo "ok    $file"
    else
        echo "FALLA $file"
        status=1
    fi
done
rm -rf "$work"
exit "$status"

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 13: heaps y colas de prioridad, heapsort, Huffman. 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-heap        prueba MinHeap (paso 1)
#   make test-top         prueba top_k y top_words (paso 3)
#   make test-tree        prueba frecuencias, árbol y códigos (pasos 4 y 5)
#   make test-compress    prueba compress y decompress, con cmp (pasos 5 y 6)
#   make test-lab         prueba todo su laboratorio; falla hasta que lo terminen
#   make bench            construir frente a insertar, con -O2 y sin sanitizers (paso 2)
#   make bench-top        heap de tamaño k frente a ordenar (paso 3)
#   make samples          crea los archivos de prueba en build/samples (paso 6)
#   make roundtrip        comprime, descomprime y compara con cmp cada archivo (paso 6)
#   make bench-compress   cuánto comprime cada archivo (paso 6)
#   make clean            borra lo generado
#
# Los programas de errors/ tienen errores a propósito. No se compilan con make:
# los compilan las pruebas, y ustedes, con los comandos de los apuntes.

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
DATA ?= ../../data
LAB_DIR ?= lab
BUILD = build
LAB_BUILD = $(BUILD)/$(LAB_DIR)
PROGRAMS = heap_demo where_is_max heapsort_demo pq_demo huffman_demo test_structures
BENCHES = build_counts heapsort_counts heapsort_cache heapsort_times top_k_bench
COMMON_HEADERS = int_heap.h heapsort.h bench.h
GIVEN = lab/bit_io.h lab/byte_file.h lab/word_count.h lab/words.h lab/not_implemented.h
LAB_HEADERS = $(LAB_DIR)/min_heap.h $(LAB_DIR)/top_k.h $(LAB_DIR)/huffman.h $(GIVEN)
LAB_INCLUDES = -I$(LAB_DIR) -Ilab
LAB_PROGRAMS = heap_tests huffman_tests top_words compress decompress
LAB_BENCHES = bench_heap bench_top bench_compress
SAMPLES = empty one_byte single_symbol all_bytes random special
SAMPLE_FILES = $(SAMPLES:%=$(BUILD)/samples/%.bin)

.PHONY: all lab samples test test-heap test-top test-tree test-compress test-lab bench bench-top roundtrip bench-compress clean

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

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

# Las mediciones van con -O2 y sin sanitizers.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(COMMON_HEADERS) | $(BUILD)
	$(CXX) $(BENCHFLAGS) $< -o $@

# LAB_DIR va primero: así las pruebas toman el código de esa carpeta.
$(LAB_BUILD)/heap_tests: lab/tests/heap_tests.cpp $(LAB_HEADERS) | $(LAB_BUILD)
	$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@

$(LAB_BUILD)/huffman_tests: lab/tests/huffman_tests.cpp $(LAB_HEADERS) | $(LAB_BUILD)
	$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@

$(LAB_BUILD)/top_words $(LAB_BUILD)/compress $(LAB_BUILD)/decompress: $(LAB_BUILD)/%: $(LAB_DIR)/%.cpp $(LAB_HEADERS) | $(LAB_BUILD)
	$(CXX) $(CXXFLAGS) $(LAB_INCLUDES) $< -o $@

$(LAB_BENCHES:%=$(LAB_BUILD)/%): $(LAB_BUILD)/%: lab/tests/%.cpp $(LAB_HEADERS) bench.h | $(LAB_BUILD)
	$(CXX) $(BENCHFLAGS) $(LAB_INCLUDES) -I. $< -o $@

$(BUILD)/make_samples: lab/tests/make_samples.cpp | $(BUILD)
	$(CXX) $(BENCHFLAGS) $< -o $@

$(SAMPLE_FILES) &: $(BUILD)/make_samples
	mkdir -p $(BUILD)/samples
	$(BUILD)/make_samples $(BUILD)/samples

samples: $(SAMPLE_FILES)

$(BUILD) $(LAB_BUILD):
	mkdir -p $@

lab: $(LAB_PROGRAMS:%=$(LAB_BUILD)/%) $(LAB_BENCHES:%=$(LAB_BUILD)/%) samples

# Si existe solution/, también se comprueba la solución del laboratorio.
test: all samples
	@if [ -d solution ]; then $(MAKE) --no-print-directory LAB_DIR=solution lab; fi
	DATA=$(DATA) python3 -m unittest discover -p 'test_*.py'

test-heap: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestHeap

test-top: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestTop

test-tree: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestTree

test-compress: lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab.TestCompress

test-lab: all lab
	DATA=$(DATA) LAB_DIR=$(LAB_DIR) python3 -m unittest -v test_lab

bench: $(LAB_BUILD)/bench_heap
	$<

bench-top: $(LAB_BUILD)/bench_top
	$< $(DATA)/tradiciones.txt

roundtrip: $(LAB_BUILD)/compress $(LAB_BUILD)/decompress samples
	lab/tests/roundtrip.sh $(LAB_BUILD) $(SAMPLE_FILES) lab/tests/fixture.txt $(DATA)/tradiciones.txt

bench-compress: $(LAB_BUILD)/bench_compress samples
	$< $(SAMPLE_FILES) $(DATA)/tradiciones.txt $(DATA)/terminos.tsv $(DATA)/palabras.txt

clean:
	rm -rf $(BUILD) __pycache__