From b8a5765d21d19768c36bc49b0e5fc6726b37222e Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 18 Sep 2026 00:52:29 +0800 Subject: [PATCH] Preserve negative vertex weights in AND/OR graph reduction --- docs/paper/reductions.typ | 6 ++--- ...imumvertexcover_minimumweightandorgraph.rs | 23 +++++++++++++------ ...imumvertexcover_minimumweightandorgraph.rs | 18 +++++++++++++++ 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ee531965e..a841725a8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11900,7 +11900,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. )[ This reduction encodes vertex cover as a minimum-weight solution subgraph problem on a three-layer AND/OR DAG. The root AND gate requires all edges to be covered; each edge becomes an OR gate selecting which endpoint covers it; and each vertex becomes a sink whose arc weight equals the vertex weight. The minimum-weight solution subgraph selects exactly the arcs corresponding to a minimum vertex cover. ][ - _Construction._ Given a Minimum Vertex Cover instance $(G = (V, E), bold(w))$ with $n = |V|$ vertices and $m = |E|$ edges, build an AND/OR graph $D$ with $1 + m + 2n$ vertices arranged in three layers: + _Construction._ Given a Minimum Vertex Cover instance $(G = (V, E), bold(w))$, let $N = {v in V : w_v < 0}$. Every optimum contains $N$: adding an omitted negative-weight vertex preserves coverage and strictly decreases cost. Remove all edges incident to $N$ and set the residual weights of vertices in $N$ to zero. Keep the original vertex indices. Below, $E$ denotes these residual edges, $m = |E|$, and $w$ denotes the nonnegative residual weights; the original optimum equals the residual optimum plus $sum_(v in N) w_v$ using the original weights. Build an AND/OR graph $D$ with $1 + m + 2n$ vertices, where $n = |V|$: - *Root (AND gate):* A single vertex $r$ (index 0) with gate type AND. - *Edge layer (OR gates):* For each edge $e_i = {u, v}$ ($i = 0, dots, m-1$), create vertex $e_i$ (index $1 + i$) with gate type OR and an arc $(r, e_i)$ of weight 1. @@ -11909,9 +11909,9 @@ the displayed rule, extracted from the corresponding `pred path` entry. Since $r$ is AND, any solution subgraph must include all arcs from $r$ to the edge-layer vertices (cost $m$). Each edge-OR vertex $e_i$ requires at least one of its two outgoing arcs to $c_u$ and $c_v$ (selecting which endpoint covers edge $i$). Each activated cover vertex $c_j$ requires its outgoing arc to $s_j$ (contributing $w_j$). The total weight is $m + |{"activated cover arcs"}| + sum_(j in C) w_j$. - _Correctness._ ($arrow.r.double$) If $C subset.eq V$ is a vertex cover with weight $W$, then for each edge $e_i = {u, v}$, at least one endpoint lies in $C$; select the arc from $e_i$ to that endpoint's cover vertex. Activate all cover-to-sink arcs for vertices in $C$. This satisfies the AND gate at the root (all edge arcs selected), every edge OR gate (at least one child selected), and all activated cover vertices (sink arc selected). The total weight is $m + |{"edge-to-cover arcs"}| + W$. ($arrow.l.double$) In any valid solution subgraph, the AND root forces all $m$ edge arcs. Each edge OR vertex selects at least one arc to a cover vertex, activating that cover vertex and its sink arc. The set of activated cover vertices forms a vertex cover (every edge has at least one endpoint activated). The sink arc weights sum to the cover weight, so any minimum-weight solution subgraph corresponds to a minimum vertex cover. + _Correctness._ ($arrow.r.double$) Given a residual cover $C$, choose exactly one endpoint in $C$ for each edge. Activate only cover vertices reached by these choices, and their sink arcs. This is a valid solution of cost at most $2m + w(C)$, since omitted cover vertices have nonnegative weight. ($arrow.l.double$) Any valid target solution activates a residual cover $C$ and costs at least $2m + w(C)$: the root requires all $m$ arcs and each edge gate requires at least one unit-weight outgoing arc. These bounds imply that the target optimum is exactly $2m$ plus the residual cover optimum, and every target optimum recovers an optimal residual cover. Adding $N$ restores an original optimum, including negative vertices that are isolated or redundant for coverage. - _Solution extraction._ Examine the cover-to-sink arcs (indices $3m, dots, 3m + n - 1$ in the arc list): $c_j = 1$ if arc $(c_j, s_j)$ is selected, $c_j = 0$ otherwise. + _Solution extraction._ Select every vertex in $N$, together with vertices whose cover-to-sink arcs (indices $3m, dots, 3m + n - 1$) are selected. Evaluate this cover using the original weights. ] #reduction-rule("MaximumMatching", "MaximumSetPacking")[ diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index 247ce8733..e78eb3ad2 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -12,7 +12,7 @@ use crate::topology::SimpleGraph; pub struct ReductionVCToAndOrGraph { target: MinimumWeightAndOrGraph, sink_arc_start: usize, - num_source_vertices: usize, + forced_cover: Vec, } impl ReductionResult for ReductionVCToAndOrGraph { @@ -30,15 +30,17 @@ impl ReductionResult for ReductionVCToAndOrGraph { crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; Ok({ - (0..self.num_source_vertices) - .map(|j| target_solution[self.sink_arc_start + j]) + self.forced_cover + .iter() + .enumerate() + .map(|(j, &forced)| forced || target_solution[self.sink_arc_start + j]) .collect() }) } } #[reduction( - transform = exact { + transform = upper_bound { num_vertices = "1 + num_edges + 2 * num_vertices", num_arcs = "3 * num_edges + num_vertices", } @@ -48,7 +50,14 @@ impl ReduceTo for MinimumVertexCover fn reduce_to(&self) -> Result { let n = self.graph().num_vertices(); - let edges = self.graph().edges(); + // Every optimum includes negative-weight vertices, even isolated ones. + let forced_cover: Vec<_> = self.weights().iter().map(|&w| w < 0).collect(); + let edges: Vec<_> = self + .graph() + .edges() + .into_iter() + .filter(|&(u, v)| !forced_cover[u] && !forced_cover[v]) + .collect(); let m = edges.len(); let num_target_vertices = 1 + m + (2 * n); @@ -80,7 +89,7 @@ impl ReduceTo for MinimumVertexCover let sink_arc_start = arcs.len(); for (j, &weight) in self.weights().iter().enumerate() { arcs.push((cover_vertex(j), sink_vertex(j))); - arc_weights.push(weight); + arc_weights.push(weight.max(0)); } let target = @@ -89,7 +98,7 @@ impl ReduceTo for MinimumVertexCover Ok(ReductionVCToAndOrGraph { target, sink_arc_start, - num_source_vertices: n, + forced_cover, }) } } diff --git a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs index 6b93320a5..29035992f 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -29,6 +29,24 @@ fn test_minimumvertexcover_to_minimumweightandorgraph_closed_loop() { ); } +#[test] +fn negative_vertices_are_selected_even_when_isolated_or_redundant() { + for (n, edges, weights) in [ + (1, vec![], vec![-1]), + (2, vec![(0, 1)], vec![-1, -1]), + (3, vec![(0, 1), (1, 2)], vec![-2, 3, 1]), + (2, vec![(0, 0), (0, 1)], vec![-1, 2]), + ] { + let source = MinimumVertexCover::new(SimpleGraph::new(n, edges), weights); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "signed vertex cover", + ); + } +} + #[test] fn test_reduction_structure() { let source = issue_example_source();