Semana 3 — Más divide y vencerás

Objetivos

  • Hallar el k-ésimo menor en tiempo lineal en el peor caso, y demostrar su costo.
  • Reducir el número de subproblemas para bajar el exponente, como en Karatsuba.
  • Resolver un problema geométrico combinando dos mitades con cuidado.

Sesión A — Selección en tiempo lineal

Lectura: Apuntes, semana 3.

Apoyo: Erickson, sec. 1.8 (Linear-Time Selection). CLRS, capítulo Medians and Order Statistics, 4.ª edición.

Temas:

  • Hallar la mediana sin ordenar. Quickselect: partir y seguir solo por un lado.
  • El peor caso de quickselect es Θ(n²): todo depende del pivote.
  • La mediana de medianas con grupos de 5: al menos unos 3n/10 elementos quedan a cada lado.
  • La recurrencia T(n) ≤ T(n/5) + T(7n/10) + O(n), y por qué da O(n): 1/5 + 7/10 < 1.
  • Por qué 5 y no 3: con grupos de 3 las dos fracciones suman 1.
  • En la práctica: las bibliotecas prefieren un pivote simple o al azar. std::nth_element.
  • Error común: aplicar el teorema maestro a esta recurrencia, que tiene dos subproblemas de distinto tamaño.

Sesión B — Karatsuba · Par más cercano

Lectura: Apuntes, semana 3.

Apoyo: Erickson, sec. 1.9 (Fast Multiplication). Sobre el par más cercano, los apuntes son el texto principal.

Temas:

  • Multiplicar números de n dígitos: la escuela da Θ(n²), y dividir en cuatro productos no mejora nada.
  • Karatsuba: tres productos en vez de cuatro, y T(n) = 3T(n/2) + O(n) = Θ(n^1,585).
  • Por qué en la práctica se cambia a la multiplicación de la escuela con n pequeño.
  • Par más cercano: fuerza bruta en Θ(n²). Dividir por una línea vertical y combinar mirando la franja.
  • En la franja, ordenada por y, cada punto solo tiene que compararse con una cantidad constante de vecinos. Demostración con el empaquetado de cuadrados.
  • O(n log n) si se mantiene el orden por y mientras se mezcla.
  • Error común: ordenar la franja en cada nivel, que da O(n log² n).

Laboratorio — El par más cercano

  1. Leer n puntos de coordenadas enteras e imprimir el cuadrado de la menor distancia, para no usar decimales.
  2. Escribir la fuerza bruta y usarla para generar pruebas al azar contra la versión rápida.
  3. Pasar todos los casos, incluidos los puntos repetidos y todos sobre una misma vertical.
  4. Calcular si el cuadrado de la distancia cabe en int con los límites del enunciado.

Preguntas de repaso

  1. ¿Qué entrada hace que quickselect con el primer elemento como pivote cueste Θ(n²)?
  2. Demuestren que la mediana de medianas deja al menos unos 3n/10 elementos a cada lado.
  3. Comprueben por inducción que T(n) ≤ T(n/5) + T(7n/10) + n da T(n) ≤ 10n.
  4. Si un algoritmo divide en 7 productos de tamaño n/2 con trabajo extra Θ(n²), ¿cuál es su costo? ¿Y comparado con 8 productos?
  5. En el par más cercano, ¿por qué no basta con mirar los puntos más cercanos a la línea en x?

Código de la semana

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

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

Ejemplos

Los programas que usan los apuntes y la clase.

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

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

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

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

#endif
karatsuba_bench.cppDescargar karatsuba_bench.cpp
// Mide la multiplicación de la escuela contra Karatsuba, con -O2 y sin sanitizers.
// Los tiempos dependen de la máquina y de esta implementación. Cada dato es la mediana de varias corridas,
// y cada corrida repite la multiplicación hasta sumar al menos unos milisegundos.
//
//   build/karatsuba_bench            hasta n = 65536
//   build/karatsuba_bench 262144     hasta el n que se indique, potencia de 2; tarda varios minutos
#include <cstdio>
#include <cstdlib>
#include <random>
#include <vector>

#include "bench.h"
#include "karatsuba.h"

static Digits random_number(int n, std::mt19937& rng) {
    Digits a(n);
    for (int& digit : a) {
        digit = static_cast<int>(rng() % 10);
    }
    a[n - 1] = 1 + static_cast<int>(rng() % 9);
    return a;
}

// Microsegundos por multiplicación: 7 corridas, o 3 si cada una tarda más de un segundo.
template <typename Fn>
double micros_per_call(Fn fn) {
    int repeats = 1;
    double first = 0;
    while (true) {
        first = median_seconds(1, [&] {
            for (int i = 0; i < repeats; i++) {
                fn();
            }
        });
        if (first >= 0.005) {
            break;
        }
        repeats *= 2;
    }
    int runs = first > 1.0 ? 3 : 7;
    return 1e6 * median_seconds(runs, [&] {
               for (int i = 0; i < repeats; i++) {
                   fn();
               }
           }) / repeats;
}

int main(int argc, char** argv) {
    int largest = argc > 1 ? std::atoi(argv[1]) : 65536;
    std::mt19937 rng(2026);
    const std::size_t hybrid = 128;
    std::printf("microsegundos por multiplicacion\n");
    std::printf("%8s %12s %12s %12s %9s %9s\n", "n", "escuela", "umbral 3", "umbral 128", "esc/u3", "esc/u128");
    for (int n = 16; n <= largest; n *= 2) {
        Digits x = random_number(n, rng);
        Digits y = random_number(n, rng);
        double school = micros_per_call([&] {
            long long ignored = 0;
            keep(school_multiply(x, y, ignored).size());
        });
        double pure = micros_per_call([&] {
            long long ignored = 0;
            keep(karatsuba(x, y, 3, ignored).size());
        });
        double mixed = micros_per_call([&] {
            long long ignored = 0;
            keep(karatsuba(x, y, hybrid, ignored).size());
        });
        std::printf("%8d %12.1f %12.1f %12.1f %9.2f %9.2f\n", n, school, pure, mixed, school / pure, school / mixed);
    }

    const int n = 16384;
    Digits x = random_number(n, rng);
    Digits y = random_number(n, rng);
    std::printf("\nn = %d con distintos umbrales\n", n);
    std::printf("%8s %12s\n", "umbral", "microseg");
    for (std::size_t threshold : {3, 8, 16, 32, 64, 128, 256, 512, 1024}) {
        double kar = micros_per_call([&] {
            long long ignored = 0;
            keep(karatsuba(x, y, threshold, ignored).size());
        });
        std::printf("%8zu %12.1f\n", threshold, kar);
    }
}
karatsuba_count.cppDescargar karatsuba_count.cpp
// Cuenta productos de un dígito por un dígito. Es un conteo exacto: no depende de la máquina.
#include <cstdio>
#include <random>

#include "karatsuba.h"

static Digits random_number(int n, std::mt19937& rng) {
    Digits a(n);
    for (int& digit : a) {
        digit = static_cast<int>(rng() % 10);
    }
    a[n - 1] = 1 + static_cast<int>(rng() % 9);
    return a;
}

