Semana 4 — Cotas inferiores y primeros greedy

Objetivos

  • Demostrar la cota Ω(n log n) para ordenar comparando.
  • Explicar por qué el azar protege a quicksort de las entradas malas.
  • Reconocer cuándo una regla greedy falla, con un contraejemplo.
  • Demostrar un greedy con un argumento de intercambio.

Sesión A — Cota inferior para ordenar · ✂️ Quicksort aleatorizado, con intuición

Lectura: Apuntes, semana 4.

Apoyo: Erickson, sec. 1.5 (Quicksort). CLRS, capítulos Sorting in Linear Time y Quicksort, 4.ª edición.

Temas:

  • Qué es una cota inferior: habla de todos los algoritmos posibles, no de uno.
  • Árbol de decisiones de un ordenamiento por comparaciones: al menos n! hojas, así que la altura es al menos log₂(n!) = Ω(n log n). En Estructuras de Datos se vio la idea; aquí se demuestra.
  • Por qué counting sort no contradice la cota: no compara.
  • ✂️ Quicksort con pivote al azar: la mitad de los pivotes son razonables, así que se esperan O(log n) niveles. Con intuición y midiendo: el análisis formal necesita Probabilidad.
  • Error común: decir que un algoritmo con mejor caso rápido rompe la cota inferior, que habla del peor caso.

Sesión B — Greedy: selección de intervalos y argumentos de intercambio

Lectura: Apuntes, semana 4.

Apoyo: Erickson, secs. 4.1 (Storing Files on Tape), 4.2 (Scheduling Classes) y 4.3 (General Pattern). CLRS, capítulo Greedy Algorithms, 4.ª edición.

Temas:

  • Una técnica fácil de escribir y difícil de justificar: tomar la mejor opción ahora y no volver atrás.
  • Selección de intervalos: máximo número de clases sin cruces en un aula. Tres reglas que fallan, con contraejemplos: empieza antes, dura menos, menos cruces.
  • La regla correcta: la que termina antes.
  • Argumento de intercambio: tomar una solución óptima, cambiar su primera diferencia por la elección greedy y comprobar que sigue siendo óptima.
  • Partición de intervalos: el mínimo de aulas es el máximo de clases simultáneas. Una cota inferior que el algoritmo alcanza.
  • Error común: demostrar que el greedy da una solución válida y creer que eso prueba que es óptima.

Laboratorio — Práctica 1 · Opcional: horarios y aulas

Práctica 1 en la sesión de laboratorio.

Problema opcional: con los horarios de un conjunto de clases, calcular cuántas caben en una sola aula y cuántas aulas hacen falta para dictarlas todas.

Entregas

  • Práctica 1: Invariantes e inducción, recurrencias y divide y vencerás. Ver practicas/README.md.

Preguntas de repaso

  1. ¿Cuántas comparaciones hacen falta como mínimo para ordenar 3 elementos en el peor caso, según el árbol de decisiones? Den un algoritmo que lo logre.
  2. Demuestren que log₂(n!) = Ω(n log n).
  3. Den un contraejemplo para cada regla: la clase más corta primero, la que empieza antes primero.
  4. Escriban el argumento de intercambio para la regla de la clase que termina antes.
  5. Si en algún momento hay 4 clases a la vez, ¿por qué hacen falta al menos 4 aulas? Si nunca hay más de 4 a la vez, ¿por qué bastan 4?

Código de la semana

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

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

Ejemplos

Los programas que usan los apuntes y la clase.

adversary_demo.cppDescargar adversary_demo.cpp
// El adversario contra inserción y contra mergesort: la entrada mala depende del algoritmo.
#include <iomanip>
#include <iostream>
#include <vector>

#include "adversary.h"
#include "comparison_sorts.h"

namespace {

template <typename Sorter>
void play(const char* name, int n, Sorter sorter) {
    Adversary adversary(n);
    std::cout << "  " << std::left << std::setw(11) << name << std::right;
    const auto ask = [&](int i, int j) { return adversary.less(i, j); };
    sorter(n, ask);
    std::cout << std::setw(3) << adversary.count() << " comparaciones, posibles al final: " << adversary.alive()
              << ", entrada:";
    for (int value : adversary.witness()) std::cout << ' ' << value;
    std::cout << '\n';
}

}  // namespace

int main() {
    // Cómo se van acabando las permutaciones, pregunta a pregunta.
    Adversary adversary(4);
    std::cout << "n = 4, inserción. Permutaciones posibles: " << adversary.alive() << '\n';
    insertion_order(4, [&](int i, int j) {
        const bool answer = adversary.less(i, j);
        std::cout << "  ¿e" << i << " < e" << j << "? " << (answer ? "sí" : "no") << ", quedan " << adversary.alive() << '\n';
        return answer;
    });
    std::cout << '\n';

    for (int n : {5, 8}) {
        std::cout << "n = " << n << ", cota " << ceil_log2_factorial(n) << '\n';
        play("inserción", n, [](int size, auto ask) { return insertion_order(size, ask); });
        play("mergesort", n, [](int size, auto ask) { return merge_order(size, ask); });
    }
}
adversary.hDescargar adversary.h
// Un adversario para ordenamientos por comparaciones. No fija la entrada de antemano: guarda todas
// las permutaciones que siguen siendo posibles, y a cada pregunta responde lo que deja más.
#pragma once

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

class Adversary {
public:
    explicit Adversary(int n) {
        std::vector<int> p(static_cast<std::size_t>(n));
        std::iota(p.begin(), p.end(), 0);
        do {
            alive_.push_back(p);
        } while (std::next_permutation(p.begin(), p.end()));
    }

    // ¿El elemento i es menor que el j? i y j son posiciones de la entrada.
    bool less(int i, int j) {
        count_++;
        std::vector<std::vector<int>> yes;
        std::vector<std::vector<int>> no;
        for (const std::vector<int>& p : alive_) {
            if (p[static_cast<std::size_t>(i)] < p[static_cast<std::size_t>(j)]) {
                yes.push_back(p);
            } else {
                no.push_back(p);
            }
        }
        const bool answer = yes.size() >= no.size();
        alive_ = answer ? std::move(yes) : std::move(no);
        return answer;
    }

    long long count() const { return count_; }
    std::size_t alive() const { return alive_.size(); }
    // Una entrada que es coherente con todas las respuestas dadas hasta ahora.
    const std::vector<int>& witness() const { return alive_.front(); }

private:
    std::vector<std::vector<int>> alive_;
    long long count_ = 0;
};

// Ordena las posiciones 0..n-1 preguntando a Compare, con inserción.
template <typename Compare>
std::vector<int> insertion_order(int n, Compare is_less) {
    std::vector<int> order(static_cast<std::size_t>(n));
    std::iota(order.begin(), order.end(), 0);
    for (std::size_t i = 1; i < order.size(); i++) {
        const int value = order[i];
        std::size_t j = i;
        while (j > 0 && is_less(value, order[j - 1])) {
            order[j] = order[j - 1];
            j--;
        }
        order[j] = value;
    }
    return order;
}

template <typename Compare>
void merge_range(std::vector<int>& order, std::size_t lo, std::size_t hi, Compare& is_less) {
    if (hi - lo < 2) return;
    const std::size_t mid = lo + (hi - lo) / 2;
    merge_range(order, lo, mid, is_less);
    merge_range(order, mid, hi, is_less);
    std::vector<int> merged;
    std::size_t i = lo;
    std::size_t j = mid;
    while (i < mid && j < hi) {
        if (is_less(order[j], order[i])) {
            merged.push_back(order[j++]);
        } else {
            merged.push_back(order[i++]);
        }
    }
    while (i < mid) merged.push_back(order[i++]);
    while (j < hi) merged.push_back(order[j++]);
    std::copy(merged.begin(), merged.end(), order.begin() + static_cast<std::ptrdiff_t>(lo));
}

