From f8ee8ccdcced909028ed2085a7bae1b2fe3eb29b Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 12:12:44 +0000 Subject: [PATCH 1/3] Add plan for #228: MinimumCutIntoBoundedSets model --- ...026-03-13-minimum-cut-into-bounded-sets.md | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md diff --git a/docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md b/docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md new file mode 100644 index 00000000..93fa3e62 --- /dev/null +++ b/docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md @@ -0,0 +1,74 @@ +# Plan: Add MinimumCutIntoBoundedSets Model (#228) + +## Overview +Add the MinimumCutIntoBoundedSets satisfaction problem from Garey & Johnson ND17. This is a graph partitioning problem that asks whether vertices can be partitioned into two bounded-size sets (containing designated source and sink vertices) with total cut weight at most K. + +## Information Checklist +1. **Problem name:** `MinimumCutIntoBoundedSets` +2. **Definition:** Given graph G=(V,E) with edge weights, source s, sink t, size bound B, cut bound K: partition V into V1 (containing s) and V2 (containing t) with |V1|<=B, |V2|<=B, and cut weight <= K. +3. **Problem type:** Satisfaction (Metric = bool) +4. **Type parameters:** `G: Graph`, `W: WeightElement` +5. **Struct fields:** `graph: G`, `edge_weights: Vec`, `source: usize`, `sink: usize`, `size_bound: usize`, `cut_bound: W::Sum` +6. **Configuration space:** `vec![2; num_vertices]` (binary: 0=V1, 1=V2) +7. **Feasibility:** x_s=0, x_t=1, |V1|<=B, |V2|<=B, cut weight <= K +8. **Objective:** bool — all constraints satisfied +9. **Complexity:** `2^num_vertices` (brute force) +10. **Solving:** BruteForce (enumerate partitions) +11. **Category:** `graph` + +## Steps + +### Step 1: Create model file +**File:** `src/models/graph/minimum_cut_into_bounded_sets.rs` + +- `inventory::submit!` for ProblemSchemaEntry with fields: graph, edge_weights, source, sink, size_bound, cut_bound +- Struct `MinimumCutIntoBoundedSets` with fields: graph, edge_weights, source, sink, size_bound, cut_bound (where cut_bound is `W::Sum`) +- Constructor `new(graph, edge_weights, source, sink, size_bound, cut_bound)` with assertions +- Accessor methods: `graph()`, `source()`, `sink()`, `size_bound()`, `cut_bound()`, `edge_weights()` +- Size getters: `num_vertices()`, `num_edges()` +- `Problem` impl: NAME="MinimumCutIntoBoundedSets", Metric=bool, dims=vec![2; n], evaluate checks all constraints +- `SatisfactionProblem` impl (marker trait) +- `declare_variants!` with `MinimumCutIntoBoundedSets => "2^num_vertices"` +- Test link: `#[cfg(test)] #[path = "../../unit_tests/models/graph/minimum_cut_into_bounded_sets.rs"] mod tests;` + +The `evaluate()` function must: +1. Check config length == num_vertices +2. Check config[source] == 0 (s in V1) +3. Check config[sink] == 1 (t in V2) +4. Count |V1| = vertices with config=0, |V2| = vertices with config=1 +5. Check |V1| <= size_bound and |V2| <= size_bound +6. Compute cut weight (sum of edge weights where endpoints differ) +7. Return cut_weight <= cut_bound + +### Step 2: Register the model +- `src/models/graph/mod.rs`: add `pub(crate) mod minimum_cut_into_bounded_sets;` and `pub use minimum_cut_into_bounded_sets::MinimumCutIntoBoundedSets;` +- `src/models/mod.rs`: add `MinimumCutIntoBoundedSets` to the graph re-export line + +### Step 3: Register in CLI +- `problemreductions-cli/src/dispatch.rs`: add `deser_sat::>` in load_problem, `try_ser` in serialize_any_problem +- `problemreductions-cli/src/problem_name.rs`: add `"minimumcutintoboundedsets" => "MinimumCutIntoBoundedSets"` + +### Step 4: Add CLI creation support +- `problemreductions-cli/src/commands/create.rs`: add creation handler that parses --graph, --edge-weights, --source, --sink, --size-bound, --cut-bound flags +- `problemreductions-cli/src/cli.rs`: add any needed flags (--source, --sink, --size-bound, --cut-bound) and update help table + +### Step 5: Write unit tests +**File:** `src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs` + +Tests: +- `test_minimumcutintoboundedsets_basic`: construct instance from issue example (8 vertices, 12 edges), verify dims +- `test_minimumcutintoboundedsets_evaluation`: test YES instance (V1={0,1,2,3}, K=6) returns true, NO instance (K=5 with same partition) returns false, invalid source/sink placement returns false +- `test_minimumcutintoboundedsets_serialization`: round-trip serde test +- `test_minimumcutintoboundedsets_solver`: BruteForce::find_satisfying finds a solution for K=6, returns None for K=5 + +Register test file in `src/unit_tests/models/graph/mod.rs`. + +### Step 6: Document in paper +Add problem-def entry in `docs/paper/reductions.typ`: +- Add to `display-name` dict: `"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets]` +- Add `#problem-def("MinimumCutIntoBoundedSets")[...][...]` with formal definition and background + +### Step 7: Verify +```bash +make check # fmt + clippy + test +``` From 3fcd5ce5b02730608d67a8bb0cf2dedc8eb18428 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 12:24:13 +0000 Subject: [PATCH 2/3] Implement #228: Add MinimumCutIntoBoundedSets model Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 11 + problemreductions-cli/src/cli.rs | 13 ++ problemreductions-cli/src/commands/create.rs | 73 +++++- problemreductions-cli/src/dispatch.rs | 4 + problemreductions-cli/src/problem_name.rs | 1 + src/lib.rs | 3 +- .../graph/minimum_cut_into_bounded_sets.rs | 218 ++++++++++++++++++ src/models/graph/mod.rs | 3 + src/models/mod.rs | 4 +- .../graph/minimum_cut_into_bounded_sets.rs | 205 ++++++++++++++++ 10 files changed, 531 insertions(+), 4 deletions(-) create mode 100644 src/models/graph/minimum_cut_into_bounded_sets.rs create mode 100644 src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 7ab3b569..6f55d26e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -56,6 +56,7 @@ "LongestCommonSubsequence": [Longest Common Subsequence], "SubsetSum": [Subset Sum], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], ) // Definition label: "def:" — each definition block must have a matching label @@ -434,6 +435,16 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co caption: [Graph with $n = 6$ vertices partitioned into $A = {v_0, v_1, v_2}$ (blue) and $B = {v_3, v_4, v_5}$ (red). The 3 crossing edges $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$ are shown in bold red; internal edges are gray.], ) ] +#problem-def("MinimumCutIntoBoundedSets")[ + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ^+$, designated vertices $s, t in V$, a positive integer $B <= |V|$, and a positive integer $K$, determine whether there exists a partition of $V$ into disjoint sets $V_1$ and $V_2$ such that $s in V_1$, $t in V_2$, $|V_1| <= B$, $|V_2| <= B$, and + $ sum_({u,v} in E: u in V_1, v in V_2) w({u,v}) <= K. $ +][ +Minimum Cut Into Bounded Sets (Garey & Johnson ND17) combines the classical minimum $s$-$t$ cut problem with a balance constraint on partition sizes. Without the balance constraint ($B = |V|$), the problem reduces to standard minimum $s$-$t$ cut, solvable in polynomial time via network flow. Adding the requirement $|V_1| <= B$ and $|V_2| <= B$ makes the problem NP-complete; it remains NP-complete even for $B = |V| slash 2$ and unit edge weights (the minimum bisection problem) @garey1976. Applications include VLSI layout, load balancing, and graph bisection. + +The best known exact algorithm is brute-force enumeration of all $2^n$ vertex partitions in $O(2^n)$ time. For the special case of minimum bisection, Cygan et al. @cygan2014 showed fixed-parameter tractability with respect to the cut size. No polynomial-time finite approximation factor exists for balanced graph partition unless $P = N P$ (Andreev and Racke, 2006). Arora, Rao, and Vazirani @arora2009 gave an $O(sqrt(log n))$-approximation for balanced separator. + +*Example.* Consider $G$ with 4 vertices and edges $(v_0, v_1)$, $(v_1, v_2)$, $(v_2, v_3)$ with unit weights, $s = v_0$, $t = v_3$, $B = 3$, $K = 1$. The partition $V_1 = {v_0, v_1}$, $V_2 = {v_2, v_3}$ gives cut weight $w({v_1, v_2}) = 1 <= K$. Both $|V_1| = 2 <= 3$ and $|V_2| = 2 <= 3$. Answer: YES. +] #problem-def("KColoring")[ Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$. ][ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 6b4cbb5b..90dc1083 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -209,6 +209,7 @@ Flags by problem type: SpinGlass --graph, --couplings, --fields KColoring --graph, --k GraphPartitioning --graph + MinimumCutIntoBoundedSets --graph, --edge-weights, --source, --sink, --size-bound, --cut-bound Factoring --target, --m, --n BinPacking --sizes, --capacity SubsetSum --sizes, --target @@ -337,6 +338,18 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: Option, + /// Source vertex index (for MinimumCutIntoBoundedSets) + #[arg(long)] + pub source: Option, + /// Sink vertex index (for MinimumCutIntoBoundedSets) + #[arg(long)] + pub sink: Option, + /// Size bound for partition sets (for MinimumCutIntoBoundedSets) + #[arg(long)] + pub size_bound: Option, + /// Cut weight bound (for MinimumCutIntoBoundedSets) + #[arg(long)] + pub cut_bound: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 2b4cb04b..4ec79bea 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -5,7 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; -use problemreductions::models::graph::GraphPartitioning; +use problemreductions::models::graph::{GraphPartitioning, MinimumCutIntoBoundedSets}; use problemreductions::models::misc::{BinPacking, LongestCommonSubsequence, PaintShop, SubsetSum}; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -49,6 +49,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.bounds.is_none() && args.strings.is_none() && args.arcs.is_none() + && args.source.is_none() + && args.sink.is_none() + && args.size_bound.is_none() + && args.cut_bound.is_none() } fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { @@ -81,6 +85,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { _ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1", }, "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", + "MinimumCutIntoBoundedSets" => { + "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3 --cut-bound 1" + } "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" } @@ -213,6 +220,39 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // Minimum cut into bounded sets (graph + edge weights + s/t/B/K) + "MinimumCutIntoBoundedSets" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create MinimumCutIntoBoundedSets --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 2 --size-bound 2 --cut-bound 1" + ) + })?; + let edge_weights = parse_edge_weights(args, graph.num_edges())?; + let source = args + .source + .context("--source is required for MinimumCutIntoBoundedSets")?; + let sink = args + .sink + .context("--sink is required for MinimumCutIntoBoundedSets")?; + let size_bound = args + .size_bound + .context("--size-bound is required for MinimumCutIntoBoundedSets")?; + let cut_bound = args + .cut_bound + .context("--cut-bound is required for MinimumCutIntoBoundedSets")?; + ( + ser(MinimumCutIntoBoundedSets::new( + graph, + edge_weights, + source, + sink, + size_bound, + cut_bound, + ))?, + resolved_variant.clone(), + ) + } + // Graph problems with edge weights "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -997,6 +1037,37 @@ fn create_random( } } + // MinimumCutIntoBoundedSets (graph + edge weights + s/t/B/K) + "MinimumCutIntoBoundedSets" => { + let edge_prob = args.edge_prob.unwrap_or(0.5); + if !(0.0..=1.0).contains(&edge_prob) { + bail!("--edge-prob must be between 0.0 and 1.0"); + } + let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); + let num_edges = graph.num_edges(); + let edge_weights = vec![1i32; num_edges]; + let source = 0; + let sink = if num_vertices > 1 { + num_vertices - 1 + } else { + 0 + }; + let size_bound = num_vertices; // no effective size constraint + let cut_bound = num_edges as i32; // generous bound + let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); + ( + ser(MinimumCutIntoBoundedSets::new( + graph, + edge_weights, + source, + sink, + size_bound, + cut_bound, + ))?, + variant, + ) + } + // GraphPartitioning (graph only, no weights; requires even vertex count) "GraphPartitioning" => { let num_vertices = if num_vertices % 2 != 0 { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index e162efc2..9ef6d26e 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -248,6 +248,9 @@ pub fn load_problem( "Knapsack" => deser_opt::(data), "LongestCommonSubsequence" => deser_opt::(data), "MinimumFeedbackVertexSet" => deser_opt::>(data), + "MinimumCutIntoBoundedSets" => { + deser_sat::>(data) + } "SubsetSum" => deser_sat::(data), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } @@ -312,6 +315,7 @@ pub fn serialize_any_problem( "Knapsack" => try_ser::(any), "LongestCommonSubsequence" => try_ser::(any), "MinimumFeedbackVertexSet" => try_ser::>(any), + "MinimumCutIntoBoundedSets" => try_ser::>(any), "SubsetSum" => try_ser::(any), _ => bail!("{}", crate::problem_name::unknown_problem_error(&canonical)), } diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index a595f61b..8becc7a4 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -57,6 +57,7 @@ pub fn resolve_alias(input: &str) -> String { "knapsack" => "Knapsack".to_string(), "lcs" | "longestcommonsubsequence" => "LongestCommonSubsequence".to_string(), "fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(), + "minimumcutintoboundedsets" => "MinimumCutIntoBoundedSets".to_string(), // G&J ND17 "subsetsum" => "SubsetSum".to_string(), _ => input.to_string(), // pass-through for exact names } diff --git a/src/lib.rs b/src/lib.rs index bdcbf5f3..56b1a56e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,8 @@ pub mod prelude { pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass}; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman, + MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackVertexSet, + MinimumVertexCover, TravelingSalesman, }; pub use crate::models::misc::{ BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum, diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs new file mode 100644 index 00000000..90f638a7 --- /dev/null +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -0,0 +1,218 @@ +//! MinimumCutIntoBoundedSets problem implementation. +//! +//! A graph partitioning problem that asks whether vertices can be partitioned +//! into two bounded-size sets (containing designated source and sink vertices) +//! with total cut weight at most K. From Garey & Johnson, A2 ND17. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::types::WeightElement; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumCutIntoBoundedSets", + module_path: module_path!(), + description: "Partition vertices into two bounded-size sets with cut weight at most K", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G = (V, E)" }, + FieldInfo { name: "edge_weights", type_name: "Vec", description: "Edge weights w: E -> Z+" }, + FieldInfo { name: "source", type_name: "usize", description: "Source vertex s (must be in V1)" }, + FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t (must be in V2)" }, + FieldInfo { name: "size_bound", type_name: "usize", description: "Maximum size B for each partition set" }, + FieldInfo { name: "cut_bound", type_name: "W::Sum", description: "Maximum total cut weight K" }, + ], + } +} + +/// Minimum Cut Into Bounded Sets (Garey & Johnson ND17). +/// +/// Given a weighted graph G = (V, E), source vertex s, sink vertex t, +/// size bound B, and cut bound K, determine whether there exists a partition +/// of V into disjoint sets V1 and V2 such that: +/// - s is in V1, t is in V2 +/// - |V1| <= B, |V2| <= B +/// - The total weight of edges crossing the partition is at most K +/// +/// # Type Parameters +/// +/// * `G` - The graph type (e.g., `SimpleGraph`) +/// * `W` - The weight type for edges (e.g., `i32`) +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::graph::MinimumCutIntoBoundedSets; +/// use problemreductions::topology::SimpleGraph; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // Simple 4-vertex path graph with unit weights, s=0, t=3 +/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); +/// let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1, 1], 0, 3, 3, 2); +/// +/// // Partition {0,1} vs {2,3}: cut edge (1,2) with weight 1 <= 2 +/// assert!(problem.evaluate(&[0, 0, 1, 1])); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumCutIntoBoundedSets { + /// The underlying graph structure. + graph: G, + /// Weights for each edge (in the same order as graph.edges()). + edge_weights: Vec, + /// Source vertex s that must be in V1. + source: usize, + /// Sink vertex t that must be in V2. + sink: usize, + /// Maximum size B for each partition set. + size_bound: usize, + /// Maximum total cut weight K. + cut_bound: W::Sum, +} + +impl MinimumCutIntoBoundedSets { + /// Create a new MinimumCutIntoBoundedSets problem. + /// + /// # Arguments + /// * `graph` - The undirected graph + /// * `edge_weights` - Weights for each edge (must match graph.num_edges()) + /// * `source` - Source vertex s (must be in V1) + /// * `sink` - Sink vertex t (must be in V2) + /// * `size_bound` - Maximum size B for each partition set + /// * `cut_bound` - Maximum total cut weight K + /// + /// # Panics + /// Panics if edge_weights length doesn't match num_edges, if source == sink, + /// or if source/sink are out of bounds. + pub fn new( + graph: G, + edge_weights: Vec, + source: usize, + sink: usize, + size_bound: usize, + cut_bound: W::Sum, + ) -> Self { + assert_eq!( + edge_weights.len(), + graph.num_edges(), + "edge_weights length must match num_edges" + ); + assert!(source < graph.num_vertices(), "source vertex out of bounds"); + assert!(sink < graph.num_vertices(), "sink vertex out of bounds"); + assert_ne!(source, sink, "source and sink must be different vertices"); + Self { + graph, + edge_weights, + source, + sink, + size_bound, + cut_bound, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the edge weights. + pub fn edge_weights(&self) -> &[W] { + &self.edge_weights + } + + /// Get the source vertex. + pub fn source(&self) -> usize { + self.source + } + + /// Get the sink vertex. + pub fn sink(&self) -> usize { + self.sink + } + + /// Get the size bound B. + pub fn size_bound(&self) -> usize { + self.size_bound + } + + /// Get the cut bound K. + pub fn cut_bound(&self) -> &W::Sum { + &self.cut_bound + } + + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } +} + +impl Problem for MinimumCutIntoBoundedSets +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "MinimumCutIntoBoundedSets"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_vertices()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + let n = self.graph.num_vertices(); + if config.len() != n { + return false; + } + + // Check source is in V1 (config=0) and sink is in V2 (config=1) + if config[self.source] != 0 { + return false; + } + if config[self.sink] != 1 { + return false; + } + + // Check size bounds + let count_v1 = config.iter().filter(|&&x| x == 0).count(); + let count_v2 = config.iter().filter(|&&x| x == 1).count(); + if count_v1 > self.size_bound || count_v2 > self.size_bound { + return false; + } + + // Compute cut weight + let mut cut_weight = W::Sum::zero(); + for ((u, v), weight) in self.graph.edges().iter().zip(self.edge_weights.iter()) { + if config[*u] != config[*v] { + cut_weight += weight.to_sum(); + } + } + + // Check cut weight <= K + cut_weight <= self.cut_bound + } +} + +impl SatisfactionProblem for MinimumCutIntoBoundedSets +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ +} + +crate::declare_variants! { + MinimumCutIntoBoundedSets => "2^num_vertices", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/minimum_cut_into_bounded_sets.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 42f46a15..d133c590 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -9,6 +9,7 @@ //! - [`MaximumClique`]: Maximum weight clique //! - [`MaxCut`]: Maximum cut on weighted graphs //! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) +//! - [`MinimumCutIntoBoundedSets`]: Minimum cut into bounded sets (Garey & Johnson ND17) //! - [`KColoring`]: K-vertex coloring //! - [`MaximumMatching`]: Maximum weight matching //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) @@ -23,6 +24,7 @@ pub(crate) mod maximal_is; pub(crate) mod maximum_clique; pub(crate) mod maximum_independent_set; pub(crate) mod maximum_matching; +pub(crate) mod minimum_cut_into_bounded_sets; pub(crate) mod minimum_dominating_set; pub(crate) mod minimum_feedback_vertex_set; pub(crate) mod minimum_vertex_cover; @@ -37,6 +39,7 @@ pub use maximal_is::MaximalIS; pub use maximum_clique::MaximumClique; pub use maximum_independent_set::MaximumIndependentSet; pub use maximum_matching::MaximumMatching; +pub use minimum_cut_into_bounded_sets::MinimumCutIntoBoundedSets; pub use minimum_dominating_set::MinimumDominatingSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; pub use minimum_vertex_cover::MinimumVertexCover; diff --git a/src/models/mod.rs b/src/models/mod.rs index 6c8ac38a..ce2c5457 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -13,8 +13,8 @@ pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO}; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique, - MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackVertexSet, - MinimumVertexCover, SpinGlass, TravelingSalesman, + MaximumIndependentSet, MaximumMatching, MinimumCutIntoBoundedSets, MinimumDominatingSet, + MinimumFeedbackVertexSet, MinimumVertexCover, SpinGlass, TravelingSalesman, }; pub use misc::{BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum}; pub use set::{MaximumSetPacking, MinimumSetCovering}; diff --git a/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs b/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs new file mode 100644 index 00000000..33ffcc32 --- /dev/null +++ b/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs @@ -0,0 +1,205 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::{Problem, SatisfactionProblem}; + +/// Build the example instance from issue #228: +/// 8 vertices, 12 edges, s=0, t=7, B=5 +fn example_instance(cut_bound: i32) -> MinimumCutIntoBoundedSets { + let graph = SimpleGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 4), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + (5, 6), + ], + ); + let edge_weights = vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1]; + MinimumCutIntoBoundedSets::new(graph, edge_weights, 0, 7, 5, cut_bound) +} + +#[test] +fn test_minimumcutintoboundedsets_basic() { + let problem = example_instance(6); + assert_eq!(problem.num_vertices(), 8); + assert_eq!(problem.num_edges(), 12); + assert_eq!(problem.source(), 0); + assert_eq!(problem.sink(), 7); + assert_eq!(problem.size_bound(), 5); + assert_eq!(problem.cut_bound(), &6); + assert_eq!(problem.dims(), vec![2; 8]); +} + +#[test] +fn test_minimumcutintoboundedsets_evaluation_yes() { + let problem = example_instance(6); + // V1={0,1,2,3}, V2={4,5,6,7} + // Cut edges: (2,4)=2, (3,5)=1, (3,6)=3 => cut=6 <= K=6 + let config = vec![0, 0, 0, 0, 1, 1, 1, 1]; + assert!(problem.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_evaluation_no() { + let problem = example_instance(5); + // Same partition: cut=6 > K=5 + let config = vec![0, 0, 0, 0, 1, 1, 1, 1]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_wrong_source() { + let problem = example_instance(6); + // Source (0) not in V1 (config[0]=1 instead of 0) + let config = vec![1, 0, 0, 0, 1, 1, 1, 1]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_wrong_sink() { + let problem = example_instance(6); + // Sink (7) not in V2 (config[7]=0 instead of 1) + let config = vec![0, 0, 0, 0, 1, 1, 1, 0]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_size_bound_violated() { + // Use B=3 so that |V1|=4 or |V2|=4 violates the bound + let graph = SimpleGraph::new( + 8, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 4), + (3, 5), + (3, 6), + (4, 5), + (4, 6), + (5, 7), + (6, 7), + (5, 6), + ], + ); + let edge_weights = vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1]; + let problem = MinimumCutIntoBoundedSets::new(graph, edge_weights, 0, 7, 3, 100); + // V1={0,1,2,3} has 4 > B=3 + let config = vec![0, 0, 0, 0, 1, 1, 1, 1]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_wrong_config_length() { + let problem = example_instance(6); + let config = vec![0, 0, 1]; // too short + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_serialization() { + let problem = example_instance(6); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: MinimumCutIntoBoundedSets = + serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.num_vertices(), 8); + assert_eq!(deserialized.num_edges(), 12); + assert_eq!(deserialized.source(), 0); + assert_eq!(deserialized.sink(), 7); + assert_eq!(deserialized.size_bound(), 5); + assert_eq!(deserialized.cut_bound(), &6); + // Verify same evaluation + let config = vec![0, 0, 0, 0, 1, 1, 1, 1]; + assert!(deserialized.evaluate(&config)); +} + +#[test] +fn test_minimumcutintoboundedsets_solver_satisfying() { + let problem = example_instance(6); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!( + solution.is_some(), + "Should find a satisfying partition for K=6" + ); + let sol = solution.unwrap(); + assert!(problem.evaluate(&sol)); +} + +#[test] +fn test_minimumcutintoboundedsets_solver_no_solution() { + // K=0 with non-trivial graph: no partition with cut=0 can have s and t separated + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1, 1], 0, 3, 3, 0); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!( + solution.is_none(), + "Should find no satisfying partition for K=0" + ); +} + +#[test] +fn test_minimumcutintoboundedsets_small_graph() { + // Simple 3-vertex path: 0-1-2, s=0, t=2, B=2, K=1 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1], 0, 2, 2, 1); + // V1={0,1}, V2={2}: cut edge (1,2)=1 <= K=1 + assert!(problem.evaluate(&[0, 0, 1])); + // V1={0}, V2={1,2}: cut edge (0,1)=1 <= K=1 + assert!(problem.evaluate(&[0, 1, 1])); +} + +#[test] +fn test_minimumcutintoboundedsets_edge_weights_accessor() { + let problem = example_instance(6); + assert_eq!(problem.edge_weights().len(), 12); + assert_eq!(problem.edge_weights()[0], 2); +} + +#[test] +fn test_minimumcutintoboundedsets_graph_accessor() { + let problem = example_instance(6); + let graph = problem.graph(); + assert_eq!(graph.num_vertices(), 8); + assert_eq!(graph.num_edges(), 12); +} + +#[test] +fn test_minimumcutintoboundedsets_all_satisfying() { + // Small graph: 3-vertex path, s=0, t=2, B=2, K=1 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumCutIntoBoundedSets::new(graph, vec![1, 1], 0, 2, 2, 1); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + // Two valid partitions: {0,1}|{2} and {0}|{1,2} + assert_eq!(solutions.len(), 2); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_minimumcutintoboundedsets_variant() { + let variant = MinimumCutIntoBoundedSets::::variant(); + assert_eq!(variant.len(), 2); + assert!(variant.iter().any(|(k, _)| *k == "graph")); + assert!(variant.iter().any(|(k, _)| *k == "weight")); +} + +// Verify SatisfactionProblem marker trait is implemented +#[test] +fn test_minimumcutintoboundedsets_is_satisfaction_problem() { + fn assert_satisfaction() {} + assert_satisfaction::>(); +} From da6ea40a6b86ae6c4933a334a4e30d756a789697 Mon Sep 17 00:00:00 2001 From: zazabap Date: Fri, 13 Mar 2026 12:24:18 +0000 Subject: [PATCH 3/3] chore: remove plan file after implementation --- ...026-03-13-minimum-cut-into-bounded-sets.md | 74 ------------------- 1 file changed, 74 deletions(-) delete mode 100644 docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md diff --git a/docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md b/docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md deleted file mode 100644 index 93fa3e62..00000000 --- a/docs/plans/2026-03-13-minimum-cut-into-bounded-sets.md +++ /dev/null @@ -1,74 +0,0 @@ -# Plan: Add MinimumCutIntoBoundedSets Model (#228) - -## Overview -Add the MinimumCutIntoBoundedSets satisfaction problem from Garey & Johnson ND17. This is a graph partitioning problem that asks whether vertices can be partitioned into two bounded-size sets (containing designated source and sink vertices) with total cut weight at most K. - -## Information Checklist -1. **Problem name:** `MinimumCutIntoBoundedSets` -2. **Definition:** Given graph G=(V,E) with edge weights, source s, sink t, size bound B, cut bound K: partition V into V1 (containing s) and V2 (containing t) with |V1|<=B, |V2|<=B, and cut weight <= K. -3. **Problem type:** Satisfaction (Metric = bool) -4. **Type parameters:** `G: Graph`, `W: WeightElement` -5. **Struct fields:** `graph: G`, `edge_weights: Vec`, `source: usize`, `sink: usize`, `size_bound: usize`, `cut_bound: W::Sum` -6. **Configuration space:** `vec![2; num_vertices]` (binary: 0=V1, 1=V2) -7. **Feasibility:** x_s=0, x_t=1, |V1|<=B, |V2|<=B, cut weight <= K -8. **Objective:** bool — all constraints satisfied -9. **Complexity:** `2^num_vertices` (brute force) -10. **Solving:** BruteForce (enumerate partitions) -11. **Category:** `graph` - -## Steps - -### Step 1: Create model file -**File:** `src/models/graph/minimum_cut_into_bounded_sets.rs` - -- `inventory::submit!` for ProblemSchemaEntry with fields: graph, edge_weights, source, sink, size_bound, cut_bound -- Struct `MinimumCutIntoBoundedSets` with fields: graph, edge_weights, source, sink, size_bound, cut_bound (where cut_bound is `W::Sum`) -- Constructor `new(graph, edge_weights, source, sink, size_bound, cut_bound)` with assertions -- Accessor methods: `graph()`, `source()`, `sink()`, `size_bound()`, `cut_bound()`, `edge_weights()` -- Size getters: `num_vertices()`, `num_edges()` -- `Problem` impl: NAME="MinimumCutIntoBoundedSets", Metric=bool, dims=vec![2; n], evaluate checks all constraints -- `SatisfactionProblem` impl (marker trait) -- `declare_variants!` with `MinimumCutIntoBoundedSets => "2^num_vertices"` -- Test link: `#[cfg(test)] #[path = "../../unit_tests/models/graph/minimum_cut_into_bounded_sets.rs"] mod tests;` - -The `evaluate()` function must: -1. Check config length == num_vertices -2. Check config[source] == 0 (s in V1) -3. Check config[sink] == 1 (t in V2) -4. Count |V1| = vertices with config=0, |V2| = vertices with config=1 -5. Check |V1| <= size_bound and |V2| <= size_bound -6. Compute cut weight (sum of edge weights where endpoints differ) -7. Return cut_weight <= cut_bound - -### Step 2: Register the model -- `src/models/graph/mod.rs`: add `pub(crate) mod minimum_cut_into_bounded_sets;` and `pub use minimum_cut_into_bounded_sets::MinimumCutIntoBoundedSets;` -- `src/models/mod.rs`: add `MinimumCutIntoBoundedSets` to the graph re-export line - -### Step 3: Register in CLI -- `problemreductions-cli/src/dispatch.rs`: add `deser_sat::>` in load_problem, `try_ser` in serialize_any_problem -- `problemreductions-cli/src/problem_name.rs`: add `"minimumcutintoboundedsets" => "MinimumCutIntoBoundedSets"` - -### Step 4: Add CLI creation support -- `problemreductions-cli/src/commands/create.rs`: add creation handler that parses --graph, --edge-weights, --source, --sink, --size-bound, --cut-bound flags -- `problemreductions-cli/src/cli.rs`: add any needed flags (--source, --sink, --size-bound, --cut-bound) and update help table - -### Step 5: Write unit tests -**File:** `src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs` - -Tests: -- `test_minimumcutintoboundedsets_basic`: construct instance from issue example (8 vertices, 12 edges), verify dims -- `test_minimumcutintoboundedsets_evaluation`: test YES instance (V1={0,1,2,3}, K=6) returns true, NO instance (K=5 with same partition) returns false, invalid source/sink placement returns false -- `test_minimumcutintoboundedsets_serialization`: round-trip serde test -- `test_minimumcutintoboundedsets_solver`: BruteForce::find_satisfying finds a solution for K=6, returns None for K=5 - -Register test file in `src/unit_tests/models/graph/mod.rs`. - -### Step 6: Document in paper -Add problem-def entry in `docs/paper/reductions.typ`: -- Add to `display-name` dict: `"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets]` -- Add `#problem-def("MinimumCutIntoBoundedSets")[...][...]` with formal definition and background - -### Step 7: Verify -```bash -make check # fmt + clippy + test -```