int main() {
    std::mt19937 rng(2026);
    std::printf("%8s %14s %8s %14s %8s\n", "n", "escuela", "razon", "karatsuba", "razon");
    long long previous_school = 0;
    long long previous_kar = 0;
    for (int n = 16; n <= 65536; n *= 2) {
        Digits x = random_number(n, rng);
        Digits y = random_number(n, rng);
        long long school = 0;
        long long kar = 0;
        Digits slow = n <= 8192 ? school_multiply(x, y, school) : Digits{};
        if (n > 8192) {
            school = static_cast<long long>(n) * n;
        }
        Digits fast = karatsuba(x, y, 3, kar);
        if (n <= 8192) {
            assert(slow == fast);
        }
        std::printf("%8d %14lld %8.2f %14lld %8.2f\n", n, school,
                    previous_school == 0 ? 0.0 : static_cast<double>(school) / static_cast<double>(previous_school),
                    kar,
                    previous_kar == 0 ? 0.0 : static_cast<double>(kar) / static_cast<double>(previous_kar));
        previous_school = school;
        previous_kar = kar;
    }
}
karatsuba_trace.cppDescargar karatsuba_trace.cpp
// Sigue el primer nivel de Karatsuba con 1234 x 5678.
#include <iostream>

#include "karatsuba.h"

int main() {
    long long products = 0;
    Digits x = from_string("1234");
    Digits y = from_string("5678");
    Digits a = from_string("12");
    Digits b = from_string("34");
    Digits c = from_string("56");
    Digits d = from_string("78");
    Digits ac = karatsuba(a, c, 3, products);
    Digits bd = karatsuba(b, d, 3, products);
    Digits sum_ab = add(a, b);
    Digits sum_cd = add(c, d);
    Digits sums = karatsuba(sum_ab, sum_cd, 3, products);
    Digits middle = subtract(subtract(sums, ac), bd);
    std::cout << "a = 12, b = 34, c = 56, d = 78, m = 2\n";
    std::cout << "ac = " << to_string(ac) << "\n";
    std::cout << "bd = " << to_string(bd) << "\n";
    std::cout << "(a + b)(c + d) = " << to_string(sum_ab) << " x " << to_string(sum_cd) << " = " << to_string(sums) << "\n";
    std::cout << "ad + bc = " << to_string(sums) << " - " << to_string(ac) << " - " << to_string(bd) << " = " << to_string(middle) << "\n";
    long long other = 0;
    std::cout << "resultado = " << to_string(karatsuba(x, y, 3, other)) << "\n";
    std::cout << "la escuela da = " << to_string(school_multiply(x, y, other)) << "\n";
}
karatsuba.hDescargar karatsuba.h
// Multiplicación de enteros no negativos de muchos dígitos.
// Un número es un vector de dígitos en base 10, del menos significativo al más significativo,
// sin ceros a la izquierda. El cero es el vector vacío.
#ifndef KARATSUBA_H
#define KARATSUBA_H

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <string>
#include <vector>

using Digits = std::vector<int>;

inline void trim(Digits& a) {
    while (!a.empty() && a.back() == 0) {
        a.pop_back();
    }
}

inline Digits from_string(const std::string& text) {
    Digits a;
    for (auto it = text.rbegin(); it != text.rend(); ++it) {
        a.push_back(*it - '0');
    }
    trim(a);
    return a;
}

inline std::string to_string(const Digits& a) {
    if (a.empty()) {
        return "0";
    }
    std::string text;
    for (auto it = a.rbegin(); it != a.rend(); ++it) {
        text.push_back(static_cast<char>('0' + *it));
    }
    return text;
}

inline Digits add(const Digits& a, const Digits& b) {
    Digits sum;
    sum.reserve(std::max(a.size(), b.size()) + 1);
    int carry = 0;
    for (std::size_t i = 0; i < a.size() || i < b.size() || carry != 0; i++) {
        int value = carry;
        if (i < a.size()) {
            value += a[i];
        }
        if (i < b.size()) {
            value += b[i];
        }
        sum.push_back(value % 10);
        carry = value / 10;
    }
    return sum;
}

// Resta a - b. Requiere a >= b.
inline Digits subtract(const Digits& a, const Digits& b) {
    Digits result = a;
    int borrow = 0;
    for (std::size_t i = 0; i < a.size(); i++) {
        int value = a[i] - borrow - (i < b.size() ? b[i] : 0);
        borrow = value < 0 ? 1 : 0;
        result[i] = value + 10 * borrow;
    }
    assert(borrow == 0);
    trim(result);
    return result;
}

// Multiplica por 10^positions.
inline Digits shifted(const Digits& a, std::size_t positions) {
    if (a.empty()) {
        return a;
    }
    Digits result(positions, 0);
    result.insert(result.end(), a.begin(), a.end());
    return result;
}

// Los dígitos desde `start` hasta antes de `stop`, sin ceros a la izquierda.
inline Digits slice(const Digits& a, std::size_t start, std::size_t stop) {
    start = std::min(start, a.size());
    stop = std::min(stop, a.size());
    Digits part(a.begin() + static_cast<long>(start), a.begin() + static_cast<long>(stop));
    trim(part);
    return part;
}

// La multiplicación de la escuela: cada dígito de a con cada dígito de b.
// `digit_products` cuenta los productos de un dígito por un dígito.
inline Digits school_multiply(const Digits& a, const Digits& b, long long& digit_products) {
    if (a.empty() || b.empty()) {
        return {};
    }
    std::vector<long long> cells(a.size() + b.size(), 0);
    for (std::size_t i = 0; i < a.size(); i++) {
        for (std::size_t j = 0; j < b.size(); j++) {
            cells[i + j] += a[i] * b[j];
        }
    }
    digit_products += static_cast<long long>(a.size()) * static_cast<long long>(b.size());
    Digits result(cells.size());
    long long carry = 0;
    for (std::size_t i = 0; i < cells.size(); i++) {
        long long value = cells[i] + carry;
        result[i] = static_cast<int>(value % 10);
        carry = value / 10;
    }
    trim(result);
    return result;
}

// Karatsuba: x = a·10^m + b, y = c·10^m + d, y tres productos en vez de cuatro.
// Con n <= threshold usa la multiplicación de la escuela. threshold debe ser al menos 3: a + b
// puede tener un dígito más que sus partes, y solo con n >= 4 los subproblemas son más cortos que n.
inline Digits karatsuba(const Digits& x, const Digits& y, std::size_t threshold, long long& digit_products) {
    assert(threshold >= 3);
    std::size_t n = std::max(x.size(), y.size());
    if (x.empty() || y.empty()) {
        return {};
    }
    if (n <= threshold) {
        return school_multiply(x, y, digit_products);
    }
    std::size_t m = n / 2;
    Digits b = slice(x, 0, m);
    Digits a = slice(x, m, x.size());
    Digits d = slice(y, 0, m);
    Digits c = slice(y, m, y.size());

    Digits ac = karatsuba(a, c, threshold, digit_products);
    Digits bd = karatsuba(b, d, threshold, digit_products);
    Digits sums = karatsuba(add(a, b), add(c, d), threshold, digit_products);
    Digits middle = subtract(subtract(sums, ac), bd);  // ad + bc

    Digits result = add(add(shifted(ac, 2 * m), shifted(middle, m)), bd);
    trim(result);
    return result;
}