// Ordena las posiciones 0..n-1 preguntando a Compare, con mergesort.
template <typename Compare>
std::vector<int> merge_order(int n, Compare is_less) {
    std::vector<int> order(static_cast<std::size_t>(n));
    std::iota(order.begin(), order.end(), 0);
    merge_range(order, 0, order.size(), is_less);
    return order;
}
comparison_sorts.hDescargar comparison_sorts.h
// Ordenamientos que solo miran los datos a través de una comparación, y un comparador
// que cuenta. Así se puede medir exactamente lo que habla la cota inferior.
#pragma once

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

// Compara y anota: cuántas comparaciones van y qué respondió cada una.
// La cadena de respuestas identifica la hoja del árbol de decisiones a la que se llega.
class CountingLess {
public:
    bool operator()(int a, int b) {
        count_++;
        const bool answer = a < b;
        path_ += answer ? 'S' : 'N';
        return answer;
    }

    long long count() const { return count_; }
    const std::string& path() const { return path_; }

private:
    long long count_ = 0;
    std::string path_;
};

// Ordena tres valores con a lo sumo 3 comparaciones.
inline void sort_three(std::vector<int>& a, CountingLess& less) {
    if (less(a[1], a[0])) std::swap(a[0], a[1]);
    // Ahora a[0] < a[1]. Falta ubicar a[2].
    if (less(a[2], a[1])) {
        std::swap(a[1], a[2]);
        if (less(a[1], a[0])) std::swap(a[0], a[1]);
    }
}

inline void insertion_sort(std::vector<int>& a, CountingLess& less) {
    for (std::size_t i = 1; i < a.size(); i++) {
        const int value = a[i];
        std::size_t j = i;
        while (j > 0 && less(value, a[j - 1])) {
            a[j] = a[j - 1];
            j--;
        }
        a[j] = value;
    }
}

// Mergesort sobre el rango semiabierto [lo, hi).
inline void merge_sort_range(std::vector<int>& a, std::size_t lo, std::size_t hi, CountingLess& less) {
    if (hi - lo < 2) return;
    const std::size_t mid = lo + (hi - lo) / 2;
    merge_sort_range(a, lo, mid, less);
    merge_sort_range(a, mid, hi, less);
    std::vector<int> merged;
    merged.reserve(hi - lo);
    std::size_t i = lo;
    std::size_t j = mid;
    while (i < mid && j < hi) {
        if (less(a[j], a[i])) {
            merged.push_back(a[j++]);
        } else {
            merged.push_back(a[i++]);
        }
    }
    while (i < mid) merged.push_back(a[i++]);
    while (j < hi) merged.push_back(a[j++]);
    for (std::size_t k = 0; k < merged.size(); k++) a[lo + k] = merged[k];
}

inline void merge_sort(std::vector<int>& a, CountingLess& less) {
    merge_sort_range(a, 0, a.size(), less);
}

// El menor h con 2^h ≥ n!, con enteros. Sirve hasta n = 20, porque 20! cabe en 64 bits.
inline int ceil_log2_factorial(int n) {
    unsigned long long factorial = 1;
    for (int i = 2; i <= n; i++) factorial *= static_cast<unsigned long long>(i);
    int h = 0;
    unsigned long long power = 1;
    while (power < factorial) {
        power *= 2;
        h++;
    }
    return h;
}
counting_bench.cppDescargar counting_bench.cpp
// Counting sort frente a std::sort con enteros pequeños. Tiempos con -O2 y un solo hilo.
#include <algorithm>
#include <chrono>
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>

#include "counting_sort.h"

template <typename Work>
double best_ms(Work work) {
    double best = -1;
    for (int attempt = 0; attempt < 5; attempt++) {
        const auto begin = std::chrono::steady_clock::now();
        work();
        const std::chrono::duration<double, std::milli> ms = std::chrono::steady_clock::now() - begin;
        if (best < 0 || ms.count() < best) best = ms.count();
    }
    return best;
}

int main() {
    std::cout << "        n          k  counting ms  std::sort ms\n";
    std::cout << std::fixed << std::setprecision(1);
    const std::vector<std::pair<int, int>> sizes = {
        {1000000, 1000}, {4000000, 1000}, {16000000, 1000},
        {1000000, 1000000}, {1000000, 10000000}, {1000, 10000000}};
    for (const auto& [n, k] : sizes) {
        std::mt19937 rng(4);
        std::vector<int> original(static_cast<std::size_t>(n));
        for (int& value : original) value = static_cast<int>(rng() % static_cast<unsigned>(k));
        const double counting = best_ms([&] {
            std::vector<int> a = original;
            counting_sort(a, k);
        });
        const double library = best_ms([&] {
            std::vector<int> a = original;
            std::sort(a.begin(), a.end());
        });
        std::cout << std::setw(9) << n << std::setw(11) << k << std::setw(13) << counting
                  << std::setw(14) << library << '\n';
    }
}
counting_sort.hDescargar counting_sort.h
// Counting sort: ordena enteros en [0, k) sin comparar dos elementos entre sí.
#pragma once

#include <cstddef>
#include <vector>

inline void counting_sort(std::vector<int>& a, int k) {
    std::vector<std::size_t> count(static_cast<std::size_t>(k), 0);
    for (int value : a) {
        count[static_cast<std::size_t>(value)]++;  // el valor se usa como índice
    }
    std::size_t next = 0;
    for (int value = 0; value < k; value++) {
        for (std::size_t c = 0; c < count[static_cast<std::size_t>(value)]; c++) {
            a[next++] = value;
        }
    }
}
greedy_rules.cppDescargar greedy_rules.cpp
// Corre las cuatro reglas sobre los contraejemplos de los apuntes.
#include <iostream>
#include <string>
#include <vector>

#include "intervals.h"

namespace {

void print_classes(const std::vector<Interval>& classes) {
    for (const Interval& c : classes) std::cout << " [" << c.start << "," << c.finish << ")";
    std::cout << '\n';
}

void run_rule(const std::string& name, const std::vector<Interval>& result) {
    std::cout << "  " << name << ": " << result.size() << (is_valid(result) ? " (válida)" : " (INVÁLIDA)") << " ->";
    print_classes(result);
}

void report(const std::string& title, const std::vector<Interval>& classes) {
    std::cout << title << "\n  clases:";
    print_classes(classes);
    std::cout << "  óptimo por fuerza bruta: " << max_compatible_brute(classes) << '\n';
    run_rule("empieza antes", rule_earliest_start(classes));
    run_rule("dura menos   ", rule_shortest(classes));
    run_rule("menos cruces ", rule_fewest_conflicts(classes));
    run_rule("termina antes", select_by_finish(classes));
    std::cout << '\n';
}

}  // namespace

