Semana 9 — Más programación dinámica
Objetivos
- Ver que el grafo de subproblemas de una programación dinámica es un DAG.
- Resolver problemas sobre intervalos, sobre DAG y sobre árboles.
- Elegir el orden de cálculo cuando no es una simple tabla de izquierda a derecha.
Sesión A — Programación dinámica sobre intervalos y sobre DAG
Lectura: Apuntes, semana 9.
Apoyo: Erickson, secs. 3.6 (Longest Increasing Subsequence), 3.9 (Optimal Binary Search Trees) y 6.4 (Memoization and Dynamic Programming). CLRS, capítulo Dynamic Programming, 4.ª edición.
Temas:
- Los subproblemas y sus dependencias forman un DAG. Cuando cada decisión depende de un solo subproblema, la solución es un camino en ese DAG; en intervalos y árboles se combinan varios.
- Camino más largo en un DAG, usando el orden topológico de la semana 6. Sin DAG, ese problema se vuelve difícil: semana 14.
- Subsecuencia creciente más larga en O(n²), y mejora a O(n log n) con búsqueda binaria.
- Intervalos: multiplicar una cadena de matrices, donde el subproblema es un rango i..j. Costo O(n³).
- Calcular por largo de intervalo: por qué el orden i, j ingenuo lee casillas sin llenar.
- Árboles binarios de búsqueda óptimos, con las mismas ideas.
- Error común: usar un orden de cálculo que lee una casilla antes de llenarla.
Sesión B — Programación dinámica sobre árboles
Lectura: Apuntes, semana 9.
Apoyo: Erickson, sec. 3.10 (Dynamic Programming on Trees).
Temas:
- Un árbol tiene un orden natural: primero los hijos.
- Conjunto independiente de peso máximo en un árbol: dos estados por nodo, con él y sin él.
- Reconstruir la solución.
- Cobertura de vértices mínima en un árbol: el mismo patrón. Ambos son difíciles en un grafo general: semana 13.
- Evitar la recursión profunda: procesar los nodos en orden inverso de un BFS.
- Error común: olvidar que un nodo sin elegir deja libres a sus hijos: pueden elegirse o no.
Laboratorio — Optimizar sobre una jerarquía
- Una empresa tiene forma de árbol, y cada persona aporta un valor si asiste a una reunión. Nadie quiere asistir junto con su jefe directo. Calcular el mayor valor posible.
- Probar primero con fuerza bruta en árboles pequeños, y comparar.
- Pasar los casos grandes, incluido un árbol que es una sola cadena, donde la recursión produce un stack overflow.
- Extensión: imprimir también quiénes asisten. Un verificador comprueba la lista.
Preguntas de repaso
- Dibujen el DAG de subproblemas de Fibonacci y el de la distancia de edición.
- Calculen la mejor forma de multiplicar matrices de 10×30, 30×5 y 5×60.
- ¿Por qué el orden i, j ingenuo falla en la tabla de intervalos, y por qué funciona llenarla por largo creciente? ¿Es el único orden posible?
- Escriban la recurrencia del conjunto independiente de peso máximo en un árbol.
- ¿Por qué ese algoritmo no sirve directamente en un grafo con ciclos?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (12)Pruebas (1)Laboratorio (7)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Árbol binario de búsqueda óptimo: los greedy que fallan, la tabla y el árbol que sale.
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "interval_dp.h"
namespace {
void show(const std::vector<std::string>& names, const std::vector<long long>& f) {
const int n = static_cast<int>(f.size());
std::cout << "Claves y frecuencias:";
for (int i = 0; i < n; i++) std::cout << ' ' << names[i] << '=' << f[i];
const BstTables t = optimal_bst(f);
std::cout << "\n C[i][j], rango [i, j)\n i\\j";
for (int j = 0; j <= n; j++) std::cout << std::setw(5) << j;
std::cout << '\n';
for (int i = 0; i <= n; i++) {
std::cout << std::setw(5) << i;
for (int j = 0; j <= n; j++) {
if (j < i) {
std::cout << std::setw(5) << ".";
} else {
std::cout << std::setw(5) << t.cost[i][j];
}
}
std::cout << '\n';
}
std::cout << " raíz[i][j]\n i\\j";
for (int j = 1; j <= n; j++) std::cout << std::setw(5) << j;
std::cout << '\n';
for (int i = 0; i < n; i++) {
std::cout << std::setw(5) << i;
for (int j = 1; j <= n; j++) {
std::cout << std::setw(5) << (j <= i ? std::string(".") : names[t.root[i][j]]);
}
std::cout << '\n';
}
std::vector<int> depth(f.size(), 0);
bst_depths(t.root, 0, n, 1, depth);
std::cout << " árbol: " << bst_shape(t.root, names, 0, n) << "\n profundidades:";
for (int i = 0; i < n; i++) std::cout << ' ' << names[i] << '=' << depth[i];
std::cout << "\n costo con la definición: " << cost_from_depths(f, depth) << '\n';
std::cout << " por fuerza bruta: " << bst_brute(f) << '\n';
std::cout << " la más frecuente a la raíz: " << bst_most_frequent_root(f, 0, n, 1) << '\n';
std::cout << " balanceado: " << bst_balanced(f, 0, n, 1) << '\n';
}
} // namespace
int main() {
show({"A", "B", "C", "D"}, {2, 1, 3, 4});
}// Mide la tabla de la cadena de matrices. Menor de 3 intentos, con -O2.
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
#include "interval_dp.h"
int main() {
std::puts("Cadena de matrices, dimensiones al azar entre 1 y 50, menor de 3 intentos");
std::puts(" n combinaciones ms");
double previous = 0;
for (int n : {200, 400, 800, 1600}) {
std::mt19937 rng(11);
std::vector<long long> d(static_cast<std::size_t>(n) + 1);
for (long long& x : d) x = 1 + static_cast<long long>(rng() % 50);
double best = 1e18;
long long combinations = 0;
for (int attempt = 0; attempt < 3; attempt++) {
combinations = 0;
const auto start = std::chrono::steady_clock::now();
matrix_chain(d, &combinations);
const auto stop = std::chrono::steady_clock::now();
best = std::min(best, std::chrono::duration<double, std::milli>(stop - start).count());
}
if (previous > 0) {
std::printf("%5d %15lld %9.1f x%.1f\n", n, combinations, best, best / previous);
} else {
std::printf("%5d %15lld %9.1f\n", n, combinations, best);
}
previous = best;
}
}// Cadena de matrices: la pregunta de repaso, el greedy que falla, la tabla por largo y el orden ingenuo.
#include <iomanip>
#include <iostream>
#include <vector>
#include "interval_dp.h"
namespace {
void print_dimensions(const std::vector<long long>& d) {
for (std::size_t i = 0; i + 1 < d.size(); i++) {
std::cout << (i ? ", " : "") << d[i] << "x" << d[i + 1];
}
}
void print_table(const CostTable& cost) {
const int n = static_cast<int>(cost.size());
std::cout << " i\\j";
for (int j = 0; j < n; j++) std::cout << std::setw(8) << j;
std::cout << '\n';
for (int i = 0; i < n; i++) {
std::cout << std::setw(5) << i;
for (int j = 0; j < n; j++) {
if (j < i) {
std::cout << std::setw(8) << ".";
} else {
std::cout << std::setw(8) << cost[i][j];
}
}
std::cout << '\n';
}
}
void show_chain(const std::vector<long long>& d) {
std::cout << "Matrices: ";
print_dimensions(d);
const ChainTables t = matrix_chain(d);
const int last = static_cast<int>(d.size()) - 2;
std::cout << '\n';
print_table(t.cost);
std::cout << "Mínimo: " << t.cost[0][last] << ", con " << parenthesize(t.split, 0, last) << '\n';
}
} // namespace
int main() {
const std::vector<long long> review = {10, 30, 5, 60};
show_chain(review);
std::cout << " (A0 A1) A2: " << 10 * 30 * 5 + 10 * 5 * 60 << '\n';
std::cout << " A0 (A1 A2): " << 30 * 5 * 60 + 10 * 30 * 60 << "\n\n";
const std::vector<long long> greedy_case = {4, 1, 3, 5};
std::cout << "Primero la multiplicación más barata\n matrices: ";
print_dimensions(greedy_case);
std::cout << "\n greedy: " << chain_cheapest_first(greedy_case) << ", óptimo por fuerza bruta: "
<< chain_brute(greedy_case) << "\n\n";
const std::vector<long long> d = {10, 20, 5, 30, 2};
show_chain(d);
std::cout << "Por fuerza bruta: " << chain_brute(d) << "\n\n";
std::cout << "Orden ingenuo: i de menor a mayor, j de menor a mayor, con la tabla en ceros\n";
for (const std::vector<long long>& instance : {review, d}) {
const NaiveOrderRun run = matrix_chain_naive_order(instance);
std::cout << " ";
print_dimensions(instance);
std::cout << '\n';
for (const std::string& line : run.first_reads) std::cout << " " << line << '\n';
std::cout << " lecturas vacías: " << run.empty_reads << ", respuesta: " << run.answer << ", correcta: "
<< matrix_chain(instance).cost[0][instance.size() - 2] << '\n';
}
std::cout << "\nOtro orden válido, i de mayor a menor y j de menor a mayor: " << matrix_chain_i_descending(d) << '\n';
long long combinations = 0;
const std::vector<long long> ten(11, 2);
matrix_chain(ten, &combinations);
std::cout << "Con n = 10 matrices, combinaciones (i, k, j) probadas: " << combinations << '\n';
}// El grafo de subproblemas, y el camino más largo en un DAG con pesos negativos y un nodo sin alcanzar.
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "dag_dp.h"
namespace {
void show_subproblem_graphs() {
std::cout << "Fibonacci, n = 5: quién lee a quién\n";
for (int n = 2; n <= 5; n++) {
std::cout << " F(" << n - 1 << ") -> F(" << n << ") F(" << n - 2 << ") -> F(" << n << ")\n";
}
std::cout << " 6 nodos, 8 aristas\n\n";
const std::string a = "casa";
const std::string b = "cosas";
const WeightedDag g = edit_subproblem_dag(a, b);
std::size_t arcs = 0;
for (const auto& out : g) arcs += out.size();
std::cout << "Distancia de edición, " << a << " -> " << b << ": " << g.size() << " nodos, " << arcs << " aristas\n";
const LongestPaths paths = longest_paths(g, edit_node(0, 0, b.size()));
const int target = edit_node(a.size(), b.size(), b.size());
std::cout << " distancia, como camino en ese DAG: " << -paths.length[target] << '\n';
std::cout << " el camino, en casillas (i, j):";
for (int v : path_to(paths, target)) {
std::cout << " (" << v / static_cast<int>(b.size() + 1) << "," << v % static_cast<int>(b.size() + 1) << ")";
}
std::cout << "\n\n";
const std::vector<KnapsackObject> objects = {{2, 3}, {3, 4}, {4, 5}, {5, 8}};
std::cout << "Mochila de la semana 7, capacidad 9, como camino más largo: " << knapsack_as_path(objects, 9) << "\n\n";
}
void show_longest_path() {
// El nodo 0 no se alcanza desde el origen 1, y es la cola de una arista muy pesada.
WeightedDag g(7);
g[0] = {{2, 100}, {6, 1}};
g[1] = {{2, 3}, {3, 5}};
g[2] = {{4, -1}};
g[3] = {{4, -4}, {5, 2}};
g[4] = {{5, 6}};
std::cout << "DAG:";
for (int u = 0; u < 7; u++) {
for (const Arc& arc : g[u]) std::cout << ' ' << u << "->" << arc.to << '(' << arc.weight << ')';
}
std::cout << "\nOrigen: 1\nOrden topológico:";
for (int u : topological_order(g)) std::cout << ' ' << u;
std::cout << "\n\n";
const int source = 1;
const LongestPaths r = longest_paths(g, source);
const std::vector<long long> anywhere = longest_from_anywhere(g);
std::cout << "nodo L(v) pred todos en 0\n";
for (int v = 0; v < 7; v++) {
std::cout << std::setw(4) << v;
if (r.reached[v]) {
std::cout << std::setw(7) << r.length[v] << std::setw(6) << r.pred[v];
} else {
std::cout << std::setw(7) << "-inf" << std::setw(6) << "-";
}
std::cout << std::setw(12) << anywhere[v] << '\n';
}
std::cout << "\nCamino más largo de 1 a 5:";
for (int v : path_to(r, 5)) std::cout << ' ' << v;
std::cout << ", largo " << r.length[5] << '\n';
long long total = 0;
std::vector<int> walk;
heaviest_arc_walk(g, source, 5, total, walk);
std::cout << "Salir por la arista más pesada:";
for (int v : walk) std::cout << ' ' << v;
std::cout << ", largo " << total << '\n';
}
} // namespace
int main() {
show_subproblem_graphs();
show_longest_path();
}// Camino más largo en un DAG, y los subproblemas de la semana 7 vistos como caminos.
#pragma once
#include <algorithm>
#include <string>
#include <vector>
struct Arc {
int to;
long long weight;
};
using WeightedDag = std::vector<std::vector<Arc>>;
// Orden topológico por DFS, como en la semana 6: lo que termina último va primero.
inline void visit_for_order(const WeightedDag& g, int u, std::vector<char>& seen, std::vector<int>& finished) {
seen[u] = 1;
for (const Arc& arc : g[u]) {
if (!seen[arc.to]) visit_for_order(g, arc.to, seen, finished);
}
finished.push_back(u);
}
inline std::vector<int> topological_order(const WeightedDag& g) {
std::vector<char> seen(g.size(), 0);
std::vector<int> finished;
for (int u = 0; u < static_cast<int>(g.size()); u++) {
if (!seen[u]) visit_for_order(g, u, seen, finished);
}
std::reverse(finished.begin(), finished.end());
return finished;
}
struct LongestPaths {
std::vector<long long> length; // solo tiene sentido si reached[v]
std::vector<char> reached; // hay camino desde el origen
std::vector<int> pred; // nodo anterior en un camino más largo, o -1
};
// Invariante: al llegar a u en el orden, ya se miraron todas las aristas que entran a u, así que
// length[u] es definitivo. De un nodo no alcanzado no se sale: no hay valor que sumarle.
inline LongestPaths longest_paths(const WeightedDag& g, int source) {
const std::size_t n = g.size();
LongestPaths r{std::vector<long long>(n, 0), std::vector<char>(n, 0), std::vector<int>(n, -1)};
r.reached[source] = 1;
for (int u : topological_order(g)) {
if (!r.reached[u]) continue;
for (const Arc& arc : g[u]) {
const long long candidate = r.length[u] + arc.weight;
if (!r.reached[arc.to] || candidate > r.length[arc.to]) {
r.reached[arc.to] = 1;
r.length[arc.to] = candidate;
r.pred[arc.to] = u;
}
}
}
return r;
}
inline std::vector<int> path_to(const LongestPaths& r, int target) {
std::vector<int> path;
for (int v = target; v != -1; v = r.pred[v]) path.push_back(v);
std::reverse(path.begin(), path.end());
return path;
}
// Error: todos empiezan en 0, como si cualquier nodo fuera origen.
inline std::vector<long long> longest_from_anywhere(const WeightedDag& g) {
std::vector<long long> length(g.size(), 0);
for (int u : topological_order(g)) {
for (const Arc& arc : g[u]) length[arc.to] = std::max(length[arc.to], length[u] + arc.weight);
}
return length;
}
// Greedy que falla: salir siempre por la arista de mayor peso. Devuelve el largo del recorrido
// si llega al destino, y escribe el recorrido en walk.
inline bool heaviest_arc_walk(const WeightedDag& g, int source, int target, long long& total, std::vector<int>& walk) {
total = 0;
walk = {source};
int u = source;
while (u != target && !g[u].empty()) {
const Arc* best = &g[u][0];
for (const Arc& arc : g[u]) {
if (arc.weight > best->weight) best = &arc;
}
total += best->weight;
u = best->to;
walk.push_back(u);
}
return u == target;
}
// Árbitro: prueba todos los caminos desde u. Máximo en best[v] para cada v alcanzable.
inline void all_paths(const WeightedDag& g, int u, long long so_far, std::vector<long long>& best,
std::vector<char>& seen) {
if (!seen[u] || so_far > best[u]) {
seen[u] = 1;
best[u] = so_far;
}
for (const Arc& arc : g[u]) all_paths(g, arc.to, so_far + arc.weight, best, seen);
}
// El grafo de subproblemas de la distancia de edición: una arista por cada casilla leída, de la
// casilla leída a la que la lee. El peso es menos el costo, para usar el camino más largo.
inline int edit_node(std::size_t i, std::size_t j, std::size_t m) {
return static_cast<int>(i * (m + 1) + j);
}
inline WeightedDag edit_subproblem_dag(const std::string& a, const std::string& b) {
const std::size_t n = a.size();
const std::size_t m = b.size();
WeightedDag g((n + 1) * (m + 1));
for (std::size_t i = 0; i <= n; i++) {
for (std::size_t j = 0; j <= m; j++) {
const int here = edit_node(i, j, m);
if (i < n) g[here].push_back({edit_node(i + 1, j, m), -1});
if (j < m) g[here].push_back({edit_node(i, j + 1, m), -1});
if (i < n && j < m) g[here].push_back({edit_node(i + 1, j + 1, m), a[i] == b[j] ? 0 : -1});
}
}
return g;
}
// El de la mochila: un nodo por (i, c), más un origen que da a todas las casillas (0, c).
struct KnapsackObject {
std::size_t weight;
long long value;
};
inline long long knapsack_as_path(const std::vector<KnapsackObject>& objects, std::size_t capacity) {
const std::size_t rows = objects.size() + 1;
const std::size_t columns = capacity + 1;
const auto node = [columns](std::size_t i, std::size_t c) { return static_cast<int>(i * columns + c); };
const int source = static_cast<int>(rows * columns);
WeightedDag g(rows * columns + 1);
for (std::size_t c = 0; c < columns; c++) g[source].push_back({node(0, c), 0});
for (std::size_t i = 1; i < rows; i++) {
const KnapsackObject& object = objects[i - 1];
for (std::size_t c = 0; c < columns; c++) {
g[node(i - 1, c)].push_back({node(i, c), 0});
if (object.weight <= c) g[node(i - 1, c - object.weight)].push_back({node(i, c), object.value});
}
}
return longest_paths(g, source).length[node(rows - 1, capacity)];
}// Una cadena de n nodos, primero sin recursión y luego con ella: deep_chain N. Se compila con -O2.
#include <cstdlib>
#include <iostream>
#include <vector>
#include "tree_dp.h"
int main(int argc, char* argv[]) {
const int n = argc > 1 ? std::atoi(argv[1]) : 100000;
RootedTree chain{std::vector<std::vector<int>>(static_cast<std::size_t>(n)), 0,
std::vector<long long>(static_cast<std::size_t>(n), 1)};
for (int v = 0; v + 1 < n; v++) chain.children[v].push_back(v + 1);
std::cout << "orden inverso de BFS, nodos bajo la raíz: " << subtree_sizes_bfs_order(chain.children, 0)[0] << std::endl;
std::cout << "recursión, conjunto independiente: " << independent_set_value(chain) << std::endl;
}// Programación dinámica sobre intervalos: cadena de matrices y árbol binario de búsqueda óptimo.
#pragma once
#include <algorithm>
#include <cstddef>
#include <limits>
#include <string>
#include <vector>
using CostTable = std::vector<std::vector<long long>>;
using SplitTable = std::vector<std::vector<int>>;
constexpr long long NO_COST = std::numeric_limits<long long>::max();
// ---------- Cadena de matrices. La matriz i mide d[i] x d[i + 1]. ----------
struct ChainTables {
CostTable cost; // cost[i][j]: el mínimo para multiplicar las matrices i..j, ambas incluidas
SplitTable split; // split[i][j]: la k de la última multiplicación
};
inline long long merge_cost(const std::vector<long long>& d, int i, int k, int j) {
return d[i] * d[k + 1] * d[j + 1];
}
// Por largo creciente: todo lo que se lee es más corto que lo que se escribe.
inline ChainTables matrix_chain(const std::vector<long long>& d, long long* combinations = nullptr) {
const int n = static_cast<int>(d.size()) - 1;
ChainTables t{CostTable(n, std::vector<long long>(n, 0)), SplitTable(n, std::vector<int>(n, -1))};
for (int length = 2; length <= n; length++) {
for (int i = 0; i + length - 1 < n; i++) {
const int j = i + length - 1;
t.cost[i][j] = NO_COST;
for (int k = i; k < j; k++) {
if (combinations) (*combinations)++;
const long long c = t.cost[i][k] + t.cost[k + 1][j] + merge_cost(d, i, k, j);
if (c < t.cost[i][j]) {
t.cost[i][j] = c;
t.split[i][j] = k;
}
}
}
}
return t;
}
// También sirve: i de mayor a menor y j de menor a mayor. Lo leído tiene i mayor, o j menor.
inline long long matrix_chain_i_descending(const std::vector<long long>& d) {
const int n = static_cast<int>(d.size()) - 1;
CostTable cost(n, std::vector<long long>(n, 0));
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
cost[i][j] = NO_COST;
for (int k = i; k < j; k++) {
cost[i][j] = std::min(cost[i][j], cost[i][k] + cost[k + 1][j] + merge_cost(d, i, k, j));
}
}
}
return n == 0 ? 0 : cost[0][n - 1];
}
// Error: i de menor a mayor y j de menor a mayor. Las casillas sin llenar valen 0 y se leen igual.
struct NaiveOrderRun {
long long answer;
long long empty_reads;
std::vector<std::string> first_reads;
};
inline NaiveOrderRun matrix_chain_naive_order(const std::vector<long long>& d) {
const int n = static_cast<int>(d.size()) - 1;
CostTable cost(n, std::vector<long long>(n, 0));
std::vector<std::vector<char>> filled(n, std::vector<char>(n, 0));
for (int i = 0; i < n; i++) filled[i][i] = 1;
NaiveOrderRun run{0, 0, {}};
const auto read = [&](int writing_i, int writing_j, int i, int j) {
if (!filled[i][j]) {
run.empty_reads++;
run.first_reads.push_back("M[" + std::to_string(writing_i) + "][" + std::to_string(writing_j) +
"] lee M[" + std::to_string(i) + "][" + std::to_string(j) + "], vacía");
}
return cost[i][j];
};
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
long long best = NO_COST;
for (int k = i; k < j; k++) {
best = std::min(best, read(i, j, i, k) + read(i, j, k + 1, j) + merge_cost(d, i, k, j));
}
cost[i][j] = best;
filled[i][j] = 1;
}
}
run.answer = n == 0 ? 0 : cost[0][n - 1];
return run;
}
inline std::string parenthesize(const SplitTable& split, int i, int j) {
if (i == j) return "A" + std::to_string(i);
const int k = split[i][j];
return "(" + parenthesize(split, i, k) + " " + parenthesize(split, k + 1, j) + ")";
}
// Árbitro: arma cada parentización y calcula su costo multiplicando de verdad las dimensiones.
struct ChainPlan {
long long rows;
long long columns;
long long cost;
};
inline std::vector<ChainPlan> all_chain_plans(const std::vector<long long>& d, int i, int j) {
if (i == j) return {{d[i], d[i + 1], 0}};
std::vector<ChainPlan> plans;
for (int k = i; k < j; k++) {
for (const ChainPlan& left : all_chain_plans(d, i, k)) {
for (const ChainPlan& right : all_chain_plans(d, k + 1, j)) {
plans.push_back({left.rows, right.columns,
left.cost + right.cost + left.rows * left.columns * right.columns});
}
}
}
return plans;
}
inline long long chain_brute(const std::vector<long long>& d) {
long long best = NO_COST;
for (const ChainPlan& plan : all_chain_plans(d, 0, static_cast<int>(d.size()) - 2)) {
best = std::min(best, plan.cost);
}
return best;
}
// Greedy que falla: hacer siempre primero la multiplicación vecina más barata.
inline long long chain_cheapest_first(std::vector<long long> d) {
long long total = 0;
while (d.size() > 2) {
std::size_t best = 1;
for (std::size_t m = 1; m + 1 < d.size(); m++) {
if (d[m - 1] * d[m] * d[m + 1] < d[best - 1] * d[best] * d[best + 1]) best = m;
}
total += d[best - 1] * d[best] * d[best + 1];
d.erase(d.begin() + static_cast<std::ptrdiff_t>(best));
}
return total;
}
// ---------- Árbol binario de búsqueda óptimo. Rangos semiabiertos [i, j). ----------
struct BstTables {
CostTable cost; // cost[i][j]: el mínimo de la suma de frecuencia por profundidad, claves i..j-1
SplitTable root; // root[i][j]: la clave que va en la raíz de ese rango
};
inline std::vector<long long> prefix_sums(const std::vector<long long>& f) {
std::vector<long long> sum(f.size() + 1, 0);
for (std::size_t i = 0; i < f.size(); i++) sum[i + 1] = sum[i] + f[i];
return sum;
}
inline BstTables optimal_bst(const std::vector<long long>& f) {
const int n = static_cast<int>(f.size());
const std::vector<long long> sum = prefix_sums(f);
BstTables t{CostTable(n + 1, std::vector<long long>(n + 1, 0)), SplitTable(n + 1, std::vector<int>(n + 1, -1))};
for (int length = 1; length <= n; length++) {
for (int i = 0; i + length <= n; i++) {
const int j = i + length;
long long best = NO_COST;
for (int r = i; r < j; r++) {
const long long c = t.cost[i][r] + t.cost[r + 1][j];
if (c < best) {
best = c;
t.root[i][j] = r;
}
}
t.cost[i][j] = best + sum[j] - sum[i];
}
}
return t;
}
// Las profundidades de cada clave en el árbol que dice la tabla. La raíz tiene profundidad 1.
inline void bst_depths(const SplitTable& root, int i, int j, int depth, std::vector<int>& out) {
if (i >= j) return;
const int r = root[i][j];
out[r] = depth;
bst_depths(root, i, r, depth + 1, out);
bst_depths(root, r + 1, j, depth + 1, out);
}
inline long long cost_from_depths(const std::vector<long long>& f, const std::vector<int>& depth) {
long long total = 0;
for (std::size_t i = 0; i < f.size(); i++) total += f[i] * depth[i];
return total;
}
inline std::string bst_shape(const SplitTable& root, const std::vector<std::string>& names, int i, int j) {
if (i >= j) return "-";
const int r = root[i][j];
if (j - i == 1) return names[r];
return names[r] + "(" + bst_shape(root, names, i, r) + " " + bst_shape(root, names, r + 1, j) + ")";
}
// Árbitro: genera todos los árboles del rango como listas de profundidades, y mide con la definición.
inline std::vector<std::vector<int>> all_bst_depths(int i, int j, int n) {
if (i >= j) return {std::vector<int>(n, 0)};
std::vector<std::vector<int>> trees;
for (int r = i; r < j; r++) {
for (const std::vector<int>& left : all_bst_depths(i, r, n)) {
for (const std::vector<int>& right : all_bst_depths(r + 1, j, n)) {
std::vector<int> depth(n, 0);
depth[r] = 1;
for (int x = i; x < r; x++) depth[x] = left[x] + 1;
for (int x = r + 1; x < j; x++) depth[x] = right[x] + 1;
trees.push_back(depth);
}
}
}
return trees;
}
inline long long bst_brute(const std::vector<long long>& f) {
const int n = static_cast<int>(f.size());
long long best = NO_COST;
for (const std::vector<int>& depth : all_bst_depths(0, n, n)) {
best = std::min(best, cost_from_depths(f, depth));
}
return best;
}
// Greedy que falla: la clave más frecuente a la raíz, y lo mismo en cada lado.
inline long long bst_most_frequent_root(const std::vector<long long>& f, int i, int j, int depth) {
if (i >= j) return 0;
int r = i;
for (int x = i; x < j; x++) {
if (f[x] > f[r]) r = x;
}
return f[r] * depth + bst_most_frequent_root(f, i, r, depth + 1) + bst_most_frequent_root(f, r + 1, j, depth + 1);
}
// La otra idea natural: el árbol balanceado, ignorando las frecuencias.
inline long long bst_balanced(const std::vector<long long>& f, int i, int j, int depth) {
if (i >= j) return 0;
const int r = i + (j - i) / 2;
return f[r] * depth + bst_balanced(f, i, r, depth + 1) + bst_balanced(f, r + 1, j, depth + 1);
}// Mide las dos versiones de la subsecuencia creciente más larga. Menor de 3 intentos, con -O2.
#include <chrono>
#include <cstdio>
#include <random>
#include <vector>
#include "lis.h"
namespace {
std::vector<int> random_values(std::size_t n, unsigned seed) {
std::mt19937 rng(seed);
std::vector<int> a(n);
for (int& x : a) x = static_cast<int>(rng() >> 2);
return a;
}
template <typename Function>
double best_ms(Function run) {
double best = 1e18;
for (int attempt = 0; attempt < 3; attempt++) {
const auto start = std::chrono::steady_clock::now();
run();
const auto stop = std::chrono::steady_clock::now();
best = std::min(best, std::chrono::duration<double, std::milli>(stop - start).count());
}
return best;
}
} // namespace
int main() {
std::puts("Subsecuencia creciente más larga, valores al azar, menor de 3 intentos");
std::puts(" n largo tabla ms finales ms");
for (std::size_t n : {10000u, 20000u, 40000u, 80000u, 1000000u, 2000000u, 4000000u}) {
const std::vector<int> a = random_values(n, 9);
std::size_t length = 0;
const double fast = best_ms([&] { length = lis_length_tails(a); });
if (n <= 80000u) {
int slow_length = 0;
const double slow = best_ms([&] { slow_length = lis_length_quadratic(a); });
if (slow_length != static_cast<int>(length)) return 1;
std::printf("%9zu %7zu %10.1f %12.1f\n", n, length, slow, fast);
} else {
std::printf("%9zu %7zu %10s %12.1f\n", n, length, "-", fast);
}
}
}// Subsecuencia creciente más larga: la tabla, el arreglo de finales paso a paso y la reconstrucción.
#include <algorithm>
#include <iomanip>
#include <iostream>
#include <string>
#include <vector>
#include "lis.h"
namespace {
void print_values(const std::vector<int>& a) {
for (std::size_t i = 0; i < a.size(); i++) std::cout << (i ? " " : "") << a[i];
}
void show_quadratic(const std::vector<int>& a) {
const LisTable t = lis_quadratic(a);
std::cout << " i:";
for (std::size_t i = 0; i < a.size(); i++) std::cout << std::setw(3) << i;
std::cout << "\na[i]:";
for (int x : a) std::cout << std::setw(3) << x;
std::cout << "\nE[i]:";
for (int e : t.ending) std::cout << std::setw(3) << e;
std::cout << "\npred:";
for (int p : t.pred) std::cout << std::setw(3) << p;
std::cout << '\n';
}
void show_tails(const std::vector<int>& a) {
std::vector<int> tails;
std::vector<int> tail_index;
std::vector<int> pred(a.size(), -1);
std::cout << " i x posición finales después índices pred[i]\n";
for (int i = 0; i < static_cast<int>(a.size()); i++) {
const auto it = std::lower_bound(tails.begin(), tails.end(), a[i]);
const std::size_t position = static_cast<std::size_t>(it - tails.begin());
if (position > 0) pred[i] = tail_index[position - 1];
if (it == tails.end()) {
tails.push_back(a[i]);
tail_index.push_back(i);
} else {
*it = a[i];
tail_index[position] = i;
}
std::cout << std::setw(2) << i << std::setw(3) << a[i] << std::setw(10) << position << " ";
std::string text;
for (int x : tails) text += std::to_string(x) + " ";
std::cout << std::left << std::setw(23) << text;
text.clear();
for (int x : tail_index) text += std::to_string(x) + " ";
std::cout << std::setw(11) << text << std::right << std::setw(4) << pred[i] << '\n';
}
}
} // namespace
int main() {
const std::vector<int> a = {3, 1, 4, 1, 5, 9, 2, 6};
std::cout << "Arreglo: ";
print_values(a);
std::cout << "\n\nLa tabla cuadrática\n";
show_quadratic(a);
std::cout << "Largo: " << lis_length_quadratic(a) << "\nCon pred, desde la primera casilla con ese largo:";
{
const LisTable t = lis_quadratic(a);
std::vector<int> walk;
for (int i = static_cast<int>(std::max_element(t.ending.begin(), t.ending.end()) - t.ending.begin()); i != -1; i = t.pred[i]) {
walk.push_back(a[i]);
}
for (std::size_t k = walk.size(); k-- > 0;) std::cout << ' ' << walk[k];
}
std::cout << "\n\nEl arreglo de finales, con lower_bound\n";
show_tails(a);
std::cout << "Largo: " << lis_length_tails(a) << "\nReconstruida:";
for (int i : lis_indices(a)) std::cout << ' ' << a[i];
std::cout << "\n\nTomar todo lo que sea mayor que lo último tomado\n";
for (const std::vector<int>& b : {std::vector<int>{5, 1, 2, 3}, std::vector<int>{1, 7, 2, 3, 4}}) {
std::cout << " ";
print_values(b);
std::cout << ": greedy " << lis_greedy_take(b) << ", óptimo " << lis_length_tails(b) << '\n';
}
const std::vector<int> c = {2, 5, 3, 7, 1};
std::cout << "\nLos finales no son una subsecuencia\n arreglo: ";
print_values(c);
std::cout << "\n finales: ";
print_values(final_tails(c));
std::cout << "\n reconstruida:";
for (int i : lis_indices(c)) std::cout << ' ' << c[i];
std::cout << '\n';
std::cout << "\nlower_bound y upper_bound\n";
for (const std::vector<int>& d : {std::vector<int>{2, 2, 2}, std::vector<int>{1, 3, 3, 5}}) {
std::cout << " ";
print_values(d);
std::cout << ": con lower_bound " << lis_length_tails(d) << ", con upper_bound "
<< non_decreasing_length_tails(d) << '\n';
}
}// Subsecuencia creciente más larga: estrictamente creciente, salvo donde se dice otra cosa.
#pragma once
#include <algorithm>
#include <cstddef>
#include <vector>
struct LisTable {
std::vector<int> ending; // ending[i]: el largo de la más larga que termina justo en a[i]
std::vector<int> pred; // índice del elemento anterior en esa subsecuencia, o -1
};
inline LisTable lis_quadratic(const std::vector<int>& a) {
const int n = static_cast<int>(a.size());
LisTable t{std::vector<int>(a.size(), 1), std::vector<int>(a.size(), -1)};
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i] && t.ending[j] + 1 > t.ending[i]) {
t.ending[i] = t.ending[j] + 1;
t.pred[i] = j;
}
}
}
return t;
}
inline int lis_length_quadratic(const std::vector<int>& a) {
const LisTable t = lis_quadratic(a);
return t.ending.empty() ? 0 : *std::max_element(t.ending.begin(), t.ending.end());
}
// Invariante: tails es estrictamente creciente, y tails[k] es el menor último valor posible de una
// subsecuencia creciente de largo k + 1 entre los elementos ya vistos.
inline std::size_t lis_length_tails(const std::vector<int>& a) {
std::vector<int> tails;
for (int x : a) {
const auto it = std::lower_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) {
tails.push_back(x);
} else {
*it = x;
}
}
return tails.size();
}
// Con upper_bound se calcula otra cosa: la más larga que no decrece.
inline std::size_t non_decreasing_length_tails(const std::vector<int>& a) {
std::vector<int> tails;
for (int x : a) {
const auto it = std::upper_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) {
tails.push_back(x);
} else {
*it = x;
}
}
return tails.size();
}
inline std::vector<int> final_tails(const std::vector<int>& a) {
std::vector<int> tails;
for (int x : a) {
const auto it = std::lower_bound(tails.begin(), tails.end(), x);
if (it == tails.end()) {
tails.push_back(x);
} else {
*it = x;
}
}
return tails;
}
// Con reconstrucción: tail_index[k] es el índice de tails[k] en a, y pred[i] se fija cuando entra a[i].
inline std::vector<int> lis_indices(const std::vector<int>& a) {
std::vector<int> tails;
std::vector<int> tail_index;
std::vector<int> pred(a.size(), -1);
for (int i = 0; i < static_cast<int>(a.size()); i++) {
const auto it = std::lower_bound(tails.begin(), tails.end(), a[i]);
const std::size_t position = static_cast<std::size_t>(it - tails.begin());
if (position > 0) pred[i] = tail_index[position - 1];
if (it == tails.end()) {
tails.push_back(a[i]);
tail_index.push_back(i);
} else {
*it = a[i];
tail_index[position] = i;
}
}
std::vector<int> indices;
if (!tail_index.empty()) {
for (int i = tail_index.back(); i != -1; i = pred[i]) indices.push_back(i);
}
std::reverse(indices.begin(), indices.end());
return indices;
}
// Greedy que falla: recorrer tomando todo lo que sea mayor que lo último tomado.
inline int lis_greedy_take(const std::vector<int>& a) {
int taken = 0;
int last = 0;
for (int x : a) {
if (taken == 0 || x > last) {
taken++;
last = x;
}
}
return taken;
}
// Árbitro: prueba los 2^n subconjuntos de posiciones.
inline int lis_brute(const std::vector<int>& a, bool strict = true) {
const std::size_t n = a.size();
int best = 0;
for (unsigned mask = 0; mask < (1u << n); mask++) {
int count = 0;
int last = 0;
bool ok = true;
for (std::size_t i = 0; i < n && ok; i++) {
if (!(mask >> i & 1u)) continue;
if (count > 0 && (strict ? a[i] <= last : a[i] < last)) ok = false;
last = a[i];
count++;
}
if (ok) best = std::max(best, count);
}
return best;
}// Conjunto independiente y cobertura en un árbol: dos estados, los errores y lo que pasa con un ciclo.
#include <iomanip>
#include <iostream>
#include <vector>
#include "tree_dp.h"
namespace {
void print_nodes(const std::vector<int>& nodes) {
std::cout << '{';
for (std::size_t i = 0; i < nodes.size(); i++) std::cout << (i ? ", " : "") << nodes[i];
std::cout << '}';
}
void compare(const char* title, const RootedTree& tree) {
std::cout << title << '\n';
std::cout << " fuerza bruta: " << independent_set_brute(tree) << '\n';
std::cout << " dos estados: " << independent_set_value(tree) << '\n';
std::cout << " niveles alternos: " << alternate_levels(tree) << '\n';
std::cout << " mayor valor primero: " << largest_value_first(tree) << '\n';
std::cout << " sin elegir obliga a hijos: " << forced_children_value(tree) << '\n';
}
} // namespace
int main() {
const RootedTree company{{{1, 2}, {3, 4}, {5}, {}, {}, {6, 7}, {}, {}}, 0, {4, 9, 6, 5, 5, 8, 3, 4}};
std::cout << "Árbol: padre -> hijos\n";
for (std::size_t v = 0; v < company.children.size(); v++) {
std::cout << " " << v << " (valor " << company.value[v] << ") ->";
for (int c : company.children[v]) std::cout << ' ' << c;
std::cout << '\n';
}
const TwoStates s = independent_set_states(company);
std::cout << "\nnodo valor con sin\n";
for (std::size_t t = company.value.size(); t-- > 0;) {
std::cout << std::setw(4) << t << std::setw(7) << company.value[t] << std::setw(6) << s.with_node[t]
<< std::setw(5) << s.without_node[t] << '\n';
}
std::cout << "Mejor valor: " << independent_set_value(company) << ", con ";
print_nodes(independent_set_nodes(company));
std::cout << "\nCada nodo por separado, sin mirar al padre: ";
print_nodes(choose_each_node_alone(company));
std::cout << "\n\n";
compare("El árbol de arriba:", company);
const RootedTree chain{{{1}, {2}, {3}, {}}, 0, {10, 1, 1, 10}};
compare("\nLa cadena 10, 1, 1, 10:", chain);
const RootedTree star{{{1, 2, 3}, {}, {}, {}}, 0, {5, 2, 2, 2}};
compare("\nUn jefe de valor 5 con tres personas de valor 2:", star);
const RootedTree negative{{{1, 2}, {}, {}}, 0, {-4, -1, -2}};
compare("\nTodos negativos:", negative);
std::cout << " elegidos: ";
print_nodes(independent_set_nodes(negative));
std::cout << '\n';
long long total = 0;
for (long long w : company.value) total += w;
std::cout << "\nCobertura de peso mínimo del primer árbol\n";
std::cout << " con su propia recurrencia: " << min_cover_value(company) << '\n';
std::cout << " fuerza bruta: " << min_cover_brute(company) << '\n';
std::cout << " total " << total << " menos conjunto independiente: " << total - independent_set_value(company) << '\n';
// Un triángulo: el árbol de BFS desde 0 pierde la arista 1-2, y la cuenta ya no sirve.
const RootedTree bfs_tree_of_triangle{{{1, 2}, {}, {}}, 0, {5, 5, 5}};
std::cout << "\nUn triángulo con valores 5, 5, 5, usando el árbol de BFS desde 0\n";
std::cout << " el programa de árboles dice: " << independent_set_value(bfs_tree_of_triangle) << ", con ";
print_nodes(independent_set_nodes(bfs_tree_of_triangle));
std::cout << "\n pero 1 y 2 son vecinos en el triángulo, y lo mejor de verdad es 5\n";
}// Programación dinámica sobre árboles: conjunto independiente y cobertura de vértices con pesos.
#pragma once
#include <algorithm>
#include <cstddef>
#include <utility>
#include <vector>
// Un árbol con raíz, dado por las listas de hijos. Los ejemplos son chicos: aquí se usa recursión.
struct RootedTree {
std::vector<std::vector<int>> children;
int root;
std::vector<long long> value;
};
struct TwoStates {
std::vector<long long> with_node; // lo mejor en el subárbol de v, con v dentro
std::vector<long long> without_node; // lo mejor en el subárbol de v, con v fuera
};
// Primero los hijos. Un nodo elegido obliga a sus hijos a quedar fuera; uno sin elegir los deja libres.
inline void fill_states(const RootedTree& tree, int v, TwoStates& s) {
s.with_node[v] = tree.value[v];
s.without_node[v] = 0;
for (int c : tree.children[v]) {
fill_states(tree, c, s);
s.with_node[v] += s.without_node[c];
s.without_node[v] += std::max(s.with_node[c], s.without_node[c]);
}
}
inline TwoStates independent_set_states(const RootedTree& tree) {
TwoStates s{std::vector<long long>(tree.value.size(), 0), std::vector<long long>(tree.value.size(), 0)};
fill_states(tree, tree.root, s);
return s;
}
inline long long independent_set_value(const RootedTree& tree) {
const TwoStates s = independent_set_states(tree);
return std::max(s.with_node[tree.root], s.without_node[tree.root]);
}
// De arriba hacia abajo: lo que se decide en un nodo depende de lo que se decidió en su padre.
inline void choose_down(const RootedTree& tree, const TwoStates& s, int v, bool parent_chosen, std::vector<int>& out) {
const bool chosen = !parent_chosen && s.with_node[v] > s.without_node[v];
if (chosen) out.push_back(v);
for (int c : tree.children[v]) choose_down(tree, s, c, chosen, out);
}
inline std::vector<int> independent_set_nodes(const RootedTree& tree) {
const TwoStates s = independent_set_states(tree);
std::vector<int> out;
choose_down(tree, s, tree.root, false, out);
std::sort(out.begin(), out.end());
return out;
}
// Error: mirar cada nodo por separado, sin preguntar qué se decidió en el padre.
inline std::vector<int> choose_each_node_alone(const RootedTree& tree) {
const TwoStates s = independent_set_states(tree);
std::vector<int> out;
for (int v = 0; v < static_cast<int>(tree.value.size()); v++) {
if (s.with_node[v] > s.without_node[v]) out.push_back(v);
}
return out;
}
// Error: un nodo sin elegir obliga a elegir a sus hijos.
inline std::pair<long long, long long> forced_children(const RootedTree& tree, int v) {
long long with_v = tree.value[v];
long long without_v = 0;
for (int c : tree.children[v]) {
const auto [with_c, without_c] = forced_children(tree, c);
with_v += without_c;
without_v += with_c;
}
return {with_v, without_v};
}
inline long long forced_children_value(const RootedTree& tree) {
const auto [with_root, without_root] = forced_children(tree, tree.root);
return std::max(with_root, without_root);
}
// Greedy que falla: los niveles pares o los impares, lo que sume más.
inline long long alternate_levels(const RootedTree& tree) {
long long sum[2] = {0, 0};
std::vector<std::pair<int, int>> stack = {{tree.root, 0}};
while (!stack.empty()) {
const auto [v, level] = stack.back();
stack.pop_back();
sum[level] += tree.value[v];
for (int c : tree.children[v]) stack.push_back({c, 1 - level});
}
return std::max(sum[0], sum[1]);
}
// Greedy que falla: el de mayor valor primero, mientras no choque con un vecino ya elegido.
inline long long largest_value_first(const RootedTree& tree) {
const int n = static_cast<int>(tree.value.size());
std::vector<int> parent(tree.value.size(), -1);
for (int v = 0; v < n; v++) {
for (int c : tree.children[v]) parent[c] = v;
}
std::vector<int> order(tree.value.size());
for (int v = 0; v < n; v++) order[v] = v;
std::stable_sort(order.begin(), order.end(), [&tree](int x, int y) { return tree.value[x] > tree.value[y]; });
std::vector<char> chosen(tree.value.size(), 0);
long long total = 0;
for (int v : order) {
if (tree.value[v] <= 0) break;
bool blocked = parent[v] != -1 && chosen[parent[v]];
for (int c : tree.children[v]) blocked = blocked || chosen[c];
if (!blocked) {
chosen[v] = 1;
total += tree.value[v];
}
}
return total;
}
// Árbitro para árboles chicos: prueba los 2^n subconjuntos.
inline long long independent_set_brute(const RootedTree& tree) {
const std::size_t n = tree.value.size();
long long best = 0;
for (unsigned mask = 0; mask < (1u << n); mask++) {
bool ok = true;
long long total = 0;
for (std::size_t v = 0; v < n && ok; v++) {
if (!(mask >> v & 1u)) continue;
total += tree.value[v];
for (int c : tree.children[v]) ok = ok && !(mask >> c & 1u);
}
if (ok) best = std::max(best, total);
}
return best;
}
inline long long min_cover_brute(const RootedTree& tree) {
const std::size_t n = tree.value.size();
long long best = 0;
bool any = false;
for (unsigned mask = 0; mask < (1u << n); mask++) {
bool ok = true;
long long total = 0;
for (std::size_t v = 0; v < n && ok; v++) {
if (mask >> v & 1u) total += tree.value[v];
for (int c : tree.children[v]) ok = ok && ((mask >> v & 1u) || (mask >> c & 1u));
}
if (ok && (!any || total < best)) {
best = total;
any = true;
}
}
return best;
}
// Cobertura de vértices de peso mínimo. El mismo patrón: un nodo fuera obliga a sus hijos a estar dentro.
inline std::pair<long long, long long> cover_states(const RootedTree& tree, int v) {
long long inside = tree.value[v];
long long outside = 0;
for (int c : tree.children[v]) {
const auto [inside_c, outside_c] = cover_states(tree, c);
inside += std::min(inside_c, outside_c);
outside += inside_c;
}
return {inside, outside};
}
inline long long min_cover_value(const RootedTree& tree) {
const auto [inside, outside] = cover_states(tree, tree.root);
return std::min(inside, outside);
}
// El orden inverso de BFS, en el cálculo más simple sobre un árbol: cuántos nodos tiene cada subárbol.
// Un padre sale siempre antes que sus hijos en un BFS, así que al revés cada nodo llega con sus hijos
// ya resueltos. No usa recursión, así que la profundidad del árbol no importa.
inline std::vector<int> subtree_sizes_bfs_order(const std::vector<std::vector<int>>& children, int root) {
std::vector<int> parent(children.size(), -1);
std::vector<int> order = {root};
for (std::size_t t = 0; t < order.size(); t++) {
for (int c : children[order[t]]) {
parent[c] = order[t];
order.push_back(c);
}
}
std::vector<int> size(children.size(), 1);
for (std::size_t t = order.size(); t-- > 1;) {
size[parent[order[t]]] += size[order[t]];
}
return size;
}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 <iostream>
#include <numeric>
#include <random>
#include <string>
#include <vector>
#include "dag_dp.h"
#include "interval_dp.h"
#include "lis.h"
#include "tree_dp.h"
namespace {
// Distancia de edición con la tabla de la semana 7, para compararla con el camino en el DAG.
int edit_table_value(const std::string& a, const std::string& b) {
std::vector<std::vector<int>> d(a.size() + 1, std::vector<int>(b.size() + 1, 0));
for (std::size_t i = 0; i <= a.size(); i++) d[i][0] = static_cast<int>(i);
for (std::size_t j = 0; j <= b.size(); j++) d[0][j] = static_cast<int>(j);
for (std::size_t i = 1; i <= a.size(); i++) {
for (std::size_t j = 1; j <= b.size(); j++) {
d[i][j] = std::min({d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] == b[j - 1] ? 0 : 1)});
}
}
return d[a.size()][b.size()];
}
long long knapsack_brute_small(const std::vector<KnapsackObject>& objects, std::size_t capacity) {
long long best = 0;
for (unsigned mask = 0; mask < (1u << objects.size()); mask++) {
std::size_t weight = 0;
long long value = 0;
for (std::size_t i = 0; i < objects.size(); i++) {
if (mask >> i & 1u) {
weight += objects[i].weight;
value += objects[i].value;
}
}
if (weight <= capacity) best = std::max(best, value);
}
return best;
}
WeightedDag random_dag(std::mt19937& rng, int n) {
// Etiquetas barajadas: el orden del DAG no coincide con el de los números.
std::vector<int> label(static_cast<std::size_t>(n));
std::iota(label.begin(), label.end(), 0);
std::shuffle(label.begin(), label.end(), rng);
WeightedDag g(static_cast<std::size_t>(n));
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (rng() % 3 == 0) g[label[i]].push_back({label[j], static_cast<long long>(rng() % 21) - 10});
}
}
return g;
}
void test_longest_path() {
std::mt19937 rng(91);
for (int round = 0; round < 3000; round++) {
const int n = 1 + static_cast<int>(rng() % 9);
const WeightedDag g = random_dag(rng, n);
const int source = static_cast<int>(rng() % static_cast<unsigned>(n));
const std::vector<int> order = topological_order(g);
std::vector<int> position(static_cast<std::size_t>(n));
for (int t = 0; t < n; t++) position[order[t]] = t;
for (int u = 0; u < n; u++) {
for (const Arc& arc : g[u]) assert(position[u] < position[arc.to]);
}
std::vector<long long> best(static_cast<std::size_t>(n), 0);
std::vector<char> seen(static_cast<std::size_t>(n), 0);
all_paths(g, source, 0, best, seen);
const LongestPaths r = longest_paths(g, source);
for (int v = 0; v < n; v++) {
assert(r.reached[v] == seen[v]);
if (!seen[v]) continue;
assert(r.length[v] == best[v]);
// El camino reconstruido empieza en el origen, usa aristas del grafo y suma ese largo.
const std::vector<int> path = path_to(r, v);
assert(path.front() == source && path.back() == v);
long long total = 0;
for (std::size_t t = 0; t + 1 < path.size(); t++) {
long long arc_best = -1000000;
for (const Arc& arc : g[path[t]]) {
if (arc.to == path[t + 1]) arc_best = std::max(arc_best, arc.weight);
}
assert(arc_best != -1000000);
total += arc_best;
}
assert(total == best[v]);
}
}
}
void test_longest_path_example() {
WeightedDag g(7);
g[0] = {{2, 100}, {6, 1}};
g[1] = {{2, 3}, {3, 5}};
g[2] = {{4, -1}};
g[3] = {{4, -4}, {5, 2}};
g[4] = {{5, 6}};
const LongestPaths r = longest_paths(g, 1);
assert(!r.reached[0] && !r.reached[6]);
assert(r.length[5] == 8);
assert(path_to(r, 5) == (std::vector<int>{1, 2, 4, 5}));
assert(longest_from_anywhere(g)[5] > r.length[5]); // sin -inf, el nodo 0 contamina
long long total = 0;
std::vector<int> walk;
assert(heaviest_arc_walk(g, 1, 5, total, walk));
assert(total == 7);
}
void test_subproblem_paths() {
std::mt19937 rng(92);
for (int round = 0; round < 1000; round++) {
std::string a(rng() % 7, 'a');
std::string b(rng() % 7, 'a');
for (char& c : a) c = static_cast<char>('a' + rng() % 3);
for (char& c : b) c = static_cast<char>('a' + rng() % 3);
const WeightedDag g = edit_subproblem_dag(a, b);
const LongestPaths r = longest_paths(g, edit_node(0, 0, b.size()));
assert(-r.length[edit_node(a.size(), b.size(), b.size())] == edit_table_value(a, b));
// Nodos y aristas: (n + 1)(m + 1) casillas, y 3nm + n + m lecturas.
std::size_t arcs = 0;
for (const auto& out : g) arcs += out.size();
assert(g.size() == (a.size() + 1) * (b.size() + 1));
assert(arcs == 3 * a.size() * b.size() + a.size() + b.size());
}
assert(edit_table_value("casa", "cosas") == 2);
for (int round = 0; round < 500; round++) {
std::vector<KnapsackObject> objects(rng() % 7);
for (KnapsackObject& o : objects) o = {1 + rng() % 6, static_cast<long long>(rng() % 20)};
const std::size_t capacity = rng() % 16;
assert(knapsack_as_path(objects, capacity) == knapsack_brute_small(objects, capacity));
}
assert(knapsack_as_path({{2, 3}, {3, 4}, {4, 5}, {5, 8}}, 9) == 13);
}
void check_lis_answer(const std::vector<int>& a) {
const std::vector<int> indices = lis_indices(a);
for (std::size_t t = 0; t + 1 < indices.size(); t++) {
assert(indices[t] < indices[t + 1]);
assert(a[indices[t]] < a[indices[t + 1]]);
}
assert(indices.size() == lis_length_tails(a));
}
void test_lis() {
std::mt19937 rng(93);
for (int round = 0; round < 3000; round++) {
std::vector<int> a(rng() % 13);
const unsigned variety = 1 + rng() % 12;
for (int& x : a) x = static_cast<int>(rng() % variety) - 3;
const int best = lis_brute(a);
assert(lis_length_quadratic(a) == best);
assert(static_cast<int>(lis_length_tails(a)) == best);
assert(static_cast<int>(non_decreasing_length_tails(a)) == lis_brute(a, false));
check_lis_answer(a);
// Es un camino más largo en el DAG con un origen virtual.
const int n = static_cast<int>(a.size());
WeightedDag g(a.size() + 1);
for (int i = 0; i < n; i++) {
g[n].push_back({i, 1});
for (int j = i + 1; j < n; j++) {
if (a[i] < a[j]) g[i].push_back({j, 1});
}
}
const LongestPaths r = longest_paths(g, n);
long long longest = 0;
for (int i = 0; i < n; i++) longest = std::max(longest, r.length[i]);
assert(longest == best);
// La invariante del arreglo de finales, contra su definición.
std::vector<int> tails;
for (int i = 0; i < n; i++) {
const auto it = std::lower_bound(tails.begin(), tails.end(), a[i]);
if (it == tails.end()) {
tails.push_back(a[i]);
} else {
*it = a[i];
}
assert(std::adjacent_find(tails.begin(), tails.end(), std::greater_equal<int>()) == tails.end());
for (std::size_t k = 0; k < tails.size(); k++) {
// Con el prefijo y solo valores menores o iguales que tails[k] hay largo k + 1,
// y con valores estrictamente menores, no.
std::vector<int> below;
std::vector<int> strictly;
for (int t = 0; t <= i; t++) {
if (a[t] <= tails[k]) below.push_back(a[t]);
if (a[t] < tails[k]) strictly.push_back(a[t]);
}
assert(lis_length_tails(below) >= k + 1);
assert(lis_length_tails(strictly) < k + 1);
}
}
}
assert(lis_length_tails({}) == 0);
assert(lis_indices({}).empty());
assert(lis_length_tails({2, 2, 2}) == 1);
assert(non_decreasing_length_tails({2, 2, 2}) == 3);
assert(lis_length_tails({1, 3, 3, 5}) == 3);
assert(non_decreasing_length_tails({1, 3, 3, 5}) == 4);
assert(lis_greedy_take({5, 1, 2, 3}) == 1);
assert(lis_length_tails({5, 1, 2, 3}) == 3);
// Los finales no son una subsecuencia: el 1 está después del 3 y del 7.
assert(final_tails({2, 5, 3, 7, 1}) == (std::vector<int>{1, 3, 7}));
assert(lis_length_tails({1, 3, 7}) == 3 && lis_length_tails({2, 5, 3, 7, 1}) == 3);
}
void test_matrix_chain() {
// La pregunta de repaso: 10x30, 30x5, 5x60.
const std::vector<long long> review = {10, 30, 5, 60};
const ChainTables t = matrix_chain(review);
assert(t.cost[0][2] == 4500);
assert(parenthesize(t.split, 0, 2) == "((A0 A1) A2)");
assert(chain_brute(review) == 4500);
std::mt19937 rng(94);
for (int round = 0; round < 1000; round++) {
std::vector<long long> d(2 + rng() % 7);
for (long long& x : d) x = 1 + static_cast<long long>(rng() % 20);
const long long last = static_cast<long long>(d.size()) - 2;
const long long value = matrix_chain(d).cost[0][last];
assert(value == chain_brute(d));
assert(value == matrix_chain_i_descending(d));
assert(chain_cheapest_first(d) >= value);
assert(matrix_chain_naive_order(d).answer <= value); // los ceros solo pueden bajarlo
if (d.size() >= 4) assert(matrix_chain_naive_order(d).empty_reads > 0);
}
// Hay tantas parentizaciones como dice el número de Catalan: C(n - 1) para n matrices.
std::vector<long long> catalan = {1};
for (long long m = 0; m < 19; m++) catalan.push_back(catalan[m] * 2 * (2 * m + 1) / (m + 2));
for (std::size_t n = 1; n <= 9; n++) {
assert(static_cast<long long>(all_chain_plans(std::vector<long long>(n + 1, 2), 0, static_cast<int>(n) - 1).size()) == catalan[n - 1]);
}
assert(catalan[19] == 1767263190LL);
// La cuenta de combinaciones: (n³ − n) / 6.
for (long long n = 1; n <= 30; n++) {
long long combinations = 0;
matrix_chain(std::vector<long long>(static_cast<std::size_t>(n) + 1, 2), &combinations);
assert(combinations == (n * n * n - n) / 6);
}
assert(chain_cheapest_first({4, 1, 3, 5}) == 72);
assert(chain_brute({4, 1, 3, 5}) == 35);
const std::vector<long long> d = {10, 20, 5, 30, 2};
assert(matrix_chain_naive_order(d).answer < chain_brute(d));
// Con una sola matriz no hay nada que multiplicar.
assert(matrix_chain({3, 4}).cost[0][0] == 0);
// Las dimensiones grandes piden long long: 2000 x 2000 x 2000 = 8e9.
assert(matrix_chain({2000, 2000, 2000}).cost[0][1] == 8000000000LL);
}
void test_optimal_bst() {
std::mt19937 rng(95);
for (int round = 0; round < 1500; round++) {
std::vector<long long> f(rng() % 8);
for (long long& x : f) x = static_cast<long long>(rng() % 15);
const int n = static_cast<int>(f.size());
const BstTables t = optimal_bst(f);
assert(t.cost[0][n] == bst_brute(f));
std::vector<int> depth(f.size(), 0);
bst_depths(t.root, 0, n, 1, depth);
assert(cost_from_depths(f, depth) == t.cost[0][n]);
assert(bst_most_frequent_root(f, 0, n, 1) >= t.cost[0][n]);
assert(bst_balanced(f, 0, n, 1) >= t.cost[0][n]);
}
const std::vector<long long> example = {2, 1, 3, 4};
assert(optimal_bst(example).cost[0][4] == 18);
assert(bst_most_frequent_root(example, 0, 4, 1) == 20);
assert(bst_balanced(example, 0, 4, 1) == 19);
assert(optimal_bst({}).cost[0][0] == 0);
assert(optimal_bst({7}).cost[0][1] == 7);
}
RootedTree random_tree(std::mt19937& rng, int n, int low, int high) {
RootedTree tree{std::vector<std::vector<int>>(static_cast<std::size_t>(n)), 0, std::vector<long long>(static_cast<std::size_t>(n))};
std::vector<int> label(static_cast<std::size_t>(n));
std::iota(label.begin(), label.end(), 0);
std::shuffle(label.begin(), label.end(), rng);
tree.root = label[0];
for (int i = 1; i < n; i++) {
const int boss = label[rng() % static_cast<unsigned>(i)];
tree.children[boss].push_back(label[i]);
}
for (long long& v : tree.value) v = low + static_cast<long long>(rng() % static_cast<unsigned>(high - low + 1));
return tree;
}
bool is_independent(const RootedTree& tree, const std::vector<int>& nodes) {
std::vector<char> inside(tree.value.size(), 0);
for (int v : nodes) inside[v] = 1;
for (std::size_t v = 0; v < tree.value.size(); v++) {
if (!inside[v]) continue;
for (int c : tree.children[v]) {
if (inside[c]) return false;
}
}
return true;
}
void test_tree_dp() {
std::mt19937 rng(96);
for (int round = 0; round < 4000; round++) {
const int n = 1 + static_cast<int>(rng() % 13);
const bool signed_values = round % 2 == 1;
const RootedTree tree = random_tree(rng, n, signed_values ? -9 : 0, 20);
const long long best = independent_set_brute(tree);
assert(independent_set_value(tree) == best);
assert(best >= 0); // el conjunto vacío siempre es válido
const std::vector<int> nodes = independent_set_nodes(tree);
assert(is_independent(tree, nodes));
long long total = 0;
for (int v : nodes) total += tree.value[v];
assert(total == best);
// Los greedy y las versiones con error nunca se pasan si no eligen vecinos.
assert(largest_value_first(tree) <= best);
assert(forced_children_value(tree) <= best);
if (!signed_values) assert(alternate_levels(tree) <= best);
// Cobertura: directa, por fuerza bruta y por el complemento, también con pesos negativos.
const long long sum = std::accumulate(tree.value.begin(), tree.value.end(), 0LL);
const long long cover = min_cover_brute(tree);
assert(min_cover_value(tree) == cover);
// Los tamaños por orden inverso de BFS, contra un recorrido con pila desde cada nodo.
const std::vector<int> sizes = subtree_sizes_bfs_order(tree.children, tree.root);
for (int v = 0; v < n; v++) {
int count = 0;
std::vector<int> pending = {v};
while (!pending.empty()) {
const int u = pending.back();
pending.pop_back();
count++;
for (int c : tree.children[u]) pending.push_back(c);
}
assert(sizes[v] == count);
}
assert(cover == sum - best);
}
// Los ejemplos del texto.
const RootedTree chain{{{1}, {2}, {3}, {}}, 0, {10, 1, 1, 10}};
assert(independent_set_value(chain) == 20);
assert(forced_children_value(chain) == 11);
assert(alternate_levels(chain) == 11);
const RootedTree company{{{1, 2}, {3, 4}, {5}, {}, {}, {6, 7}, {}, {}}, 0, {4, 9, 6, 5, 5, 8, 3, 4}};
assert(independent_set_value(company) > alternate_levels(company));
assert(independent_set_value(company) > largest_value_first(company));
assert(independent_set_value(company) > forced_children_value(company));
assert(!is_independent(company, choose_each_node_alone(company)));
const RootedTree negative{{{1, 2}, {}, {}}, 0, {-4, -1, -2}};
assert(independent_set_value(negative) == 0);
assert(independent_set_nodes(negative).empty());
// Un árbol de un solo nodo, con valor positivo y negativo.
assert(independent_set_value(RootedTree{{{}}, 0, {7}}) == 7);
assert(independent_set_value(RootedTree{{{}}, 0, {-7}}) == 0);
assert(min_cover_value(RootedTree{{{}}, 0, {7}}) == 0);
assert(min_cover_value(RootedTree{{{}}, 0, {-7}}) == -7); // una sola opción negativa sí conviene
// Una cadena larga sin recursión: el orden inverso de BFS no usa la pila.
const int n = 200000;
std::vector<std::vector<int>> children(static_cast<std::size_t>(n));
for (int v = 0; v + 1 < n; v++) children[v].push_back(v + 1);
const std::vector<int> sizes = subtree_sizes_bfs_order(children, 0);
assert(sizes[0] == n && sizes[n - 1] == 1);
}
} // namespace
int main() {
test_longest_path();
test_longest_path_example();
test_subproblem_paths();
test_lis();
test_matrix_chain();
test_optimal_bst();
test_tree_dp();
std::cout << "Todas las pruebas de la semana 9 pasaron.\n";
}Laboratorio
Archivos de partida del laboratorio de la semana.
TIEMPO = 3
VERIFICADOR = verificador.cpp
include ../../../lab.mk# La reunión de la empresa, con la lista
Esta es la extensión del problema `company_meeting`: la misma empresa, la misma regla y los mismos aportes. Ahora la
gerencia no se conforma con saber cuánto puede aportar la reunión. También quiere saber a quién invitar.
Nadie quiere asistir junto con su jefe directo. Una reunión sin nadie está permitida, y su aporte es 0.
## Entrada
La misma de `company_meeting`. La primera línea tiene n, la cantidad de personas, numeradas de 1 a n. Luego vienen n
líneas. La línea i tiene dos enteros: vᵢ, el aporte de la persona i, y bᵢ, el número de su jefe directo, o 0 si no
tiene jefe.
Los números no siguen el orden de la jerarquía. Hay exactamente una persona sin jefe, y las relaciones forman un árbol.
## Salida
Tres líneas:
1. El mayor aporte total posible.
2. k, la cantidad de personas invitadas.
3. Los k números de las personas invitadas, separados por espacios, en cualquier orden. Si k es 0, esta línea queda
vacía.
Puede haber varias reuniones con el mayor aporte, y cualquiera vale: un verificador revisa su respuesta. Comprueba que
el total sea el mayor posible, que cada número esté entre 1 y n, que ninguno se repita, que nadie vaya junto con su
jefe directo y que los aportes de la lista sumen ese total.
## Límites
- 1 ≤ n ≤ 1 000 000
- −1 000 000 000 ≤ vᵢ ≤ 1 000 000 000
- 0 ≤ bᵢ ≤ n
- Hay un caso en que toda la empresa es una sola cadena de mando de 1 000 000 de personas.
- El programa corre con la pila del entorno del curso, de 8 MB (`ulimit -s` responde 8192).
## Ejemplo
Entrada:
```text
6
7 4
3 1
6 6
2 0
1 1
4 4
```
Salida:
```text
13
2
1 3
```
## Tiempo
3 segundos por caso, compilado con `-O2`. Corran `make test` para probar con el juez.// La reunión de la empresa, con la lista: lee la entrada y propone una reunión vacía. Es una lista
// válida, pero casi nunca la mejor. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::size_t n = 0;
std::cin >> n;
std::vector<int> value(n + 1, 0);
std::vector<int> boss(n + 1, 0);
for (std::size_t i = 1; i <= n; i++) std::cin >> value[i] >> boss[i];
// Por hacer: calcular el mayor aporte y a quién invitar.
std::cout << 0 << '\n' << 0 << '\n' << '\n';
}// Verificador de "La reunión de la empresa, con la lista": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA está solo el mayor aporte. Aquí se comprueba que la salida diga ese total, y que su
// lista sea una reunión válida que suma eso. Termina con 0 si todo está bien, y con 1 y un mensaje si no.
#include <cstddef>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace {
[[noreturn]] void reject(const std::string& reason) {
std::cout << reason << '\n';
std::exit(1);
}
// Convierte una palabra en número sin riesgo de overflow: signo opcional y a lo sumo 17 dígitos,
// que alcanzan para el mayor total posible, 10^15.
bool parse_number(const std::string& word, long long& value) {
std::size_t start = word[0] == '-' ? 1 : 0;
if (word.size() == start || word.size() - start > 17) return false;
value = 0;
for (std::size_t i = start; i < word.size(); i++) {
if (word[i] < '0' || word[i] > '9') return false;
value = value * 10 + (word[i] - '0');
}
if (start == 1) value = -value;
return true;
}
} // namespace
int main(int argc, char* argv[]) {
if (argc != 4) reject("uso: verificador ENTRADA ESPERADA SALIDA");
std::ifstream input(argv[1]);
std::ifstream expected(argv[2]);
std::ifstream output(argv[3]);
long long n = 0;
input >> n;
std::vector<long long> value(static_cast<std::size_t>(n) + 1, 0);
std::vector<long long> boss(static_cast<std::size_t>(n) + 1, 0);
for (long long i = 1; i <= n; i++) input >> value[i] >> boss[i];
long long best = 0;
expected >> best;
std::string word;
long long claimed = 0;
if (!(output >> word) || !parse_number(word, claimed)) reject("la primera palabra no es un número");
if (claimed != best) reject("mayor aporte esperado: " + std::to_string(best) + ", salió " + word);
long long count = 0;
if (!(output >> word) || !parse_number(word, count)) reject("falta k, o no es un número");
if (count < 0 || count > n) reject("k fuera de rango: " + word);
std::vector<char> inside(static_cast<std::size_t>(n) + 1, 0);
long long total = 0;
for (long long t = 0; t < count; t++) {
long long person = 0;
if (!(output >> word)) reject("faltan personas en la lista");
if (!parse_number(word, person)) reject("número inválido: " + word);
if (person < 1 || person > n) reject("persona fuera de rango: " + word);
if (inside[person]) reject("persona repetida: " + word);
inside[person] = 1;
total += value[person];
}
if (output >> word) reject("sobra texto después de la lista: " + word);
for (long long i = 1; i <= n; i++) {
if (inside[i] && inside[boss[i]]) {
reject("la persona " + std::to_string(i) + " va junto con su jefe directo, " + std::to_string(boss[i]));
}
}
if (total != best) reject("la lista suma " + std::to_string(total) + ", y debería sumar " + std::to_string(best));
return 0;
}TIEMPO = 3
include ../../../lab.mk# La reunión de la empresa
Una empresa quiere organizar una reunión con parte de su personal. Cada persona tiene un jefe directo, salvo la
directora o el director general, que no tiene jefe, y todas dependen de esa persona a través de su cadena de mando.
La gerencia estimó cuánto aporta cada persona a la reunión. El aporte puede ser negativo: alguien puede estorbar más
de lo que ayuda. Hay una sola regla: **nadie quiere asistir junto con su jefe directo**. Sí pueden asistir juntos una
persona y el jefe de su jefe, y también dos personas que tienen el mismo jefe.
Ustedes calculan el mayor aporte total que puede tener una reunión. Una reunión sin nadie está permitida, y su aporte
es 0.
## Entrada
La primera línea tiene n, la cantidad de personas, numeradas de 1 a n. Luego vienen n líneas. La línea i tiene dos
enteros: vᵢ, el aporte de la persona i, y bᵢ, el número de su jefe directo, o 0 si no tiene jefe.
Los números no siguen el orden de la jerarquía: el jefe de una persona puede tener un número mayor o menor que ella.
Hay exactamente una persona sin jefe, y las relaciones forman un árbol: desde cualquier persona, subiendo de jefe en
jefe, se llega a esa persona.
## Salida
Una línea con el mayor aporte total posible.
## Límites
- 1 ≤ n ≤ 1 000 000
- −1 000 000 000 ≤ vᵢ ≤ 1 000 000 000
- 0 ≤ bᵢ ≤ n
- Hay casos en que toda la empresa es una sola cadena de mando de 1 000 000 de personas.
- El programa corre con la pila del entorno del curso, de 8 MB (`ulimit -s` responde 8192).
## Ejemplo
Entrada:
```text
6
7 4
3 1
6 6
2 0
1 1
4 4
```
Salida:
```text
13
```
La persona 4 no tiene jefe. Sus subordinados directos son las personas 1 y 6, los de la persona 1 son las personas 2 y
5, y el de la persona 6 es la persona 3. La mejor reunión tiene a las personas 1 y 3, con aporte 7 + 6 = 13. Llevar a
las personas 4, 2, 5 y 3, que no tienen a su jefe directo en la sala, da 12.
## Tiempo
3 segundos por caso, compilado con `-O2`. Corran `make test` para probar con el juez.
Antes de ir por los casos grandes, conviene escribir una versión que pruebe todas las reuniones posibles, y comparar
con ella en empresas de pocas personas.// La reunión de la empresa: lee la entrada y responde 0, como si nadie asistiera. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::size_t n = 0;
std::cin >> n;
std::vector<int> value(n + 1, 0);
std::vector<int> boss(n + 1, 0);
for (std::size_t i = 1; i <= n; i++) std::cin >> value[i] >> boss[i];
// Por hacer: calcular el mayor aporte total.
std::cout << 0 << '\n';
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 9: más programación dinámica. El DAG de subproblemas, camino más largo, subsecuencia
# creciente más larga, cadena de matrices, árboles de búsqueda óptimos y programación dinámica
# sobre árboles. Se usa dentro del entorno del curso.
#
# make compila los ejemplos en build/
# make test corre las pruebas de los ejemplos, con sanitizers
# make bench corre las mediciones, con -O2
# make deep muestra la recursión profunda que agota la pila, con -O2
# make clean borra lo generado
#
# Los problemas de lab/ tienen su propio Makefile y no entran aquí.
CXX = g++
CXXFLAGS = -Wall -Wextra -std=c++20 -g -fsanitize=address,undefined
BENCHFLAGS = -Wall -Wextra -std=c++20 -O2
BUILD = build
HEADERS = dag_dp.h lis.h interval_dp.h tree_dp.h
PROGRAMS = dag_demo lis_demo chain_demo bst_demo tree_demo test_examples
BENCHES = lis_bench chain_bench deep_chain
.PHONY: all test bench deep clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BENCHES:%=$(BUILD)/%)
$(PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
$(CXX) $(CXXFLAGS) $< -o $@
# Lo que mide tiempos o la pila va con -O2 y sin sanitizers, como en el juez.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
$(CXX) $(BENCHFLAGS) $< -o $@
$(BUILD):
mkdir -p $@
test: $(BUILD)/test_examples
$(BUILD)/test_examples
bench: $(BUILD)/lis_bench $(BUILD)/chain_bench
$(BUILD)/lis_bench
$(BUILD)/chain_bench
deep: $(BUILD)/deep_chain
-bash -c '$(BUILD)/deep_chain 100000; echo "código de salida: $$?"' 2>&1
-bash -c '$(BUILD)/deep_chain 1000000; echo "código de salida: $$?"' 2>&1
clean:
rm -rf $(BUILD)