diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1e3b2aa72..c262979c7 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -113,6 +113,7 @@ "SubsetSum": [Subset Sum], "MinimumFeedbackArcSet": [Minimum Feedback Arc Set], "MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set], + "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], "MultipleChoiceBranching": [Multiple Choice Branching], "PartitionIntoPathsOfLength2": [Partition into Paths of Length 2], "ResourceConstrainedScheduling": [Resource Constrained Scheduling], @@ -569,6 +570,16 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] ] } +#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("BiconnectivityAugmentation")[ Given an undirected graph $G = (V, E)$, a set $F$ of candidate edges on $V$ with $F inter E = emptyset$, weights $w: F -> RR$, and a budget $B in RR$, find $F' subset.eq F$ such that $sum_(e in F') w(e) <= B$ and the augmented graph $G' = (V, E union F')$ is biconnected, meaning $G'$ is connected and deleting any single vertex leaves it connected. ][ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 1ff70ff78..bf3b22ea5 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -226,6 +226,7 @@ Flags by problem type: PartitionIntoTriangles --graph GraphPartitioning --graph GeneralizedHex --graph, --source, --sink + MinimumCutIntoBoundedSets --graph, --edge-weights, --source, --sink, --size-bound, --cut-bound HamiltonianCircuit, HC --graph BoundedComponentSpanningForest --graph, --weights, --k, --bound UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 @@ -328,10 +329,10 @@ pub struct CreateArgs { /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) #[arg(long)] pub capacities: Option, - /// Source vertex for path-based graph problems + /// Source vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub source: Option, - /// Sink vertex for path-based graph problems + /// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets #[arg(long)] pub sink: Option, /// Required number of paths for LengthBoundedDisjointPaths @@ -478,6 +479,12 @@ pub struct CreateArgs { /// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0) #[arg(long)] pub arcs: 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, /// Item values (e.g., 3,4,5,7) for PartiallyOrderedKnapsack #[arg(long)] pub values: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 745e5a639..465817dd3 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -10,8 +10,8 @@ use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExamp use problemreductions::models::algebraic::{ClosestVectorProblem, ConsecutiveOnesSubmatrix, BMF}; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, - LengthBoundedDisjointPaths, MinimumMultiwayCut, MultipleChoiceBranching, SteinerTree, - StrongConnectivityAugmentation, + LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut, + MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ BinPacking, CbqRelation, ConjunctiveBooleanQuery, FlowShopScheduling, LongestCommonSubsequence, @@ -86,6 +86,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.pattern.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() && args.values.is_none() && args.precedences.is_none() && args.distance_matrix.is_none() @@ -290,6 +294,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { }, "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5", + "MinimumCutIntoBoundedSets" => { + "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --source 0 --sink 3 --size-bound 3 --cut-bound 1" + } "BoundedComponentSpanningForest" => { "--graph 0-1,1-2,2-3,3-4,4-5,5-6,6-7,0-7,1-5,2-6 --weights 2,3,1,2,3,1,2,1 --k 3 --bound 6" } @@ -726,6 +733,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(), + ) + } + // Hamiltonian Circuit (graph only, no weights) "HamiltonianCircuit" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -3616,6 +3656,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/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 33d6d520b..9f6a8f134 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -2821,7 +2821,11 @@ fn test_create_model_example_steiner_tree() { #[test] fn test_create_missing_model_example() { let output = pred() - .args(["create", "--example", "MaximumIndependentSet/KingsSubgraph/One"]) + .args([ + "create", + "--example", + "MaximumIndependentSet/KingsSubgraph/One", + ]) .output() .unwrap(); assert!(!output.status.success()); diff --git a/src/lib.rs b/src/lib.rs index d37c48624..e00906145 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,10 +53,11 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, - MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, - RuralPostman, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, + MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, + MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, + MultipleChoiceBranching, OptimalLinearArrangement, PartitionIntoPathsOfLength2, + PartitionIntoTriangles, RuralPostman, TravelingSalesman, + UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, Factoring, 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 000000000..6523ed5aa --- /dev/null +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -0,0 +1,258 @@ +//! 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, VariantDimension}; +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", + display_name: "Minimum Cut Into Bounded Sets", + aliases: &[], + dimensions: &[ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i32", &["i32"]), + ], + 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, +{ +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_cut_into_bounded_sets_i32", + instance: Box::new(MinimumCutIntoBoundedSets::new( + 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), + ], + ), + vec![2, 3, 1, 4, 2, 1, 3, 2, 1, 2, 3, 1], + 0, + 7, + 5, + 6, + )), + // V1={0,1,2,3}, V2={4,5,6,7}: cut edges (2,4)=2,(3,5)=1,(3,6)=3 => 6 + optimal_config: vec![0, 0, 0, 0, 1, 1, 1, 1], + optimal_value: serde_json::json!(true), + }] +} + +crate::declare_variants! { + default sat 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 d4bf3cf3b..81c9fd0cd 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) //! - [`HamiltonianCircuit`]: Hamiltonian circuit (decision problem) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) //! - [`KthBestSpanningTree`]: K distinct bounded spanning trees (satisfaction) @@ -54,6 +55,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_arc_set; pub(crate) mod minimum_feedback_vertex_set; @@ -90,6 +92,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_arc_set::MinimumFeedbackArcSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; @@ -126,6 +129,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec 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")); +} + +#[test] +fn test_minimumcutintoboundedsets_solver_no_solution_issue_instance() { + // Issue #228 NO instance: K=5 on the 8-vertex graph has no valid partition + let problem = example_instance(5); + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!( + solution.is_none(), + "Should find no satisfying partition for K=5 on the 8-vertex instance" + ); +} + +// Verify SatisfactionProblem marker trait is implemented +#[test] +fn test_minimumcutintoboundedsets_is_satisfaction_problem() { + fn assert_satisfaction() {} + assert_satisfaction::>(); +}