#endif
mom_trace.cppDescargar mom_trace.cpp
// Sigue una vuelta de la mediana de medianas con 23 elementos: grupos, medianas, pivote y conteos.
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <vector>

#include "select.h"

static void print(const std::vector<int>& v) {
    for (std::size_t i = 0; i < v.size(); i++) {
        std::cout << (i == 0 ? "" : " ") << v[i];
    }
}

// Sigue select_linear: una línea por llamada, sin entrar en la búsqueda de cada pivote.
static int trace_select(std::vector<int> a, std::size_t k) {
    long long ignored = 0;
    std::cout << "|A| = " << a.size() << ", k = " << k;
    if (a.size() <= 5) {
        std::sort(a.begin(), a.end());
        std::cout << ": se ordena [";
        print(a);
        std::cout << "] y la respuesta es " << a[k] << "\n";
        return a[k];
    }
    int pivot = median_of_medians(a, ignored);
    Parts parts = partition3(a, pivot, ignored);
    std::cout << ": pivote " << pivot << ", |L| = " << parts.less.size() << ", |E| = " << parts.equal.size()
              << ", |G| = " << parts.greater.size() << "\n";
    if (k < parts.less.size()) {
        return trace_select(parts.less, k);
    }
    if (k < parts.less.size() + parts.equal.size()) {
        std::cout << "k cae en E: la respuesta es " << pivot << "\n";
        return pivot;
    }
    return trace_select(parts.greater, k - parts.less.size() - parts.equal.size());
}

int main() {
    std::vector<int> a = {12, 3, 25, 8, 17, 40, 6, 31, 22, 9, 15, 37, 1, 28, 19, 44, 11, 33, 5, 26, 14, 30, 2};
    long long comparisons = 0;
    std::cout << "n = " << a.size() << "\n";
    for (std::size_t start = 0; start < a.size(); start += 5) {
        std::size_t stop = std::min(start + 5, a.size());
        std::vector<int> group(a.begin() + start, a.begin() + stop);
        std::cout << "grupo: ";
        print(group);
        sort_group(group, comparisons);
        std::cout << "  ordenado: ";
        print(group);
        std::cout << "  mediana: " << group[(group.size() - 1) / 2] << "\n";
    }
    comparisons = 0;
    std::vector<int> medians = group_medians(a, comparisons);
    std::cout << "medianas: ";
    print(medians);
    std::cout << "\n";
    int pivot = median_of_medians(a, comparisons);
    std::cout << "pivote (mediana inferior de las medianas): " << pivot << "\n";
    auto at_most = std::count_if(a.begin(), a.end(), [&](int x) { return x <= pivot; });
    auto at_least = std::count_if(a.begin(), a.end(), [&](int x) { return x >= pivot; });
    std::cout << "elementos <= pivote: " << at_most << "\n";
    std::cout << "elementos >= pivote: " << at_least << "\n";
    std::cout << "cota 3n/10 - 3: " << 3.0 * static_cast<double>(a.size()) / 10 - 3 << "\n";
    long long ignored = 0;
    Parts parts = partition3(a, pivot, ignored);
    std::cout << "menores: " << parts.less.size() << "  iguales: " << parts.equal.size()
              << "  mayores: " << parts.greater.size() << "\n";
    std::cout << "cota 7n/10 + 3: " << 7.0 * static_cast<double>(a.size()) / 10 + 3 << "\n";

    std::cout << "\nla mediana, k = 11\n";
    int answer = trace_select(a, 11);
    std::vector<int> sorted = a;
    std::sort(sorted.begin(), sorted.end());
    std::cout << "ordenando, la posicion 11 es " << sorted[11] << "; la funcion dio " << answer << "\n";
}
pivot_bound.cppDescargar pivot_bound.cpp
// Cuántos elementos deja a cada lado el pivote de la mediana de medianas, con varios tipos de entrada.
#include <algorithm>
#include <cstdio>
#include <random>
#include <string>
#include <utility>
#include <vector>

#include "select.h"

static void report(const std::string& name, const std::vector<int>& a) {
    long long comparisons = 0;
    int pivot = median_of_medians(a, comparisons);
    auto at_most = std::count_if(a.begin(), a.end(), [&](int x) { return x <= pivot; });
    auto at_least = std::count_if(a.begin(), a.end(), [&](int x) { return x >= pivot; });
    std::printf("%-20s %8ld %8ld\n", name.c_str(), static_cast<long>(at_most), static_cast<long>(at_least));
}

int main() {
    const int n = 1000;
    std::mt19937 rng(2026);
    std::vector<std::pair<std::string, std::vector<int>>> inputs;

    std::vector<int> random_values(n);
    for (int& x : random_values) {
        x = static_cast<int>(rng() % 1000000);
    }
    inputs.push_back({"al azar", random_values});

    std::vector<int> sorted(n);
    for (int i = 0; i < n; i++) {
        sorted[i] = i;
    }
    inputs.push_back({"ordenado", sorted});

    inputs.push_back({"todos iguales", std::vector<int>(n, 7)});

    std::vector<int> few(n);
    for (int& x : few) {
        x = static_cast<int>(rng() % 3);
    }
    inputs.push_back({"tres valores", few});

    inputs.push_back({"construida", tight_input(n)});

    std::printf("n = %d, cota 3n/10 - 3 = %d\n", n, 3 * n / 10 - 3);
    std::printf("%-20s %8s %8s\n", "entrada", "<= piv", ">= piv");
    for (const auto& [name, values] : inputs) {
        report(name, values);
    }
}
quickselect_trace.cppDescargar quickselect_trace.cpp
// Sigue quickselect con el primer elemento como pivote, vuelta por vuelta.
#include <cstddef>
#include <iostream>
#include <vector>

#include "select.h"

static void print(const std::vector<int>& v) {
    std::cout << "[";
    for (std::size_t i = 0; i < v.size(); i++) {
        std::cout << (i == 0 ? "" : " ") << v[i];
    }
    std::cout << "]";
}