int main() {
    report("Contra empieza antes:", {{8, 18}, {9, 11}, {11, 13}, {13, 15}});
    report("Contra dura menos:", {{8, 12}, {12, 16}, {11, 13}});
    const std::vector<Interval> crowded = {
        {0, 2}, {2, 4}, {4, 6}, {6, 8},   // las cuatro de la solución óptima
        {1, 3}, {1, 3}, {1, 3},           // tres iguales entre las dos primeras
        {3, 5},                           // la del centro: solo dos cruces
        {5, 7}, {5, 7}, {5, 7}};          // tres iguales entre las dos últimas
    report("Contra menos cruces:", crowded);

    std::cout << "En esa lista, cruces de cada clase:\n";
    for (std::size_t i = 0; i < crowded.size(); i++) {
        std::cout << "  [" << crowded[i].start << "," << crowded[i].finish << "): " << conflicts(crowded, i)
                  << (in_some_optimum(crowded, i) ? "" : "  (no está en ninguna solución óptima)") << '\n';
    }
    const auto [low, high] = fewest_conflicts_range(crowded);
    std::cout << "Menos cruces, con cualquier forma de romper empates: entre " << low << " y " << high << '\n';
}
interval_traces.cppDescargar interval_traces.cpp
// Sigue los ejemplos de los apuntes: selección, aulas y una idea para las aulas que falla.
#include <iostream>
#include <vector>

#include "intervals.h"

int main() {
    const std::vector<Interval> week = {{8, 10}, {9, 11}, {10, 12}, {11, 13}, {8, 13}, {12, 14}, {13, 15}, {15, 16}};
    const auto chosen = select_by_finish(week);
    std::cout << "Selección (" << chosen.size() << " clases, óptimo " << max_compatible_brute(week) << "):";
    for (const Interval& c : chosen) std::cout << " [" << c.start << "," << c.finish << ")";
    std::cout << '\n';

    // El intercambio, a partir de una solución óptima que no se parece a la del greedy.
    std::vector<Interval> optimal = {{9, 11}, {11, 13}, {13, 15}, {15, 16}};
    std::cout << "Intercambio, empezando en:";
    for (const Interval& c : optimal) std::cout << " [" << c.start << "," << c.finish << ")";
    std::cout << '\n';
    for (std::size_t j = 0; j < chosen.size(); j++) {
        if (optimal[j] == chosen[j]) continue;
        optimal[j] = chosen[j];
        std::cout << "  posición " << j + 1 << ":";
        for (const Interval& c : optimal) std::cout << " [" << c.start << "," << c.finish << ")";
        std::cout << (is_valid(optimal) ? "  válida\n" : "  INVÁLIDA\n");
    }

    const auto room = assign_rooms(week);
    std::cout << "Aulas: " << rooms_used(room) << ", máximo simultáneo: " << max_simultaneous(week) << '\n';
    for (std::size_t i = 0; i < week.size(); i++) {
        std::cout << "  [" << week[i].start << "," << week[i].finish << ") -> aula " << room[i] << '\n';
    }

    const std::vector<Interval> tricky = {{10, 11}, {8, 12}, {11, 17}, {15, 16}};
    std::cout << "Clases [10,11) [8,12) [11,17) [15,16)\n";
    std::cout << "  llenar aula por aula con la selección: " << rooms_by_repeated_selection(tricky) << " aulas\n";
    std::cout << "  por orden de inicio: " << rooms_used(assign_rooms(tricky)) << " aulas\n";
    std::cout << "  mínimo por fuerza bruta: " << min_rooms_brute(tricky) << " aulas\n";
    std::cout << "  máximo simultáneo: " << max_simultaneous(tricky) << '\n';

    const std::vector<Interval> touching = {{8, 10}, {10, 12}};
    std::cout << "Clases [8,10) y [10,12): se cruzan = " << (overlap(touching[0], touching[1]) ? "sí" : "no")
              << ", aulas = " << rooms_used(assign_rooms(touching)) << '\n';
}
intervals.hDescargar intervals.h
// Intervalos semiabiertos [start, finish): una clase que termina a las 10 no se cruza con
// una que empieza a las 10. Siempre start < finish.
#pragma once

#include <algorithm>
#include <cstddef>
#include <functional>
#include <queue>
#include <utility>
#include <vector>

struct Interval {
    int start;
    int finish;
    bool operator==(const Interval&) const = default;
};

inline bool overlap(const Interval& a, const Interval& b) {
    return a.start < b.finish && b.start < a.finish;
}

// Válida: ningún par se cruza. No dice nada sobre si es la más grande posible.
inline bool is_valid(const std::vector<Interval>& chosen) {
    for (std::size_t i = 0; i < chosen.size(); i++) {
        for (std::size_t j = i + 1; j < chosen.size(); j++) {
            if (overlap(chosen[i], chosen[j])) return false;
        }
    }
    return true;
}

// La regla correcta: en orden de finalización, tomar lo que no se cruce con lo último tomado.
inline std::vector<Interval> select_by_finish(std::vector<Interval> classes) {
    std::sort(classes.begin(), classes.end(),
              [](const Interval& a, const Interval& b) { return a.finish < b.finish; });
    std::vector<Interval> chosen;
    for (const Interval& c : classes) {
        // Invariante: chosen es válida y está ordenada por tiempo.
        if (chosen.empty() || chosen.back().finish <= c.start) {
            chosen.push_back(c);
        }
    }
    return chosen;
}

// Un greedy genérico: recorre según una clave y toma lo que no se cruce con nada de lo tomado.
template <typename Key>
std::vector<Interval> greedy_by_key(std::vector<Interval> classes, Key key) {
    std::stable_sort(classes.begin(), classes.end(),
                     [&](const Interval& a, const Interval& b) { return key(a) < key(b); });
    std::vector<Interval> chosen;
    for (const Interval& c : classes) {
        const bool fits = std::none_of(chosen.begin(), chosen.end(),
                                       [&](const Interval& x) { return overlap(x, c); });
        if (fits) chosen.push_back(c);
    }
    return chosen;
}

inline std::vector<Interval> rule_earliest_start(const std::vector<Interval>& classes) {
    return greedy_by_key(classes, [](const Interval& c) { return c.start; });
}

inline std::vector<Interval> rule_shortest(const std::vector<Interval>& classes) {
    return greedy_by_key(classes, [](const Interval& c) { return c.finish - c.start; });
}

inline std::vector<Interval> rule_earliest_finish(const std::vector<Interval>& classes) {
    return greedy_by_key(classes, [](const Interval& c) { return c.finish; });
}

// Cuántas clases de `pool` se cruzan con pool[i], sin contarla a ella.
inline int conflicts(const std::vector<Interval>& pool, std::size_t i) {
    int total = 0;
    for (std::size_t j = 0; j < pool.size(); j++) {
        if (j != i && overlap(pool[i], pool[j])) total++;
    }
    return total;
}

// Menos cruces: entre las clases que quedan, tomar la que menos se cruza con las demás que
// quedan, sacar las que se cruzan con ella y repetir. Con empate, la primera de la lista.
inline std::vector<Interval> rule_fewest_conflicts(std::vector<Interval> pool) {
    std::vector<Interval> chosen;
    while (!pool.empty()) {
        std::size_t best = 0;
        for (std::size_t i = 1; i < pool.size(); i++) {
            if (conflicts(pool, i) < conflicts(pool, best)) best = i;
        }
        const Interval picked = pool[best];
        chosen.push_back(picked);
        std::erase_if(pool, [&](const Interval& c) { return overlap(c, picked); });
    }
    return chosen;
}

