Semana 12 — Modelar con flujo
Objetivos
- Reducir un problema a flujo máximo y demostrar que la reducción es correcta.
- Resolver matching bipartito y caminos disjuntos.
- Usar el corte mínimo para elegir, como en la selección de proyectos.
Sesión A — Matching bipartito · Caminos disjuntos
Lectura: Apuntes, semana 12.
Apoyo: Erickson, secs. 11.1 (Edge-Disjoint Paths), 11.2 (Vertex Capacities and Vertex-Disjoint Paths) y 11.3 (Bipartite Matching). CLRS, capítulo Matchings in Bipartite Graphs, 4.ª edición.
Temas:
- Qué es una reducción: resolver un problema nuevo con un algoritmo que ya existe. Volverá en la semana 13 con otro propósito.
- Matching bipartito: fuente, sumidero y capacidades 1. Demostración en las dos direcciones: un matching da un flujo y un flujo entero da un matching.
- Por qué importa que el flujo sea entero.
- Caminos disjuntos en aristas y en nodos: dividir un nodo en dos para darle capacidad. Teorema de Menger.
- Costo: O(VE) para el matching con Ford-Fulkerson, porque el flujo es a lo sumo V/2.
- Error común: probar solo una dirección de la reducción.
Sesión B — Modelar con flujo: asignación y selección de proyectos
Lectura: Apuntes, semana 12.
Apoyo: Erickson, secs. 11.4 (Tuple Selection), 11.6 (Baseball Elimination) y 11.7 (Project Selection).
Temas:
- Asignación con cupos: cada persona a una opción permitida, con cupo máximo por opción.
- Eliminación en un campeonato: decidir con flujo si un equipo todavía puede quedar primero.
- Selección de proyectos: con prerrequisitos y ganancias o costos. Corte mínimo y aristas de capacidad infinita.
- Cómo leer la solución en el corte.
- Cómo reconocer un problema de flujo: recursos que se reparten y elecciones “de a uno”.
- Error común: modelar una restricción que no es de capacidad, como “al menos dos de cada grupo”, sin ajustar el modelo.
Laboratorio — Asignar estudiantes a secciones
- Cada estudiante puede asistir a ciertas secciones según su horario, y cada sección tiene un cupo. Asignar la mayor cantidad posible de estudiantes. Un verificador comprueba la asignación.
- Construir la red e implementar Edmonds-Karp, o reutilizar su código de flujo.
- Hacer que pase todos los casos.
- Extensión: si no todos entran, usar el corte mínimo para decir qué grupo de secciones limita el resultado.
Preguntas de repaso
- Dibujen la red para un matching bipartito de 3 por 3 y den un flujo máximo.
- Demuestren las dos direcciones de la reducción de matching bipartito a flujo.
- ¿Cómo se limita a 1 el número de caminos que pasan por un nodo?
- En la selección de proyectos, ¿por qué las aristas de prerrequisito tienen capacidad infinita?
- Dos estudiantes quieren la misma sección con cupo 1, y otra sección queda libre. ¿Cómo se ve eso en la red?
Código de la semana
Compilar y probar dentro del contenedor del curso con make y make test.
Ejemplos (13)Pruebas (1)Laboratorio (8)Compilación (1)
Ejemplos
Los programas que usan los apuntes y la clase.
// Eliminación en un campeonato de vóley sin empates. Los equipos son inventados.
#include <iostream>
#include <vector>
#include "baseball.h"
int main() {
// 0: Lima 1: Cusco 2: Ica 3: Piura 4: Callao
Championship league{{"Lima", "Cusco", "Ica", "Piura", "Callao"},
{27, 26, 26, 25, 20},
{{0, 2, 2, 0, 0},
{2, 0, 2, 0, 0},
{2, 2, 0, 0, 0},
{0, 0, 0, 0, 3},
{0, 0, 0, 3, 0}}};
std::cout << "equipo ganados faltan máximo ¿eliminado?\n";
for (int x = 0; x < 5; x++) {
Elimination result = check_team(league, x);
std::cout << league.names[static_cast<std::size_t>(x)];
for (std::size_t k = league.names[static_cast<std::size_t>(x)].size(); k < 9; k++) std::cout << ' ';
std::cout << league.wins[static_cast<std::size_t>(x)] << " " << remaining_of(league, x) << " "
<< result.best << " ";
if (!result.eliminated) {
std::cout << "no (flujo " << result.flow << " de " << result.games_left << ")\n";
} else if (result.trivial) {
std::cout << "sí, a simple vista: "
<< league.names[static_cast<std::size_t>(result.certificate[0])] << " ya lo pasó\n";
} else {
std::cout << "sí (flujo " << result.flow << " de " << result.games_left << ")\n";
}
}
Elimination piura = check_team(league, 3);
std::cout << "\nPiura: el grupo que lo elimina es";
long long total = 0;
for (int i : piura.certificate) {
std::cout << ' ' << league.names[static_cast<std::size_t>(i)];
total += league.wins[static_cast<std::size_t>(i)];
}
std::cout << "\n victorias ya ganadas: " << total << '\n';
long long inside = 0;
for (std::size_t a = 0; a < piura.certificate.size(); a++) {
for (std::size_t b = a + 1; b < piura.certificate.size(); b++) {
inside += league.games[static_cast<std::size_t>(piura.certificate[a])]
[static_cast<std::size_t>(piura.certificate[b])];
}
}
std::cout << " partidos entre ellos: " << inside << '\n';
std::cout << " máximo para que nadie pase a Piura: " << piura.certificate.size() << " x " << piura.best << " = "
<< static_cast<long long>(piura.certificate.size()) * piura.best << '\n';
std::cout << " fuerza bruta, ¿puede quedar primero?: " << (brute_can_finish_first(league, 3) ? "sí" : "no") << '\n';
}// Eliminación en un campeonato sin empates: ¿puede un equipo todavía quedar primero, aunque sea empatado?
#pragma once
#include <cassert>
#include <cstddef>
#include <string>
#include <vector>
#include "flow.h"
struct Championship {
std::vector<std::string> names;
std::vector<int> wins;
std::vector<std::vector<int>> games; // games[i][j]: partidos que faltan entre i y j, simétrica
};
struct Elimination {
bool eliminated = false;
bool trivial = false; // algún equipo ya tiene más victorias de las que x puede alcanzar
long long best = 0; // lo más que x puede alcanzar: W
long long flow = 0; // lo que pasó por la red
long long games_left = 0; // lo que debería pasar: los partidos que no juega x
std::vector<int> certificate; // equipos que, entre todos, no caben por debajo de W
};
inline int remaining_of(const Championship& league, int team) {
int total = 0;
for (int g : league.games[static_cast<std::size_t>(team)]) total += g;
return total;
}
inline Elimination check_team(const Championship& league, int x) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
int teams = static_cast<int>(league.wins.size());
Elimination result;
result.best = league.wins[at(x)] + remaining_of(league, x);
for (int i = 0; i < teams; i++) {
if (league.wins[at(i)] > result.best) {
// Sin este caso, la capacidad W - w sería negativa.
result.eliminated = true;
result.trivial = true;
result.certificate = {i};
return result;
}
}
int game_nodes = 0;
for (int i = 0; i < teams; i++) {
for (int j = i + 1; j < teams; j++) {
if (i != x && j != x && league.games[at(i)][at(j)] > 0) game_nodes++;
}
}
int source = teams;
int sink = teams + 1;
int next = teams + 2;
MaxFlow flow(teams + 2 + game_nodes);
for (int i = 0; i < teams; i++) {
for (int j = i + 1; j < teams; j++) {
if (i != x && j != x) result.games_left += league.games[at(i)][at(j)];
}
}
long long no_limit = result.games_left + 1; // ningún partido reparte más que eso
for (int i = 0; i < teams; i++) {
for (int j = i + 1; j < teams; j++) {
if (i == x || j == x || league.games[at(i)][at(j)] == 0) continue;
flow.add_edge(source, next, league.games[at(i)][at(j)]);
flow.add_edge(next, i, no_limit);
flow.add_edge(next, j, no_limit);
next++;
}
}
for (int i = 0; i < teams; i++) {
if (i != x) flow.add_edge(i, sink, result.best - league.wins[at(i)]);
}
result.flow = flow.edmonds_karp(source, sink);
result.eliminated = result.flow < result.games_left;
if (result.eliminated) {
std::vector<bool> reached = flow.reachable_from(source);
for (int i = 0; i < teams; i++) {
if (i != x && reached[at(i)]) result.certificate.push_back(i);
}
}
return result;
}
// Lo que dice el certificado: las victorias de R, contando los partidos entre ellos, no caben debajo de W.
inline bool certificate_holds(const Championship& league, long long best, const std::vector<int>& group) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
long long total = 0;
for (std::size_t a = 0; a < group.size(); a++) {
total += league.wins[at(group[a])];
for (std::size_t b = a + 1; b < group.size(); b++) total += league.games[at(group[a])][at(group[b])];
}
return total > static_cast<long long>(group.size()) * best;
}
// Árbitro: prueba todos los resultados de todos los partidos.
inline bool brute_can_finish_first(const Championship& league, int x) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
int teams = static_cast<int>(league.wins.size());
std::vector<std::pair<int, int>> pending;
for (int i = 0; i < teams; i++) {
for (int j = i + 1; j < teams; j++) {
for (int k = 0; k < league.games[at(i)][at(j)]; k++) pending.emplace_back(i, j);
}
}
std::vector<int> final_wins = league.wins;
auto search = [&](auto&& self, std::size_t next) -> bool {
if (next == pending.size()) {
for (int i = 0; i < teams; i++) {
if (final_wins[at(i)] > final_wins[at(x)]) return false;
}
return true;
}
for (int winner : {pending[next].first, pending[next].second}) {
final_wins[at(winner)]++;
bool can = self(self, next + 1);
final_wins[at(winner)]--;
if (can) return true;
}
return false;
};
return search(search, 0);
}// Caminos disjuntos: un flujo con un ciclo, aristas y nodos, no dirigido, y qué pasa si s y t son vecinos.
#include <iostream>
#include <string>
#include <vector>
#include "disjoint_paths.h"
namespace {
void print_trail(const std::vector<int>& trail, const std::string& names) {
for (std::size_t k = 0; k < trail.size(); k++) {
std::cout << (k ? " " : "") << names[static_cast<std::size_t>(trail[k])];
}
}
void print_decomposition(const Decomposition& result, const std::string& names) {
std::cout << " caminos: " << result.paths.size() << '\n';
for (const auto& path : result.paths) {
std::cout << " ";
print_trail(path, names);
std::cout << '\n';
}
for (const auto& cycle : result.cycles) {
std::cout << " ciclo apartado: ";
print_trail(cycle, names);
std::cout << '\n';
}
}
} // namespace
int main() {
std::cout << "Un flujo de valor 2 con un ciclo, escrito a mano:\n";
// s = 0, a = 1, b = 2, c = 3, d = 4, t = 5
std::vector<Arc> with_cycle = {{0, 1}, {1, 2}, {2, 5}, {0, 3}, {3, 5}, {2, 4}, {4, 3}, {3, 2}};
print_decomposition(decompose_flow(6, with_cycle, 0, 5), "sabcdt");
// Un cuello de botella en c: s = 0, a = 1, b = 2, c = 3, d = 4, e = 5, t = 6
const std::string names = "sabcdet";
std::vector<Arc> arcs = {{0, 1}, {0, 2}, {1, 3}, {2, 3}, {3, 4}, {3, 5}, {4, 6}, {5, 6}};
std::cout << "\nDirigido, sin compartir aristas:\n";
print_decomposition(edge_disjoint_paths(7, arcs, 0, 6), names);
std::cout << " corte mínimo:";
for (const Arc& arc : minimum_edge_cut(7, arcs, 0, 6)) {
std::cout << ' ' << names[static_cast<std::size_t>(arc.from)] << "->" << names[static_cast<std::size_t>(arc.to)];
}
std::cout << "\n\nEl mismo grafo, sin compartir nodos:\n";
print_decomposition(vertex_disjoint_paths(7, arcs, 0, 6), names);
std::cout << " separador mínimo:";
for (int v : minimum_vertex_separator(7, arcs, 0, 6)) std::cout << ' ' << names[static_cast<std::size_t>(v)];
std::cout << '\n';
std::cout << "\nNo dirigido: el flujo de esta red usa una arista en las dos direcciones.\n";
std::vector<Arc> edges = {{0, 4}, {6, 5}, {0, 2}, {2, 5}, {3, 6}, {1, 4}, {3, 1}, {0, 6}, {5, 4}};
Decomposition undirected = undirected_edge_disjoint_paths(7, edges, 0, 6);
std::cout << " aristas canceladas: " << undirected.cancelled << '\n';
print_decomposition(undirected, "sabcdet");
std::cout << "\nCon una arista directa s -> t:\n";
// s = 0, a = 1, b = 2, t = 3
std::vector<Arc> adjacent = {{0, 3}, {0, 1}, {1, 3}, {0, 2}, {2, 3}};
print_decomposition(vertex_disjoint_paths(4, adjacent, 0, 3), "sabt");
SplitNetwork unlimited = split_network(4, adjacent, 0, 3, 5);
std::cout << " flujo si las aristas tienen capacidad \"sin límite\" (aquí, 5): "
<< unlimited.flow.edmonds_karp(unlimited.source, unlimited.sink) << '\n';
}// Caminos disjuntos en aristas y en nodos, como flujo con capacidades 1.
#pragma once
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <utility>
#include <vector>
#include "flow.h"
struct Arc {
int from;
int to;
};
struct Decomposition {
std::vector<std::vector<int>> paths;
std::vector<std::vector<int>> cycles;
int cancelled = 0; // aristas no dirigidas que el flujo usó en las dos direcciones
};
// Descompone un flujo entero en caminos simples de s a t y ciclos. flow_arcs tiene una entrada por cada
// unidad de flujo. Se camina por aristas sin usar: si se repite un nodo, lo recorrido desde su primera
// aparición es un ciclo y se aparta; si se llega a t, ahí hay un camino. La conservación impide atascarse.
inline Decomposition decompose_flow(int nodes, const std::vector<Arc>& flow_arcs, int s, int t) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
std::vector<std::vector<int>> out(at(nodes));
int balance = 0; // el valor del flujo: lo que sale de s menos lo que entra
for (const Arc& arc : flow_arcs) {
out[at(arc.from)].push_back(arc.to);
if (arc.from == s) balance++;
if (arc.to == s) balance--;
}
assert(balance >= 0);
Decomposition result;
std::vector<int> position(at(nodes), -1);
// Camina desde start. Se detiene en stop, o si stop es -1, cuando un ciclo la devuelve a start.
auto walk = [&](int start, int stop) {
std::vector<int> trail = {start};
position[at(start)] = 0;
while (true) {
int v = trail.back();
if (v == stop) {
for (int u : trail) position[at(u)] = -1;
result.paths.push_back(trail);
return;
}
assert(!out[at(v)].empty()); // lo garantiza la conservación
int w = out[at(v)].back();
out[at(v)].pop_back();
if (position[at(w)] >= 0) {
std::vector<int> cycle(trail.begin() + position[at(w)], trail.end());
cycle.push_back(w);
result.cycles.push_back(cycle);
for (std::size_t k = at(position[at(w)]) + 1; k < trail.size(); k++) position[at(trail[k])] = -1;
trail.resize(at(position[at(w)]) + 1);
// Sin destino, se termina al volver al inicio: lo que queda en trail todavía no forma un ciclo.
if (stop < 0 && trail.size() == 1) {
position[at(start)] = -1;
return;
}
} else {
position[at(w)] = static_cast<int>(trail.size());
trail.push_back(w);
}
}
};
for (int k = 0; k < balance; k++) walk(s, t);
// Lo que queda está balanceado en todos los nodos: son solo ciclos.
for (int v = 0; v < nodes; v++) {
while (!out[at(v)].empty()) walk(v, -1);
}
return result;
}
// Dirigido: cada arista con capacidad 1.
inline Decomposition edge_disjoint_paths(int nodes, const std::vector<Arc>& arcs, int s, int t) {
MaxFlow flow(nodes);
std::vector<int> ids;
for (const Arc& arc : arcs) ids.push_back(flow.add_edge(arc.from, arc.to, 1));
flow.edmonds_karp(s, t);
std::vector<Arc> used;
for (std::size_t k = 0; k < arcs.size(); k++) {
if (flow.flow_on(ids[k]) == 1) used.push_back(arcs[k]);
}
return decompose_flow(nodes, used, s, t);
}
// No dirigido: cada arista son dos dirigidas de capacidad 1. Si el flujo usa las dos direcciones de una
// misma arista, se cancelan: siguen la conservación y el valor, y la arista queda libre.
inline Decomposition undirected_edge_disjoint_paths(int nodes, const std::vector<Arc>& edges, int s, int t) {
MaxFlow flow(nodes);
std::vector<std::pair<int, int>> ids;
for (const Arc& edge : edges) {
int forward = flow.add_edge(edge.from, edge.to, 1);
int backward = flow.add_edge(edge.to, edge.from, 1);
ids.emplace_back(forward, backward);
}
flow.edmonds_karp(s, t);
std::vector<Arc> used;
int cancelled = 0;
for (std::size_t k = 0; k < edges.size(); k++) {
long long forward = flow.flow_on(ids[k].first);
long long backward = flow.flow_on(ids[k].second);
if (forward == 1 && backward == 1) cancelled++;
if (forward == 1 && backward == 0) used.push_back(edges[k]);
if (forward == 0 && backward == 1) used.push_back({edges[k].to, edges[k].from});
}
Decomposition result = decompose_flow(nodes, used, s, t);
result.cancelled = cancelled;
return result;
}
// Corte mínimo con capacidades 1: las aristas que salen de lo alcanzable. Son las que hay que quitar.
inline std::vector<Arc> minimum_edge_cut(int nodes, const std::vector<Arc>& arcs, int s, int t) {
MaxFlow flow(nodes);
for (const Arc& arc : arcs) flow.add_edge(arc.from, arc.to, 1);
flow.edmonds_karp(s, t);
std::vector<bool> reached = flow.reachable_from(s);
std::vector<Arc> cut;
for (const Arc& arc : arcs) {
if (reached[static_cast<std::size_t>(arc.from)] && !reached[static_cast<std::size_t>(arc.to)]) cut.push_back(arc);
}
return cut;
}
// División de nodos. Cada nodo v distinto de s y t pasa a ser v_in = 2v y v_out = 2v + 1, unidos por
// una arista de capacidad 1. s y t no se dividen: usan un solo número, 2v + 1.
struct SplitNetwork {
MaxFlow flow;
std::vector<int> split_edge; // la arista v_in → v_out de cada nodo, o -1 en s y t
std::vector<int> arc_edge; // la arista de la red de cada arista original
int source;
int sink;
};
// arc_capacity es la capacidad de las aristas originales. La de los nodos siempre es 1.
inline SplitNetwork split_network(int nodes, const std::vector<Arc>& arcs, int s, int t, long long arc_capacity) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
auto in = [&](int v) { return (v == s || v == t) ? 2 * v + 1 : 2 * v; };
auto out = [](int v) { return 2 * v + 1; };
SplitNetwork net{MaxFlow(2 * nodes), std::vector<int>(at(nodes), -1), {}, 2 * s + 1, 2 * t + 1};
for (int v = 0; v < nodes; v++) {
if (v != s && v != t) net.split_edge[at(v)] = net.flow.add_edge(in(v), out(v), 1);
}
for (const Arc& arc : arcs) net.arc_edge.push_back(net.flow.add_edge(out(arc.from), in(arc.to), arc_capacity));
return net;
}
// Máximo de caminos que no comparten nodos internos. Una arista directa s → t cuenta como un camino.
inline Decomposition vertex_disjoint_paths(int nodes, const std::vector<Arc>& arcs, int s, int t) {
SplitNetwork net = split_network(nodes, arcs, s, t, 1);
net.flow.edmonds_karp(net.source, net.sink);
std::vector<Arc> used;
for (std::size_t k = 0; k < arcs.size(); k++) {
if (net.flow.flow_on(net.arc_edge[k]) == 1) used.push_back(arcs[k]);
}
return decompose_flow(nodes, used, s, t);
}
// Separador mínimo, leído del corte. Solo tiene sentido si no hay arista directa s → t.
inline std::vector<int> minimum_vertex_separator(int nodes, const std::vector<Arc>& arcs, int s, int t) {
long long no_limit = nodes + 1; // ningún corte que importe paga tanto: hay uno de a lo sumo V - 2
SplitNetwork net = split_network(nodes, arcs, s, t, no_limit);
net.flow.edmonds_karp(net.source, net.sink);
std::vector<bool> reached = net.flow.reachable_from(net.source);
std::vector<int> separator;
for (int v = 0; v < nodes; v++) {
if (v != s && v != t && reached[static_cast<std::size_t>(2 * v)] &&
!reached[static_cast<std::size_t>(2 * v + 1)]) {
separator.push_back(v);
}
}
return separator;
}
// ¿Se llega de s a t sin pasar por lo prohibido?
inline bool connected_without(int nodes, const std::vector<Arc>& arcs, int s, int t,
const std::vector<bool>& node_removed, const std::vector<bool>& arc_removed) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
std::vector<bool> seen(at(nodes), false);
std::vector<int> pending = {s};
seen[at(s)] = true;
while (!pending.empty()) {
int v = pending.back();
pending.pop_back();
for (std::size_t k = 0; k < arcs.size(); k++) {
if (!arc_removed[k] && arcs[k].from == v && !seen[at(arcs[k].to)] && !node_removed[at(arcs[k].to)]) {
seen[at(arcs[k].to)] = true;
pending.push_back(arcs[k].to);
}
}
}
return seen[at(t)];
}// Flujo máximo: la clase que se usa en todo el capítulo. Las aristas se guardan en pares: la arista
// e y su inversa e ^ 1, que nace con capacidad 0. Guardar la capacidad que queda es guardar la red
// residual.
#pragma once
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <limits>
#include <queue>
#include <vector>
class MaxFlow {
public:
explicit MaxFlow(int nodes) : adjacency_(static_cast<std::size_t>(nodes)) {}
// Devuelve el número de la arista, para preguntar después cuánto flujo lleva.
int add_edge(int from, int to, long long capacity) {
assert(capacity >= 0);
int id = static_cast<int>(arcs_.size());
arcs_.push_back({to, capacity});
arcs_.push_back({from, 0});
adjacency_[index(from)].push_back(id);
adjacency_[index(to)].push_back(id + 1);
return id;
}
// Camino de aumento con menos aristas, hallado con BFS. Se detiene apenas llega a t.
long long edmonds_karp(int s, int t) {
long long total = 0;
while (true) {
std::vector<int> arrived_by(adjacency_.size(), -1);
std::vector<bool> seen(adjacency_.size(), false);
std::queue<int> pending;
seen[index(s)] = true;
pending.push(s);
while (!pending.empty() && !seen[index(t)]) {
int v = pending.front();
pending.pop();
for (int id : adjacency_[index(v)]) {
const Arc& arc = arcs_[index(id)];
if (arc.residual > 0 && !seen[index(arc.to)]) {
seen[index(arc.to)] = true;
arrived_by[index(arc.to)] = id;
pending.push(arc.to);
}
}
}
if (!seen[index(t)]) return total;
total += push_along(arrived_by, s, t);
}
}
// Camino de aumento cualquiera, el primero que encuentra un DFS.
long long ford_fulkerson(int s, int t) {
long long total = 0;
while (true) {
std::vector<int> arrived_by(adjacency_.size(), -1);
std::vector<bool> seen(adjacency_.size(), false);
std::vector<int> pending = {s};
seen[index(s)] = true;
while (!pending.empty() && !seen[index(t)]) {
int v = pending.back();
pending.pop_back();
for (int id : adjacency_[index(v)]) {
const Arc& arc = arcs_[index(id)];
if (arc.residual > 0 && !seen[index(arc.to)]) {
seen[index(arc.to)] = true;
arrived_by[index(arc.to)] = id;
pending.push_back(arc.to);
}
}
}
if (!seen[index(t)]) return total;
total += push_along(arrived_by, s, t);
}
}
// Lo alcanzable desde s en la red residual. Al terminar el flujo, es el lado de s de un corte mínimo.
std::vector<bool> reachable_from(int s) const {
std::vector<bool> seen(adjacency_.size(), false);
std::vector<int> pending = {s};
seen[index(s)] = true;
while (!pending.empty()) {
int v = pending.back();
pending.pop_back();
for (int id : adjacency_[index(v)]) {
const Arc& arc = arcs_[index(id)];
if (arc.residual > 0 && !seen[index(arc.to)]) {
seen[index(arc.to)] = true;
pending.push_back(arc.to);
}
}
}
return seen;
}
// El flujo de una arista es lo que su inversa puede devolver.
long long flow_on(int id) const { return arcs_[index(id ^ 1)].residual; }
// Cuánta capacidad le queda a una arista.
long long left_on(int id) const { return arcs_[index(id)].residual; }
// Quita una arista de la red, junto con lo que deshacerla permitiría.
void remove_edge(int id) {
arcs_[index(id)].residual = 0;
arcs_[index(id ^ 1)].residual = 0;
}
int augmentations() const { return augmentations_; }
// Los caminos de aumento usados, si se pidió guardarlos.
void keep_paths() { keep_paths_ = true; }
const std::vector<std::vector<int>>& paths() const { return paths_; }
private:
struct Arc {
int to;
long long residual;
};
static std::size_t index(int i) { return static_cast<std::size_t>(i); }
long long push_along(const std::vector<int>& arrived_by, int s, int t) {
long long amount = std::numeric_limits<long long>::max();
std::vector<int> path = {t};
for (int v = t; v != s; v = arcs_[index(arrived_by[index(v)] ^ 1)].to) {
amount = std::min(amount, arcs_[index(arrived_by[index(v)])].residual);
path.push_back(arcs_[index(arrived_by[index(v)] ^ 1)].to);
}
for (int v = t; v != s; v = arcs_[index(arrived_by[index(v)] ^ 1)].to) {
int id = arrived_by[index(v)];
arcs_[index(id)].residual -= amount;
arcs_[index(id ^ 1)].residual += amount;
}
augmentations_++;
if (keep_paths_) {
std::reverse(path.begin(), path.end());
paths_.push_back(path);
}
return amount;
}
std::vector<std::vector<int>> adjacency_;
std::vector<Arc> arcs_;
int augmentations_ = 0;
bool keep_paths_ = false;
std::vector<std::vector<int>> paths_;
};// "Al menos dos de cada grupo" no es una capacidad. Tres modelos de la misma situación.
#include <iostream>
#include <vector>
#include "lower_bounds.h"
namespace {
// s = 0, A = 1, B = 2, T1 = 3, T2 = 4, t = 5
std::vector<BoundedEdge> network(long long low, long long high_group) {
return {{0, 1, low, high_group}, {0, 2, low, high_group}, {1, 3, 0, 3}, {1, 4, 0, 1},
{2, 4, 0, 2}, {3, 5, 0, 3}, {4, 5, 0, 2}};
}
void report(const BoundedFlow& result) {
std::cout << " total " << result.value << ", grupo A " << result.on_edge[0] << ", grupo B " << result.on_edge[1] << '\n';
}
} // namespace
int main() {
std::cout << "Grupo A: 4 personas. Grupo B: 2 personas. Turnos: T1 con cupo 3, T2 con cupo 2.\n";
std::cout << "A puede ir a T1 (hasta 3) o a T2 (1). B solo puede ir a T2.\n\n";
std::cout << "Sin restricción:\n";
report(max_flow_with_lower_bounds(6, network(0, 4), 0, 5));
std::cout << "Capacidad 2 en s -> grupo, que significa \"a lo sumo dos\":\n";
report(max_flow_with_lower_bounds(6, network(0, 2), 0, 5));
std::cout << "Cota inferior 2 en s -> grupo:\n";
report(max_flow_with_lower_bounds(6, network(2, 4), 0, 5));
std::cout << " fuerza bruta: " << brute_bounded(6, network(2, 4), 0, 5) << '\n';
std::cout << "\nSi B pudiera aportar solo por T2 y T2 tuviera cupo 1, no hay solución:\n";
std::vector<BoundedEdge> impossible = network(2, 4);
impossible[6].high = 1;
BoundedFlow none = max_flow_with_lower_bounds(6, impossible, 0, 5);
std::cout << " factible: " << (none.feasible ? "sí" : "no") << ", fuerza bruta: " << brute_bounded(6, impossible, 0, 5) << '\n';
}// Flujo con cotas inferiores: cada arista debe llevar entre low y high.
#pragma once
#include <cassert>
#include <cstddef>
#include <vector>
#include "flow.h"
struct BoundedEdge {
int from;
int to;
long long low;
long long high;
};
struct BoundedFlow {
bool feasible = false;
long long value = 0;
std::vector<long long> on_edge;
};
// La transformación: cada arista queda con capacidad high - low, y las low unidades obligatorias se piden
// con una fuente y un sumidero nuevos. Con la arista t → s, buscar un flujo factible se vuelve un flujo
// máximo que debe saturar la nueva fuente. Después se quita t → s y se aumenta de s a t.
inline BoundedFlow max_flow_with_lower_bounds(int nodes, const std::vector<BoundedEdge>& edges, int s, int t) {
int new_source = nodes;
int new_sink = nodes + 1;
MaxFlow flow(nodes + 2);
std::vector<int> ids;
long long required = 0;
long long no_limit = 1;
for (const BoundedEdge& e : edges) no_limit += e.high;
for (const BoundedEdge& e : edges) {
assert(0 <= e.low && e.low <= e.high);
ids.push_back(flow.add_edge(e.from, e.to, e.high - e.low));
if (e.low > 0) {
flow.add_edge(new_source, e.to, e.low);
flow.add_edge(e.from, new_sink, e.low);
required += e.low;
}
}
int back = flow.add_edge(t, s, no_limit);
BoundedFlow result;
if (flow.edmonds_karp(new_source, new_sink) < required) return result;
result.feasible = true;
long long first_part = flow.flow_on(back);
flow.remove_edge(back);
result.value = first_part + flow.edmonds_karp(s, t);
for (std::size_t k = 0; k < edges.size(); k++) result.on_edge.push_back(edges[k].low + flow.flow_on(ids[k]));
return result;
}
// Árbitro: prueba todos los valores de todas las aristas. Devuelve -1 si no hay flujo factible.
inline long long brute_bounded(int nodes, const std::vector<BoundedEdge>& edges, int s, int t) {
auto at = [](int i) { return static_cast<std::size_t>(i); };
std::vector<long long> net(at(nodes), 0);
long long best = -1;
auto search = [&](auto&& self, std::size_t k) -> void {
if (k == edges.size()) {
for (int v = 0; v < nodes; v++) {
if (v != s && v != t && net[at(v)] != 0) return;
}
if (net[at(s)] > best) best = net[at(s)];
return;
}
for (long long x = edges[k].low; x <= edges[k].high; x++) {
net[at(edges[k].from)] += x;
net[at(edges[k].to)] -= x;
self(self, k + 1);
net[at(edges[k].from)] -= x;
net[at(edges[k].to)] += x;
}
};
search(search, 0);
return best;
}// Cuánto cuesta el matching como flujo: aumentos y tiempo, con grafos al azar de grado fijo. Se compila con -O2.
#include <chrono>
#include <cstdint>
#include <iostream>
#include <random>
#include <vector>
#include "matching.h"
namespace {
Options random_options(int n, int degree, std::uint32_t seed) {
std::mt19937 random(seed);
Options options(static_cast<std::size_t>(n));
for (auto& list : options) {
for (int k = 0; k < degree; k++) {
int j = static_cast<int>(random() % static_cast<std::uint32_t>(n));
if (std::find(list.begin(), list.end(), j) == list.end()) list.push_back(j);
}
}
return options;
}
} // namespace
int main() {
std::cout << " n aristas matching aumentos EK ms EK aumentos DFS ms DFS\n";
for (int n : {2000, 4000, 8000, 16000, 32000}) {
Options options = random_options(n, 3, 12345);
std::size_t edges = 0;
for (const auto& list : options) edges += list.size();
MatchingNetwork bfs = matching_network(options, n);
auto start = std::chrono::steady_clock::now();
long long value = bfs.flow.edmonds_karp(bfs.source, bfs.sink);
auto middle = std::chrono::steady_clock::now();
MatchingNetwork dfs = matching_network(options, n);
auto middle2 = std::chrono::steady_clock::now();
long long value2 = dfs.flow.ford_fulkerson(dfs.source, dfs.sink);
auto end = std::chrono::steady_clock::now();
assert(value == value2);
std::cout.width(6);
std::cout << n;
std::cout.width(10);
std::cout << edges;
std::cout.width(10);
std::cout << value;
std::cout.width(13);
std::cout << bfs.flow.augmentations();
std::cout.width(7);
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(middle - start).count();
std::cout.width(14);
std::cout << dfs.flow.augmentations();
std::cout.width(8);
std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - middle2).count() << '\n';
}
}// Matching bipartito: la red, los caminos de aumento, los greedy que fallan y la capacidad que no se puede olvidar.
#include <iostream>
#include <string>
#include <vector>
#include "matching.h"
namespace {
std::string node_name(int v, int left, int sink) {
if (v == 0) return "s";
if (v == sink) return "t";
if (v <= left) return "E" + std::to_string(v - 1);
return "S" + std::to_string(v - left - 1);
}
void print_options(const Options& options) {
for (std::size_t i = 0; i < options.size(); i++) {
std::cout << " E" << i << ":";
for (int j : options[i]) std::cout << " S" << j;
std::cout << '\n';
}
}
void print_matching(const std::vector<int>& partner) {
std::cout << assigned_count(partner) << " (";
bool first = true;
for (std::size_t i = 0; i < partner.size(); i++) {
if (partner[i] < 0) continue;
std::cout << (first ? "" : ", ") << "E" << i << "-S" << partner[i];
first = false;
}
std::cout << ")\n";
}
void run_network(const Options& options, int right) {
int left = static_cast<int>(options.size());
MatchingNetwork net = matching_network(options, right);
net.flow.keep_paths();
long long value = net.flow.edmonds_karp(net.source, net.sink);
for (const auto& path : net.flow.paths()) {
std::cout << " camino:";
for (int v : path) std::cout << ' ' << node_name(v, left, net.sink);
std::cout << '\n';
}
std::cout << " flujo máximo: " << value << '\n';
std::cout << " matching: ";
print_matching(read_matching(net, options));
}
void compare_greedy(const Options& options, int right) {
std::vector<int> quota(static_cast<std::size_t>(right), 1);
std::vector<int> copy = quota;
std::cout << " primera opción libre: ";
print_matching(first_fit(options, quota));
std::cout << " menos opciones primero: ";
print_matching(fewest_options_first(options, quota));
std::cout << " óptimo por fuerza bruta: " << brute_assignment(options, copy) << '\n';
std::cout << " flujo: ";
print_matching(max_matching(options, right));
}
} // namespace
int main() {
std::cout << "Una red de 3 por 3:\n";
Options three = {{0, 1}, {0}, {1, 2}};
print_options(three);
run_network(three, 3);
std::cout << "\nCada uno a su primera opción libre falla:\n";
Options first = {{0, 1}, {0}};
print_options(first);
compare_greedy(first, 2);
std::cout << "\nMenos opciones primero también falla:\n";
Options fewest = {{0, 1}, {2, 0}, {2}};
print_options(fewest);
compare_greedy(fewest, 3);
std::cout << "\nSin el límite en s -> estudiante, E0 cuenta dos veces:\n";
Options greedy_student = {{0, 1}, {}};
print_options(greedy_student);
MatchingNetwork wide = matching_network(greedy_student, 2, 1000);
std::cout << " flujo con capacidad 1000 en s -> E: " << wide.flow.edmonds_karp(wide.source, wide.sink) << '\n';
std::vector<int> quota = {1, 1};
std::cout << " matching máximo real: " << brute_assignment(greedy_student, quota) << '\n';
}// Matching bipartito como flujo, y las ideas parecidas que no alcanzan.
// options[i] son los nodos de la derecha que admite el nodo i de la izquierda.
#pragma once
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <numeric>
#include <vector>
#include "flow.h"
using Options = std::vector<std::vector<int>>;
inline std::size_t at(int i) { return static_cast<std::size_t>(i); }
// La red de la reducción. Numeración: s = 0, izquierda 1..L, derecha L+1..L+R, t = L+R+1.
struct MatchingNetwork {
MaxFlow flow;
int source;
int sink;
std::vector<std::vector<int>> middle; // el número de cada arista izquierda → derecha
};
// source_capacity es 1 en la reducción correcta. Se deja como parámetro para ver qué pasa sin ese límite.
inline MatchingNetwork matching_network(const Options& options, int right, long long source_capacity = 1) {
int left = static_cast<int>(options.size());
MatchingNetwork net{MaxFlow(left + right + 2), 0, left + right + 1, {}};
for (int i = 0; i < left; i++) net.flow.add_edge(net.source, 1 + i, source_capacity);
for (int j = 0; j < right; j++) net.flow.add_edge(1 + left + j, net.sink, 1);
for (int i = 0; i < left; i++) {
net.middle.emplace_back();
for (int j : options[at(i)]) net.middle.back().push_back(net.flow.add_edge(1 + i, 1 + left + j, 1));
}
return net;
}
// Un flujo entero se lee como matching: las aristas del medio que llevan 1.
inline std::vector<int> read_matching(const MatchingNetwork& net, const Options& options) {
std::vector<int> partner(options.size(), -1);
for (std::size_t i = 0; i < options.size(); i++) {
for (std::size_t k = 0; k < options[i].size(); k++) {
if (net.flow.flow_on(net.middle[i][k]) == 1) partner[i] = options[i][k];
}
}
return partner;
}
// Devuelve la pareja de cada nodo de la izquierda, o -1.
inline std::vector<int> max_matching(const Options& options, int right) {
MatchingNetwork net = matching_network(options, right);
net.flow.edmonds_karp(net.source, net.sink);
return read_matching(net, options);
}
inline int assigned_count(const std::vector<int>& partner) {
int count = 0;
for (int p : partner) count += p >= 0;
return count;
}
// ¿Es un matching de verdad? Cada pareja permitida, y nadie de la derecha repetido.
inline bool is_matching(const Options& options, int right, const std::vector<int>& partner) {
std::vector<int> used(at(right), 0);
for (std::size_t i = 0; i < options.size(); i++) {
if (partner[i] < 0) continue;
if (std::find(options[i].begin(), options[i].end(), partner[i]) == options[i].end()) return false;
if (++used[at(partner[i])] > 1) return false;
}
return true;
}
// Greedy 1: cada uno, en orden, a su primera opción con cupo.
inline std::vector<int> first_fit(const Options& options, std::vector<int> quota) {
std::vector<int> partner(options.size(), -1);
for (std::size_t i = 0; i < options.size(); i++) {
for (int j : options[i]) {
if (quota[at(j)] > 0) {
quota[at(j)]--;
partner[i] = j;
break;
}
}
}
return partner;
}
// Greedy 2: primero el que tiene menos opciones en la entrada, sin recalcular, y luego igual que el anterior.
inline std::vector<int> fewest_options_first(const Options& options, std::vector<int> quota) {
std::vector<std::size_t> order(options.size());
std::iota(order.begin(), order.end(), std::size_t{0});
std::stable_sort(order.begin(), order.end(),
[&](std::size_t a, std::size_t b) { return options[a].size() < options[b].size(); });
std::vector<int> partner(options.size(), -1);
for (std::size_t i : order) {
for (int j : options[i]) {
if (quota[at(j)] > 0) {
quota[at(j)]--;
partner[i] = j;
break;
}
}
}
return partner;
}
// Fuerza bruta: cada uno de la izquierda se queda sin nada o toma una opción con cupo. Es el árbitro.
inline int brute_assignment(const Options& options, std::vector<int>& quota, std::size_t i = 0) {
if (i == options.size()) return 0;
int best = brute_assignment(options, quota, i + 1);
for (int j : options[i]) {
if (quota[at(j)] > 0) {
quota[at(j)]--;
best = std::max(best, 1 + brute_assignment(options, quota, i + 1));
quota[at(j)]++;
}
}
return best;
}// Selección de proyectos: un greedy que falla, la red y lo que pasa con una capacidad finita.
#include <iostream>
#include <string>
#include <vector>
#include "projects.h"
namespace {
void print_choice(const std::vector<int>& chosen, const std::vector<std::string>& names) {
if (chosen.empty()) std::cout << " (ninguno)";
for (int i : chosen) std::cout << ' ' << names[static_cast<std::size_t>(i)];
std::cout << '\n';
}
} // namespace
int main() {
const std::vector<std::string> names = {"tienda", "reportes", "app", "servidor", "api", "auditoria", "robot", "sensor"};
const std::vector<long long> profit = {5, 4, 9, -6, -8, 3, 2, -5};
const std::vector<Requirement> needs = {{0, 3}, {1, 3}, {2, 3}, {2, 4}, {5, 4}, {6, 7}};
std::cout << "proyecto ganancia\n";
for (std::size_t i = 0; i < names.size(); i++) {
std::cout << names[i];
for (std::size_t k = names[i].size(); k < 12; k++) std::cout << ' ';
std::cout << profit[i] << '\n';
}
std::cout << "requisitos: tienda, reportes y app piden servidor; app y auditoria piden api; robot pide sensor\n\n";
std::cout << "Greedy, paquete por paquete:";
std::vector<int> greedy = greedy_packages(profit, needs);
print_choice(greedy, names);
std::cout << " ganancia: " << total_profit(profit, greedy) << '\n';
ProjectChoice choice = best_projects(profit, needs);
std::cout << "\nCorte mínimo:\n";
std::cout << " suma de ganancias positivas: " << choice.positive_total << '\n';
std::cout << " corte mínimo: " << choice.min_cut << '\n';
std::cout << " ganancia máxima: " << choice.value << '\n';
std::cout << " elegidos:";
print_choice(choice.chosen, names);
std::cout << " fuerza bruta: " << brute_projects(profit, needs) << '\n';
std::cout << "\nSi los prerrequisitos tuvieran capacidad 1:\n";
ProjectChoice broken = best_projects(profit, needs, 1);
std::cout << " la cuenta dice: " << broken.value << '\n';
std::cout << " elegidos:";
print_choice(broken.chosen, names);
std::cout << " ¿respetan los requisitos? " << (is_closed(8, needs, broken.chosen) ? "sí" : "no") << '\n';
std::cout << " suma de sus ganancias: " << total_profit(profit, broken.chosen) << '\n';
}// Selección de proyectos: elegir un conjunto cerrado por prerrequisitos con la mayor ganancia total.
#pragma once
#include <cassert>
#include <cstddef>
#include <vector>
#include "flow.h"
struct Requirement {
int project;
int needs;
};
struct ProjectChoice {
long long value = 0;
long long min_cut = 0;
long long positive_total = 0;
std::vector<int> chosen;
};
// profit[i] > 0 es una ganancia, profit[i] < 0 un costo. prerequisite_capacity = -1 usa el "infinito"
// de la reducción. Se deja como parámetro para ver qué pasa con una capacidad finita.
inline ProjectChoice best_projects(const std::vector<long long>& profit, const std::vector<Requirement>& requires_list,
long long prerequisite_capacity = -1) {
int n = static_cast<int>(profit.size());
int source = n;
int sink = n + 1;
ProjectChoice result;
long long negative_total = 0;
for (long long p : profit) {
if (p > 0) result.positive_total += p;
if (p < 0) negative_total -= p;
}
// El corte {s} ya cuesta positive_total. Una arista más cara que eso nunca está en un corte mínimo.
long long infinity = result.positive_total + 1;
assert(negative_total >= 0);
MaxFlow flow(n + 2);
for (int i = 0; i < n; i++) {
long long p = profit[static_cast<std::size_t>(i)];
if (p > 0) flow.add_edge(source, i, p);
if (p < 0) flow.add_edge(i, sink, -p);
}
for (const Requirement& r : requires_list) {
flow.add_edge(r.project, r.needs, prerequisite_capacity < 0 ? infinity : prerequisite_capacity);
}
result.min_cut = flow.edmonds_karp(source, sink);
result.value = result.positive_total - result.min_cut;
std::vector<bool> reached = flow.reachable_from(source);
for (int i = 0; i < n; i++) {
if (reached[static_cast<std::size_t>(i)]) result.chosen.push_back(i);
}
return result;
}
inline long long total_profit(const std::vector<long long>& profit, const std::vector<int>& chosen) {
long long total = 0;
for (int i : chosen) total += profit[static_cast<std::size_t>(i)];
return total;
}
inline bool is_closed(int n, const std::vector<Requirement>& requires_list, const std::vector<int>& chosen) {
std::vector<bool> in(static_cast<std::size_t>(n), false);
for (int i : chosen) in[static_cast<std::size_t>(i)] = true;
for (const Requirement& r : requires_list) {
if (in[static_cast<std::size_t>(r.project)] && !in[static_cast<std::size_t>(r.needs)]) return false;
}
return true;
}
// Árbitro: prueba los 2ⁿ subconjuntos.
inline long long brute_projects(const std::vector<long long>& profit, const std::vector<Requirement>& requires_list) {
int n = static_cast<int>(profit.size());
long long best = 0;
for (unsigned mask = 0; mask < (1u << n); mask++) {
std::vector<int> chosen;
for (int i = 0; i < n; i++) {
if (mask >> i & 1u) chosen.push_back(i);
}
if (is_closed(n, requires_list, chosen)) best = std::max(best, total_profit(profit, chosen));
}
return best;
}
// Greedy: tomar un proyecto con su cadena de prerrequisitos solo si ese paquete, por sí solo, deja ganancia.
// Lo que ya se tomó no se vuelve a pagar.
inline std::vector<int> greedy_packages(const std::vector<long long>& profit, const std::vector<Requirement>& requires_list) {
int n = static_cast<int>(profit.size());
std::vector<bool> taken(static_cast<std::size_t>(n), false);
for (int i = 0; i < n; i++) {
std::vector<bool> package = taken;
std::vector<int> pending = {i};
package[static_cast<std::size_t>(i)] = true;
while (!pending.empty()) {
int v = pending.back();
pending.pop_back();
for (const Requirement& r : requires_list) {
if (r.project == v && !package[static_cast<std::size_t>(r.needs)]) {
package[static_cast<std::size_t>(r.needs)] = true;
pending.push_back(r.needs);
}
}
}
long long gain = 0;
for (int k = 0; k < n; k++) {
if (package[static_cast<std::size_t>(k)] && !taken[static_cast<std::size_t>(k)]) gain += profit[static_cast<std::size_t>(k)];
}
if (gain > 0) taken = package;
}
std::vector<int> chosen;
for (int i = 0; i < n; i++) {
if (taken[static_cast<std::size_t>(i)]) chosen.push_back(i);
}
return chosen;
}// Asignación con cupos. Una red escrita a mano, arista por arista, para poder compararla con un dibujo.
#include <iostream>
#include <string>
#include <utility>
#include <vector>
#include "matching.h"
namespace {
// Dos estudiantes quieren S0, que tiene cupo 1, y S1 queda libre: la pregunta de repaso.
void same_section(bool one_can_move) {
// s = 0, E0 = 1, E1 = 2, S0 = 3, S1 = 4, t = 5
MaxFlow flow(6);
flow.add_edge(0, 1, 1);
flow.add_edge(0, 2, 1);
flow.add_edge(1, 3, 1);
flow.add_edge(2, 3, 1);
if (one_can_move) flow.add_edge(2, 4, 1);
int to_sink_0 = flow.add_edge(3, 5, 1);
int to_sink_1 = flow.add_edge(4, 5, 1);
const std::vector<std::string> names = {"s", "E0", "E1", "S0", "S1", "t"};
std::cout << " flujo: " << flow.edmonds_karp(0, 5) << '\n';
std::cout << " S0 -> t lleva " << flow.flow_on(to_sink_0) << ", S1 -> t lleva " << flow.flow_on(to_sink_1) << '\n';
std::vector<bool> reached = flow.reachable_from(0);
std::cout << " alcanzables desde s:";
for (std::size_t v = 0; v < names.size(); v++) {
if (reached[v]) std::cout << ' ' << names[v];
}
std::cout << '\n';
}
} // namespace
int main() {
const Options options = {{0}, {0}, {2, 1}, {0, 2}, {2}, {0}};
const std::vector<int> quota = {2, 1, 2};
std::cout << "Seis estudiantes y tres secciones, con cupos S0: 2, S1: 1, S2: 2\n";
for (std::size_t i = 0; i < options.size(); i++) {
std::cout << " E" << i << ":";
for (int j : options[i]) std::cout << " S" << j;
std::cout << '\n';
}
std::vector<int> quota_copy = quota;
std::cout << "\nLos greedy y el árbitro:\n";
std::cout << " primera opción libre: " << assigned_count(first_fit(options, quota)) << '\n';
std::cout << " menos opciones primero: " << assigned_count(fewest_options_first(options, quota)) << '\n';
std::cout << " fuerza bruta: " << brute_assignment(options, quota_copy) << '\n';
// s = 0, E0..E5 = 1..6, S0..S2 = 7..9, t = 10
MaxFlow flow(11);
for (int e = 1; e <= 6; e++) flow.add_edge(0, e, 1);
flow.add_edge(7, 10, 2);
flow.add_edge(8, 10, 1);
flow.add_edge(9, 10, 2);
const std::vector<std::pair<int, int>> middle = {{0, 0}, {1, 0}, {2, 2}, {2, 1}, {3, 0}, {3, 2}, {4, 2}, {5, 0}};
std::vector<int> ids;
ids.push_back(flow.add_edge(1, 7, 1));
ids.push_back(flow.add_edge(2, 7, 1));
ids.push_back(flow.add_edge(3, 9, 1));
ids.push_back(flow.add_edge(3, 8, 1));
ids.push_back(flow.add_edge(4, 7, 1));
ids.push_back(flow.add_edge(4, 9, 1));
ids.push_back(flow.add_edge(5, 9, 1));
ids.push_back(flow.add_edge(6, 7, 1));
std::cout << "\nFlujo máximo: " << flow.edmonds_karp(0, 10) << " de 6\n";
std::cout << "Aristas del medio con flujo:";
for (std::size_t k = 0; k < middle.size(); k++) {
if (flow.flow_on(ids[k]) == 1) std::cout << " E" << middle[k].first << "-S" << middle[k].second;
}
std::vector<bool> reached = flow.reachable_from(0);
std::cout << "\nEstudiantes alcanzables desde s:";
for (int e = 1; e <= 6; e++) {
if (reached[static_cast<std::size_t>(e)]) std::cout << " E" << e - 1;
}
std::cout << "\nSecciones alcanzables desde s:";
for (int j = 7; j <= 9; j++) {
if (reached[static_cast<std::size_t>(j)]) std::cout << " S" << j - 7;
}
std::cout << "\n\nDos quieren S0, con cupo 1, y S1 queda libre porque ninguno puede ir:\n";
same_section(false);
std::cout << "Lo mismo, pero E1 también puede ir a S1:\n";
same_section(true);
}Pruebas
Comprueban lo que afirman los apuntes; se ejecutan con make test.
// Las pruebas de lo que afirman los apuntes de la semana 12.
#include <bit>
#include <cassert>
#include <cstdint>
#include <iostream>
#include <random>
#include <set>
#include <vector>
#include "baseball.h"
#include "disjoint_paths.h"
#include "lower_bounds.h"
#include "matching.h"
#include "projects.h"
namespace {
using Random = std::mt19937;
int below(Random& random, int limit) { return static_cast<int>(random() % static_cast<std::uint32_t>(limit)); }
Options random_options(Random& random, int left, int right, int max_degree) {
Options options(static_cast<std::size_t>(left));
for (auto& list : options) {
int degree = below(random, max_degree + 1);
for (int k = 0; k < degree; k++) {
int j = below(random, right);
if (std::find(list.begin(), list.end(), j) == list.end()) list.push_back(j);
}
}
return options;
}
void test_matching_examples() {
// El ejemplo de 3 por 3, cuyo tercer camino usa aristas inversas.
Options three = {{0, 1}, {0}, {1, 2}};
MatchingNetwork net = matching_network(three, 3);
net.flow.keep_paths();
assert(net.flow.edmonds_karp(net.source, net.sink) == 3);
assert(net.flow.paths().size() == 3);
assert(net.flow.paths()[2].size() == 8);
assert(is_matching(three, 3, read_matching(net, three)));
// Los dos greedy fallan donde dicen los apuntes.
Options first = {{0, 1}, {0}};
assert(assigned_count(first_fit(first, {1, 1})) == 1);
assert(assigned_count(fewest_options_first(first, {1, 1})) == 2);
assert(assigned_count(max_matching(first, 2)) == 2);
Options fewest = {{0, 1}, {2, 0}, {2}};
assert(assigned_count(fewest_options_first(fewest, {1, 1, 1})) == 2);
assert(assigned_count(max_matching(fewest, 3)) == 3);
// Sin capacidad en s -> E, el valor deja de ser el tamaño de un matching: falla una sola dirección.
Options wide = {{0, 1}, {}};
MatchingNetwork no_limit = matching_network(wide, 2, 1000);
assert(no_limit.flow.edmonds_karp(no_limit.source, no_limit.sink) == 2);
assert(assigned_count(max_matching(wide, 2)) == 1);
// En un ciclo de 4 también existe un flujo de valor 2 con un medio en cada arista. El programa, que
// solo empuja enteros, devuelve un matching de verdad.
Options cycle = {{0, 1}, {0, 1}};
assert(assigned_count(max_matching(cycle, 2)) == 2);
}
void test_matching_random() {
Random random(1212);
for (int round = 0; round < 3000; round++) {
int left = 1 + below(random, 7);
int right = 1 + below(random, 7);
Options options = random_options(random, left, right, 3);
std::vector<int> quota(static_cast<std::size_t>(right), 1);
std::vector<int> copy = quota;
int expected = brute_assignment(options, copy);
MatchingNetwork net = matching_network(options, right);
long long value = net.flow.edmonds_karp(net.source, net.sink);
std::vector<int> partner = read_matching(net, options);
assert(value == expected);
assert(assigned_count(partner) == expected);
assert(is_matching(options, right, partner));
// A lo sumo min(L, R) aumentos.
assert(net.flow.augmentations() <= std::min(left, right));
MatchingNetwork other = matching_network(options, right);
assert(other.flow.ford_fulkerson(other.source, other.sink) == expected);
assert(assigned_count(first_fit(options, quota)) <= expected);
assert(assigned_count(fewest_options_first(options, quota)) <= expected);
}
}
void test_quota_example() {
const Options options = {{0}, {0}, {2, 1}, {0, 2}, {2}, {0}};
std::vector<int> quota = {2, 1, 2};
assert(assigned_count(first_fit(options, quota)) == 4);
assert(assigned_count(fewest_options_first(options, quota)) == 4);
assert(brute_assignment(options, quota) == 5);
// La condición de Hall con cupos: E0, E1 y E5 solo pueden ir a S0, que tiene cupo 2.
}
// Un flujo entero al azar se arma sumando caminos y ciclos, sin ninguna red detrás.
void test_decomposition() {
std::vector<Arc> with_cycle = {{0, 1}, {1, 2}, {2, 5}, {0, 3}, {3, 5}, {2, 4}, {4, 3}, {3, 2}};
Decomposition result = decompose_flow(6, with_cycle, 0, 5);
assert(result.paths.size() == 2);
assert(result.cycles.size() == 1);
// Solo un ciclo: valor 0. Un ciclo que pasa por s y por t no se confunde con un camino.
Decomposition only_cycle = decompose_flow(3, {{0, 1}, {1, 2}, {2, 0}}, 0, 1);
assert(only_cycle.paths.empty());
assert(only_cycle.cycles.size() == 1);
Random random(99);
for (int round = 0; round < 2000; round++) {
int nodes = 3 + below(random, 6);
int s = 0;
int t = nodes - 1;
std::vector<Arc> arcs;
int paths = below(random, 4);
for (int p = 0; p < paths; p++) {
int v = s;
for (int step = below(random, 4); step > 0; step--) {
int w = below(random, nodes);
if (w != v) {
arcs.push_back({v, w});
v = w;
}
}
if (v != t) arcs.push_back({v, t});
}
for (int c = below(random, 3); c > 0; c--) {
int a = below(random, nodes);
int b = below(random, nodes);
int d = below(random, nodes);
if (a != b && b != d && d != a) {
arcs.push_back({a, b});
arcs.push_back({b, d});
arcs.push_back({d, a});
}
}
Decomposition parts = decompose_flow(nodes, arcs, s, t);
std::size_t used = 0;
int value = 0;
for (const Arc& arc : arcs) value += (arc.from == s) - (arc.to == s);
assert(static_cast<int>(parts.paths.size()) == value);
for (const auto& path : parts.paths) {
assert(path.front() == s && path.back() == t);
assert(std::set<int>(path.begin(), path.end()).size() == path.size()); // camino simple
used += path.size() - 1;
}
for (const auto& cycle : parts.cycles) {
assert(cycle.front() == cycle.back());
used += cycle.size() - 1;
}
assert(used == arcs.size()); // ninguna unidad se pierde: todo está en un camino o en un ciclo
}
}
std::vector<Arc> random_arcs(Random& random, int nodes, int tries) {
std::vector<Arc> arcs;
std::set<std::pair<int, int>> seen;
for (int k = 0; k < tries; k++) {
int a = below(random, nodes);
int b = below(random, nodes);
if (a != b && seen.insert({a, b}).second) arcs.push_back({a, b});
}
return arcs;
}
int brute_edge_cut(int nodes, const std::vector<Arc>& arcs, int s, int t) {
int best = static_cast<int>(arcs.size());
std::vector<bool> no_nodes(static_cast<std::size_t>(nodes), false);
for (unsigned mask = 0; mask < (1u << arcs.size()); mask++) {
std::vector<bool> removed(arcs.size());
for (std::size_t k = 0; k < arcs.size(); k++) removed[k] = (mask >> k) & 1u;
if (!connected_without(nodes, arcs, s, t, no_nodes, removed)) best = std::min(best, std::popcount(mask));
}
return best;
}
int brute_vertex_separator(int nodes, const std::vector<Arc>& arcs, int s, int t) {
int best = nodes;
std::vector<bool> no_arcs(arcs.size(), false);
for (unsigned mask = 0; mask < (1u << nodes); mask++) {
if ((mask >> s & 1u) || (mask >> t & 1u)) continue;
std::vector<bool> removed(static_cast<std::size_t>(nodes));
for (int v = 0; v < nodes; v++) removed[static_cast<std::size_t>(v)] = (mask >> v) & 1u;
if (!connected_without(nodes, arcs, s, t, removed, no_arcs)) best = std::min(best, std::popcount(mask));
}
return best;
}
// Menger: los caminos son disjuntos de verdad y son tantos como el mínimo que los separa, por fuerza bruta.
void test_menger() {
Random random(2024);
for (int round = 0; round < 1500; round++) {
int nodes = 2 + below(random, 5);
int s = 0;
int t = nodes - 1;
std::vector<Arc> arcs = random_arcs(random, nodes, 1 + below(random, 10));
Decomposition by_edges = edge_disjoint_paths(nodes, arcs, s, t);
std::set<std::pair<int, int>> used;
for (const auto& path : by_edges.paths) {
for (std::size_t k = 0; k + 1 < path.size(); k++) assert(used.insert({path[k], path[k + 1]}).second);
}
int cut = brute_edge_cut(nodes, arcs, s, t);
assert(static_cast<int>(by_edges.paths.size()) == cut);
assert(static_cast<int>(minimum_edge_cut(nodes, arcs, s, t).size()) == cut);
Decomposition by_nodes = vertex_disjoint_paths(nodes, arcs, s, t);
std::set<int> inner;
int direct = 0;
for (const auto& path : by_nodes.paths) {
direct += path.size() == 2;
for (std::size_t k = 1; k + 1 < path.size(); k++) assert(inner.insert(path[k]).second);
}
// Sin la arista directa, Menger con nodos. Con ella, son uno más que sin ella.
std::vector<Arc> without;
for (const Arc& arc : arcs) {
if (!(arc.from == s && arc.to == t)) without.push_back(arc);
}
bool adjacent = without.size() != arcs.size();
assert(direct == (adjacent ? 1 : 0));
int separator = brute_vertex_separator(nodes, without, s, t);
assert(static_cast<int>(by_nodes.paths.size()) == separator + (adjacent ? 1 : 0));
assert(static_cast<int>(minimum_vertex_separator(nodes, without, s, t).size()) == separator);
}
}
// No dirigido: el mismo mínimo con dos arcos por arista, y aristas de verdad sin repetir.
void test_undirected() {
Random random(77);
for (int round = 0; round < 1500; round++) {
int nodes = 2 + below(random, 6);
int s = 0;
int t = nodes - 1;
std::vector<Arc> edges;
std::set<std::pair<int, int>> seen;
for (int k = 0, tries = 1 + below(random, 9); k < tries; k++) {
int a = below(random, nodes);
int b = below(random, nodes);
if (a != b && !seen.count({a, b}) && !seen.count({b, a})) {
seen.insert({a, b});
edges.push_back({a, b});
}
}
Decomposition result = undirected_edge_disjoint_paths(nodes, edges, s, t);
std::set<std::pair<int, int>> used;
for (const auto& path : result.paths) {
for (std::size_t k = 0; k + 1 < path.size(); k++) {
int a = std::min(path[k], path[k + 1]);
int b = std::max(path[k], path[k + 1]);
assert(used.insert({a, b}).second);
}
}
std::vector<Arc> both;
for (const Arc& e : edges) {
both.push_back(e);
both.push_back({e.to, e.from});
}
// Quitar una arista no dirigida quita sus dos arcos, pero para s y t basta el arco que cruza.
assert(static_cast<int>(result.paths.size()) == static_cast<int>(minimum_edge_cut(nodes, both, s, t).size()));
}
// La red de los apuntes usa una arista en las dos direcciones.
std::vector<Arc> edges = {{0, 4}, {6, 5}, {0, 2}, {2, 5}, {3, 6}, {1, 4}, {3, 1}, {0, 6}, {5, 4}};
Decomposition example = undirected_edge_disjoint_paths(7, edges, 0, 6);
assert(example.cancelled == 1);
assert(example.paths.size() == 3);
}
void test_baseball() {
Championship league{{"Lima", "Cusco", "Ica", "Piura", "Callao"},
{27, 26, 26, 25, 20},
{{0, 2, 2, 0, 0}, {2, 0, 2, 0, 0}, {2, 2, 0, 0, 0}, {0, 0, 0, 0, 3}, {0, 0, 0, 3, 0}}};
assert(!check_team(league, 0).eliminated);
Elimination piura = check_team(league, 3);
assert(piura.eliminated && !piura.trivial && piura.flow == 5 && piura.games_left == 6);
assert((piura.certificate == std::vector<int>{0, 1, 2}));
Elimination callao = check_team(league, 4);
assert(callao.eliminated && callao.trivial);
Random random(31);
int nontrivial = 0;
for (int round = 0; round < 2500; round++) {
int teams = 2 + below(random, 4);
Championship random_league;
random_league.games.assign(static_cast<std::size_t>(teams), std::vector<int>(static_cast<std::size_t>(teams), 0));
random_league.names.assign(static_cast<std::size_t>(teams), "");
for (int i = 0; i < teams; i++) random_league.wins.push_back(below(random, 7));
int budget = 9;
for (int i = 0; i < teams; i++) {
for (int j = i + 1; j < teams; j++) {
int g = std::min(budget, below(random, 3));
budget -= g;
random_league.games[static_cast<std::size_t>(i)][static_cast<std::size_t>(j)] = g;
random_league.games[static_cast<std::size_t>(j)][static_cast<std::size_t>(i)] = g;
}
}
for (int x = 0; x < teams; x++) {
Elimination result = check_team(random_league, x);
assert(result.eliminated == !brute_can_finish_first(random_league, x));
if (result.eliminated) {
assert(!result.certificate.empty());
assert(certificate_holds(random_league, result.best, result.certificate));
nontrivial += !result.trivial;
}
}
}
assert(nontrivial > 0);
}
void test_projects() {
const std::vector<long long> profit = {5, 4, 9, -6, -8, 3, 2, -5};
const std::vector<Requirement> needs = {{0, 3}, {1, 3}, {2, 3}, {2, 4}, {5, 4}, {6, 7}};
ProjectChoice choice = best_projects(profit, needs);
assert(choice.value == 7 && choice.min_cut == 16 && choice.positive_total == 23);
assert((choice.chosen == std::vector<int>{0, 1, 2, 3, 4, 5}));
assert(greedy_packages(profit, needs).empty());
assert(!is_closed(8, needs, best_projects(profit, needs, 1).chosen));
Random random(555);
for (int round = 0; round < 3000; round++) {
int n = 1 + below(random, 8);
std::vector<long long> values;
for (int i = 0; i < n; i++) values.push_back(below(random, 21) - 10);
std::vector<Requirement> requirements;
for (int k = below(random, 2 * n); k > 0; k--) {
int a = below(random, n);
int b = below(random, n);
if (a != b) requirements.push_back({a, b}); // también puede haber ciclos de requisitos
}
ProjectChoice result = best_projects(values, requirements);
assert(result.value == brute_projects(values, requirements));
assert(is_closed(n, requirements, result.chosen));
assert(total_profit(values, result.chosen) == result.value);
}
// Capacidades grandes: el "infinito" y las sumas caben en long long, no en int.
const std::vector<long long> big = {3000000000, -2000000000, 1000000000};
ProjectChoice large = best_projects(big, {{0, 1}, {2, 1}});
assert(large.value == 2000000000);
}
void test_lower_bounds() {
auto network = [](long long low, long long high) {
return std::vector<BoundedEdge>{{0, 1, low, high}, {0, 2, low, high}, {1, 3, 0, 3}, {1, 4, 0, 1},
{2, 4, 0, 2}, {3, 5, 0, 3}, {4, 5, 0, 2}};
};
BoundedFlow plain = max_flow_with_lower_bounds(6, network(0, 4), 0, 5);
assert(plain.value == 5 && plain.on_edge[1] == 1); // el máximo sin restricción deja a B con 1
BoundedFlow at_most = max_flow_with_lower_bounds(6, network(0, 2), 0, 5);
assert(at_most.value == 4);
BoundedFlow at_least = max_flow_with_lower_bounds(6, network(2, 4), 0, 5);
assert(at_least.feasible && at_least.value == 5 && at_least.on_edge[0] == 3 && at_least.on_edge[1] == 2);
Random random(808);
for (int round = 0; round < 1500; round++) {
int nodes = 2 + below(random, 3);
int s = 0;
int t = nodes - 1;
std::vector<BoundedEdge> edges;
for (int k = 1 + below(random, 5); k > 0; k--) {
int a = below(random, nodes);
int b = below(random, nodes);
if (a == b || b == s || a == t) continue; // la fuerza bruta cuenta el valor como lo que sale de s
long long low = below(random, 2);
edges.push_back({a, b, low, low + below(random, 3)});
}
long long expected = brute_bounded(nodes, edges, s, t);
BoundedFlow result = max_flow_with_lower_bounds(nodes, edges, s, t);
assert(result.feasible == (expected >= 0));
if (result.feasible) {
assert(result.value == expected);
for (std::size_t k = 0; k < edges.size(); k++) {
assert(edges[k].low <= result.on_edge[k] && result.on_edge[k] <= edges[k].high);
}
}
}
}
} // namespace
int main() {
test_matching_examples();
test_matching_random();
test_quota_example();
test_decomposition();
test_menger();
test_undirected();
test_baseball();
test_projects();
test_lower_bounds();
std::cout << "Todas las pruebas de la semana 12 pasaron.\n";
}Laboratorio
Archivos de partida del laboratorio de la semana.
TIEMPO = 1
VERIFICADOR = verificador.cpp
include ../../../lab.mk# Asignar estudiantes a secciones
Empieza la matrícula de un curso con varias secciones de laboratorio. Cada estudiante, según su horario, puede
asistir solo a algunas de ellas, y cada sección tiene un cupo, porque el aula tiene una cantidad fija de
computadoras. La oficina de matrícula quiere asignar a la mayor cantidad posible de estudiantes. Un estudiante va a
una sola sección, o se queda sin sección. Decidir bien importa: que alguien tome la primera sección con cupo puede
dejar fuera a otra persona que solo podía ir a esa.
Ustedes reciben los horarios y los cupos, y escriben la asignación.
## Entrada
La primera línea tiene dos enteros: n, la cantidad de estudiantes, y m, la cantidad de secciones. Los dos se numeran
desde 1.
La segunda línea tiene m enteros: el cupo de cada sección. Un cupo puede ser 0, y puede ser mayor que n.
Luego vienen n líneas, una por estudiante. Cada una empieza con d, la cantidad de secciones a las que ese estudiante
puede asistir, seguida de d números de sección distintos. d puede ser 0: hay estudiantes sin ninguna opción.
## Salida
La primera línea tiene k, la mayor cantidad de estudiantes que se puede asignar. Luego vienen k líneas, en cualquier
orden, cada una con dos enteros: un estudiante y la sección que le toca.
Ningún estudiante puede aparecer dos veces, cada uno debe ir a una sección de su lista, y ninguna sección puede
superar su cupo.
Puede haber varias asignaciones con k estudiantes, y cualquiera vale: un verificador revisa su respuesta.
## Límites
- 1 ≤ n ≤ 2000
- 1 ≤ m ≤ 2000
- 0 ≤ cupo ≤ 5000
- La suma de todos los d es a lo sumo 40000.
## Ejemplo
Entrada:
```text
5 3
1 2 1
2 1 2
1 1
2 2 3
1 1
1 3
```
Salida:
```text
4
1 2
2 1
3 2
5 3
```
Los estudiantes 2 y 4 solo pueden ir a la sección 1, que tiene cupo 1, así que uno de los dos se queda fuera y no se
puede con más de 4. También vale asignar a 4 en vez de 2, o enviar a 3 a otra sección si cabe.
## Tiempo
1 segundo por caso, compilado con `-O2`. Corran `make test` para probar con el juez.// Asignar estudiantes a secciones: lee la entrada y no asigna a nadie. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::size_t n = 0;
std::size_t m = 0;
std::cin >> n >> m;
std::vector<int> quota(m);
for (int& q : quota) std::cin >> q;
std::vector<std::vector<int>> options(n); // secciones numeradas desde 1, como en la entrada
for (auto& list : options) {
std::size_t count = 0;
std::cin >> count;
list.resize(count);
for (int& section : list) std::cin >> section;
}
// Por hacer: decidir quién va a qué sección.
std::cout << 0 << '\n';
}// Verificador de "Asignar estudiantes a secciones": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA está solo la mayor cantidad posible. Primero se comprueba que la lista sea una asignación
// válida y después que tenga esa cantidad. Termina con 0 si todo está bien, y con 1 y un mensaje si no.
#include <algorithm>
#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: solo dígitos, y a lo sumo 9.
bool parse_number(const std::string& word, std::size_t& value) {
if (word.empty() || word.size() > 9) return false;
value = 0;
for (char c : word) {
if (c < '0' || c > '9') return false;
value = value * 10 + static_cast<std::size_t>(c - '0');
}
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]);
std::size_t n = 0;
std::size_t m = 0;
input >> n >> m;
std::vector<long long> room(m);
for (long long& q : room) input >> q;
std::vector<std::vector<std::size_t>> options(n);
for (auto& list : options) {
std::size_t count = 0;
input >> count;
list.resize(count);
for (std::size_t& section : list) input >> section;
}
std::size_t best = 0;
expected >> best;
std::string word;
std::size_t count = 0;
if (!(output >> word) || !parse_number(word, count)) reject("la primera palabra no es un número");
std::vector<bool> assigned(n, false);
for (std::size_t k = 0; k < count; k++) {
std::string student_word;
std::string section_word;
std::size_t student = 0;
std::size_t section = 0;
if (!(output >> student_word >> section_word)) reject("anunció " + word + " pares y faltan");
if (!parse_number(student_word, student) || student < 1 || student > n) {
reject("estudiante inválido: " + student_word);
}
if (!parse_number(section_word, section) || section < 1 || section > m) {
reject("sección inválida: " + section_word);
}
if (assigned[student - 1]) reject("el estudiante " + student_word + " aparece dos veces");
assigned[student - 1] = true;
const auto& list = options[student - 1];
if (std::find(list.begin(), list.end(), section) == list.end()) {
reject("el estudiante " + student_word + " no puede ir a la sección " + section_word);
}
if (--room[section - 1] < 0) reject("la sección " + section_word + " supera su cupo");
}
if (output >> word) reject("sobra texto después de la lista: " + word);
if (count != best) {
reject("la asignación es válida, pero con " + std::to_string(count) + " asignados, y se puede con " +
std::to_string(best));
}
return 0;
}TIEMPO = 1
VERIFICADOR = verificador.cpp
include ../../../lab.mk# Qué secciones limitan la matrícula (extensión opcional)
Esta es la continuación opcional de `section_assignment`. Háganla después de resolver ese problema: los datos son
los mismos.
Cuando no todos entran, a la oficina de matrícula no le basta con el número. Quiere una explicación que pueda
comprobar sin confiar en el programa, para decidir dónde abrir más cupos. La explicación es un grupo de estudiantes
y el grupo de secciones a las que ellos pueden asistir. Si el grupo tiene más estudiantes que cupos esas secciones,
alguien de ese grupo se queda fuera, sin importar cómo se asigne.
Formalmente, sea A un conjunto de estudiantes, y sea N(A) el conjunto de todas las secciones que aparecen en la
lista de por lo menos un estudiante de A. Sea c(N(A)) la suma de sus cupos. Para cualquier asignación, al menos
|A| − c(N(A)) estudiantes de A se quedan sin sección. Si k es la mayor cantidad de estudiantes que se puede asignar,
ustedes deben dar un A que explique **todo** lo que falta:
|A| − c(N(A)) = n − k
Ese A siempre existe. Si entran todos, k = n, y A puede ser vacío.
## Entrada
Igual que en `section_assignment`. La primera línea tiene n y m. La segunda tiene los m cupos. Luego vienen n líneas,
cada una con d y d números de sección distintos.
## Salida
Tres líneas:
1. k, la mayor cantidad de estudiantes que se puede asignar.
2. a, la cantidad de estudiantes de A, seguida de esos a números de estudiante.
3. b, la cantidad de secciones de N(A), seguida de esos b números de sección.
Los números pueden ir en cualquier orden, y no se repiten. La línea 3 debe tener exactamente N(A): ni una sección de
más, ni una de menos. Si a es 0, b también es 0.
Puede haber varios conjuntos válidos, y cualquiera vale: un verificador revisa su respuesta. No hace falta imprimir
la asignación.
## Límites
- 1 ≤ n ≤ 2000
- 1 ≤ m ≤ 2000
- 0 ≤ cupo ≤ 5000
- La suma de todos los d es a lo sumo 40000.
## Ejemplo
Entrada:
```text
5 3
1 2 1
2 1 2
1 1
2 2 3
1 1
1 3
```
Salida:
```text
4
2 2 4
1 1
```
Se pueden asignar 4 de 5. Los estudiantes 2 y 4 solo pueden ir a la sección 1, que tiene cupo 1: |A| − c(N(A)) = 2 −
1 = 1, que es justo n − k = 5 − 4. Esto demuestra además que 4 es lo máximo.
## Tiempo
1 segundo por caso, compilado con `-O2`. Corran `make test` para probar con el juez.// Qué secciones limitan la matrícula: lee la entrada y responde con grupos vacíos. Reemplacen el cálculo.
#include <cstddef>
#include <iostream>
#include <vector>
int main() {
std::size_t n = 0;
std::size_t m = 0;
std::cin >> n >> m;
std::vector<int> quota(m);
for (int& q : quota) std::cin >> q;
std::vector<std::vector<int>> options(n); // secciones numeradas desde 1, como en la entrada
for (auto& list : options) {
std::size_t count = 0;
std::cin >> count;
list.resize(count);
for (int& section : list) std::cin >> section;
}
// Por hacer: calcular k y el grupo que explica lo que falta.
std::cout << 0 << '\n' << 0 << '\n' << 0 << '\n';
}// Verificador de "Qué secciones limitan la matrícula": verificador ENTRADA ESPERADA SALIDA.
// En ESPERADA está solo k. Se comprueba que A y su lista de secciones estén bien escritas, que esa lista
// sea exactamente N(A) y que |A| − c(N(A)) = n − k. 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: solo dígitos, y a lo sumo 9.
bool read_number(std::ifstream& output, std::size_t& value, const std::string& what) {
std::string word;
if (!(output >> word)) return false;
if (word.empty() || word.size() > 9) reject(what + " inválido: " + word);
value = 0;
for (char c : word) {
if (c < '0' || c > '9') reject(what + " inválido: " + word);
value = value * 10 + static_cast<std::size_t>(c - '0');
}
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]);
std::size_t n = 0;
std::size_t m = 0;
input >> n >> m;
std::vector<long long> quota(m);
for (long long& q : quota) input >> q;
std::vector<std::vector<std::size_t>> options(n);
for (auto& list : options) {
std::size_t count = 0;
input >> count;
list.resize(count);
for (std::size_t& section : list) input >> section;
}
std::size_t best = 0;
expected >> best;
std::size_t k = 0;
if (!read_number(output, k, "k")) reject("falta k");
if (k != best) reject("k esperado: " + std::to_string(best) + ", salió " + std::to_string(k));
std::size_t a = 0;
if (!read_number(output, a, "a")) reject("falta la cantidad de estudiantes del grupo");
if (a > n) reject("el grupo tiene más estudiantes que n");
std::vector<bool> in_group(n, false);
std::vector<bool> needed(m, false);
for (std::size_t i = 0; i < a; i++) {
std::size_t student = 0;
if (!read_number(output, student, "estudiante")) reject("faltan estudiantes en el grupo");
if (student < 1 || student > n) reject("estudiante fuera de rango: " + std::to_string(student));
if (in_group[student - 1]) reject("el estudiante " + std::to_string(student) + " aparece dos veces");
in_group[student - 1] = true;
for (std::size_t section : options[student - 1]) needed[section - 1] = true;
}
std::size_t b = 0;
if (!read_number(output, b, "b")) reject("falta la cantidad de secciones");
if (b > m) reject("hay más secciones que m");
std::vector<bool> listed(m, false);
for (std::size_t j = 0; j < b; j++) {
std::size_t section = 0;
if (!read_number(output, section, "sección")) reject("faltan secciones");
if (section < 1 || section > m) reject("sección fuera de rango: " + std::to_string(section));
if (listed[section - 1]) reject("la sección " + std::to_string(section) + " aparece dos veces");
listed[section - 1] = true;
}
std::string extra;
if (output >> extra) reject("sobra texto después de las secciones: " + extra);
long long capacity = 0;
for (std::size_t j = 0; j < m; j++) {
if (needed[j] && !listed[j]) reject("falta la sección " + std::to_string(j + 1) + ", que alguien del grupo puede usar");
if (!needed[j] && listed[j]) reject("sobra la sección " + std::to_string(j + 1) + ": nadie del grupo puede usarla");
if (listed[j]) capacity += quota[j];
}
long long deficit = static_cast<long long>(a) - capacity;
long long missing = static_cast<long long>(n) - static_cast<long long>(k);
if (deficit != missing) {
reject("el grupo explica " + std::to_string(deficit) + " y faltan " + std::to_string(missing));
}
return 0;
}Compilación
Cómo se compilan y ejecutan los ejemplos.
# Semana 12: modelar con flujo. Matching bipartito, caminos disjuntos, eliminación en un campeonato,
# selección de proyectos y cotas inferiores. 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 la medición del matching, 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 = flow.h matching.h disjoint_paths.h baseball.h projects.h lower_bounds.h
PROGRAMS = matching_demo disjoint_demo quota_demo baseball_demo projects_demo lower_bound_demo test_examples
BENCHES = matching_bench
.PHONY: all test bench clean
all: $(PROGRAMS:%=$(BUILD)/%) $(BENCHES:%=$(BUILD)/%)
$(PROGRAMS:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
$(CXX) $(CXXFLAGS) $< -o $@
# Lo que mide tiempos va con -O2 y sin sanitizers.
$(BENCHES:%=$(BUILD)/%): $(BUILD)/%: %.cpp $(HEADERS) | $(BUILD)
$(CXX) $(BENCHFLAGS) $< -o $@
$(BUILD):
mkdir -p $@
test: $(BUILD)/test_examples
$(BUILD)/test_examples
bench: $(BUILD)/matching_bench
$(BUILD)/matching_bench
clean:
rm -rf $(BUILD)