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
- ¿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.
- Demuestren que log₂(n!) = Ω(n log n).
- Den un contraejemplo para cada regla: la clase más corta primero, la que empieza antes primero.
- Escriban el argumento de intercambio para la regla de la clase que termina antes.
- 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.
// 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); });
}
}// 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;
}// 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 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: 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;
}
}
}// 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';
}// 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';
}// 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;
}// 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';
}
}// 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";
}// 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 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);
}// 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);
}// 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.
// 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.
TIEMPO = 1
include ../../../lab.mk# 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.// 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.
# 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)