// Los tamaños que puede dar menos cruces según cómo se resuelvan los empates: (menor, mayor).
inline std::pair<int, int> fewest_conflicts_range(const std::vector<Interval>& pool) {
    if (pool.empty()) return {0, 0};
    int fewest = conflicts(pool, 0);
    for (std::size_t i = 1; i < pool.size(); i++) {
        fewest = std::min(fewest, conflicts(pool, i));
    }
    int low = static_cast<int>(pool.size());
    int high = 0;
    for (std::size_t i = 0; i < pool.size(); i++) {
        if (conflicts(pool, i) != fewest) continue;
        std::vector<Interval> rest;
        for (const Interval& c : pool) {
            if (!overlap(c, pool[i])) rest.push_back(c);
        }
        const auto [rest_low, rest_high] = fewest_conflicts_range(rest);
        low = std::min(low, rest_low + 1);
        high = std::max(high, rest_high + 1);
    }
    return {low, high};
}

// El óptimo por fuerza bruta: prueba los 2^n subconjuntos. Solo para n pequeño.
inline int max_compatible_brute(const std::vector<Interval>& classes) {
    const unsigned n = static_cast<unsigned>(classes.size());
    int best = 0;
    for (unsigned mask = 0; mask < (1u << n); mask++) {
        std::vector<Interval> subset;
        for (unsigned i = 0; i < n; i++) {
            if (mask & (1u << i)) subset.push_back(classes[i]);
        }
        if (static_cast<int>(subset.size()) > best && is_valid(subset)) {
            best = static_cast<int>(subset.size());
        }
    }
    return best;
}

// ¿Alguna solución óptima contiene a classes[index]? También por fuerza bruta.
inline bool in_some_optimum(const std::vector<Interval>& classes, std::size_t index) {
    std::vector<Interval> rest;
    for (std::size_t i = 0; i < classes.size(); i++) {
        if (!overlap(classes[i], classes[index])) rest.push_back(classes[i]);
    }
    return 1 + max_compatible_brute(rest) == max_compatible_brute(classes);
}

// Cuántas clases hay a la vez en el momento más lleno. En un mismo instante, primero salen
// las que terminan y luego entran las que empiezan: por eso el fin se ordena con -1.
inline int max_simultaneous(const std::vector<Interval>& classes) {
    std::vector<std::pair<int, int>> events;
    for (const Interval& c : classes) {
        events.push_back({c.start, +1});
        events.push_back({c.finish, -1});
    }
    std::sort(events.begin(), events.end());
    int now = 0;
    int most = 0;
    for (const auto& event : events) {
        now += event.second;
        most = std::max(most, now);
    }
    return most;
}

// Reparte las clases en aulas: devuelve el número de aula (desde 0) de cada clase, en el
// orden de entrada. Por orden de inicio, cada clase va a un aula libre si existe.
inline std::vector<int> assign_rooms(const std::vector<Interval>& classes) {
    std::vector<std::size_t> order(classes.size());
    for (std::size_t i = 0; i < order.size(); i++) order[i] = i;
    std::sort(order.begin(), order.end(), [&](std::size_t x, std::size_t y) {
        if (classes[x].start != classes[y].start) return classes[x].start < classes[y].start;
        return x < y;  // con empate, el orden de entrada: así el resultado no depende de std::sort
    });

    using RoomEnd = std::pair<int, int>;  // (hora en que se desocupa, aula)
    std::priority_queue<RoomEnd, std::vector<RoomEnd>, std::greater<RoomEnd>> busy;
    std::vector<int> room(classes.size());
    int opened = 0;
    for (std::size_t i : order) {
        if (!busy.empty() && busy.top().first <= classes[i].start) {
            room[i] = busy.top().second;
            busy.pop();
        } else {
            room[i] = opened++;
        }
        busy.push({classes[i].finish, room[i]});
    }
    return room;
}

inline int rooms_used(const std::vector<int>& room) {
    return room.empty() ? 0 : 1 + *std::max_element(room.begin(), room.end());
}

// Una idea que suena bien para las aulas: llenar la primera con la selección óptima, quitar
// esas clases, llenar la segunda con lo que quede, y así. Devuelve cuántas aulas usa.
inline int rooms_by_repeated_selection(std::vector<Interval> classes) {
    int rooms = 0;
    while (!classes.empty()) {
        std::vector<Interval> filled = select_by_finish(classes);
        for (const Interval& c : filled) {
            classes.erase(std::find(classes.begin(), classes.end(), c));
        }
        rooms++;
    }
    return rooms;
}

// El mínimo de aulas por vuelta atrás, sin usar ningún teorema: se prueba poner cada clase
// en cada aula abierta o en una nueva. Solo para n pequeño.
inline void min_rooms_search(const std::vector<Interval>& classes, std::size_t next,
                             std::vector<std::vector<Interval>>& rooms, int& best) {
    if (static_cast<int>(rooms.size()) >= best) return;
    if (next == classes.size()) {
        best = static_cast<int>(rooms.size());
        return;
    }
    const Interval c = classes[next];
    for (std::size_t r = 0; r < rooms.size(); r++) {
        const bool fits = std::none_of(rooms[r].begin(), rooms[r].end(),
                                       [&](const Interval& x) { return overlap(x, c); });
        if (fits) {
            rooms[r].push_back(c);
            min_rooms_search(classes, next + 1, rooms, best);
            rooms[r].pop_back();
        }
    }
    rooms.push_back({c});
    min_rooms_search(classes, next + 1, rooms, best);
    rooms.pop_back();
}

inline int min_rooms_brute(const std::vector<Interval>& classes) {
    if (classes.empty()) return 0;
    int best = static_cast<int>(classes.size()) + 1;
    std::vector<std::vector<Interval>> rooms;
    min_rooms_search(classes, 0, rooms, best);
    return best;
}
lower_bound_table.cppDescargar lower_bound_table.cpp
// Primera tabla: recorre todas las permutaciones y cuenta las comparaciones de dos
// ordenamientos reales, frente a la cota ⌈log₂ n!⌉.
// Segunda tabla: compara log₂ n! con las expresiones de la demostración.
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <set>
#include <string>
#include <vector>

#include "comparison_sorts.h"

struct Summary {
    long long best = -1;
    long long worst = 0;
    std::size_t leaves = 0;
};

template <typename Sort>
Summary explore(int n, Sort sort_function) {
    std::vector<int> input(static_cast<std::size_t>(n));
    std::iota(input.begin(), input.end(), 0);
    Summary s;
    std::set<std::string> paths;
    do {
        std::vector<int> a = input;
        CountingLess less;
        sort_function(a, less);
        s.best = s.best < 0 ? less.count() : std::min(s.best, less.count());
        s.worst = std::max(s.worst, less.count());
        paths.insert(less.path());
    } while (std::next_permutation(input.begin(), input.end()));
    s.leaves = paths.size();
    return s;
}

int main() {
    std::cout << " n       n!  cota  inserción       mergesort       hojas distintas\n";
    std::cout << "                   mejor  peor     mejor  peor     ins.     merge\n";
    for (int n = 1; n <= 9; n++) {
        const Summary ins = explore(n, insertion_sort);
        const Summary mer = explore(n, merge_sort);
        unsigned long long factorial = 1;
        for (int i = 2; i <= n; i++) factorial *= static_cast<unsigned long long>(i);
        std::cout << std::setw(2) << n << std::setw(9) << factorial << std::setw(6)
                  << ceil_log2_factorial(n) << std::setw(8) << ins.best << std::setw(6) << ins.worst
                  << std::setw(9) << mer.best << std::setw(6) << mer.worst << std::setw(9)
                  << ins.leaves << std::setw(10) << mer.leaves << '\n';
    }

    std::cout << '\n' << "       n     (n/4) log n   (n/2) log(n/2)        log n!         n log n\n";
    std::cout << std::fixed << std::setprecision(0);
    double log_factorial = 0;
    int n = 1;
    for (int target : {4, 16, 256, 4096, 65536, 1048576}) {
        while (n < target) {
            n++;
            log_factorial += std::log2(n);
        }
        const double m = n;
        std::cout << std::setw(8) << n << std::setw(15) << m / 4 * std::log2(m) << std::setw(17)
                  << m / 2 * std::log2(m / 2) << std::setw(14) << log_factorial << std::setw(16)
                  << m * std::log2(m) << '\n';
    }
}
quicksort_levels.cppDescargar quicksort_levels.cpp
// Mide quicksort: los conteos son exactos y salen iguales con la misma semilla.
// Los tiempos dependen de la máquina. Compilado con -O2.
#include <algorithm>
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>