int main() {
    std::vector<int> a = {6, 3, 9, 1, 7, 2, 8, 5, 4};
    std::size_t k = 4;
    long long comparisons = 0;
    while (true) {
        std::cout << "k = " << k << "  A = ";
        print(a);
        std::cout << "\n";
        if (a.size() == 1) {
            std::cout << "  un solo elemento: respuesta " << a[0] << "\n";
            break;
        }
        int pivot = a[0];
        Parts parts = partition3(a, pivot, comparisons);
        std::cout << "  pivote " << pivot << "  menores ";
        print(parts.less);
        std::cout << "  iguales ";
        print(parts.equal);
        std::cout << "  mayores ";
        print(parts.greater);
        std::cout << "\n";
        if (k < parts.less.size()) {
            a = parts.less;
        } else if (k < parts.less.size() + parts.equal.size()) {
            std::cout << "  k cae en los iguales: respuesta " << pivot << "\n";
            break;
        } else {
            k -= parts.less.size() + parts.equal.size();
            a = parts.greater;
        }
    }
    std::cout << comparisons << " comparaciones\n\n";

    std::cout << "arreglo ordenado, k = n - 1, primer elemento como pivote\n";
    std::cout << "        n   comparaciones   n(n+1)/2 - 1\n";
    for (long long n = 1000; n <= 8000; n *= 2) {
        std::vector<int> sorted(n);
        for (long long i = 0; i < n; i++) {
            sorted[i] = static_cast<int>(i);
        }
        long long count = 0;
        quickselect_first(sorted, n - 1, count);
        std::cout.width(9);
        std::cout << n;
        std::cout.width(16);
        std::cout << count;
        std::cout.width(15);
        std::cout << n * (n + 1) / 2 - 1 << "\n";
    }
}
recurrence_table.cppDescargar recurrence_table.cpp
// Calcula las recurrencias con grupos de 5 y de 3, tal como salen de contar elementos, y compara T(n)/n.
//   T5(n) = T5(techo(n/5)) + T5(piso(7n/10) + 3) + n    para n >= 80
//   T3(n) = T3(techo(n/3)) + T3(piso(2n/3) + 2) + n     para n >= 80
// Con n < 80 se toma T(n) = n.
#include <cstdio>
#include <vector>

int main() {
    const long long limit = 10240000;
    std::vector<long long> t5(limit + 1);
    std::vector<long long> t3(limit + 1);
    for (long long n = 0; n <= limit; n++) {
        if (n < 80) {
            t5[n] = n;
            t3[n] = n;
        } else {
            t5[n] = t5[(n + 4) / 5] + t5[7 * n / 10 + 3] + n;
            t3[n] = t3[(n + 2) / 3] + t3[2 * n / 3 + 2] + n;
        }
    }
    std::printf("%10s %10s %10s\n", "n", "T5(n)/n", "T3(n)/n");
    for (long long n = 10000; n <= limit; n *= 4) {
        std::printf("%10lld %10.2f %10.2f\n", n,
                    static_cast<double>(t5[n]) / static_cast<double>(n),
                    static_cast<double>(t3[n]) / static_cast<double>(n));
    }
    double worst = 0;
    for (long long n = 1; n <= limit; n++) {
        double ratio = static_cast<double>(t5[n]) / static_cast<double>(n);
        if (ratio > worst) {
            worst = ratio;
        }
    }
    std::printf("mayor T5(n)/n con n hasta %lld: %.2f\n", limit, worst);
}
select_bench.cppDescargar select_bench.cpp
// Mide selección con entradas al azar: comparaciones exactas y tiempo, con -O2 y sin sanitizers.
// Los tiempos dependen de la máquina. Son la mediana de 7 corridas.
#include <algorithm>
#include <cstdio>
#include <random>
#include <vector>

#include "bench.h"
#include "select.h"

int main() {
    std::printf("%9s | %11s %11s | %9s %9s %9s %9s\n", "n", "cmp.azar/n", "cmp.mdm/n", "sort ms", "nth ms", "azar ms", "mdm ms");
    for (int n = 125000; n <= 8000000; n *= 2) {
        std::mt19937 rng(2026);
        std::vector<int> a(n);
        for (int& x : a) {
            x = static_cast<int>(rng() >> 1);
        }
        std::size_t k = a.size() / 2;
        long long random_count = 0;
        long long linear_count = 0;
        std::mt19937 pivot_rng(7);
        int answer = quickselect_random(a, k, pivot_rng, random_count);
        int same = select_linear(a, k, linear_count);
        assert(answer == same);

        double sort_ms = 1000 * median_seconds(7, [&] {
            std::vector<int> copy = a;
            std::sort(copy.begin(), copy.end());
            keep(copy[k]);
        });
        double nth_ms = 1000 * median_seconds(7, [&] {
            std::vector<int> copy = a;
            std::nth_element(copy.begin(), copy.begin() + static_cast<long>(k), copy.end());
            keep(copy[k]);
        });
        double random_ms = 1000 * median_seconds(7, [&] {
            long long ignored = 0;
            keep(quickselect_random(a, k, pivot_rng, ignored));
        });
        double linear_ms = 1000 * median_seconds(7, [&] {
            long long ignored = 0;
            keep(select_linear(a, k, ignored));
        });
        std::printf("%9d | %11.2f %11.2f | %9.1f %9.1f %9.1f %9.1f\n", n,
                    static_cast<double>(random_count) / n, static_cast<double>(linear_count) / n,
                    sort_ms, nth_ms, random_ms, linear_ms);
    }
}
select.hDescargar select.h
// Selección: el k-ésimo menor de un arreglo, con k contado desde 0.
// `comparisons` cuenta cada vez que un elemento se compara con un pivote o, dentro de un
// grupo, con otro elemento. Es un conteo exacto: no depende de la máquina.
#ifndef SELECT_H
#define SELECT_H

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

struct Parts {
    std::vector<int> less;
    std::vector<int> equal;
    std::vector<int> greater;
};

// Parte en tres: menores, iguales y mayores que el pivote. Cuesta una comparación por elemento.
inline Parts partition3(const std::vector<int>& a, int pivot, long long& comparisons) {
    Parts parts;
    for (int x : a) {
        comparisons++;
        if (x < pivot) {
            parts.less.push_back(x);
        } else if (x > pivot) {
            parts.greater.push_back(x);
        } else {
            parts.equal.push_back(x);
        }
    }
    return parts;
}

// Quickselect con una regla para elegir el pivote.
// Invariante: la respuesta buscada es el k-ésimo menor de `a`.
template <typename PivotRule>
int quickselect(std::vector<int> a, std::size_t k, PivotRule choose_pivot, long long& comparisons) {
    assert(k < a.size());
    while (a.size() > 1) {
        Parts parts = partition3(a, choose_pivot(a), comparisons);
        if (k < parts.less.size()) {
            a = std::move(parts.less);
        } else if (k < parts.less.size() + parts.equal.size()) {
            return parts.equal[0];
        } else {
            k -= parts.less.size() + parts.equal.size();
            a = std::move(parts.greater);
        }
    }
    return a[0];
}

inline int quickselect_first(std::vector<int> a, std::size_t k, long long& comparisons) {
    return quickselect(std::move(a), k, [](const std::vector<int>& v) { return v[0]; }, comparisons);
}

inline int quickselect_random(std::vector<int> a, std::size_t k, std::mt19937& rng, long long& comparisons) {
    auto rule = [&rng](const std::vector<int>& v) { return v[rng() % v.size()]; };
    return quickselect(std::move(a), k, rule, comparisons);
}

// Ordena un grupo pequeño por inserción. En un grupo de 5 son a lo sumo 10 comparaciones.
inline void sort_group(std::vector<int>& group, long long& comparisons) {
    for (std::size_t i = 1; i < group.size(); i++) {
        int value = group[i];
        std::size_t j = i;
        while (j > 0) {
            comparisons++;
            if (group[j - 1] <= value) {
                break;
            }
            group[j] = group[j - 1];
            j--;
        }
        group[j] = value;
    }
}

