From f489c3b0ba71830328c9af8855ea28bc2db410c9 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:27:27 -0800 Subject: [PATCH] Fix #250: [Rule] VERTEX COVER to MINIMUM CUT INTO BOUNDED SETS Adds the rule DecisionMinimumVertexCover -> MinimumCutIntoBoundedSets, with unit tests, a canonical example and a paper entry. Makes MinimumCutIntoBoundedSets::try_new public. --- docs/paper/reductions.typ | 70 +++ .../graph/minimum_cut_into_bounded_sets.rs | 7 +- ...umvertexcover_minimumcutintoboundedsets.rs | 280 +++++++++++ src/rules/mod.rs | 4 + .../parameter_formula_validation.rs | 56 +++ ...umvertexcover_minimumcutintoboundedsets.rs | 465 ++++++++++++++++++ 6 files changed, 881 insertions(+), 1 deletion(-) create mode 100644 src/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs create mode 100644 src/unit_tests/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 8e0462b7a..fc53f87e7 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11662,6 +11662,76 @@ the displayed rule, extracted from the corresponding `pred path` entry. _Value and solution extraction._ The target is Decision Minimum Sum Multicenter with bound $B$ (or $-1$ for a negative source bound). Its predicate enforces the cost bound. Decode a YES witness by removing the auxiliary coordinates; completed YES and NO answers pass through unchanged. ] +#let dmvc_mcibs = load-example( + "DecisionMinimumVertexCover", + "MinimumCutIntoBoundedSets", + source-variant: (graph: "SimpleGraph", weight: "One"), + target-variant: (graph: "SimpleGraph", weight: "i64"), +) +#let dmvc_mcibs_sol = dmvc_mcibs.solutions.at(0) +#let dmvc_mcibs_n = dmvc_mcibs.source.instance.inner.graph.num_vertices +#let dmvc_mcibs_edges = dmvc_mcibs.source.instance.inner.graph.edges +#let dmvc_mcibs_k = dmvc_mcibs.source.instance.bound +#let dmvc_mcibs_tgt = dmvc_mcibs.target.instance +#let dmvc_mcibs_tedges = dmvc_mcibs_tgt.graph.edges +#let dmvc_mcibs_w = dmvc_mcibs_tgt.edge_weights +#let dmvc_mcibs_s = dmvc_mcibs_tgt.source +#let dmvc_mcibs_t = dmvc_mcibs_tgt.sink +#let dmvc_mcibs_B = dmvc_mcibs_tgt.size_bound +// The padding weight M = Phi + 1 is the largest weight of the target. +#let dmvc_mcibs_M = calc.max(..dmvc_mcibs_w) +#let dmvc_mcibs_phi = dmvc_mcibs_M - 1 +#let dmvc_mcibs_h = dmvc_mcibs_tedges.filter(e => e.at(0) < dmvc_mcibs_n and e.at(1) < dmvc_mcibs_n) +#let dmvc_mcibs_is-edge(i, j) = dmvc_mcibs_edges.any(e => (e.at(0) == i and e.at(1) == j) or (e.at(0) == j and e.at(1) == i)) +#let dmvc_mcibs_clamp-t(k) = calc.min(calc.max(dmvc_mcibs_n - k, 0), dmvc_mcibs_n + 1) +// Vertex sets I of the original vertices with |I| <= limit, as masks, with the value Phi - 2 e_H(I) - |I|. +#let dmvc_mcibs_cuts(limit) = range(calc.pow(2, dmvc_mcibs_n)).map(mask => { + let members = range(dmvc_mcibs_n).filter(i => calc.rem(calc.quo(mask, calc.pow(2, i)), 2) == 1) + let inside = dmvc_mcibs_h.filter(e => members.contains(e.at(0)) and members.contains(e.at(1))).len() + (members: members, cut: dmvc_mcibs_phi - 2 * inside - members.len()) +}).filter(c => c.members.len() <= limit) +#let dmvc_mcibs_sink-side = dmvc_mcibs_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => i) +#let dmvc_mcibs_I = dmvc_mcibs_sink-side.filter(i => i < dmvc_mcibs_n) +#let dmvc_mcibs_eh = dmvc_mcibs_h.filter(e => dmvc_mcibs_I.contains(e.at(0)) and dmvc_mcibs_I.contains(e.at(1))).len() +#let dmvc_mcibs_cut = dmvc_mcibs_tedges.enumerate().filter(((i, e)) => dmvc_mcibs_sol.target_config.at(e.at(0)) != dmvc_mcibs_sol.target_config.at(e.at(1))).map(((i, e)) => dmvc_mcibs_w.at(i)).sum() +#reduction-rule("DecisionMinimumVertexCover", "MinimumCutIntoBoundedSets", + example: true, + example-source-variant: (graph: "SimpleGraph", weight: "One"), + example-target-variant: (graph: "SimpleGraph", weight: "i64"), + example-caption: [6-vertex graph with cover bound $k = 3$: the complement graph, two terminals and three padding vertices], + extra: [ + #pred-commands( + "pred create --example " + rule-spec(dmvc_mcibs) + " -o vc.json", + "pred reduce vc.json --via route.json -o bundle.json", + "pred solve bundle.json", + "pred evaluate vc.json --config " + cli-config(dmvc_mcibs_sol.source_config), + ) + *Step 1 -- Source instance.* The source graph has vertices $V = {0, dots, #(dmvc_mcibs_n - 1)}$, $n = #dmvc_mcibs_n$ and edges #{dmvc_mcibs_edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, so $m = #dmvc_mcibs_edges.len()$. The bound is $k = #dmvc_mcibs_k$ and the stored cover is $C = {#dmvc_mcibs_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$. + + *Step 2 -- Sizes.* The complement graph $H$ has $binom(n, 2) - m = #(dmvc_mcibs_n * (dmvc_mcibs_n - 1) / 2) - #dmvc_mcibs_edges.len() = #dmvc_mcibs_h.len()$ edges #{dmvc_mcibs_h.map(e => $(#e.at(0), #e.at(1))$).join(", ")}. Hence $Phi = 2 dot #dmvc_mcibs_h.len() + 2 dot #dmvc_mcibs_n = #dmvc_mcibs_phi$ and $M = #dmvc_mcibs_M$. With $T = n - k = #dmvc_mcibs_clamp-t(dmvc_mcibs_k)$ the target has $q = n - T = #(dmvc_mcibs_n - dmvc_mcibs_clamp-t(dmvc_mcibs_k))$ padding vertices, so $#graph-num-vertices(dmvc_mcibs_tgt) = #dmvc_mcibs_n + 2 + #(dmvc_mcibs_n - dmvc_mcibs_clamp-t(dmvc_mcibs_k))$ vertices ($s = #dmvc_mcibs_s$, $t = #dmvc_mcibs_t$) and $#graph-num-edges(dmvc_mcibs_tgt)$ edges; the side bound is $B = #dmvc_mcibs_B$ and the threshold is $theta = Phi - T^2 = #(dmvc_mcibs_phi - calc.pow(dmvc_mcibs_clamp-t(dmvc_mcibs_k), 2))$. The weights are $1$ on the complement edges and on the $#dmvc_mcibs_n$ edges $(i, s)$, then #dmvc_mcibs_w.slice(dmvc_mcibs_h.len() + dmvc_mcibs_n, count: dmvc_mcibs_n).map(str).join(", ") on the edges $(i, t)$, and $M$ on the padding edges. + + *Step 3 -- Verify the stored witness.* The sink's side is ${#dmvc_mcibs_sink-side.map(str).join(", ")}$: the independent set $I = {#dmvc_mcibs_sink-side.filter(i => i < dmvc_mcibs_n).map(str).join(", ")}$ of $G$, the sink and the padding vertices. The sink's side has #dmvc_mcibs_sink-side.len() vertices and the source's side has #(graph-num-vertices(dmvc_mcibs_tgt) - dmvc_mcibs_sink-side.len()), both at most $B = #dmvc_mcibs_B$. Its cut weight is $#dmvc_mcibs_cut = Phi - 2 e_H (I) - |I| = #dmvc_mcibs_phi - 2 dot #dmvc_mcibs_eh - #dmvc_mcibs_I.len()$, which equals $theta$. The original vertices on the source's side, ${#dmvc_mcibs_sol.target_config.slice(0, count: dmvc_mcibs_n).enumerate().filter(((i, x)) => not x).map(((i, _)) => str(i)).join(", ")}$, are the extracted cover of size $#dmvc_mcibs_sol.target_config.slice(0, count: dmvc_mcibs_n).filter(x => not x).len() <= k$ #sym.checkmark + + *Step 4 -- A NO instance.* Take the same graph with $k = #(dmvc_mcibs_k - 1)$. Then $T = #dmvc_mcibs_clamp-t(dmvc_mcibs_k - 1)$, $q = #(dmvc_mcibs_n - dmvc_mcibs_clamp-t(dmvc_mcibs_k - 1))$ and $theta = #(dmvc_mcibs_phi - calc.pow(dmvc_mcibs_clamp-t(dmvc_mcibs_k - 1), 2))$ ($Phi$ and $M$ do not depend on $k$). A cut within $theta$ has no padding vertex on the source's side, so it is $Phi - 2 e_H(I) - |I|$ for an $I$ with $|I| <= T$. Over all #(dmvc_mcibs_cuts(dmvc_mcibs_clamp-t(dmvc_mcibs_k - 1)).len()) such sets, the least value is $#calc.min(..dmvc_mcibs_cuts(dmvc_mcibs_clamp-t(dmvc_mcibs_k - 1)).map(c => c.cut))$, which exceeds $theta$: $G$ has no independent set of size $T$, hence no cover of size at most $k$. + ], +)[ + Given a Decision Minimum Vertex Cover instance $(G, k)$ with $n$ vertices and $m$ edges, let $H$ be the complement graph, $Phi = 2 |E(H)| + 2 n$ and $T = min(max(n - k, 0), n + 1)$. The target has the $n$ original vertices, a source $s$, a sink $t$ and $q = n - T$ padding vertices ($q = 0$ when $T = n + 1$): $H$ with unit weights, $(i, s)$ of weight $1$, $(i, t)$ of weight $deg_H (i) + 2$, and $(t, p)$ of weight $M = Phi + 1$ for each padding vertex $p$, with side bound $B = n + 1$. $G$ has a cover of size at most $k$ iff the optimal cut is at most $theta = Phi - T^2$. The target has at most $2 n + 2$ vertices and $binom(n, 2) - m + 3 n$ edges, all weights are positive, and $|theta| <= (n + 1)^2$. +][ + _Construction._ Write $V = {0, dots, n - 1}$, $s = n$, $t = n + 1$ and let $P$ be the set of $q = n - T$ padding vertices. The target graph $G'$ on $V union {s, t} union P$ has the edges $\{i, j\}$ for $i < j$ with $\{i, j\} in.not E$ (weight $1$), $\{i, s\}$ (weight $1$) and $\{i, t\}$ (weight $deg_H (i) + 2$) for $i in V$, and $\{t, p\}$ (weight $M = Phi + 1$) for $p in P$. The source is $s$, the sink is $t$, and each side may hold $B = n + 1$ vertices. All numbers are polynomial in $n$ and independent of the magnitude of $k$ ($T$ is clamped), and all weights are positive. + + _Cut weight._ Consider a feasible assignment. Let $I subset.eq V$ be the original vertices on $t$'s side, $e_H (I)$ the number of edges of $H$ inside $I$, and $p$ the number of padding vertices on $s$'s side. An edge $\{i, j\}$ of $H$ is cut iff exactly one end is in $I$, which gives $sum_(i in I) deg_H (i) - 2 e_H (I)$. The edge $\{i, s\}$ is cut iff $i in I$, contributing $|I|$, and $\{i, t\}$ is cut iff $i in.not I$, contributing $sum_(i in.not I) (deg_H (i) + 2)$. Together with $M p$ for the padding edges and $sum_(i in V) deg_H (i) = 2 |E(H)|$ the cut weight is + $ Phi - 2 e_H (I) - |I| + M p. $ + The two sides have $1 + |I| + q - p$ and $1 + (n - |I|) + p$ vertices. Since $e_H (I) <= |E(H)|$ and $|I| <= n$, the cut weight is at least $n + M p$. Since $e_H (I) <= binom(|I|, 2)$, also $2 e_H (I) + |I| <= |I|^2$, with equality iff $I$ is a clique of $H$, i.e. an independent set of $G$. + + _Correctness._ ($arrow.r.double$) Let $C$ be a cover with $|C| <= k$. Then $k >= 0$, so $T = max(n - k, 0) <= n$, and $V without C$ is an independent set of size at least $n - k$ and at least $0$, so it contains an independent set $I$ with $|I| = T$. Put $I$, $t$ and all of $P$ on $t$'s side and everything else on $s$'s side. The sink's side holds $1 + T + q = n + 1$ vertices and the source's side $1 + n - T <= n + 1$, so the assignment is feasible. As $I$ is independent in $G$ it is a clique of $H$, so the cut weight is $Phi - T^2 = theta$. The target has finitely many assignments, so an optimal one exists and is at most $theta$. + ($arrow.l.double$) Let a feasible assignment have cut weight at most $theta$. If $p >= 1$ the cut weight would be at least $M > Phi >= theta$, so $p = 0$. The sink's side then has $1 + |I| + q <= n + 1$ vertices, so $|I| <= n - q$. If $T <= n$ this is $|I| <= T$; if $T = n + 1$ then $q = 0$ and $|I| <= n$. The bound gives $Phi - 2 e_H (I) - |I| <= Phi - T^2$, hence $T^2 <= 2 e_H (I) + |I| <= |I|^2$. The case $T = n + 1$ is impossible because $|I|^2 <= n^2$. So $T <= n$ and $|I| = T$, and equality throughout shows that $I$ is a clique of $H$. Then $V without I$ meets every edge of $G$ and has $n - T$ vertices, where $n - T = min(k, n) <= k$. + Hence $G$ has a cover of size at most $k$ iff the optimum cut is at most $theta$. + + _Solution extraction._ Extraction validates the target configuration once: it must be a feasible partition of the right length whose cut weight is at most $theta$. Otherwise it is rejected. The cover is the set of original vertices on $s$'s side, i.e. the complement of $I$. By the argument above this is a cover of size at most $k$, for every such configuration (optimality is not needed). The special cases are $n = 0$ (the target has only $s$ and $t$), $k >= n$ ($T = 0$, $I = emptyset$, $q = n$) and $k < 0$ ($T = n + 1$, no cut within $theta$). + + _Numeric range._ $theta$, $M$ and every weight are computed with checked 64-bit signed arithmetic, as is the total weight $3 |E(H)| + 3 n + q M$, which bounds every cut, so evaluating the target cannot overflow; unrepresentable sizes are reported as a reduction error before anything is allocated. +] + #let mvc_lcs = load-example("MinimumVertexCover", "LongestCommonSubsequence") #let mvc_lcs_sol = mvc_lcs.solutions.at(0) #reduction-rule("MinimumVertexCover", "LongestCommonSubsequence", diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 449f1402f..59bfeb3c9 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -152,7 +152,12 @@ impl MinimumCutIntoBoundedSets { .unwrap_or_else(|error| panic!("{error}")) } - fn try_new( + /// Create a new MinimumCutIntoBoundedSets problem, reporting invalid input as a typed error. + /// + /// # Errors + /// Returns an error if `edge_weights.len()` differs from `graph.num_edges()`, if `source` + /// or `sink` is out of bounds, or if `source == sink`. + pub fn try_new( graph: G, edge_weights: Vec, source: usize, diff --git a/src/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs b/src/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs new file mode 100644 index 000000000..5a281e0fe --- /dev/null +++ b/src/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs @@ -0,0 +1,280 @@ +//! Reduction from Decision Minimum Vertex Cover to Minimum Cut Into Bounded Sets. +//! +//! A cover of size at most `k` exists exactly when the graph has an independent set of size +//! `T = n - k`, i.e. when the complement graph `H` has a clique of size `T` (`T` is clamped to +//! `0..=n + 1`). Let `I` be the original vertices on the sink's side. The target consists of +//! +//! * the complement edges `{i, j}` of `H`, weight 1; +//! * a source `s = n` and a sink `t = n + 1`, with the edges `{i, s}` of weight 1 and `{i, t}` of +//! weight `deg_H(i) + 2` for every original vertex `i`; +//! * `q = n - T` padding vertices, each joined to `t` by an edge of weight `M = Phi + 1`, where +//! `Phi = 2 |E(H)| + 2 n`; +//! * the side bound `n + 1`. +//! +//! A feasible assignment cuts `Phi - 2 e_H(I) - |I| + M p`, where `e_H(I)` counts the edges of `H` +//! inside `I` and `p` the padding vertices on the source's side. Since `M` exceeds the threshold +//! `Phi - T^2`, no padding vertex is on the source's side within the threshold. The padding then +//! fills the sink's side and the side bound gives `|I| <= T`; with `2 e_H(I) + |I| <= |I|^2` the +//! cut is at most the threshold exactly when `|I| = T` and `I` is a clique of `H`, i.e. when the +//! original vertices outside `I` form a cover of size `n - T <= k`. + +use crate::models::decision::Decision; +use crate::models::graph::{MinimumCutIntoBoundedSets, MinimumVertexCover}; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::{Graph, SimpleGraph}; +use crate::types::{Min, One, Or}; + +type Source = Decision>; +type Target = MinimumCutIntoBoundedSets; + +/// Result of reducing Decision Minimum Vertex Cover to Minimum Cut Into Bounded Sets. +/// +/// The source is a YES instance exactly when the optimal cut of the target is at most +/// `threshold`. +#[derive(Debug, Clone)] +pub struct ReductionDecisionMinimumVertexCoverToMinimumCutIntoBoundedSets { + target: Target, + source_num_vertices: usize, + threshold: i64, +} + +impl ReductionResult for ReductionDecisionMinimumVertexCoverToMinimumCutIntoBoundedSets { + type Source = Source; + type Target = Target; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution( + &self, + target_solution: &::Solution, + ) -> crate::rules::ExtractionResult<::Solution> { + crate::rules::traits::validate_target_witness( + self.target_problem(), + target_solution, + |value| crate::rules::AggregateReductionResult::extract_value(self, value).0, + "target partition is infeasible or cuts more than the threshold, so it does not certify a vertex cover within the source bound", + )?; + // The cover is the set of original vertices on the source's side (`false`); the + // source, the sink and the padding vertices follow the original vertices. + Ok(target_solution[..self.source_num_vertices] + .iter() + .map(|on_sink_side| !on_sink_side) + .collect()) + } +} + +#[crate::aggregate_reduction] +impl crate::rules::AggregateReductionResult + for ReductionDecisionMinimumVertexCoverToMinimumCutIntoBoundedSets +{ + type Source = Decision>; + type Target = MinimumCutIntoBoundedSets; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: Min) -> Or { + Or(value.0.is_some_and(|cut| cut <= self.threshold)) + } +} + +/// Counts and numbers of the construction, computed before anything is allocated. +#[derive(Debug, Clone, PartialEq, Eq)] +struct Parameters { + /// `T`: the independent-set size sought, `n - bound` clamped to `0..=n + 1`. + independent_size: usize, + /// `q = n - T` (0 when `T = n + 1`): the number of padding vertices. + padding: usize, + /// `n + 2 + q`. + target_vertices: usize, + /// `|E(H)| = n (n - 1) / 2 - m`: the number of complement edges. + complement_edges: usize, + /// `|E(H)| + 2 n + q`. + target_edges: usize, + /// `Phi + 1`, the weight of a padding edge. + heavy_weight: i64, + /// `Phi - T^2`. + threshold: i64, +} + +/// Compute the construction's parameters with checked arithmetic. Besides the sizes, this checks +/// that every weight and the total weight of all edges (which bounds every cut) fit in `i64`, so +/// no evaluation of the target can overflow. +fn construction_parameters( + n: usize, + m: usize, + bound: i64, +) -> Result { + let overflow = || { + crate::rules::ReductionError::integer_overflow::( + "encoding the bounded-sets construction parameters", + ) + }; + let n_plus_one = n.checked_add(1).ok_or_else(overflow)?; + // T = n - bound clamped to 0..=n + 1; a negative bound gives n + 1. + let independent_size = if bound < 0 { + n_plus_one + } else { + // A bound beyond `usize` exceeds every vertex count, so it saturates. + n.saturating_sub(usize::try_from(bound).unwrap_or(usize::MAX)) + }; + let padding = n.saturating_sub(independent_size); + let target_vertices = n_plus_one + .checked_add(1) + .and_then(|v| v.checked_add(padding)) + .ok_or_else(overflow)?; + let pairs = n.checked_mul(n.saturating_sub(1)).ok_or_else(overflow)? / 2; + let complement_edges = pairs.checked_sub(m).ok_or_else(overflow)?; + let target_edges = n + .checked_mul(2) + .and_then(|e| e.checked_add(complement_edges)) + .and_then(|e| e.checked_add(padding)) + .ok_or_else(overflow)?; + + let to_i64 = |value: usize| { + >::exact_i64(value, "encoding a construction count") + }; + let n_i64 = to_i64(n)?; + let complement_i64 = to_i64(complement_edges)?; + // Phi = 2 |E(H)| + 2 n. + let phi = complement_i64 + .checked_mul(2) + .and_then(|p| p.checked_add(n_i64.checked_mul(2)?)) + .ok_or_else(overflow)?; + let heavy_weight = phi.checked_add(1).ok_or_else(overflow)?; + let t_i64 = to_i64(independent_size)?; + let threshold = t_i64 + .checked_mul(t_i64) + .and_then(|square| phi.checked_sub(square)) + .ok_or_else(overflow)?; + // The total weight |E(H)| + n + (2 |E(H)| + 2 n) + q M bounds every cut, so evaluating the + // target cannot overflow. Every other weight is at most n + 1 <= M. + to_i64(padding)? + .checked_mul(heavy_weight) + .and_then(|padding_weight| { + padding_weight + .checked_add(phi)? + .checked_add(n_i64)? + .checked_add(complement_i64) + }) + .ok_or_else(overflow)?; + Ok(Parameters { + independent_size, + padding, + target_vertices, + complement_edges, + target_edges, + heavy_weight, + threshold, + }) +} + +#[reduction( + transform = upper_bound { + num_vertices = "2 * num_vertices + 2", + num_edges = "num_vertices * (num_vertices - 1) / 2 - num_edges + 3 * num_vertices", + } +)] +impl ReduceTo> + for Decision> +{ + type Result = ReductionDecisionMinimumVertexCoverToMinimumCutIntoBoundedSets; + + fn reduce_to(&self) -> Result { + let graph = self.inner().graph(); + let n = graph.num_vertices(); + let parameters = construction_parameters(n, graph.num_edges(), *self.bound())?; + let source_vertex = n; + let sink_vertex = n + 1; + + // Complement edges, each of weight 1; `degree[i]` is the degree of `i` in `H`. + let mut edges = Vec::with_capacity(parameters.target_edges); + let mut weights = Vec::with_capacity(parameters.target_edges); + let mut degree = vec![0usize; n]; + for j in 0..n { + for i in 0..j { + if !graph.has_edge(i, j) { + edges.push((i, j)); + weights.push(1); + degree[i] += 1; + degree[j] += 1; + } + } + } + // {i, s} of weight 1 for every original vertex. + for i in 0..n { + edges.push((i, source_vertex)); + weights.push(1); + } + // {i, t} of weight deg_H(i) + 2 for every original vertex. + for (i, degree) in degree.into_iter().enumerate() { + edges.push((i, sink_vertex)); + // deg_H(i) + 2 <= n + 1 <= M, which `construction_parameters` showed fits in `i64`. + weights.push( + >::exact_i64(degree, "encoding a complement degree")? + + 2, + ); + } + // {t, padding} of weight M for every padding vertex. + for padding_vertex in sink_vertex + 1..parameters.target_vertices { + edges.push((sink_vertex, padding_vertex)); + weights.push(parameters.heavy_weight); + } + + let target = Target::try_new( + SimpleGraph::try_new(parameters.target_vertices, edges) + .map_err(>::target_construction)?, + weights, + source_vertex, + sink_vertex, + n + 1, + ) + .map_err(>::target_construction)?; + Ok( + ReductionDecisionMinimumVertexCoverToMinimumCutIntoBoundedSets { + target, + source_num_vertices: n, + threshold: parameters.threshold, + }, + ) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::export::SolutionPair; + + vec![crate::example_db::specs::RuleExampleSpec { + id: "decisionminimumvertexcover_to_minimumcutintoboundedsets", + build: || { + crate::example_db::specs::rule_example_with_witness::<_, Target>( + Decision::new( + MinimumVertexCover::new( + SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)], + ), + vec![One; 6], + ), + 3, + ), + SolutionPair { + source_config: serde_json::json!(vec![false, true, true, false, true, false]), + // Independent set {0, 3, 5}, the sink t = 7 and the three padding vertices + // on the sink's side; the source s = 6 and the cover {1, 2, 4} on the other. + target_config: serde_json::json!(vec![ + true, false, false, true, false, true, false, true, true, true, true + ]), + }, + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 7606f4923..fda749bd4 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -29,6 +29,7 @@ pub(crate) mod decisionmaximumindependentset_integralflowbundles; pub(crate) mod decisionminimumdominatingset_minimumsummulticenter; pub(crate) mod decisionminimumdominatingset_minmaxmulticenter; pub(crate) mod decisionminimumvertexcover_hamiltoniancircuit; +pub(crate) mod decisionminimumvertexcover_minimumcutintoboundedsets; pub(crate) mod ensemblecomputation_ilp; pub(crate) mod exactcoverby3sets_algebraicequationsovergf2; pub(crate) mod exactcoverby3sets_boundeddiameterspanningtree; @@ -351,6 +352,9 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>() + }; + // Empty, edgeless, sparse and complete graphs, with bounds below, inside and above `0..=n`. + let graphs = [ + (0, vec![]), + (4, vec![]), + (5, vec![(0, 1), (1, 2), (3, 4)]), + ( + 6, + vec![(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)], + ), + (5, complete(5)), + ]; + for (n, edges) in graphs { + for bound in [-3, -1, 0, 1, n as i64 / 2, n as i64, n as i64 + 3] { + let source = Decision::new( + MinimumVertexCover::new(SimpleGraph::new(n, edges.clone()), vec![One; n]), + bound, + ); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let target = + ReduceTo::>::reduce_to(&source) + .unwrap(); + let actual = crate::rules::ReductionResult::target_problem(&target).parameters(); + for field in ["num_vertices", "num_edges"] { + assert!( + predicted.get(field).unwrap() >= actual.get(field).unwrap(), + "{field}: n={n}, edges={edges:?}, bound={bound}" + ); + } + } + } +} diff --git a/src/unit_tests/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs b/src/unit_tests/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs new file mode 100644 index 000000000..6e3af1946 --- /dev/null +++ b/src/unit_tests/rules/decisionminimumvertexcover_minimumcutintoboundedsets.rs @@ -0,0 +1,465 @@ +use super::{Parameters, construction_parameters}; +use crate::models::decision::Decision; +use crate::models::graph::{MinimumCutIntoBoundedSets, MinimumVertexCover}; +use crate::rules::{ReduceTo, ReductionError, ReductionResult}; +use crate::solvers::BruteForce; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; +use crate::types::{Min, One, Or}; + +type Source = Decision>; +type Target = MinimumCutIntoBoundedSets; +type Reduction = super::ReductionDecisionMinimumVertexCoverToMinimumCutIntoBoundedSets; + +const EXAMPLE_EDGES: [(usize, usize); 7] = [(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4), (4, 5)]; + +fn source(n: usize, edges: &[(usize, usize)], bound: i64) -> Source { + Decision::new( + MinimumVertexCover::new(SimpleGraph::new(n, edges.to_vec()), vec![One; n]), + bound, + ) +} + +fn reduce(n: usize, edges: &[(usize, usize)], bound: i64) -> Reduction { + ReduceTo::::reduce_to(&source(n, edges, bound)).expect("reduction should succeed") +} + +/// The target of the issue's example graph on six vertices. +fn example(bound: i64) -> (Source, Reduction) { + let source = source(6, &EXAMPLE_EDGES, bound); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + (source, reduction) +} + +fn value_map(reduction: &Reduction, value: Min) -> Or { + crate::rules::AggregateReductionResult::extract_value(reduction, value) +} + +fn optimum(target: &Target) -> Option { + BruteForce::new() + .find_all_witnesses(target) + .unwrap() + .first() + .map(|solution| target.evaluate(solution).unwrap().0.unwrap()) +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_structure() { + let (_, reduction) = example(3); + let target = reduction.target_problem(); + // 6 original vertices, s = 6, t = 7 and q = n - T = 3 padding vertices. + assert_eq!(target.num_vertices(), 11); + assert_eq!((target.source(), target.sink()), (6, 7)); + assert_eq!(target.size_bound(), 7); + // The 8 complement edges, then {i, s}, {i, t} and the padding edges {t, pad}. + let complement = [ + (0, 3), + (2, 3), + (0, 4), + (1, 4), + (0, 5), + (1, 5), + (2, 5), + (3, 5), + ]; + let mut edges = complement.to_vec(); + edges.extend((0..6).map(|i| (i, 6))); + edges.extend((0..6).map(|i| (i, 7))); + edges.extend((8..11).map(|pad| (7, pad))); + assert_eq!(target.graph().edges(), edges); + assert_eq!(target.num_edges(), 23); + // Phi = 2 * 8 + 2 * 6 = 28, M = 29; deg_H = (3, 2, 2, 3, 2, 4). + let mut weights = vec![1; 8]; + weights.extend([1; 6]); + weights.extend([5, 4, 4, 5, 4, 6]); + weights.extend([29; 3]); + assert_eq!(target.edge_weights(), weights.as_slice()); + assert!(target.edge_weights().iter().all(|&weight| weight > 0)); + // Threshold Phi - T^2 = 28 - 9. + assert_eq!(reduction.threshold, 19); +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_closed_loop() { + let (source, reduction) = example(3); + let target = reduction.target_problem(); + crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target( + &source, + &reduction, + "DecisionMinimumVertexCover -> MinimumCutIntoBoundedSets", + ); + let witnesses = BruteForce::new().find_all_witnesses(target).unwrap(); + assert!(!witnesses.is_empty()); + for witness in witnesses { + let value = target.evaluate(&witness).unwrap(); + assert_eq!(value, Min(Some(19))); + assert_eq!(value_map(&reduction, value), Or(true)); + let cover = reduction.extract_solution(&witness).unwrap(); + assert_eq!(cover, witness[..6].iter().map(|&b| !b).collect::>()); + } + // The cover {1, 2, 4} comes from the independent set {0, 3, 5} on the sink's side. + let partition = vec![ + true, false, false, true, false, true, false, true, true, true, true, + ]; + assert_eq!(target.evaluate(&partition).unwrap(), Min(Some(19))); + assert_eq!( + reduction.extract_solution(&partition).unwrap(), + vec![false, true, true, false, true, false] + ); +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_no_instance() { + // The minimum cover has size 3, so the bound 2 is a NO instance: T = 4 and the threshold is + // 28 - 16 = 12, below the optimal cut. + let (source, reduction) = example(2); + let target = reduction.target_problem(); + assert_eq!(reduction.threshold, 12); + assert_eq!(BruteForce::new().solve(&source).unwrap(), None); + let best = optimum(target).unwrap(); + assert!(best > 12, "optimal cut {best} must exceed the threshold"); + for witness in BruteForce::new().find_all_witnesses(target).unwrap() { + let value = target.evaluate(&witness).unwrap(); + assert_eq!(value_map(&reduction, value), Or(false)); + assert!(reduction.extract_solution(&witness).is_err()); + } +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_infeasible_bound() { + // A negative bound has no cover; T = n + 1, so no cut reaches the threshold Phi - (n + 1)^2. + for bound in [-1, i64::MIN] { + let (source, reduction) = example(bound); + let target = reduction.target_problem(); + assert_eq!(BruteForce::new().solve(&source).unwrap(), None); + // No padding: the target is the bare construction with q = 0. + assert_eq!(target.num_vertices(), 8); + assert_eq!(reduction.threshold, 28 - 49); + let best = optimum(target).unwrap(); + assert!(best > reduction.threshold); + assert_eq!(value_map(&reduction, Min(Some(best))), Or(false)); + } +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_empty_graph() { + // n = 0: only s = 0 and t = 1, one side of size at most 1, no edges. + let reduction = reduce(0, &[], 0); + let target = reduction.target_problem(); + assert_eq!(target.num_vertices(), 2); + assert_eq!(target.num_edges(), 0); + assert_eq!(target.size_bound(), 1); + assert_eq!(reduction.threshold, 0); + let partition = vec![false, true]; + assert_eq!(target.evaluate(&partition).unwrap(), Min(Some(0))); + assert_eq!( + reduction.extract_solution(&partition).unwrap(), + Vec::::new() + ); + // A negative bound has no cover even for the empty graph: T = 1, threshold -1. + let reduction = reduce(0, &[], -1); + assert_eq!(reduction.threshold, -1); + assert_eq!(reduction.target_problem().num_vertices(), 2); + assert!(reduction.extract_solution(&vec![false, true]).is_err()); +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_bound_equals_vertex_count() { + // k = n gives T = 0, so n padding vertices; k = 0 on one vertex gives T = n = 1, q = 0. + let reduction = reduce(1, &[], 0); + let target = reduction.target_problem(); + assert_eq!(target.num_vertices(), 3); + assert_eq!(target.graph().edges(), vec![(0, 1), (0, 2)]); + assert_eq!(target.edge_weights(), &[1, 2]); + assert_eq!(reduction.threshold, 1); // Phi = 2, T = 1 + // The lone vertex on the sink's side cuts only {0, s} and is the empty cover. + assert_eq!( + reduction + .extract_solution(&vec![true, false, true]) + .unwrap(), + vec![false] + ); + // Putting it on the source's side cuts {0, t} of weight 2 > 1: not certifying. + assert!( + reduction + .extract_solution(&vec![false, false, true]) + .is_err() + ); + + // For k >= n every bound gives the same instance, with T = 0 and n padding vertices. + let edges = [(0, 1), (1, 2)]; + let at_n = reduce(3, &edges, 3); + for bound in [4, 100, i64::MAX] { + let other = reduce(3, &edges, bound); + assert_eq!(other.target.graph().edges(), at_n.target.graph().edges()); + assert_eq!(other.target.edge_weights(), at_n.target.edge_weights()); + assert_eq!(other.threshold, at_n.threshold); + } + assert_eq!(at_n.target.num_vertices(), 3 + 2 + 3); + assert_eq!(at_n.threshold, 8); // |E(H)| = 1, Phi = 2 + 6, T = 0 +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_intermediate_bound() { + // Path 0-1-2, k = 1: T = 2, one padding vertex, the cover {1}. + let (source, reduction) = ( + source(3, &[(0, 1), (1, 2)], 1), + reduce(3, &[(0, 1), (1, 2)], 1), + ); + let target = reduction.target_problem(); + assert_eq!(target.num_vertices(), 6); + // H has the single edge {0, 2}; Phi = 2 + 6 = 8, M = 9, threshold 8 - 4 = 4. + assert_eq!(target.graph().edges()[0], (0, 2)); + assert_eq!(reduction.threshold, 4); + // I = {0, 2} and the padding vertex on t's side: cut = Phi - 2 * 1 - 2 = 4. + let partition = vec![true, false, true, false, true, true]; + assert_eq!(target.evaluate(&partition).unwrap(), Min(Some(4))); + let cover = reduction.extract_solution(&partition).unwrap(); + assert_eq!(cover, vec![false, true, false]); + assert_eq!(source.evaluate(&cover).unwrap(), Or(true)); + // The padding vertex on the source's side costs M and is rejected. + let padding_on_source = vec![true, false, true, false, true, false]; + assert_eq!( + target.evaluate(&padding_on_source).unwrap(), + Min(Some(4 + 9)) + ); + assert!(reduction.extract_solution(&padding_on_source).is_err()); +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_edge_order_is_irrelevant() { + let forward = reduce(4, &[(0, 1), (2, 3)], 2); + let shuffled = reduce(4, &[(3, 2), (1, 0)], 2); + assert_eq!( + forward.target.graph().edges(), + shuffled.target.graph().edges() + ); + assert_eq!( + forward.target.edge_weights(), + shuffled.target.edge_weights() + ); + assert_eq!(forward.threshold, shuffled.threshold); +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_extraction_rejects() { + let (_, reduction) = example(3); + let target = reduction.target_problem(); + let good = vec![ + true, false, false, true, false, true, false, true, true, true, true, + ]; + assert!(reduction.extract_solution(&good).is_ok()); + // Wrong lengths. + assert!(reduction.extract_solution(&good[..10].to_vec()).is_err()); + let mut longer = good.clone(); + longer.push(true); + assert!(reduction.extract_solution(&longer).is_err()); + assert!(reduction.extract_solution(&Vec::new()).is_err()); + // The source on the sink's side, or the sink on the source's side: infeasible. + let mut bad = good.clone(); + bad[6] = true; + assert_eq!(target.evaluate(&bad).unwrap(), Min(None)); + assert!(reduction.extract_solution(&bad).is_err()); + let mut bad = good.clone(); + bad[7] = false; + assert!(reduction.extract_solution(&bad).is_err()); + // A side larger than the size bound 7 is infeasible: all three paddings and everything else + // on the sink's side. + let mut oversized = vec![true; 11]; + oversized[6] = false; + assert_eq!(target.evaluate(&oversized).unwrap(), Min(None)); + assert!(reduction.extract_solution(&oversized).is_err()); + // Feasible but above the threshold: the padding on the source's side. + let mut above = good.clone(); + above[8] = false; + assert!(target.evaluate(&above).unwrap().0.unwrap() > 19); + assert!(reduction.extract_solution(&above).is_err()); + // Feasible, within the threshold, but not an independent set: nothing within the bound + // exists, so every other feasible partition is above the threshold (checked exhaustively). + for mask in 0usize..1 << 11 { + let partition: Vec = (0..11).map(|i| mask & (1 << i) != 0).collect(); + if let Ok(cover) = reduction.extract_solution(&partition) { + assert_eq!( + Source::evaluate(&source(6, &EXAMPLE_EDGES, 3), &cover).unwrap(), + Or(true) + ); + } + } +} + +// Every graph on at most four vertices, every bound around the interesting range, and EVERY +// target assignment (feasible or not): both directions of the equivalence, and extraction never +// returns a false witness. +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_all_small_graphs() { + for n in 0..=4usize { + let pairs: Vec<_> = (0..n) + .flat_map(|u| (u + 1..n).map(move |v| (u, v))) + .collect(); + for graph_mask in 0usize..1 << pairs.len() { + let edges: Vec<_> = pairs + .iter() + .enumerate() + .filter(|(i, _)| graph_mask & (1 << i) != 0) + .map(|(_, &edge)| edge) + .collect(); + let bounds = [i64::MIN, -1, i64::MAX] + .into_iter() + .chain(0..=i64::try_from(n).unwrap() + 1); + for bound in bounds { + let source = source(n, &edges, bound); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let vertices = target.num_vertices(); + assert!(vertices <= 2 * n + 2); + assert!(target.num_edges() <= n * n.saturating_sub(1) / 2 - edges.len() + 3 * n); + let source_yes = BruteForce::new().solve(&source).unwrap().is_some(); + let mut best: Option = None; + for mask in 0usize..1 << vertices { + let partition: Vec = + (0..vertices).map(|i| mask & (1 << i) != 0).collect(); + let value = target.evaluate(&partition).unwrap(); + if let Some(cut) = value.0 { + best = Some(best.map_or(cut, |previous| previous.min(cut))); + } + let accepted = value_map(&reduction, value).0; + match reduction.extract_solution(&partition) { + Ok(cover) => { + assert!(accepted); + assert_eq!(source.evaluate(&cover).unwrap(), Or(true)); + } + Err(_) => assert!(!accepted), + } + } + assert_eq!( + value_map(&reduction, Min(best)), + Or(source_yes), + "n={n}, edges={edges:?}, bound={bound}" + ); + } + } + } +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_larger_graphs() { + // The five-cycle (minimum cover 3) and the star K_{1,4} (minimum cover 1). + let cycle = [(0, 1), (1, 2), (2, 3), (3, 4), (0, 4)]; + let star = [(0, 1), (0, 2), (0, 3), (0, 4)]; + for (edges, yes_from) in [(&cycle[..], 3), (&star[..], 1)] { + for bound in 0..=5 { + let reduction = reduce(5, edges, bound); + let best = optimum(reduction.target_problem()).unwrap(); + let yes = best <= reduction.threshold; + assert_eq!(yes, bound >= yes_from, "edges={edges:?}, bound={bound}"); + for witness in BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap() + { + assert_eq!(reduction.extract_solution(&witness).is_ok(), yes); + } + } + } +} + +fn parameters(n: usize, m: usize, bound: i64) -> Result { + construction_parameters(n, m, bound) +} + +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_parameters() { + assert_eq!( + parameters(6, 7, 3).unwrap(), + Parameters { + independent_size: 3, + padding: 3, + target_vertices: 11, + complement_edges: 8, + target_edges: 23, + heavy_weight: 29, + threshold: 19, + } + ); + // Clamping of T = n - bound to 0..=n + 1 at every boundary of the bound. + for (bound, t, q) in [ + (i64::MIN, 7, 0), + (-1, 7, 0), + (0, 6, 0), + (5, 1, 5), + (6, 0, 6), + (7, 0, 6), + (i64::MAX, 0, 6), + ] { + let p = parameters(6, 0, bound).unwrap(); + assert_eq!((p.independent_size, p.padding), (t, q), "bound={bound}"); + } + // The number of edges of the source cannot exceed the number of pairs. + assert!(parameters(4, 6, 1).is_ok()); + assert!(matches!( + parameters(4, 7, 1), + Err(ReductionError::IntegerOverflow { .. }) + )); + // Counts that do not fit in `usize`. + assert!(matches!( + parameters(usize::MAX, 0, 0), + Err(ReductionError::IntegerOverflow { .. }) + )); + assert!(matches!( + parameters(usize::MAX - 1, 0, -1), + Err(ReductionError::IntegerOverflow { .. }) + )); +} + +#[cfg(target_pointer_width = "64")] +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_parameters_64_bit_overflow() { + for (n, bound) in [(1usize << 63, i64::MAX), (1 << 32, 0), ((1 << 32) + 1, 0)] { + assert!( + matches!( + parameters(n, 0, bound), + Err(ReductionError::IntegerOverflow { .. }) + ), + "n={n}, bound={bound}" + ); + } +} + +/// The total weight of the construction, computed in `u128` without any overflow. +#[cfg(target_pointer_width = "64")] +fn total_weight(n: u128, m: u128, bound: i64) -> u128 { + let n_signed = i128::try_from(n).unwrap(); + let t = u128::try_from((n_signed - i128::from(bound)).clamp(0, n_signed + 1)).unwrap(); + let q = n.saturating_sub(t); + let complement = n * n.saturating_sub(1) / 2 - m; + let phi = 2 * complement + 2 * n; + complement + n + (phi) + q * (phi + 1) +} + +#[cfg(target_pointer_width = "64")] +#[test] +fn test_decisionminimumvertexcover_to_minimumcutintoboundedsets_weight_boundary() { + // For each regime of the bound, find by bisection the largest n whose total weight (an upper + // bound on every cut) fits in `i64`, and require the parameters to succeed there and fail + // one vertex later. + for bound in [i64::MAX, 0, -1] { + let (mut low, mut high) = (1u128, 1u128 << 40); + while low + 1 < high { + let mid = (low + high) / 2; + if total_weight(mid, 0, bound) <= u128::try_from(i64::MAX).unwrap() { + low = mid; + } else { + high = mid; + } + } + let n = usize::try_from(low).unwrap(); + assert!(parameters(n, 0, bound).is_ok()); + assert!(i64::try_from(total_weight(low, 0, bound)).is_ok()); + assert!( + matches!( + parameters(n + 1, 0, bound), + Err(ReductionError::IntegerOverflow { .. }) + ), + "bound={bound}, n={n}" + ); + } +}