#include "quicksort.h"

using Sorter = void (*)(std::vector<int>&, int, int, Pivot, std::mt19937&, QuickStats&, int);

std::vector<int> make_input(const std::string& kind, int n, std::mt19937& rng) {
    std::vector<int> a(static_cast<std::size_t>(n));
    if (kind == "iguales") return a;
    std::iota(a.begin(), a.end(), 0);
    if (kind == "al azar") {
        for (std::size_t i = a.size(); i > 1; i--) std::swap(a[i - 1], a[rng() % i]);
    }
    return a;
}

void row(const std::string& name, Sorter sorter, Pivot rule, const std::string& kind, int n) {
    std::mt19937 rng(2026);
    std::vector<int> a = make_input(kind, n, rng);
    QuickStats stats;
    const auto begin = std::chrono::steady_clock::now();
    sorter(a, 0, n, rule, rng, stats, 1);
    const std::chrono::duration<double, std::milli> ms = std::chrono::steady_clock::now() - begin;
    if (!std::is_sorted(a.begin(), a.end())) std::cout << "NO QUEDO ORDENADO\n";
    std::cout << std::left << std::setw(18) << name << std::setw(10) << kind << std::right
              << std::setw(7) << n << std::setw(12) << stats.comparisons << std::setw(8)
              << stats.max_depth << std::setw(8) << std::setprecision(2) << std::fixed
              << static_cast<double>(stats.comparisons) / (n * std::log2(n)) << std::setw(10)
              << std::setprecision(1) << ms.count() << '\n';
}

int main() {
    std::cout << "pivote y partición  entrada       n  comparac.  niveles  c/nlogn      ms\n";
    Sorter two = quicksort_two_way;
    Sorter three = quicksort_three_way;
    for (int n : {4000, 16000, 64000}) {
        row("primero, en dos", two, Pivot::First, "ordenada", n);
        row("primero, en dos", two, Pivot::First, "al azar", n);
        row("azar, en dos", two, Pivot::Random, "ordenada", n);
        row("azar, en dos", two, Pivot::Random, "al azar", n);
        row("azar, en dos", two, Pivot::Random, "iguales", n);
        row("azar, en tres", three, Pivot::Random, "iguales", n);
        row("azar, en tres", three, Pivot::Random, "ordenada", n);
        std::cout << '\n';
    }

    // La misma entrada ordenada, con 1000 semillas: lo único que cambia es el azar.
    const int n = 64000;
    long long min_c = -1;
    long long max_c = 0;
    long long sum_c = 0;
    int min_d = n;
    int max_d = 0;
    long long sum_d = 0;
    long long calls = 0;
    long long good = 0;
    const int seeds = 1000;
    for (int seed = 1; seed <= seeds; seed++) {
        std::mt19937 rng(static_cast<unsigned>(seed));
        std::vector<int> a = make_input("ordenada", n, rng);
        QuickStats stats;
        quicksort_two_way(a, 0, n, Pivot::Random, rng, stats, 1);
        min_c = min_c < 0 ? stats.comparisons : std::min(min_c, stats.comparisons);
        max_c = std::max(max_c, stats.comparisons);
        sum_c += stats.comparisons;
        min_d = std::min(min_d, stats.max_depth);
        max_d = std::max(max_d, stats.max_depth);
        sum_d += stats.max_depth;
        calls += stats.large_calls;
        good += stats.good_pivots;
    }
    std::cout << "entrada ordenada, n = " << n << ", " << seeds << " semillas\n";
    std::cout << "comparaciones: mínimo " << min_c << ", promedio " << sum_c / seeds << ", máximo " << max_c << '\n';
    std::cout << "niveles:       mínimo " << min_d << ", promedio " << sum_d / seeds << ", máximo " << max_d << '\n';
    std::cout << "pivotes buenos, en llamadas de 16 o más elementos: " << std::setprecision(1)
              << 100.0 * static_cast<double>(good) / static_cast<double>(calls) << " %\n";
}
quicksort_trace.cppDescargar quicksort_trace.cpp
// Tamaños de los rangos por nivel, con 16 elementos ya ordenados. Un punto es un rango de un elemento.
#include <iostream>
#include <map>
#include <numeric>
#include <random>
#include <utility>
#include <vector>

#include "quicksort.h"

namespace {

using Levels = std::map<int, std::vector<int>>;

void sizes(std::vector<int>& a, int lo, int hi, Pivot rule, std::mt19937& rng, int depth, Levels& levels) {
    if (hi - lo < 1) return;
    levels[depth].push_back(hi - lo);
    if (hi - lo < 2) return;
    choose_pivot(a, lo, hi, rule, rng);
    const int pivot = a[static_cast<std::size_t>(lo)];
    int p = lo;
    for (int i = lo + 1; i < hi; i++) {
        if (a[static_cast<std::size_t>(i)] < pivot) {
            p++;
            std::swap(a[static_cast<std::size_t>(i)], a[static_cast<std::size_t>(p)]);
        }
    }
    std::swap(a[static_cast<std::size_t>(lo)], a[static_cast<std::size_t>(p)]);
    sizes(a, lo, p, rule, rng, depth + 1, levels);
    sizes(a, p + 1, hi, rule, rng, depth + 1, levels);
}

void show(const char* title, Pivot rule) {
    std::vector<int> a(16);
    std::iota(a.begin(), a.end(), 0);
    std::mt19937 rng(7);
    Levels levels;
    sizes(a, 0, 16, rule, rng, 1, levels);
    std::cout << title << '\n';
    for (const auto& [depth, list] : levels) {
        std::cout << "  nivel " << depth << ":";
        for (int size : list) {
            if (size == 1) {
                std::cout << " .";
            } else {
                std::cout << ' ' << size;
            }
        }
        std::cout << '\n';
    }
}

}  // namespace

int main() {
    show("Primer elemento como pivote:", Pivot::First);
    show("Pivote al azar, semilla 7:", Pivot::Random);
}
quicksort.hDescargar quicksort.h
// Quicksort con contadores: comparaciones, profundidad y pivotes buenos.
// Trabaja sobre rangos semiabiertos [lo, hi).
#pragma once

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

enum class Pivot { First, Random };

struct QuickStats {
    long long comparisons = 0;
    int max_depth = 0;           // niveles de llamadas con al menos dos elementos
    long long large_calls = 0;   // llamadas con al menos 16 elementos
    long long good_pivots = 0;   // de esas, las que dejaron al pivote en el medio
};

// El pivote va a a[lo] según la regla. % tiene un sesgo despreciable aquí, y da los mismos
// resultados en toda máquina, cosa que std::uniform_int_distribution no promete.
inline void choose_pivot(std::vector<int>& a, int lo, int hi, Pivot rule, std::mt19937& rng) {
    if (rule == Pivot::Random) {
        const auto size = static_cast<unsigned>(hi - lo);
        std::swap(a[static_cast<std::size_t>(lo)], a[static_cast<std::size_t>(lo) + rng() % size]);
    }
}