// Reparte en grupos de 5 por posición y devuelve la mediana inferior de cada uno. El último grupo puede ser más corto.
inline std::vector<int> group_medians(const std::vector<int>& a, long long& comparisons) {
    std::vector<int> medians;
    for (std::size_t start = 0; start < a.size(); start += 5) {
        std::size_t stop = std::min(start + 5, a.size());
        std::vector<int> group(a.begin() + start, a.begin() + stop);
        sort_group(group, comparisons);
        medians.push_back(group[(group.size() - 1) / 2]);
    }
    return medians;
}

int select_linear(std::vector<int> a, std::size_t k, long long& comparisons);

// El pivote de la mediana de medianas: la mediana inferior de las medianas de los grupos.
inline int median_of_medians(const std::vector<int>& a, long long& comparisons) {
    std::vector<int> medians = group_medians(a, comparisons);
    std::size_t middle = (medians.size() - 1) / 2;
    return select_linear(std::move(medians), middle, comparisons);
}

// Selección en tiempo lineal en el peor caso.
inline int select_linear(std::vector<int> a, std::size_t k, long long& comparisons) {
    assert(k < a.size());
    if (a.size() <= 5) {
        sort_group(a, comparisons);
        return a[k];
    }
    int pivot = median_of_medians(a, comparisons);
    Parts parts = partition3(a, pivot, comparisons);
    if (k < parts.less.size()) {
        return select_linear(std::move(parts.less), k, comparisons);
    }
    if (k < parts.less.size() + parts.equal.size()) {
        return pivot;
    }
    return select_linear(std::move(parts.greater), k - parts.less.size() - parts.equal.size(), comparisons);
}

// Una entrada que lleva la mediana de medianas casi hasta su cota: n múltiplo de 5, todos distintos.
// La mitad de los grupos (redondeada hacia arriba) tiene sus tres menores entre los valores
// más pequeños de todos, y el resto de los valores queda por encima del pivote.
inline std::vector<int> tight_input(int n) {
    assert(n % 5 == 0);
    int groups = n / 5;
    int low_groups = (groups + 1) / 2;
    int next_low = 0;
    int next_high = 3 * low_groups;
    std::vector<int> a;
    for (int g = 0; g < groups; g++) {
        int lows = g < low_groups ? 3 : 0;
        for (int i = 0; i < lows; i++) {
            a.push_back(next_low++);
        }
        for (int i = lows; i < 5; i++) {
            a.push_back(next_high++);
        }
    }
    return a;
}

#endif
strip_demo.cppDescargar strip_demo.cpp
// Cuatro experimentos con la franja del par más cercano, con fuerza bruta y un solo nivel de la división.
#include <cstdio>
#include <vector>

#include "strip.h"

static void print_step(const RootStep& step) {
    std::printf("linea x = %lld\n", step.line);
    std::printf("mejor izquierda = %lld, mejor derecha = %lld, delta^2 = %lld\n", step.left_best, step.right_best, step.delta2);
    std::printf("franja por y:");
    for (const StripPoint& p : step.strip) {
        std::printf(" (%lld,%lld)%c", p.point.x, p.point.y, p.from_left ? 'I' : 'D');
    }
    std::printf("\n");
}

int main() {
    std::printf("1. Ejemplo de los apuntes\n");
    std::vector<Point> example = {{1, 9}, {2, 3}, {3, 7}, {4, 1}, {5, 5}, {6, 4}, {7, 10}, {8, 8}, {9, 2}, {10, 6}};
    RootStep root = analyze_root(example);
    print_step(root);
    for (std::size_t i = 0; i < root.strip.size(); i++) {
        for (std::size_t j = i + 1; j < root.strip.size(); j++) {
            long long d = dist2(root.strip[i].point, root.strip[j].point);
            std::printf("  posiciones %zu y %zu: distancia^2 %lld%s\n", i, j, d, d < root.delta2 ? "  mejora" : "");
        }
    }
    std::printf("respuesta = %lld, fuerza bruta = %lld\n\n", root.best_in_strip, brute_force(example));

    std::printf("2. No basta con los puntos mas cercanos a la linea en x\n");
    std::vector<Point> near_line = {{-2, 0}, {-1, 100}, {1, -100}, {2, 0}};
    RootStep second = analyze_root(near_line);
    print_step(second);
    std::printf("distancia^2 entre los mas cercanos a la linea: %lld\n", dist2({-1, 100}, {1, -100}));
    std::printf("fuerza bruta = %lld\n\n", brute_force(near_line));

    std::printf("3. No basta con comparar con el siguiente\n");
    std::vector<Point> next_only = {{-9, 50}, {-1, 0}, {0, 2}, {8, 1}};
    RootStep third = analyze_root(next_only);
    print_step(third);
    std::printf("solo con el siguiente: %lld\n", best_with_window(third, 1));
    std::printf("con los 7 siguientes: %lld\n", best_with_window(third, 7));
    std::printf("fuerza bruta = %lld\n\n", brute_force(next_only));

    std::printf("4. La respuesta a 3 posiciones\n");
    std::vector<Point> three_apart = {{-99, 50}, {0, 0}, {1, 99}, {99, 51}};
    RootStep fourth = analyze_root(three_apart);
    print_step(fourth);
    for (std::size_t window = 1; window <= 3; window++) {
        std::printf("con los %zu siguientes: %lld\n", window, best_with_window(fourth, window));
    }
    std::printf("fuerza bruta = %lld\n", brute_force(three_apart));
}
strip.hDescargar strip.h
// Herramientas para revisar el lema de la franja del par más cercano. Todo es fuerza bruta:
// estos programas comprueban afirmaciones de los apuntes con pocos puntos, y no sirven para
// muchos puntos porque cuestan Θ(n²).
#ifndef STRIP_H
#define STRIP_H

#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <vector>

struct Point {
    long long x;
    long long y;
    bool operator==(const Point&) const = default;
};

inline long long dist2(const Point& p, const Point& q) {
    long long dx = p.x - q.x;
    long long dy = p.y - q.y;
    return dx * dx + dy * dy;
}

inline long long brute_force(const std::vector<Point>& points) {
    long long best = std::numeric_limits<long long>::max();
    for (std::size_t i = 0; i < points.size(); i++) {
        for (std::size_t j = i + 1; j < points.size(); j++) {
            best = std::min(best, dist2(points[i], points[j]));
        }
    }
    return best;
}

struct StripPoint {
    Point point;
    bool from_left;
};

struct RootStep {
    long long line;                // ℓ: la x del punto en la posición n div 2
    long long left_best;           // δ² de la mitad izquierda
    long long right_best;          // δ² de la mitad derecha
    long long delta2;              // δ²: el menor de los dos
    std::vector<StripPoint> strip;  // los puntos con (x - ℓ)² < δ², ordenados por y
    long long max_gap;              // el mayor j - i entre puntos de la franja con distancia² < δ²
    long long best_in_strip;        // la menor distancia² entre esos pares; δ² si no hay ninguno
    bool all_crossing;              // ¿todos esos pares tienen un punto de cada mitad?
};

