diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 7eb5af767..5726fe72c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -96,6 +96,7 @@ "BoundedComponentSpanningForest": [Bounded Component Spanning Forest], "BinPacking": [Bin Packing], "ClosestVectorProblem": [Closest Vector Problem], + "MinimumMultiwayCut": [Minimum Multiway Cut], "OptimalLinearArrangement": [Optimal Linear Arrangement], "RuralPostman": [Rural Postman], "LongestCommonSubsequence": [Longest Common Subsequence], @@ -1069,6 +1070,50 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] ] } +#{ + let x = load-model-example("MinimumMultiwayCut") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let weights = x.instance.edge_weights + let terminals = x.instance.terminals + let sol = x.optimal.at(0) + let cut-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let cut-edges = cut-edge-indices.map(i => edges.at(i)) + let cost = sol.metric.Valid + [ + #problem-def("MinimumMultiwayCut")[ + Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR_(>0)$ and a set of $k$ terminal vertices $T = {t_1, ..., t_k} subset.eq V$, find a minimum-weight set of edges $C subset.eq E$ such that no two terminals remain in the same connected component of $G' = (V, E backslash C)$. + ][ + The Minimum Multiway Cut problem generalizes the classical minimum $s$-$t$ cut: for $k=2$ it reduces to max-flow and is solvable in polynomial time, but for $k >= 3$ on general graphs it becomes NP-hard @dahlhaus1994. The problem arises in VLSI design, image segmentation, and network design. A $(2 - 2 slash k)$-approximation is achievable in polynomial time by taking the union of the $k - 1$ cheapest isolating cuts @dahlhaus1994. The best known exact algorithm runs in $O^*(1.84^k)$ time (suppressing polynomial factors) via submodular functions on isolating cuts @cao2013. + + *Example.* Consider a graph with $n = #nv$ vertices, $m = #ne$ edges, and $k = #terminals.len()$ terminals $T = {#terminals.map(t => $#t$).join(", ")}$, with edge weights #edges.zip(weights).map(((e, w)) => $w(#(e.at(0)), #(e.at(1))) = #w$).join(", "). The optimal multiway cut removes edges ${#cut-edges.map(e => $(#(e.at(0)), #(e.at(1)))$).join(", ")}$ with total weight #cut-edge-indices.map(i => $#(weights.at(i))$).join($+$) $= #cost$, placing each terminal in a distinct component. + + #figure({ + let verts = ((0, 0.8), (1.2, 1.5), (2.4, 0.8), (1.8, -0.2), (0.6, -0.2)) + canvas(length: 1cm, { + for (idx, (u, v)) in edges.enumerate() { + let is-cut = cut-edge-indices.contains(idx) + g-edge(verts.at(u), verts.at(v), + stroke: if is-cut { (paint: red, thickness: 2pt, dash: "dashed") } else { 1pt + luma(120) }) + let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 + let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 + let dy = if idx == 5 { 0.15 } else { 0 } + draw.content((mx, my + dy), text(7pt, fill: luma(80))[#weights.at(idx)]) + } + for (k, pos) in verts.enumerate() { + let is-terminal = terminals.contains(k) + g-node(pos, name: "v" + str(k), + fill: if is-terminal { graph-colors.at(0) } else { luma(180) }, + label: text(fill: white)[$#k$]) + } + }) + }, + caption: [Minimum Multiway Cut with terminals ${#terminals.map(t => $#t$).join(", ")}$ (blue). Dashed red edges form the optimal cut (weight #cost).], + ) + ] + ] +} #problem-def("OptimalLinearArrangement")[ Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$? ][ diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 6482b48a2..a2ec9b00e 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -514,6 +514,26 @@ @article{ibarra1975 doi = {10.1145/321906.321909} } +@article{dahlhaus1994, + author = {Elias Dahlhaus and David S. Johnson and Christos H. Papadimitriou and Paul D. Seymour and Mihalis Yannakakis}, + title = {The Complexity of Multiterminal Cuts}, + journal = {SIAM Journal on Computing}, + volume = {23}, + number = {4}, + pages = {864--894}, + year = {1994}, + doi = {10.1137/S0097539292225297} +} + +@inproceedings{cao2013, + author = {Yixin Cao and Jianer Chen and Jianxin Wang}, + title = {An Improved Fixed-Parameter Algorithm for the Minimum Weight Multiway Cut Problem}, + booktitle = {Fundamentals of Computation Theory (FCT 2013)}, + pages = {96--107}, + year = {2013}, + doi = {10.1007/978-3-642-40164-0_11} +} + @article{maier1978, author = {David Maier}, title = {The Complexity of Some Problems on Subsequences and Supersequences}, diff --git a/docs/src/cli.md b/docs/src/cli.md index 5c595e905..78f34e46c 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -111,29 +111,82 @@ Lists all registered problem types with their short aliases. ```bash $ pred list -Registered problems: 17 types, 48 reductions, 25 variant nodes - - Problem Aliases Variants Reduces to - ───────────────────── ────────── ──────── ────────── - CircuitSAT 1 1 - Factoring 1 2 - ILP 1 1 - KColoring 2 3 - KSatisfiability 3SAT, KSAT 3 7 - MaxCut 1 1 - MaximumClique 1 1 - MaximumIndependentSet MIS 4 10 - MaximumMatching 1 2 - MaximumSetPacking 2 4 - MinimumDominatingSet 1 1 - MinimumSetCovering 1 1 - MinimumVertexCover MVC 1 4 - QUBO 1 1 - Satisfiability SAT 1 5 - SpinGlass 2 3 - TravelingSalesman TSP 1 1 - -Use `pred show ` to see variants, reductions, and fields. +Registered problems: 50 types, 59 reductions, 69 variant nodes + + Problem Aliases Rules Complexity + ──────────────────────────────────────────────── ─────────── ───── ────────────────────────────────────────────────────────────────── + BMF * O(2^(cols * rank + rank * rows)) + BicliqueCover * O(2^num_vertices) + BiconnectivityAugmentation/SimpleGraph/i32 * O(2^num_potential_edges) + BinPacking/f64 1 O(2^num_items) + BinPacking/i32 * O(2^num_items) + BoundedComponentSpanningForest/SimpleGraph/i32 * O(3^num_vertices) + CircuitSAT * 2 O(2^num_variables) + ClosestVectorProblem/f64 CVP O(2^num_basis_vectors) + ClosestVectorProblem/i32 * O(2^num_basis_vectors) + DirectedTwoCommodityIntegralFlow * D2CIF O((max_capacity + 1)^(2 * num_arcs)) + ExactCoverBy3Sets * X3C O(2^universe_size) + Factoring * 2 O(exp((m + n)^0.3333333333333333 * log(m + n)^0.6666666666666666)) + FlowShopScheduling * O(factorial(num_jobs)) + GraphPartitioning/SimpleGraph * O(2^num_vertices) + HamiltonianPath/SimpleGraph * O(1.657^num_vertices) + ILP/bool * 2 O(2^num_vars) + ILP/i32 O(num_vars^num_vars) + IsomorphicSpanningTree * O(factorial(num_vertices)) + KColoring/SimpleGraph/KN * 3 O(2^num_vertices) + KColoring/SimpleGraph/K2 O(num_edges + num_vertices) + KColoring/SimpleGraph/K3 O(1.3289^num_vertices) + KColoring/SimpleGraph/K4 O(1.7159^num_vertices) + KColoring/SimpleGraph/K5 O(2^num_vertices) + KSatisfiability/KN * KSAT 6 O(2^num_variables) + KSatisfiability/K2 O(num_clauses + num_variables) + KSatisfiability/K3 O(1.307^num_variables) + Knapsack * 1 O(2^(0.5 * num_items)) + LengthBoundedDisjointPaths/SimpleGraph * O(2^(num_paths_required * num_vertices)) + LongestCommonSubsequence * LCS 1 O(2^min_string_length) + MaxCut/SimpleGraph/i32 * 1 O(2^(0.7906666666666666 * num_vertices)) + MaximalIS/SimpleGraph/i32 * O(3^(0.3333333333333333 * num_vertices)) + MaximumClique/SimpleGraph/i32 * 2 O(1.1996^num_vertices) + MaximumIndependentSet/SimpleGraph/One * MIS 14 O(1.1996^num_vertices) + MaximumIndependentSet/KingsSubgraph/One O(2^sqrt(num_vertices)) + MaximumIndependentSet/SimpleGraph/i32 O(1.1996^num_vertices) + MaximumIndependentSet/UnitDiskGraph/One O(2^sqrt(num_vertices)) + MaximumIndependentSet/KingsSubgraph/i32 O(2^sqrt(num_vertices)) + MaximumIndependentSet/TriangularSubgraph/i32 O(2^sqrt(num_vertices)) + MaximumIndependentSet/UnitDiskGraph/i32 O(2^sqrt(num_vertices)) + MaximumMatching/SimpleGraph/i32 * MaxMatching 2 O(num_vertices^3) + MaximumSetPacking/One * 6 O(2^num_sets) + MaximumSetPacking/f64 O(2^num_sets) + MaximumSetPacking/i32 O(2^num_sets) + MinimumDominatingSet/SimpleGraph/i32 * 1 O(1.4969^num_vertices) + MinimumFeedbackArcSet/i32 * FAS O(2^num_vertices) + MinimumFeedbackVertexSet/i32 * FVS O(1.9977^num_vertices) + MinimumMultiwayCut/SimpleGraph/i32 * O(num_vertices^3 * 1.84^num_terminals) + MinimumSetCovering/i32 * 1 O(2^num_sets) + MinimumSumMulticenter/SimpleGraph/i32 * pmedian O(2^num_vertices) + MinimumTardinessSequencing * O(2^num_tasks) + MinimumVertexCover/SimpleGraph/i32 * MVC 2 O(1.1996^num_vertices) + MultipleChoiceBranching/i32 * O(2^num_arcs) + OptimalLinearArrangement/SimpleGraph * OLA O(2^num_vertices) + PaintShop * O(2^num_cars) + PartitionIntoTriangles/SimpleGraph * O(2^num_vertices) + QUBO/f64 * 2 O(2^num_vars) + RuralPostman/SimpleGraph/i32 * RPP O(num_vertices^2 * 2^num_vertices) + Satisfiability * SAT 5 O(2^num_variables) + SequencingWithinIntervals * O(2^num_tasks) + SetBasis * O(2^(basis_size * universe_size)) + ShortestCommonSupersequence * SCS O(alphabet_size^bound) + SpinGlass/SimpleGraph/f64 3 O(2^num_spins) + SpinGlass/SimpleGraph/i32 * O(2^num_spins) + SteinerTree/SimpleGraph/One O(num_vertices * 3^num_terminals) + SteinerTree/SimpleGraph/i32 * O(num_vertices * 3^num_terminals) + SubgraphIsomorphism * O(num_host_vertices^num_pattern_vertices) + SubsetSum * O(2^(0.5 * num_elements)) + TravelingSalesman/SimpleGraph/i32 * TSP 2 O(2^num_vertices) + UndirectedTwoCommodityIntegralFlow * O(5^num_edges) + +* = default variant +Use `pred show ` to see reductions and fields. ``` ### `pred show` — Inspect a problem @@ -291,6 +344,7 @@ pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json pred create SpinGlass --graph 0-1,1-2 -o sg.json pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json +pred create MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2,1,5,6,1 --terminals 0,2,4 -o steiner.json pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 -o utcif.json pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json @@ -511,6 +565,8 @@ You can use short aliases instead of full problem names (shown in `pred list`): | `SAT` | `Satisfiability` | | `3SAT` / `KSAT` | `KSatisfiability` | | `TSP` | `TravelingSalesman` | +| `CVP` | `ClosestVectorProblem` | +| `MaxMatching` | `MaximumMatching` | You can also specify variants with a slash: `MIS/UnitDiskGraph`, `SpinGlass/SimpleGraph`. diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 6f3a2f47f..f9fdf0c62 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -221,6 +221,7 @@ Flags by problem type: QUBO --matrix SpinGlass --graph, --couplings, --fields KColoring --graph, --k + MinimumMultiwayCut --graph, --terminals, --edge-weights PartitionIntoTriangles --graph GraphPartitioning --graph BoundedComponentSpanningForest --graph, --weights, --k, --bound @@ -419,7 +420,7 @@ pub struct CreateArgs { /// Processing lengths for SequencingWithinIntervals (comma-separated, e.g., "3,1,1") #[arg(long)] pub lengths: Option, - /// Terminal vertices for SteinerTree (comma-separated indices, e.g., "0,2,4") + /// Terminal vertices for SteinerTree or MinimumMultiwayCut (comma-separated indices, e.g., "0,2,4") #[arg(long)] pub terminals: Option, /// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 3866e66bf..477db6bba 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -9,8 +9,8 @@ use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; use problemreductions::models::graph::{ - GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths, MultipleChoiceBranching, - SteinerTree, + GraphPartitioning, HamiltonianPath, LengthBoundedDisjointPaths, MinimumMultiwayCut, + MultipleChoiceBranching, SteinerTree, }; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, @@ -223,6 +223,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { }, "Vec" => "comma-separated integers: 1,1,2", "Vec" => "comma-separated: 1,2,3", + "Vec" => "comma-separated indices: 0,2,4", "Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => { "comma-separated weighted edges: 0-2:3,1-3:5" } @@ -279,6 +280,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "PartitionIntoTriangles" => "--graph 0-1,1-2,0-2", "Factoring" => "--target 15 --m 4 --n 4", + "MinimumMultiwayCut" => "--graph 0-1,1-2,2-3 --terminals 0,2 --edge-weights 1,1,1", "SequencingWithinIntervals" => "--release-times 0,0,5 --deadlines 11,11,6 --lengths 3,1,1", "SteinerTree" => "--graph 0-1,1-2,1-3,3-4 --edge-weights 2,2,1,1 --terminals 0,2,4", "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", @@ -1181,6 +1183,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // MinimumMultiwayCut + "MinimumMultiwayCut" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create MinimumMultiwayCut --graph 0-1,1-2,2-3 --terminals 0,2 [--edge-weights 1,1,1]" + ) + })?; + let terminals = parse_terminals(args, graph.num_vertices())?; + let edge_weights = parse_edge_weights(args, graph.num_edges())?; + ( + ser(MinimumMultiwayCut::new(graph, terminals, edge_weights))?, + resolved_variant.clone(), + ) + } + // MinimumTardinessSequencing "MinimumTardinessSequencing" => { let deadlines_str = args.deadlines.as_deref().ok_or_else(|| { @@ -1794,7 +1811,7 @@ fn parse_terminals(args: &CreateArgs, num_vertices: usize) -> Result> let s = args .terminals .as_deref() - .ok_or_else(|| anyhow::anyhow!("SteinerTree requires --terminals (e.g., \"0,2,4\")"))?; + .ok_or_else(|| anyhow::anyhow!("--terminals required (e.g., \"0,2,4\")"))?; let terminals: Vec = s .split(',') .map(|t| t.trim().parse::()) @@ -2514,8 +2531,8 @@ fn create_random( #[cfg(test)] mod tests { - use super::*; use super::problem_help_flag_name; + use super::*; #[test] fn test_problem_help_uses_bound_for_length_bounded_disjoint_paths() { @@ -2538,7 +2555,6 @@ mod tests { ); } - fn empty_args() -> CreateArgs { CreateArgs { problem: Some("BiconnectivityAugmentation".to_string()), diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..6bbdc5f12 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4777,6 +4777,39 @@ fn test_create_weighted_mis_round_trips_into_solve() { assert_eq!(json["evaluation"], "Valid(5)"); } +#[test] +fn test_create_minimum_multiway_cut() { + let output_file = std::env::temp_dir().join("pred_test_create_minimum_multiway_cut.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "MinimumMultiwayCut", + "--graph", + "0-1,1-2,2-3", + "--terminals", + "0,2", + "--edge-weights", + "1,1,1", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "MinimumMultiwayCut"); + assert_eq!(json["variant"]["graph"], "SimpleGraph"); + assert_eq!(json["variant"]["weight"], "i32"); + assert_eq!(json["data"]["terminals"], serde_json::json!([0, 2])); + assert_eq!(json["data"]["edge_weights"], serde_json::json!([1, 1, 1])); + std::fs::remove_file(&output_file).ok(); +} + #[test] fn test_create_sequencing_within_intervals() { let output_file = @@ -4816,6 +4849,24 @@ fn test_create_sequencing_within_intervals() { std::fs::remove_file(&output_file).ok(); } +#[test] +fn test_create_model_example_minimum_multiway_cut() { + let output = pred() + .args(["create", "--example", "MinimumMultiwayCut"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "MinimumMultiwayCut"); + assert_eq!(json["variant"]["graph"], "SimpleGraph"); + assert_eq!(json["variant"]["weight"], "i32"); +} + #[test] fn test_create_model_example_sequencing_within_intervals() { let output = pred() @@ -4832,6 +4883,29 @@ fn test_create_model_example_sequencing_within_intervals() { assert_eq!(json["type"], "SequencingWithinIntervals"); } +#[test] +fn test_create_minimum_multiway_cut_rejects_single_terminal() { + let output = pred() + .args([ + "create", + "MinimumMultiwayCut", + "--graph", + "0-1,1-2", + "--edge-weights", + "1,1", + "--terminals", + "0", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("terminal") || stderr.contains("Terminal"), + "expected terminal-related error, got: {stderr}" + ); +} + #[test] fn test_create_sequencing_within_intervals_rejects_empty_window() { let output = pred() diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 48c677752..4266df37b 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -24,6 +24,7 @@ {"problem":"MaximumSetPacking","variant":{"weight":"i32"},"instance":{"sets":[[0,1],[1,2],[2,3],[3,4]],"weights":[1,1,1,1]},"samples":[{"config":[1,0,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,1,0,1],"metric":{"Valid":2}},{"config":[1,0,0,1],"metric":{"Valid":2}},{"config":[1,0,1,0],"metric":{"Valid":2}}]}, {"problem":"MinimumDominatingSet","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,0,1,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":2}},{"config":[0,1,0,0,1],"metric":{"Valid":2}},{"config":[0,1,0,1,0],"metric":{"Valid":2}},{"config":[0,1,1,0,0],"metric":{"Valid":2}},{"config":[1,0,0,0,1],"metric":{"Valid":2}},{"config":[1,0,0,1,0],"metric":{"Valid":2}},{"config":[1,0,1,0,0],"metric":{"Valid":2}}]}, {"problem":"MinimumFeedbackVertexSet","variant":{"weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[0,3,null],[3,4,null],[4,1,null],[4,2,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[1,0,0,0,0],"metric":{"Valid":1}}],"optimal":[{"config":[0,0,1,0,0],"metric":{"Valid":1}},{"config":[1,0,0,0,0],"metric":{"Valid":1}}]}, + {"problem":"MinimumMultiwayCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,3,1,2,4,5],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null],[0,4,null],[1,3,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,0,1,1,0],"metric":{"Valid":8}}],"optimal":[{"config":[1,0,0,1,1,0],"metric":{"Valid":8}}]}, {"problem":"MinimumSetCovering","variant":{"weight":"i32"},"instance":{"sets":[[0,1,2],[1,3],[2,3,4]],"universe_size":5,"weights":[1,1,1]},"samples":[{"config":[1,0,1],"metric":{"Valid":2}}],"optimal":[{"config":[1,0,1],"metric":{"Valid":2}}]}, {"problem":"MinimumSumMulticenter","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_lengths":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null],[4,5,null],[5,6,null],[0,6,null],[2,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null]}},"k":2,"vertex_weights":[1,1,1,1,1,1,1]},"samples":[{"config":[0,0,1,0,0,1,0],"metric":{"Valid":6}}],"optimal":[{"config":[0,0,0,1,0,0,1],"metric":{"Valid":6}},{"config":[0,0,1,0,0,0,1],"metric":{"Valid":6}},{"config":[0,0,1,0,0,1,0],"metric":{"Valid":6}},{"config":[0,1,0,0,0,1,0],"metric":{"Valid":6}},{"config":[0,1,0,0,1,0,0],"metric":{"Valid":6}},{"config":[1,0,0,0,0,1,0],"metric":{"Valid":6}},{"config":[1,0,0,0,1,0,0],"metric":{"Valid":6}},{"config":[1,0,0,1,0,0,0],"metric":{"Valid":6}},{"config":[1,0,1,0,0,0,0],"metric":{"Valid":6}}]}, {"problem":"MinimumTardinessSequencing","variant":{},"instance":{"deadlines":[2,3,1,4],"num_tasks":4,"precedences":[[0,2]]},"samples":[{"config":[0,0,0,0],"metric":{"Valid":1}}],"optimal":[{"config":[0,0,0,0],"metric":{"Valid":1}},{"config":[0,0,1,0],"metric":{"Valid":1}},{"config":[0,1,0,0],"metric":{"Valid":1}},{"config":[0,2,0,0],"metric":{"Valid":1}},{"config":[1,0,0,0],"metric":{"Valid":1}},{"config":[1,0,1,0],"metric":{"Valid":1}},{"config":[3,0,0,0],"metric":{"Valid":1}}]}, diff --git a/src/lib.rs b/src/lib.rs index 3b8dc3bae..982c0a67a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ pub mod prelude { }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, - MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, + MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs new file mode 100644 index 000000000..1dd075b78 --- /dev/null +++ b/src/models/graph/minimum_multiway_cut.rs @@ -0,0 +1,222 @@ +//! Minimum Multiway Cut problem implementation. +//! +//! The Minimum Multiway Cut problem asks for a minimum weight set of edges +//! whose removal disconnects all terminal pairs. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize, WeightElement}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +inventory::submit! { + ProblemSchemaEntry { + name: "MinimumMultiwayCut", + display_name: "Minimum Multiway Cut", + aliases: &[], + dimensions: &[ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i32", &["i32"]), + ], + module_path: module_path!(), + description: "Find minimum weight set of edges whose removal disconnects all terminal pairs", + fields: &[ + FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" }, + FieldInfo { name: "terminals", type_name: "Vec", description: "Terminal vertices that must be separated" }, + FieldInfo { name: "edge_weights", type_name: "Vec", description: "Edge weights w: E -> R (same order as graph.edges())" }, + ], + } +} + +/// The Minimum Multiway Cut problem. +/// +/// Given an undirected weighted graph G = (V, E, w) and a set of k terminal +/// vertices T = {t_1, ..., t_k}, find a minimum-weight set of edges C ⊆ E +/// such that no two terminals remain in the same connected component of +/// G' = (V, E \ C). +/// +/// # Representation +/// +/// Each edge is assigned a binary variable: +/// - 0: edge is kept +/// - 1: edge is removed (in the cut) +/// +/// A configuration is feasible if removing the cut edges disconnects all +/// terminal pairs. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MinimumMultiwayCut { + graph: G, + terminals: Vec, + edge_weights: Vec, +} + +impl MinimumMultiwayCut { + /// Create a MinimumMultiwayCut problem. + /// + /// `edge_weights` must have one entry per edge, in the same order as + /// [`Graph::edges()`](crate::topology::Graph::edges). Each binary + /// variable corresponds to an edge: 0 = keep, 1 = cut. + /// + /// # Panics + /// - If `edge_weights.len() != graph.num_edges()` + /// - If `terminals.len() < 2` + /// - If any terminal index is out of bounds + /// - If there are duplicate terminal indices + pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { + assert_eq!( + edge_weights.len(), + graph.num_edges(), + "edge_weights length must match num_edges" + ); + assert!(terminals.len() >= 2, "need at least 2 terminals"); + let mut sorted = terminals.clone(); + sorted.sort(); + sorted.dedup(); + assert_eq!(sorted.len(), terminals.len(), "duplicate terminal indices"); + for &t in &terminals { + assert!(t < graph.num_vertices(), "terminal index out of bounds"); + } + Self { + graph, + terminals, + edge_weights, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &G { + &self.graph + } + + /// Get the terminal vertices. + pub fn terminals(&self) -> &[usize] { + &self.terminals + } + + /// Get the edge weights. + pub fn edge_weights(&self) -> &[W] { + &self.edge_weights + } +} + +impl MinimumMultiwayCut { + /// Number of vertices in the graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Number of edges in the graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Number of terminal vertices. + pub fn num_terminals(&self) -> usize { + self.terminals.len() + } +} + +/// Check if all terminals are in distinct connected components +/// when edges marked as cut (config[e] == 1) are removed. +fn terminals_separated(graph: &G, terminals: &[usize], config: &[usize]) -> bool { + let n = graph.num_vertices(); + let edges = graph.edges(); + + // Build adjacency list from non-cut edges + let mut adj: Vec> = vec![vec![]; n]; + for (idx, (u, v)) in edges.iter().enumerate() { + if config.get(idx).copied().unwrap_or(0) == 0 { + adj[*u].push(*v); + adj[*v].push(*u); + } + } + + // BFS from each terminal; if a terminal is already visited by a previous + // terminal's BFS, they share a component => infeasible. + let mut component = vec![usize::MAX; n]; + for (comp_id, &t) in terminals.iter().enumerate() { + if component[t] != usize::MAX { + return false; + } + let mut queue = VecDeque::new(); + queue.push_back(t); + component[t] = comp_id; + while let Some(u) = queue.pop_front() { + for &v in &adj[u] { + if component[v] == usize::MAX { + component[v] = comp_id; + queue.push_back(v); + } + } + } + } + true +} + +impl Problem for MinimumMultiwayCut +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "MinimumMultiwayCut"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G, W] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if !terminals_separated(&self.graph, &self.terminals, config) { + return SolutionSize::Invalid; + } + let mut total = W::Sum::zero(); + for (idx, &selected) in config.iter().enumerate() { + if selected == 1 { + if let Some(w) = self.edge_weights.get(idx) { + total += w.to_sum(); + } + } + } + SolutionSize::Valid(total) + } +} + +impl OptimizationProblem for MinimumMultiwayCut +where + G: Graph + crate::variant::VariantParam, + W: WeightElement + crate::variant::VariantParam, +{ + type Value = W::Sum; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +crate::declare_variants! { + default opt MinimumMultiwayCut => "1.84^num_terminals * num_vertices^3", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "minimum_multiway_cut_simplegraph_i32", + build: || { + // 5 vertices, terminals {0, 2, 4}, 6 edges + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + // Optimal cut: edges {(0,1), (3,4), (0,4)} = config [1,0,0,1,1,0], weight=8 + crate::example_db::specs::optimization_example(problem, vec![vec![1, 0, 0, 1, 1, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/minimum_multiway_cut.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 2e68a846d..c621d113d 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -15,6 +15,7 @@ //! - [`MaximumMatching`]: Maximum weight matching //! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle) //! - [`SpinGlass`]: Ising model Hamiltonian +//! - [`MinimumMultiwayCut`]: Minimum weight multiway cut //! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex) //! - [`BicliqueCover`]: Biclique cover on bipartite graphs //! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges @@ -47,6 +48,7 @@ pub(crate) mod maximum_matching; pub(crate) mod minimum_dominating_set; pub(crate) mod minimum_feedback_arc_set; pub(crate) mod minimum_feedback_vertex_set; +pub(crate) mod minimum_multiway_cut; pub(crate) mod minimum_sum_multicenter; pub(crate) mod minimum_vertex_cover; pub(crate) mod multiple_choice_branching; @@ -76,6 +78,7 @@ pub use maximum_matching::MaximumMatching; pub use minimum_dominating_set::MinimumDominatingSet; pub use minimum_feedback_arc_set::MinimumFeedbackArcSet; pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet; +pub use minimum_multiway_cut::MinimumMultiwayCut; pub use minimum_sum_multicenter::MinimumSumMulticenter; pub use minimum_vertex_cover::MinimumVertexCover; pub use multiple_choice_branching::MultipleChoiceBranching; @@ -104,6 +107,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec indices 0, 3, 4 + // config: [1, 0, 0, 1, 1, 0] => weight 2 + 2 + 4 = 8 + let config = vec![1, 0, 0, 1, 1, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(8)); +} + +#[test] +fn test_minimummultiwaycut_evaluate_invalid() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + // No edges cut: all terminals connected => invalid + let config = vec![0, 0, 0, 0, 0, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Invalid); +} + +#[test] +fn test_minimummultiwaycut_direction() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_minimummultiwaycut_brute_force() { + // Issue example: optimal cut has weight 8 + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + let val = problem.evaluate(sol); + assert_eq!(val, SolutionSize::Valid(8)); + } + // Verify the claimed optimal cut [1,0,0,1,1,0] is among solutions + let claimed_optimal = vec![1, 0, 0, 1, 1, 0]; + assert!( + solutions.contains(&claimed_optimal), + "expected optimal config {:?} not found in brute-force solutions", + claimed_optimal + ); +} + +#[test] +fn test_minimummultiwaycut_two_terminals() { + // k=2: classical min s-t cut. Path graph: 0-1-2, terminals {0,2} + // Edges: (0,1)w=3, (1,2)w=5 + // Min cut: remove (0,1) with weight 3 + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(3)); + } +} + +#[test] +fn test_minimummultiwaycut_all_edges_cut() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let config = vec![1, 1, 1, 1, 1, 1]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(2 + 3 + 1 + 2 + 4 + 5)); +} + +#[test] +fn test_minimummultiwaycut_already_disconnected() { + // Terminals already in different components => empty cut is valid + // Graph: 0-1 2-3, terminals {0, 2} + let graph = SimpleGraph::new(4, vec![(0, 1), (2, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 1]); + let config = vec![0, 0]; + let result = problem.evaluate(&config); + assert_eq!(result, SolutionSize::Valid(0)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_best(&problem); + for sol in &solutions { + assert_eq!(problem.evaluate(sol), SolutionSize::Valid(0)); + } +} + +#[test] +fn test_minimummultiwaycut_serialization() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32, 2]); + let json = serde_json::to_string(&problem).unwrap(); + let restored: MinimumMultiwayCut = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 3); + assert_eq!(restored.num_edges(), 2); + assert_eq!(restored.terminals(), &[0, 2]); +} + +#[test] +fn test_minimummultiwaycut_name() { + assert_eq!( + as Problem>::NAME, + "MinimumMultiwayCut" + ); +} + +#[test] +#[should_panic(expected = "edge_weights length must match num_edges")] +fn test_minimummultiwaycut_panic_wrong_weights_len() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + MinimumMultiwayCut::new(graph, vec![0, 2], vec![1i32]); +} + +#[test] +#[should_panic(expected = "need at least 2 terminals")] +fn test_minimummultiwaycut_panic_too_few_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + MinimumMultiwayCut::new(graph, vec![0], vec![1i32, 1]); +} + +#[test] +#[should_panic(expected = "duplicate terminal indices")] +fn test_minimummultiwaycut_panic_duplicate_terminals() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + MinimumMultiwayCut::new(graph, vec![0, 0], vec![1i32, 1]); +} + +#[test] +#[should_panic(expected = "terminal index out of bounds")] +fn test_minimummultiwaycut_panic_terminal_out_of_bounds() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + MinimumMultiwayCut::new(graph, vec![0, 10], vec![1i32, 1]); +} + +#[test] +fn test_minimummultiwaycut_getters() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2], vec![3i32, 5]); + assert_eq!(problem.graph().num_vertices(), 3); + assert_eq!(problem.edge_weights(), &[3, 5]); +} + +#[test] +fn test_minimummultiwaycut_short_config_no_panic() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + + // Short config: only 2 of 6 edges specified, terminals remain connected + let short_config = vec![1, 0]; + let result = problem.evaluate(&short_config); + assert_eq!(result, SolutionSize::Invalid); + + // Empty config: no edges cut, all terminals connected + let empty_config: Vec = vec![]; + let result = problem.evaluate(&empty_config); + assert_eq!(result, SolutionSize::Invalid); +}