// Partición en dos: al terminar, [lo, p) < pivote ≤ [p + 1, hi).
inline void quicksort_two_way(std::vector<int>& a, int lo, int hi, Pivot rule, std::mt19937& rng,
                              QuickStats& stats, int depth = 1) {
    const int size = hi - lo;
    if (size < 2) return;
    if (depth > stats.max_depth) stats.max_depth = depth;
    choose_pivot(a, lo, hi, rule, rng);
    const int pivot = a[static_cast<std::size_t>(lo)];
    int p = lo;
    for (int i = lo + 1; i < hi; i++) {
        stats.comparisons++;
        if (a[static_cast<std::size_t>(i)] < pivot) {
            p++;
            std::swap(a[static_cast<std::size_t>(i)], a[static_cast<std::size_t>(p)]);
        }
    }
    std::swap(a[static_cast<std::size_t>(lo)], a[static_cast<std::size_t>(p)]);
    if (size >= 16) {
        // Pivote bueno: su posición final cae en la mitad central del rango.
        const int rank = p - lo;
        stats.large_calls++;
        if (size / 4 <= rank && rank < size - size / 4) stats.good_pivots++;
    }
    quicksort_two_way(a, lo, p, rule, rng, stats, depth + 1);
    quicksort_two_way(a, p + 1, hi, rule, rng, stats, depth + 1);
}

// Partición en tres: [lo, lt) < pivote, [lt, gt) = pivote, [gt, hi) > pivote.
// Los iguales al pivote ya quedan en su lugar y no vuelven a tocarse.
inline void quicksort_three_way(std::vector<int>& a, int lo, int hi, Pivot rule, std::mt19937& rng,
                                QuickStats& stats, int depth = 1) {
    if (hi - lo < 2) return;
    if (depth > stats.max_depth) stats.max_depth = depth;
    choose_pivot(a, lo, hi, rule, rng);
    const int pivot = a[static_cast<std::size_t>(lo)];
    int lt = lo;
    int gt = hi;
    int i = lo + 1;
    while (i < gt) {
        const int value = a[static_cast<std::size_t>(i)];
        stats.comparisons++;
        if (value < pivot) {
            std::swap(a[static_cast<std::size_t>(i)], a[static_cast<std::size_t>(lt)]);
            lt++;
            i++;
        } else {
            stats.comparisons++;
            if (value > pivot) {
                gt--;
                std::swap(a[static_cast<std::size_t>(i)], a[static_cast<std::size_t>(gt)]);
            } else {
                i++;
            }
        }
    }
    quicksort_three_way(a, lo, lt, rule, rng, stats, depth + 1);
    quicksort_three_way(a, gt, hi, rule, rng, stats, depth + 1);
}
rules_random.cppDescargar rules_random.cpp
// Compara las cuatro reglas con la fuerza bruta en instancias al azar.
#include <iomanip>
#include <iostream>
#include <random>
#include <vector>

#include "intervals.h"

int main() {
    std::mt19937 rng(404);
    const int rounds = 20000;
    int invalid = 0;
    int bad_start = 0;
    int bad_short = 0;
    int bad_conflicts = 0;
    int bad_finish = 0;
    for (int round = 0; round < rounds; round++) {
        const int n = 2 + static_cast<int>(rng() % 11);  // de 2 a 12 clases
        std::vector<Interval> classes;
        for (int i = 0; i < n; i++) {
            const int start = static_cast<int>(rng() % 20);
            const int length = 1 + static_cast<int>(rng() % 8);
            classes.push_back({start, start + length});
        }
        const std::size_t best = static_cast<std::size_t>(max_compatible_brute(classes));
        const auto by_start = rule_earliest_start(classes);
        const auto by_short = rule_shortest(classes);
        const auto by_conflicts = rule_fewest_conflicts(classes);
        const auto by_finish = select_by_finish(classes);
        if (!is_valid(by_start) || !is_valid(by_short) || !is_valid(by_conflicts) || !is_valid(by_finish)) invalid++;
        bad_start += by_start.size() != best;
        bad_short += by_short.size() != best;
        bad_conflicts += by_conflicts.size() != best;
        bad_finish += by_finish.size() != best;
    }
    std::cout << rounds << " instancias de 2 a 12 clases\n";
    std::cout << "respuestas inválidas de cualquier regla: " << invalid << '\n';
    std::cout << "instancias en que la regla no llega al óptimo:\n";
    const auto line = [&](const char* name, int bad) {
        std::cout << "  " << name << std::setw(7) << bad << "  (" << std::fixed << std::setprecision(2)
                  << 100.0 * bad / rounds << " %)\n";
    };
    line("empieza antes", bad_start);
    line("dura menos   ", bad_short);
    line("menos cruces ", bad_conflicts);
    line("termina antes", bad_finish);
}
sort_three.cppDescargar sort_three.cpp
// Sigue sort_three con las 6 permutaciones: cada una termina en una hoja distinta.
#include <algorithm>
#include <iostream>
#include <vector>

#include "comparison_sorts.h"

int main() {
    std::vector<int> input = {1, 2, 3};
    std::cout << "entrada  respuestas  comparaciones  salida\n";
    do {
        std::vector<int> a = input;
        CountingLess less;
        sort_three(a, less);
        std::cout << input[0] << ' ' << input[1] << ' ' << input[2] << "    " << less.path();
        for (std::size_t pad = less.path().size(); pad < 12; pad++) std::cout << ' ';
        std::cout << less.count() << "              " << a[0] << ' ' << a[1] << ' ' << a[2] << '\n';
    } while (std::next_permutation(input.begin(), input.end()));
}

Pruebas

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

test_examples.cppDescargar test_examples.cpp
// Comprueba lo que afirman los apuntes. Se compila con sanitizers.
#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <numeric>
#include <random>
#include <set>
#include <string>
#include <vector>

#include "adversary.h"
#include "comparison_sorts.h"
#include "counting_sort.h"
#include "intervals.h"
#include "quicksort.h"