// El primer nivel de la división, calculado por fuerza bruta. `points` ya debe venir ordenado por x
// y el orden entre puntos de igual y en la franja es el que traiga la entrada: `points` puede
// barajarse antes para probar otros desempates.
inline RootStep analyze_root(const std::vector<Point>& points) {
    assert(points.size() >= 4);
    std::size_t mid = points.size() / 2;
    std::vector<Point> left(points.begin(), points.begin() + static_cast<long>(mid));
    std::vector<Point> right(points.begin() + static_cast<long>(mid), points.end());
    RootStep step{};
    step.line = points[mid].x;
    step.left_best = brute_force(left);
    step.right_best = brute_force(right);
    step.delta2 = std::min(step.left_best, step.right_best);
    for (std::size_t i = 0; i < points.size(); i++) {
        long long dx = points[i].x - step.line;
        if (dx * dx < step.delta2) {
            step.strip.push_back({points[i], i < mid});
        }
    }
    std::stable_sort(step.strip.begin(), step.strip.end(),
                     [](const StripPoint& p, const StripPoint& q) { return p.point.y < q.point.y; });
    step.best_in_strip = step.delta2;
    step.all_crossing = true;
    for (std::size_t i = 0; i < step.strip.size(); i++) {
        for (std::size_t j = i + 1; j < step.strip.size(); j++) {
            long long d = dist2(step.strip[i].point, step.strip[j].point);
            if (d < step.delta2) {
                step.max_gap = std::max(step.max_gap, static_cast<long long>(j - i));
                step.best_in_strip = std::min(step.best_in_strip, d);
                if (step.strip[i].from_left == step.strip[j].from_left) {
                    step.all_crossing = false;
                }
            }
        }
    }
    return step;
}

// Compara cada punto de la franja solo con sus `window` siguientes.
inline long long best_with_window(const RootStep& step, std::size_t window) {
    long long best = step.delta2;
    for (std::size_t i = 0; i < step.strip.size(); i++) {
        for (std::size_t j = i + 1; j < step.strip.size() && j <= i + window; j++) {
            best = std::min(best, dist2(step.strip[i].point, step.strip[j].point));
        }
    }
    return best;
}

inline void sort_by_x(std::vector<Point>& points) {
    std::stable_sort(points.begin(), points.end(), [](const Point& p, const Point& q) { return p.x < q.x; });
}

#endif

Pruebas

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

test_karatsuba.cppDescargar test_karatsuba.cpp
// Pruebas de la multiplicación: Karatsuba contra la escuela, y contra productos que caben en 64 bits.
#include <cassert>
#include <cstdint>
#include <cstdio>
#include <random>
#include <string>

#include "karatsuba.h"

static Digits random_number(std::size_t n, std::mt19937& rng) {
    Digits a(n);
    for (int& digit : a) {
        digit = static_cast<int>(rng() % 10);
    }
    trim(a);
    return a;
}

static void test_known() {
    long long c = 0;
    assert(to_string(karatsuba(from_string("1234"), from_string("5678"), 3, c)) == "7006652");
    assert(to_string(karatsuba(from_string("0"), from_string("5678"), 3, c)) == "0");
    assert(to_string(karatsuba(from_string("99999"), from_string("99999"), 3, c)) == "9999800001");
    // a + b tiene un dígito más que sus partes.
    assert(to_string(karatsuba(from_string("9999"), from_string("9999"), 3, c)) == "99980001");
    assert(to_string(school_multiply(from_string("1234"), from_string("5678"), c)) == "7006652");
}

static void test_against_64_bits() {
    std::mt19937 rng(1);
    for (int round = 0; round < 3000; round++) {
        std::uint64_t p = rng() % 4000000000u;
        std::uint64_t q = rng() % 4000000000u;
        long long c = 0;
        Digits product = karatsuba(from_string(std::to_string(p)), from_string(std::to_string(q)), 3, c);
        assert(to_string(product) == std::to_string(p * q));
    }
}

static void test_against_school() {
    std::mt19937 rng(2);
    for (int round = 0; round < 400; round++) {
        std::size_t n = 1 + rng() % 300;
        std::size_t m = round % 2 == 0 ? n : 1 + rng() % 300;
        Digits x = random_number(n, rng);
        Digits y = random_number(m, rng);
        if (round % 5 == 0) {
            x.assign(n, 9);  // todos nueves: el peor caso de los acarreos
            y.assign(m, 9);
        }
        long long c = 0;
        Digits expected = school_multiply(x, y, c);
        for (std::size_t threshold : {std::size_t{3}, std::size_t{7}, std::size_t{32}}) {
            assert(karatsuba(x, y, threshold, c) == expected);
        }
    }
}

static void test_counts() {
    std::mt19937 rng(3);
    // La escuela hace exactamente n² productos.
    for (std::size_t n : {std::size_t{5}, std::size_t{64}, std::size_t{500}}) {
        Digits x(n, 7);
        long long c = 0;
        school_multiply(x, x, c);
        assert(c == static_cast<long long>(n * n));
    }
    // Con n = 2^k y sin acarreos que alarguen las sumas, 3^(k-1) hojas de 2 x 2 dígitos: 4·3^(k-1) productos.
    long long c = 0;
    karatsuba(from_string("1111"), from_string("1111"), 3, c);
    assert(c == 4 * 3);
    c = 0;
    karatsuba(from_string("11111111"), from_string("11111111"), 3, c);
    assert(c == 4 * 9);
    // Al duplicar n, el conteo se multiplica por cerca de 3, no por 4.
    long long previous = 0;
    for (std::size_t n = 1024; n <= 8192; n *= 2) {
        Digits x = random_number(n, rng);
        Digits y = random_number(n, rng);
        long long count = 0;
        karatsuba(x, y, 3, count);
        if (previous != 0) {
            double ratio = static_cast<double>(count) / static_cast<double>(previous);
            assert(ratio > 2.8 && ratio < 3.2);
        }
        previous = count;
    }
}

int main() {
    test_known();
    test_against_64_bits();
    test_against_school();
    test_counts();
    std::puts("test_karatsuba: ok");
}
test_select.cppDescargar test_select.cpp
// Pruebas de selección. Comprueban lo que afirman los apuntes.
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <random>
#include <vector>

#include "select.h"

static long long count_at_most(const std::vector<int>& a, int pivot) {
    return std::count_if(a.begin(), a.end(), [&](int x) { return x <= pivot; });
}

static long long count_at_least(const std::vector<int>& a, int pivot) {
    return std::count_if(a.begin(), a.end(), [&](int x) { return x >= pivot; });
}

// El lema: al menos 3n/10 - 3 elementos a cada lado, y por lo tanto a lo sumo 7n/10 + 3 en cada parte.
static void check_pivot_bound(const std::vector<int>& a) {
    long long n = static_cast<long long>(a.size());
    long long comparisons = 0;
    int pivot = median_of_medians(a, comparisons);
    assert(10 * count_at_most(a, pivot) >= 3 * n - 30);
    assert(10 * count_at_least(a, pivot) >= 3 * n - 30);
    Parts parts = partition3(a, pivot, comparisons);
    assert(10 * static_cast<long long>(parts.less.size()) <= 7 * n + 30);
    assert(10 * static_cast<long long>(parts.greater.size()) <= 7 * n + 30);
    assert(!parts.equal.empty());
}

