diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 4b87601c..449db0e5 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11098,10 +11098,17 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead ] -#let mis_clique = load-example("MaximumIndependentSet", "MaximumClique") +#let mis_clique = load-example( + "MaximumIndependentSet", + "MaximumClique", + source-variant: (graph: "SimpleGraph", weight: "i32"), + target-variant: (graph: "SimpleGraph", weight: "i32"), +) #let mis_clique_sol = mis_clique.solutions.at(0) #reduction-rule("MaximumIndependentSet", "MaximumClique", example: true, + example-source-variant: (graph: "SimpleGraph", weight: "i32"), + example-target-variant: (graph: "SimpleGraph", weight: "i32"), example-caption: [Path graph $P_5$: IS $arrow.r$ Clique via complement], extra: [ #pred-commands( @@ -12689,10 +12696,17 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ Identity: return the ILP variable vector $bold(c)$ as the Integer Knapsack multiplicities. ] -#let clique_mis = load-example("MaximumClique", "MaximumIndependentSet") +#let clique_mis = load-example( + "MaximumClique", + "MaximumIndependentSet", + source-variant: (graph: "SimpleGraph", weight: "i32"), + target-variant: (graph: "SimpleGraph", weight: "i32"), +) #let clique_mis_sol = clique_mis.solutions.at(0) #reduction-rule("MaximumClique", "MaximumIndependentSet", example: true, + example-source-variant: (graph: "SimpleGraph", weight: "i32"), + example-target-variant: (graph: "SimpleGraph", weight: "i32"), example-caption: [Path graph $P_4$: clique in $G$ maps to independent set in complement $overline(G)$.], extra: [ #pred-commands( diff --git a/src/models/graph/maximum_clique.rs b/src/models/graph/maximum_clique.rs index 6dd4cb28..849bef38 100644 --- a/src/models/graph/maximum_clique.rs +++ b/src/models/graph/maximum_clique.rs @@ -6,7 +6,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use crate::types::{Max, WeightElement}; +use crate::types::{Max, One, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; @@ -17,7 +17,7 @@ inventory::submit! { aliases: &[], dimensions: &[ VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), - VariantDimension::new("weight", "i32", &["i32"]), + VariantDimension::new("weight", "One", &["One", "i32"]), ], module_path: module_path!(), description: "Find maximum weight clique in a graph", @@ -165,7 +165,8 @@ fn is_clique_config(graph: &G, config: &[usize]) -> bool { } crate::declare_variants! { - default MaximumClique => "1.1996^num_vertices", + MaximumClique => "1.1996^num_vertices", + default MaximumClique => "1.1996^num_vertices", } #[cfg(feature = "example-db")] diff --git a/src/rules/graph_helpers.rs b/src/rules/graph_helpers.rs index 58c54a34..bdc02ae8 100644 --- a/src/rules/graph_helpers.rs +++ b/src/rules/graph_helpers.rs @@ -1,6 +1,6 @@ //! Shared helpers for graph-based reductions. -use crate::topology::Graph; +use crate::topology::{Graph, SimpleGraph}; /// Extract a Hamiltonian cycle vertex ordering from edge-selection configs on complete graphs. /// @@ -57,3 +57,17 @@ pub(crate) fn edges_to_cycle_order(graph: &G, target_solution: &[usize order } + +/// Build the complement graph edges: edges between all non-adjacent vertex pairs. +pub(crate) fn complement_edges(graph: &SimpleGraph) -> Vec<(usize, usize)> { + let n = graph.num_vertices(); + let mut edges = Vec::new(); + for u in 0..n { + for v in (u + 1)..n { + if !graph.has_edge(u, v) { + edges.push((u, v)); + } + } + } + edges +} diff --git a/src/rules/kcoloring_partitionintocliques.rs b/src/rules/kcoloring_partitionintocliques.rs index 156f8c78..081a0d82 100644 --- a/src/rules/kcoloring_partitionintocliques.rs +++ b/src/rules/kcoloring_partitionintocliques.rs @@ -5,6 +5,7 @@ use crate::models::graph::{KColoring, PartitionIntoCliques}; use crate::reduction; +use crate::rules::graph_helpers::complement_edges; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; use crate::variant::KN; @@ -29,19 +30,6 @@ impl ReductionResult for ReductionKColoringToPartitionIntoCliques { } } -fn complement_edges(graph: &SimpleGraph) -> Vec<(usize, usize)> { - let n = graph.num_vertices(); - let mut edges = Vec::new(); - for u in 0..n { - for v in (u + 1)..n { - if !graph.has_edge(u, v) { - edges.push((u, v)); - } - } - } - edges -} - #[reduction( overhead = { num_vertices = "num_vertices", diff --git a/src/rules/maximumclique_maximumindependentset.rs b/src/rules/maximumclique_maximumindependentset.rs index 38c00bcd..91bd6ebb 100644 --- a/src/rules/maximumclique_maximumindependentset.rs +++ b/src/rules/maximumclique_maximumindependentset.rs @@ -7,7 +7,7 @@ use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -use crate::types::WeightElement; +use crate::types::{One, WeightElement}; /// Result of reducing MaximumClique to MaximumIndependentSet. #[derive(Debug, Clone)] @@ -33,18 +33,15 @@ where } } -/// Build the complement graph: edges between all non-adjacent vertex pairs. -fn complement_edges(graph: &SimpleGraph) -> Vec<(usize, usize)> { - let n = graph.num_vertices(); - let mut edges = Vec::new(); - for u in 0..n { - for v in (u + 1)..n { - if !graph.has_edge(u, v) { - edges.push((u, v)); - } - } - } - edges +fn reduce_clique_to_is( + src: &MaximumClique, +) -> ReductionCliqueToIS { + let comp_edges = super::graph_helpers::complement_edges(src.graph()); + let target = MaximumIndependentSet::new( + SimpleGraph::new(src.graph().num_vertices(), comp_edges), + src.weights().to_vec(), + ); + ReductionCliqueToIS { target } } #[reduction( @@ -57,12 +54,21 @@ impl ReduceTo> for MaximumClique; fn reduce_to(&self) -> Self::Result { - let comp_edges = complement_edges(self.graph()); - let target = MaximumIndependentSet::new( - SimpleGraph::new(self.graph().num_vertices(), comp_edges), - self.weights().to_vec(), - ); - ReductionCliqueToIS { target } + reduce_clique_to_is(self) + } +} + +#[reduction( + overhead = { + num_vertices = "num_vertices", + num_edges = "num_vertices * (num_vertices - 1) / 2 - num_edges", + } +)] +impl ReduceTo> for MaximumClique { + type Result = ReductionCliqueToIS; + + fn reduce_to(&self) -> Self::Result { + reduce_clique_to_is(self) } } @@ -70,25 +76,46 @@ impl ReduceTo> for MaximumClique Vec { use crate::export::SolutionPair; - vec![crate::example_db::specs::RuleExampleSpec { - id: "maximumclique_to_maximumindependentset", - build: || { - let source = MaximumClique::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), - vec![1i32; 4], - ); - crate::example_db::specs::rule_example_with_witness::< - _, - MaximumIndependentSet, - >( - source, - SolutionPair { - source_config: vec![0, 1, 1, 0], - target_config: vec![0, 1, 1, 0], - }, - ) + vec![ + crate::example_db::specs::RuleExampleSpec { + id: "maximumclique_to_maximumindependentset", + build: || { + let source = MaximumClique::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1i32; 4], + ); + crate::example_db::specs::rule_example_with_witness::< + _, + MaximumIndependentSet, + >( + source, + SolutionPair { + source_config: vec![0, 1, 1, 0], + target_config: vec![0, 1, 1, 0], + }, + ) + }, + }, + crate::example_db::specs::RuleExampleSpec { + id: "maximumclique_to_maximumindependentset_one", + build: || { + let source = MaximumClique::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![One; 4], + ); + crate::example_db::specs::rule_example_with_witness::< + _, + MaximumIndependentSet, + >( + source, + SolutionPair { + source_config: vec![0, 1, 1, 0], + target_config: vec![0, 1, 1, 0], + }, + ) + }, }, - }] + ] } #[cfg(test)] diff --git a/src/rules/maximumindependentset_maximumclique.rs b/src/rules/maximumindependentset_maximumclique.rs index 5701b3c4..f65042b5 100644 --- a/src/rules/maximumindependentset_maximumclique.rs +++ b/src/rules/maximumindependentset_maximumclique.rs @@ -7,7 +7,7 @@ use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -use crate::types::WeightElement; +use crate::types::{One, WeightElement}; /// Result of reducing MaximumIndependentSet to MaximumClique. #[derive(Debug, Clone)] @@ -33,6 +33,17 @@ where } } +fn reduce_is_to_clique( + src: &MaximumIndependentSet, +) -> ReductionISToClique { + let comp_edges = super::graph_helpers::complement_edges(src.graph()); + let target = MaximumClique::new( + SimpleGraph::new(src.graph().num_vertices(), comp_edges), + src.weights().to_vec(), + ); + ReductionISToClique { target } +} + #[reduction( overhead = { num_vertices = "num_vertices", @@ -43,21 +54,21 @@ impl ReduceTo> for MaximumIndependentSet; fn reduce_to(&self) -> Self::Result { - let n = self.graph().num_vertices(); - // Build complement graph edges - let mut complement_edges = Vec::new(); - for u in 0..n { - for v in (u + 1)..n { - if !self.graph().has_edge(u, v) { - complement_edges.push((u, v)); - } - } - } - let target = MaximumClique::new( - SimpleGraph::new(n, complement_edges), - self.weights().to_vec(), - ); - ReductionISToClique { target } + reduce_is_to_clique(self) + } +} + +#[reduction( + overhead = { + num_vertices = "num_vertices", + num_edges = "num_vertices * (num_vertices - 1) / 2 - num_edges", + } +)] +impl ReduceTo> for MaximumIndependentSet { + type Result = ReductionISToClique; + + fn reduce_to(&self) -> Self::Result { + reduce_is_to_clique(self) } } @@ -65,22 +76,46 @@ impl ReduceTo> for MaximumIndependentSet Vec { use crate::export::SolutionPair; - vec![crate::example_db::specs::RuleExampleSpec { - id: "maximumindependentset_to_maximumclique", - build: || { - let source = MaximumIndependentSet::new( - SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), - vec![1i32; 5], - ); - crate::example_db::specs::rule_example_with_witness::<_, MaximumClique>( - source, - SolutionPair { - source_config: vec![1, 0, 1, 0, 1], - target_config: vec![1, 0, 1, 0, 1], - }, - ) + vec![ + crate::example_db::specs::RuleExampleSpec { + id: "maximumindependentset_to_maximumclique", + build: || { + let source = MaximumIndependentSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![1i32; 5], + ); + crate::example_db::specs::rule_example_with_witness::< + _, + MaximumClique, + >( + source, + SolutionPair { + source_config: vec![1, 0, 1, 0, 1], + target_config: vec![1, 0, 1, 0, 1], + }, + ) + }, + }, + crate::example_db::specs::RuleExampleSpec { + id: "maximumindependentset_to_maximumclique_one", + build: || { + let source = MaximumIndependentSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![One; 5], + ); + crate::example_db::specs::rule_example_with_witness::< + _, + MaximumClique, + >( + source, + SolutionPair { + source_config: vec![1, 0, 1, 0, 1], + target_config: vec![1, 0, 1, 0, 1], + }, + ) + }, }, - }] + ] } #[cfg(test)] diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 625d33b1..a91da8e3 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -1,7 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::types::Max; +use crate::types::{Max, One}; #[test] fn test_clique_creation() { @@ -281,6 +281,29 @@ fn test_size_getters() { assert_eq!(problem.num_edges(), 2); } +#[test] +fn test_clique_one_weights_evaluate_and_solve() { + use crate::traits::Problem; + + // Triangle with unit weights: max clique covers all 3 vertices. + let problem = MaximumClique::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![One; 3], + ); + assert!(!problem.is_weighted()); + assert_eq!(problem.evaluate(&[1, 1, 1]), Max(Some(3))); + assert_eq!(problem.evaluate(&[1, 1, 0]), Max(Some(2))); + // Invalid clique on this graph? K3 is complete, so every subset is a clique. + // Re-verify invalidity on a path graph: + let path = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![One; 3]); + assert_eq!(path.evaluate(&[1, 0, 1]), Max(None)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_witnesses(&problem); + assert_eq!(solutions.len(), 1); + assert_eq!(solutions[0], vec![1, 1, 1]); +} + #[test] fn test_clique_paper_example() { use crate::traits::Problem; diff --git a/src/unit_tests/rules/maximumclique_maximumindependentset.rs b/src/unit_tests/rules/maximumclique_maximumindependentset.rs index 62520343..39ede01a 100644 --- a/src/unit_tests/rules/maximumclique_maximumindependentset.rs +++ b/src/unit_tests/rules/maximumclique_maximumindependentset.rs @@ -3,6 +3,7 @@ use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; +use crate::types::One; #[test] fn test_maximumclique_to_maximumindependentset_closed_loop() { @@ -84,6 +85,27 @@ fn test_maximumclique_to_maximumindependentset_empty_graph() { .all(|s| s.iter().sum::() == 1)); } +#[test] +fn test_maximumclique_to_maximumindependentset_one_weights_closed_loop() { + // Same P4 as the i32 closed-loop test, but with unit weights so the + // reduction stays on the endpoint (no i32 detour). + let source = MaximumClique::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![One; 4], + ); + let reduction = ReduceTo::>::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.graph().num_vertices(), 4); + assert_eq!(target.graph().num_edges(), 3); + + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "MaximumClique->MaximumIndependentSet closed loop", + ); +} + #[test] fn test_maximumclique_to_maximumindependentset_overhead() { // Verify overhead formula: complement edges = n*(n-1)/2 - m diff --git a/src/unit_tests/rules/maximumindependentset_maximumclique.rs b/src/unit_tests/rules/maximumindependentset_maximumclique.rs index a8d48518..57e94117 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumclique.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumclique.rs @@ -2,6 +2,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::traits::Problem; +use crate::types::One; #[test] fn test_maximumindependentset_to_maximumclique_closed_loop() { @@ -66,6 +67,26 @@ fn test_maximumindependentset_to_maximumclique_empty_graph() { assert!(best_target.iter().all(|s| s.iter().sum::() == 4)); } +#[test] +fn test_maximumindependentset_to_maximumclique_one_weights_closed_loop() { + // Unit-weight closed loop: endpoint stays on One all the way. + let source = MaximumIndependentSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![One; 5], + ); + let reduction = ReduceTo::>::reduce_to(&source); + let target = reduction.target_problem(); + + assert_eq!(target.num_vertices(), 5); + assert_eq!(target.num_edges(), 6); + + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "MaximumIndependentSet->MaximumClique closed loop", + ); +} + #[test] fn test_maximumindependentset_to_maximumclique_complete_graph() { // Complete graph K4 - complement is empty graph