namespace {

// Ordenar 3 elementos: 3 comparaciones en el peor caso y 6 hojas distintas.
void test_sort_three() {
    std::vector<int> input = {1, 2, 3};
    long long worst = 0;
    std::set<std::string> leaves;
    do {
        std::vector<int> a = input;
        CountingLess less;
        sort_three(a, less);
        assert(std::is_sorted(a.begin(), a.end()));
        worst = std::max(worst, less.count());
        leaves.insert(less.path());
    } while (std::next_permutation(input.begin(), input.end()));
    assert(worst == 3);
    assert(leaves.size() == 6);
    assert(ceil_log2_factorial(3) == 3);
}

template <typename Sort>
void check_bound(int n, Sort sort_function, long long& best, long long& worst) {
    std::vector<int> input(static_cast<std::size_t>(n));
    std::iota(input.begin(), input.end(), 0);
    std::set<std::string> leaves;
    best = -1;
    worst = 0;
    do {
        std::vector<int> a = input;
        CountingLess less;
        sort_function(a, less);
        assert(std::is_sorted(a.begin(), a.end()));
        best = best < 0 ? less.count() : std::min(best, less.count());
        worst = std::max(worst, less.count());
        leaves.insert(less.path());
    } while (std::next_permutation(input.begin(), input.end()));
    std::size_t factorial = 1;
    for (int i = 2; i <= n; i++) factorial *= static_cast<std::size_t>(i);
    assert(leaves.size() == factorial);          // una hoja por permutación
    assert(worst >= ceil_log2_factorial(n));     // nadie baja de la cota
}

// Ningún ordenamiento real baja de la cota, aunque su mejor caso sí esté por debajo.
void test_lower_bound() {
    assert(ceil_log2_factorial(1) == 0);
    assert(ceil_log2_factorial(2) == 1);
    assert(ceil_log2_factorial(4) == 5);
    assert(ceil_log2_factorial(5) == 7);
    assert(ceil_log2_factorial(20) == 62);
    for (int n = 2; n <= 7; n++) {
        long long best = 0;
        long long worst = 0;
        check_bound(n, insertion_sort, best, worst);
        assert(best == n - 1);
        assert(worst == n * (n - 1) / 2);
        check_bound(n, merge_sort, best, worst);
    }
    // El mejor caso de inserción está por debajo de la cota: no la contradice.
    long long best = 0;
    long long worst = 0;
    check_bound(6, insertion_sort, best, worst);
    assert(best < ceil_log2_factorial(6));
}

// El adversario obliga a cualquier ordenamiento a llegar a la cota, y su entrada lo reproduce:
// el mismo algoritmo, corrido con esa entrada ya fija, hace las mismas comparaciones.
void test_adversary() {
    for (int n = 2; n <= 7; n++) {
        Adversary against_insertion(n);
        const auto order = insertion_order(n, [&](int i, int j) { return against_insertion.less(i, j); });
        assert(against_insertion.count() >= ceil_log2_factorial(n));
        assert(against_insertion.alive() == 1);  // si quedaran dos, el algoritmo no habría terminado
        const std::vector<int> input = against_insertion.witness();
        long long replay = 0;
        const auto again = insertion_order(n, [&](int i, int j) {
            replay++;
            return input[static_cast<std::size_t>(i)] < input[static_cast<std::size_t>(j)];
        });
        assert(replay == against_insertion.count());
        assert(again == order);

        Adversary against_merge(n);
        merge_order(n, [&](int i, int j) { return against_merge.less(i, j); });
        assert(against_merge.count() >= ceil_log2_factorial(n));
        assert(against_merge.alive() == 1);
    }
}

// La desigualdad de la demostración: log₂ n! ≥ (n/2) log₂(n/2) ≥ (n/4) log₂ n para n ≥ 4.
void test_factorial_inequality() {
    double log_factorial = std::log2(2.0) + std::log2(3.0);
    for (int n = 4; n <= 100000; n++) {
        log_factorial += std::log2(static_cast<double>(n));
        const double m = n;
        assert(log_factorial >= m / 2 * std::log2(m / 2));
        assert(m / 2 * std::log2(m / 2) >= m / 4 * std::log2(m) - 1e-9);
        assert(log_factorial <= m * std::log2(m));
    }
}

void test_counting_sort() {
    std::mt19937 rng(11);
    for (int round = 0; round < 200; round++) {
        const int k = 1 + static_cast<int>(rng() % 50);
        std::vector<int> a(rng() % 100);
        for (int& value : a) value = static_cast<int>(rng() % static_cast<unsigned>(k));
        std::vector<int> expected = a;
        std::sort(expected.begin(), expected.end());
        counting_sort(a, k);
        assert(a == expected);
    }
}

void test_quicksort() {
    std::mt19937 rng(12);
    // Todas las variantes ordenan, con repetidos o sin ellos.
    for (int round = 0; round < 300; round++) {
        std::vector<int> a(rng() % 200);
        const unsigned range = 1 + rng() % 300;
        for (int& value : a) value = static_cast<int>(rng() % range);
        std::vector<int> expected = a;
        std::sort(expected.begin(), expected.end());
        const int n = static_cast<int>(a.size());
        for (Pivot rule : {Pivot::First, Pivot::Random}) {
            std::vector<int> b = a;
            QuickStats s2;
            quicksort_two_way(b, 0, n, rule, rng, s2);
            assert(b == expected);
            std::vector<int> c = a;
            QuickStats s3;
            quicksort_three_way(c, 0, n, rule, rng, s3);
            assert(c == expected);
        }
    }

    const int n = 1000;
    std::vector<int> sorted(n);
    std::iota(sorted.begin(), sorted.end(), 0);

    // Primer elemento sobre una entrada ordenada: exactamente n(n-1)/2 y n-1 niveles.
    {
        std::vector<int> a = sorted;
        QuickStats s;
        quicksort_two_way(a, 0, n, Pivot::First, rng, s);
        assert(s.comparisons == n * (n - 1) / 2);
        assert(s.max_depth == n - 1);
    }
    // Pivote al azar sobre la misma entrada: niveles logarítmicos, con esta semilla fija.
    {
        std::mt19937 fixed(2026);
        std::vector<int> a = sorted;
        QuickStats s;
        quicksort_two_way(a, 0, n, Pivot::Random, fixed, s);
        assert(s.comparisons < n * (n - 1) / 20);
        assert(s.max_depth < 60);
    }
    // Todos iguales: el azar no salva a la partición en dos; la partición en tres sí.
    {
        std::vector<int> equal(n, 7);
        std::mt19937 fixed(2026);
        QuickStats two;
        quicksort_two_way(equal, 0, n, Pivot::Random, fixed, two);
        assert(two.comparisons == n * (n - 1) / 2);
        QuickStats three;
        quicksort_three_way(equal, 0, n, Pivot::Random, fixed, three);
        assert(three.max_depth == 1);
        assert(three.comparisons == 2 * (n - 1));
    }
}

// Intervalos semiabiertos: el borde no es un cruce.
void test_half_open() {
    assert(!overlap({8, 10}, {10, 12}));
    assert(!overlap({10, 12}, {8, 10}));
    assert(overlap({8, 10}, {9, 12}));
    assert(overlap({8, 12}, {9, 10}));
    assert(overlap({8, 10}, {8, 10}));
    assert(is_valid({{8, 10}, {10, 12}, {12, 14}}));
    assert(!is_valid({{8, 11}, {10, 12}}));

    const std::vector<Interval> touching = {{8, 10}, {10, 12}};
    assert(select_by_finish(touching).size() == 2);
    assert(max_simultaneous(touching) == 1);
    assert(rooms_used(assign_rooms(touching)) == 1);
    assert(min_rooms_brute(touching) == 1);
}

// Los tres contraejemplos: la respuesta es válida y aun así no es óptima, y la primera
// elección de la regla no está en ninguna solución óptima.
void test_counterexamples() {
    const std::vector<Interval> a = {{8, 18}, {9, 11}, {11, 13}, {13, 15}};
    assert(max_compatible_brute(a) == 3);
    assert(rule_earliest_start(a).size() == 1);
    assert(is_valid(rule_earliest_start(a)));
    assert(!in_some_optimum(a, 0));
    assert(select_by_finish(a).size() == 3);

    const std::vector<Interval> b = {{8, 12}, {12, 16}, {11, 13}};
    assert(max_compatible_brute(b) == 2);
    assert(rule_shortest(b).size() == 1);
    assert(is_valid(rule_shortest(b)));
    assert(!in_some_optimum(b, 2));
    assert(select_by_finish(b).size() == 2);

    const std::vector<Interval> c = {{0, 2}, {2, 4}, {4, 6}, {6, 8}, {1, 3}, {1, 3},
                                     {1, 3}, {3, 5}, {5, 7}, {5, 7}, {5, 7}};
    assert(max_compatible_brute(c) == 4);
    assert(conflicts(c, 7) == 2);
    for (std::size_t i = 0; i < c.size(); i++) {
        if (i != 7) assert(conflicts(c, i) > 2);  // la del centro es la única con menos cruces
    }
    assert(rule_fewest_conflicts(c).size() == 3);
    assert(is_valid(rule_fewest_conflicts(c)));
    assert(!in_some_optimum(c, 7));
    assert((fewest_conflicts_range(c) == std::pair<int, int>{3, 3}));
    assert(select_by_finish(c).size() == 4);
}

// Termina antes coincide con la fuerza bruta; aulas coincide con el máximo simultáneo y
// con la vuelta atrás. Tiempos en un rango chico para que abunden empates y bordes.
void test_random_against_brute() {
    std::mt19937 rng(13);
    for (int round = 0; round < 1500; round++) {
        const int n = 1 + static_cast<int>(rng() % 9);
        std::vector<Interval> classes;
        for (int i = 0; i < n; i++) {
            const int start = static_cast<int>(rng() % 10);
            classes.push_back({start, start + 1 + static_cast<int>(rng() % 5)});
        }
        const auto chosen = select_by_finish(classes);
        assert(is_valid(chosen));
        assert(static_cast<int>(chosen.size()) == max_compatible_brute(classes));
        assert(is_valid(rule_earliest_start(classes)));
        assert(is_valid(rule_shortest(classes)));
        assert(is_valid(rule_fewest_conflicts(classes)));

        const auto room = assign_rooms(classes);
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                const auto ui = static_cast<std::size_t>(i);
                const auto uj = static_cast<std::size_t>(j);
                if (room[ui] == room[uj]) assert(!overlap(classes[ui], classes[uj]));
            }
        }
        const int depth = max_simultaneous(classes);
        assert(rooms_used(room) == depth);
        assert(min_rooms_brute(classes) == depth);
    }
}