static void test_agree_with_sort() {
    std::mt19937 rng(1);
    for (int round = 0; round < 200; round++) {
        int n = 1 + static_cast<int>(rng() % 60);
        int range = round % 4 == 0 ? 3 : 1000;
        std::vector<int> a(n);
        for (int& x : a) {
            x = static_cast<int>(rng() % range);
        }
        std::vector<int> sorted = a;
        std::sort(sorted.begin(), sorted.end());
        for (int k = 0; k < n; k++) {
            long long c1 = 0;
            long long c2 = 0;
            long long c3 = 0;
            assert(quickselect_first(a, k, c1) == sorted[k]);
            assert(quickselect_random(a, k, rng, c2) == sorted[k]);
            assert(select_linear(a, k, c3) == sorted[k]);
            std::vector<int> copy = a;
            std::nth_element(copy.begin(), copy.begin() + k, copy.end());
            assert(copy[k] == sorted[k]);
        }
    }
}

static void test_bound() {
    std::mt19937 rng(2);
    for (int n = 1; n <= 400; n++) {
        std::vector<int> random_values(n);
        std::vector<int> few(n);
        std::vector<int> sorted(n);
        for (int i = 0; i < n; i++) {
            random_values[i] = static_cast<int>(rng() % 100000);
            few[i] = static_cast<int>(rng() % 3);
            sorted[i] = i;
        }
        std::vector<int> reversed(sorted.rbegin(), sorted.rend());
        check_pivot_bound(random_values);
        check_pivot_bound(few);
        check_pivot_bound(sorted);
        check_pivot_bound(reversed);
        check_pivot_bound(std::vector<int>(n, 5));
    }
}

// La entrada construida queda en exactamente 3·techo(m/2) del lado de los <=.
static void test_tight_input() {
    for (int n = 5; n <= 1000; n += 5) {
        std::vector<int> a = tight_input(n);
        long long comparisons = 0;
        int pivot = median_of_medians(a, comparisons);
        int groups = n / 5;
        assert(count_at_most(a, pivot) == 3 * ((groups + 1) / 2));
        check_pivot_bound(a);
    }
}

// Ordenado y k = n - 1: las partes miden n, n - 1, ..., 2, y luego queda un solo elemento.
static void test_worst_case_first_pivot() {
    for (long long n = 2; n <= 3000; n += 37) {
        std::vector<int> sorted(n);
        for (long long i = 0; i < n; i++) {
            sorted[i] = static_cast<int>(i);
        }
        long long count = 0;
        assert(quickselect_first(sorted, n - 1, count) == n - 1);
        assert(count == n * (n + 1) / 2 - 1);

        // Con k = 0 el primer pivote ya es la respuesta: una sola pasada.
        count = 0;
        assert(quickselect_first(sorted, 0, count) == 0);
        assert(count == n);

        // Al revés, k = 0 es el caso malo.
        std::vector<int> reversed(sorted.rbegin(), sorted.rend());
        count = 0;
        assert(quickselect_first(reversed, 0, count) == 0);
        assert(count == n * (n + 1) / 2 - 1);
    }
}

// Las comparaciones crecen como n. Cada vuelta paga a lo sumo 3n: 10 por grupo de 5 y 1 por elemento con el
// pivote. La demostración da entonces c = 3 y T(n) <= 20·c·n = 60n, si el rango pequeño no pide más.
static void test_linear_comparisons() {
    std::mt19937 rng(3);
    for (int n : {1000, 10000, 100000}) {
        std::vector<int> a(n);
        for (int& x : a) {
            x = static_cast<int>(rng());
        }
        long long count = 0;
        select_linear(a, a.size() / 2, count);
        assert(count <= 60LL * n);
    }
}

int main() {
    test_agree_with_sort();
    test_bound();
    test_tight_input();
    test_worst_case_first_pivot();
    test_linear_comparisons();
    std::puts("test_select: ok");
}
test_strip.cppDescargar test_strip.cpp
// Pruebas de lo que los apuntes afirman sobre la franja del par más cercano.
#include <algorithm>
#include <cassert>
#include <cstdio>
#include <random>
#include <vector>

#include "strip.h"

static void test_notes_examples() {
    std::vector<Point> example = {{1, 9}, {2, 3}, {3, 7}, {4, 1}, {5, 5}, {6, 4}, {7, 10}, {8, 8}, {9, 2}, {10, 6}};
    RootStep root = analyze_root(example);
    assert(root.line == 6);
    assert(root.left_best == 8);
    assert(root.right_best == 5);
    assert(root.delta2 == 5);
    assert(root.strip.size() == 5);
    assert(root.best_in_strip == 2);
    assert(brute_force(example) == 2);

    std::vector<Point> near_line = {{-2, 0}, {-1, 100}, {1, -100}, {2, 0}};
    RootStep second = analyze_root(near_line);
    assert(brute_force(near_line) == 16);
    assert(second.best_in_strip == 16);
    assert(dist2({-1, 100}, {1, -100}) == 40004);

    std::vector<Point> next_only = {{-9, 50}, {-1, 0}, {0, 2}, {8, 1}};
    RootStep third = analyze_root(next_only);
    assert(third.delta2 == 65);
    assert(best_with_window(third, 1) == 65);
    assert(best_with_window(third, 7) == 5);
    assert(brute_force(next_only) == 5);

    std::vector<Point> three_apart = {{-99, 50}, {0, 0}, {1, 99}, {99, 51}};
    RootStep fourth = analyze_root(three_apart);
    assert(fourth.max_gap == 3);
    assert(best_with_window(fourth, 2) == 11908);
    assert(best_with_window(fourth, 3) == 9802);
    assert(brute_force(three_apart) == 9802);

    // Un par por debajo de δ² a 5 posiciones, aunque la respuesta sea otro par.
    std::vector<Point> far_apart = {{-99, 15}, {-20, 98}, {0, 0}, {1, 99}, {25, 1}, {99, 70}};
    RootStep fifth = analyze_root(far_apart);
    assert(fifth.max_gap == 5);
    assert(dist2({0, 0}, {1, 99}) == 9802 && fifth.delta2 == 10004);
    assert(brute_force(far_apart) == 442);
}

// Todos sobre una vertical, con dos desempates en el orden por x: la respuesta no cambia.
static void test_vertical_ties() {
    std::vector<Point> mixed = {{5, 0}, {5, 10}, {5, 20}, {5, 30}, {5, 3}, {5, 13}, {5, 23}, {5, 33}};
    RootStep first = analyze_root(mixed);
    assert(first.line == 5);
    assert(first.delta2 == 100);
    assert(first.strip.size() == 8);
    assert(first.best_in_strip == 9);
    assert(first.max_gap == 1);

    std::vector<Point> by_height = {{5, 0}, {5, 3}, {5, 10}, {5, 13}, {5, 20}, {5, 23}, {5, 30}, {5, 33}};
    RootStep second = analyze_root(by_height);
    assert(second.delta2 == 9);
    assert(second.strip.size() == 8);
    assert(second.best_in_strip == 9);
}