void test_rooms_examples() {
    const std::vector<Interval> week = {{8, 10}, {9, 11}, {10, 12}, {11, 13}, {8, 13}, {12, 14}, {13, 15}, {15, 16}};
    assert(select_by_finish(week).size() == 4);
    assert(max_compatible_brute(week) == 4);
    // El intercambio de los apuntes: en cada paso sigue válida y con 4 clases.
    std::vector<Interval> optimal = {{9, 11}, {11, 13}, {13, 15}, {15, 16}};
    assert(is_valid(optimal));
    const auto chosen = select_by_finish(week);
    for (std::size_t j = 0; j < chosen.size(); j++) {
        assert(chosen[j].finish <= optimal[j].finish);
        optimal[j] = chosen[j];
        assert(is_valid(optimal));
    }
    assert(optimal == chosen);
    assert(max_simultaneous(week) == 3);
    assert(rooms_used(assign_rooms(week)) == 3);

    // Llenar aula por aula gasta una de más.
    const std::vector<Interval> tricky = {{10, 11}, {8, 12}, {11, 17}, {15, 16}};
    assert(rooms_by_repeated_selection(tricky) == 3);
    assert(rooms_used(assign_rooms(tricky)) == 2);
    assert(min_rooms_brute(tricky) == 2);
}

}  // namespace

int main() {
    test_sort_three();
    test_lower_bound();
    test_adversary();
    test_factorial_inequality();
    test_counting_sort();
    test_quicksort();
    test_half_open();
    test_counterexamples();
    test_random_against_brute();
    test_rooms_examples();
    std::cout << "Todas las pruebas de la semana 4 pasaron.\n";
}

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/class_schedule/MakefileDescargar lab/class_schedule/Makefile
TIEMPO = 1
include ../../../lab.mk
lab/class_schedule/README.mdDescargar lab/class_schedule/README.md
# Horarios y aulas

La oficina académica de un instituto recibe la lista de clases de un día y tiene que responder dos
preguntas antes de abrir la matrícula. Si solo consiguiera un aula, ¿cuántas clases de la lista
podría dictar como máximo? Y si quiere dictarlas todas, ¿cuántas aulas tiene que pedir como mínimo?

El sistema del instituto guarda cada instante como un número de segundos contados desde una fecha
fija, y de cada clase guarda cuándo empieza y cuánto dura. Además, después de cada clase el aula
queda ocupada g segundos más, para limpiarla y preparar la siguiente.

Una clase que empieza en el instante s y dura d segundos ocupa su aula desde s hasta justo antes de
s + d + g. Otra clase puede empezar en esa misma aula exactamente en el instante s + d + g: ahí el
aula ya está libre. Dos clases en una misma aula nunca pueden ocuparla a la vez.

## Entrada

La primera línea tiene dos enteros: n, la cantidad de clases, y g, los segundos de preparación.
Cada una de las n líneas siguientes tiene dos enteros: s, el inicio de una clase, y d, su duración.
Las clases vienen en cualquier orden, y puede haber varias con los mismos datos.

## Salida

Una línea con dos enteros separados por un espacio: la mayor cantidad de clases que se pueden dictar
en una sola aula, y la menor cantidad de aulas con la que se pueden dictar todas.

## Límites

- 1 ≤ n ≤ 200 000
- 0 ≤ g ≤ 1 000 000 000
- 0 ≤ s ≤ 2 000 000 000
- 1 ≤ d ≤ 1 000 000 000

## Ejemplo

Entrada:

```text
5 1
0 2
3 2
2 1
6 1
5 3
```

Salida:

```text
3 2
```

Con una sola aula se pueden dictar las clases que empiezan en 0, 3 y 6. La primera deja el aula libre
en 0 + 2 + 1 = 3, y justo ahí empieza la segunda, que la deja libre en 6. No caben cuatro. Para dictarlas
todas bastan dos aulas: una con las clases que empiezan en 0, 3 y 6, y otra con las que empiezan en 2 y 5.
No basta una, porque la clase que empieza en 2 ocupa el aula hasta justo antes de 4, y la que empieza en 3
ya estaría adentro.

## Tiempo

1 segundo por caso, compilado con `-O2`. Corran `make test` para probar con el juez.
lab/class_schedule/solution.cppDescargar lab/class_schedule/solution.cpp
// Horarios y aulas: lee las clases e imprime dos ceros. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>

struct ClassTime {
    int start;
    int duration;
};

int main() {
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);

    int n = 0;
    int gap = 0;
    std::cin >> n >> gap;
    std::vector<ClassTime> classes(static_cast<std::size_t>(n));
    for (ClassTime& c : classes) {
        std::cin >> c.start >> c.duration;
    }

    // Por hacer: calcular las dos respuestas con classes y gap.
    int in_one_room = 0;
    int rooms_needed = 0;

    std::cout << in_one_room << ' ' << rooms_needed << '\n';
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 4: cota inferior para ordenar, quicksort aleatorizado y greedy con intervalos.
# Se usa dentro del entorno del curso.
#
#   make        compila los ejemplos en build/
#   make test   corre las pruebas de los ejemplos, con sanitizers
#   make clean  borra lo generado
#
# El problema de lab/ tiene su propio Makefile y no entra aquí.

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
HEADERS = comparison_sorts.h adversary.h counting_sort.h quicksort.h intervals.h
PROGRAMS = sort_three adversary_demo quicksort_trace interval_traces greedy_rules test_examples
BENCHES = lower_bound_table counting_bench quicksort_levels rules_random

.PHONY: all test clean

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

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

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

$(BUILD):
	mkdir -p $@

test: all
	$(BUILD)/test_examples

clean:
	rm -rf $(BUILD)