// Puntos repetidos a los dos lados de la línea: δ > 0 y aun así la respuesta es 0.
static void test_duplicate_across_line() {
    std::vector<Point> points = {{0, 0}, {5, 5}, {5, 5}, {9, 9}};
    RootStep step = analyze_root(points);
    assert(step.delta2 == 32);
    assert(step.best_in_strip == 0);
    assert(step.max_gap <= 7);
}

// Un punto repetido dentro de una mitad da δ = 0, y la franja queda vacía.
static void test_duplicate_inside_half() {
    std::vector<Point> points = {{1, 1}, {1, 1}, {4, 2}, {7, 3}};
    RootStep step = analyze_root(points);
    assert(step.delta2 == 0);
    assert(step.strip.empty());
    assert(step.best_in_strip == 0);
}

// El lema: para toda entrada y todo desempate, un par de la franja con distancia menor que δ está a
// lo sumo a 7 posiciones. Además, ese par siempre cruza, y con 7 vecinos se halla la respuesta correcta.
static long long test_lemma(unsigned seed, int rounds, int max_n, long long range, bool vertical) {
    std::mt19937 rng(seed);
    long long largest_gap = 0;
    for (int round = 0; round < rounds; round++) {
        int n = 4 + static_cast<int>(rng() % static_cast<unsigned>(max_n - 3));
        std::vector<Point> points(n);
        for (Point& p : points) {
            p.x = vertical ? 0 : static_cast<long long>(rng() % range);
            p.y = static_cast<long long>(rng() % range);
        }
        std::shuffle(points.begin(), points.end(), rng);
        sort_by_x(points);
        RootStep step = analyze_root(points);
        assert(step.max_gap <= 7);
        assert(step.all_crossing);
        assert(best_with_window(step, 7) == brute_force(points));
        largest_gap = std::max(largest_gap, step.max_gap);
    }
    return largest_gap;
}

int main() {
    test_notes_examples();
    test_vertical_ties();
    test_duplicate_across_line();
    test_duplicate_inside_half();
    long long largest = 0;
    for (long long range : {4LL, 8LL, 16LL, 64LL, 1000LL, 2000000000LL}) {
        largest = std::max(largest, test_lemma(1, 4000, 40, range, false));
        largest = std::max(largest, test_lemma(2, 2000, 40, range, true));
    }
    std::printf("test_strip: ok (mayor salto observado: %lld)\n", largest);
}

Laboratorio

Archivos de partida del laboratorio de la semana.

lab/closest_pair/MakefileDescargar lab/closest_pair/Makefile
TIEMPO = 2
include ../../../lab.mk
lab/closest_pair/README.mdDescargar lab/closest_pair/README.md
# El par más cercano

Una empresa instala sensores de calidad del aire en una ciudad. Cada sensor está en un punto del
plano, con coordenadas enteras en metros, y dos sensores muy cercanos miden casi lo mismo. Antes de
aprobar el plano, la empresa quiere saber qué tan cerca quedaron los dos sensores más próximos. El plano
puede tener errores: dos filas pueden traer exactamente el mismo punto, y muchos sensores pueden estar
sobre una misma calle recta.

Para no trabajar con decimales, se pide el **cuadrado** de la menor distancia entre dos sensores. Si
dos sensores están en (x₁, y₁) y (x₂, y₂), ese cuadrado es (x₁ − x₂)² + (y₁ − y₂)².

## Entrada

La primera línea tiene un entero n, la cantidad de sensores. Cada una de las n líneas siguientes tiene dos
enteros x e y, las coordenadas de un sensor. Un mismo punto puede aparecer varias veces.

## Salida

Una línea con un entero: el cuadrado de la menor distancia entre dos sensores distintos, es decir, dos filas
distintas de la entrada. Si dos filas traen el mismo punto, la respuesta es 0.

## Límites

- 2 ≤ n ≤ 200 000
- −10⁹ ≤ x, y ≤ 10⁹

## Ejemplo

Entrada:

```text
6
2 3
12 30
40 50
5 1
12 10
3 4
```

Salida:

```text
2
```

Los sensores más cercanos son (2, 3) y (3, 4): (2 − 3)² + (3 − 4)² = 2.

## Tiempo

2 segundos por caso. Se compila con `g++ -Wall -Wextra -std=c++20 -O2`.

## Cómo avanzar

```sh
make        # compila solution.cpp en build/
make test   # corre el juez con todos los casos
```

Los casos grandes no están guardados: `make test` los genera con los programas `casos/gen_NN.cpp`.

1. Antes de programar, calculen con los límites el mayor valor posible de la respuesta. ¿Cabe en `int`? ¿Y en
   `long long`? ¿Qué pasa con cada resta y con cada producto en el camino? Escriban la cuenta.
2. Escriban la fuerza bruta en un archivo aparte y comprueben que el juez la acepta en los casos pequeños.
3. Escriban la versión rápida. Hagan un pequeño generador de entradas al azar y comparen las dos versiones con
   muchas entradas pequeñas, incluidas entradas con pocos valores distintos, para que salgan repetidos.
4. Pasen todos los casos, incluidos los puntos repetidos y todos los puntos sobre una misma vertical.
lab/closest_pair/solution.cppDescargar lab/closest_pair/solution.cpp
// El par más cercano. Lee los puntos e imprime el cuadrado de la menor distancia.
#include <iostream>
#include <vector>

struct Point {
    long long x;
    long long y;
};

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

    int n;
    std::cin >> n;
    std::vector<Point> points(n);
    for (Point& p : points) {
        std::cin >> p.x >> p.y;
    }

    // Por hacer: calcular la respuesta. Este valor nunca es correcto.
    long long answer = -1;
    std::cout << answer << '\n';
}

Compilación

Cómo se compilan y ejecutan los ejemplos.

MakefileDescargar Makefile
# Semana 3: selección en tiempo lineal, Karatsuba y el par más cercano.
# Se usa dentro del entorno del curso.
#
#   make           compila los programas de la clase en build/
#   make test      corre las pruebas de los ejemplos, con sanitizers
#   make bench     corre las mediciones, con -O2 y sin sanitizers
#   make clean     borra lo generado
#
# El laboratorio está en lab/closest_pair y tiene su propio Makefile: estas reglas no lo tocan.

CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
PROGRAMS = quickselect_trace mom_trace pivot_bound karatsuba_trace strip_demo
TESTS = test_select test_karatsuba test_strip
COUNTS = recurrence_table karatsuba_count
BENCHES = select_bench karatsuba_bench
HEADERS = select.h karatsuba.h strip.h bench.h

.PHONY: all test bench clean

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

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

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

$(BUILD):
	mkdir -p $@

test: $(TESTS:%=$(BUILD)/%) $(PROGRAMS:%=$(BUILD)/%)
	@for t in $(TESTS); do $(BUILD)/$$t || exit 1; done
	@echo "Todas las pruebas pasaron."

bench: $(BENCHES:%=$(BUILD)/%)
	$(BUILD)/select_bench
	$(BUILD)/karatsuba_bench

clean:
	rm -rf $(BUILD)