Skip to content

Commit 6cc8cb6

Browse files
zazabapclaudeGiggleLiu
authored
Fix #164: MaximumClique to MaximumIndependentSet reduction (#604)
* feat: add MaximumClique to MaximumIndependentSet reduction (#164) Implement complement graph reduction (Karp 1972): a clique in G corresponds to an independent set in the complement graph. - Reduction rule with overhead: n vertices, n(n-1)/2 - m edges - 5 unit tests including closed-loop, triangle, empty graph, weights - Example program with JSON export - Paper entry with proof and worked example Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Regenerate reduction_graph.json after merge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 63bbdf9 commit 6cc8cb6

7 files changed

Lines changed: 348 additions & 0 deletions

File tree

docs/paper/reductions.typ

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1674,6 +1674,19 @@ The following reductions to Integer Linear Programming are straightforward formu
16741674
_Solution extraction._ $K = {v : x_v = 1}$.
16751675
]
16761676

1677+
#reduction-rule("MaximumClique", "MaximumIndependentSet",
1678+
example: true,
1679+
example-caption: [Path graph $P_4$: clique in $G$ maps to independent set in complement $overline(G)$.],
1680+
)[
1681+
A clique in $G$ is an independent set in the complement graph $overline(G)$, where $overline(G) = (V, overline(E))$ with $overline(E) = {(u,v) : u != v, (u,v) in.not E}$. This classical reduction @karp1972 preserves vertices and weights; only the edge set changes.
1682+
][
1683+
_Construction._ Given MaximumClique instance $(G = (V, E), bold(w))$ with $n = |V|$ and $m = |E|$, create MaximumIndependentSet instance $(overline(G) = (V, overline(E)), bold(w))$ where $overline(E) = {(u,v) : u != v, (u,v) in.not E}$. The complement graph has $n(n-1)/2 - m$ edges. Weights are preserved identically.
1684+
1685+
_Correctness._ ($arrow.r.double$) If $S$ is a clique in $G$, then all pairs in $S$ are adjacent in $G$, so no pair in $S$ is adjacent in $overline(G)$, making $S$ an independent set in $overline(G)$. ($arrow.l.double$) If $S$ is an independent set in $overline(G)$, then no pair in $S$ is adjacent in $overline(G)$, so all pairs in $S$ are adjacent in $G$, making $S$ a clique. Since both problems maximize $sum_(v in S) w_v$, optimal values coincide.
1686+
1687+
_Solution extraction._ Identity: the configuration is the same in both problems, since vertices are preserved one-to-one.
1688+
]
1689+
16771690
#reduction-rule("BinPacking", "ILP")[
16781691
The assignment-based formulation introduces a binary indicator for each item--bin pair and a binary variable for each bin being open. Assignment constraints ensure each item is placed in exactly one bin; capacity constraints link bin usage to item weights.
16791692
][

docs/src/reductions/reduction_graph.json

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,21 @@
412412
}
413413
],
414414
"edges": [
415+
{
416+
"source": 3,
417+
"target": 9,
418+
"overhead": [
419+
{
420+
"field": "num_vars",
421+
"formula": "num_items * num_items + num_items"
422+
},
423+
{
424+
"field": "num_constraints",
425+
"formula": "2 * num_items"
426+
}
427+
],
428+
"doc_path": "rules/binpacking_ilp/index.html"
429+
},
415430
{
416431
"source": 4,
417432
"target": 9,
@@ -651,6 +666,21 @@
651666
],
652667
"doc_path": "rules/maximumclique_ilp/index.html"
653668
},
669+
{
670+
"source": 22,
671+
"target": 26,
672+
"overhead": [
673+
{
674+
"field": "num_vertices",
675+
"formula": "num_vertices"
676+
},
677+
{
678+
"field": "num_edges",
679+
"formula": "num_vertices * (num_vertices + -1 * 1) * 2^-1 + -1 * num_edges"
680+
}
681+
],
682+
"doc_path": "rules/maximumclique_maximumindependentset/index.html"
683+
},
654684
{
655685
"source": 23,
656686
"target": 24,
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// # Maximum Clique to Maximum Independent Set Reduction
2+
//
3+
// ## Complement Graph Reduction (Karp 1972)
4+
// A set S is a clique in G iff S is an independent set in the complement
5+
// graph. The reduction builds the complement graph and preserves weights.
6+
//
7+
// ## This Example
8+
// - Instance: Path graph P4 with 4 vertices, edges {(0,1),(1,2),(2,3)}
9+
// - Complement has edges {(0,2),(0,3),(1,3)}
10+
// - Maximum clique = any edge = size 2
11+
// - Maximum independent set in complement = size 2
12+
//
13+
// ## Output
14+
// Exports `docs/paper/examples/maximumclique_to_maximumindependentset.json` and `maximumclique_to_maximumindependentset.result.json`.
15+
//
16+
// See docs/paper/reductions.typ for the full reduction specification.
17+
18+
use problemreductions::export::*;
19+
use problemreductions::prelude::*;
20+
use problemreductions::topology::{Graph, SimpleGraph};
21+
22+
pub fn run() {
23+
// Path graph P4: 4 vertices, 3 edges
24+
let clique = MaximumClique::new(
25+
SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]),
26+
vec![1i32; 4],
27+
);
28+
29+
let reduction = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&clique);
30+
let is = reduction.target_problem();
31+
32+
println!("\n=== Problem Transformation ===");
33+
println!(
34+
"Source: MaximumClique with {} variables",
35+
clique.num_variables()
36+
);
37+
println!(
38+
"Target: MaximumIndependentSet with {} variables",
39+
is.num_variables()
40+
);
41+
println!(
42+
"Source edges: {}, Target (complement) edges: {}",
43+
clique.graph().num_edges(),
44+
is.graph().num_edges()
45+
);
46+
47+
let solver = BruteForce::new();
48+
let is_solutions = solver.find_all_best(is);
49+
println!("\n=== Solution ===");
50+
println!("Target solutions found: {}", is_solutions.len());
51+
52+
// Extract and verify solutions
53+
let mut solutions = Vec::new();
54+
for target_sol in &is_solutions {
55+
let source_sol = reduction.extract_solution(target_sol);
56+
let size = clique.evaluate(&source_sol);
57+
assert!(size.is_valid());
58+
solutions.push(SolutionPair {
59+
source_config: source_sol.clone(),
60+
target_config: target_sol.clone(),
61+
});
62+
}
63+
64+
let clique_solution = reduction.extract_solution(&is_solutions[0]);
65+
println!("Source Clique solution: {:?}", clique_solution);
66+
67+
let size = clique.evaluate(&clique_solution);
68+
println!("Solution size: {:?}", size);
69+
assert!(size.is_valid());
70+
println!("\nReduction verified successfully");
71+
72+
// Export JSON
73+
let source_edges = clique.graph().edges();
74+
let target_edges = is.graph().edges();
75+
let source_variant = variant_to_map(MaximumClique::<SimpleGraph, i32>::variant());
76+
let target_variant = variant_to_map(MaximumIndependentSet::<SimpleGraph, i32>::variant());
77+
let overhead = lookup_overhead(
78+
"MaximumClique",
79+
&source_variant,
80+
"MaximumIndependentSet",
81+
&target_variant,
82+
)
83+
.expect("MaximumClique -> MaximumIndependentSet overhead not found");
84+
85+
let data = ReductionData {
86+
source: ProblemSide {
87+
problem: MaximumClique::<SimpleGraph, i32>::NAME.to_string(),
88+
variant: source_variant,
89+
instance: serde_json::json!({
90+
"num_vertices": clique.graph().num_vertices(),
91+
"num_edges": clique.graph().num_edges(),
92+
"edges": source_edges,
93+
}),
94+
},
95+
target: ProblemSide {
96+
problem: MaximumIndependentSet::<SimpleGraph, i32>::NAME.to_string(),
97+
variant: target_variant,
98+
instance: serde_json::json!({
99+
"num_vertices": is.graph().num_vertices(),
100+
"num_edges": is.graph().num_edges(),
101+
"edges": target_edges,
102+
}),
103+
},
104+
overhead: overhead_to_json(&overhead),
105+
};
106+
107+
let results = ResultData { solutions };
108+
let name = "maximumclique_to_maximumindependentset";
109+
write_example(name, &data, &results);
110+
}
111+
112+
fn main() {
113+
run()
114+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! Reduction from MaximumClique to MaximumIndependentSet via complement graph.
2+
//!
3+
//! A clique in G corresponds to an independent set in the complement graph.
4+
//! This is one of Karp's classical reductions (1972).
5+
6+
use crate::models::graph::{MaximumClique, MaximumIndependentSet};
7+
use crate::reduction;
8+
use crate::rules::traits::{ReduceTo, ReductionResult};
9+
use crate::topology::{Graph, SimpleGraph};
10+
use crate::types::WeightElement;
11+
12+
/// Result of reducing MaximumClique to MaximumIndependentSet.
13+
#[derive(Debug, Clone)]
14+
pub struct ReductionCliqueToIS<W> {
15+
target: MaximumIndependentSet<SimpleGraph, W>,
16+
}
17+
18+
impl<W> ReductionResult for ReductionCliqueToIS<W>
19+
where
20+
W: WeightElement + crate::variant::VariantParam,
21+
{
22+
type Source = MaximumClique<SimpleGraph, W>;
23+
type Target = MaximumIndependentSet<SimpleGraph, W>;
24+
25+
fn target_problem(&self) -> &Self::Target {
26+
&self.target
27+
}
28+
29+
/// Solution extraction: identity mapping.
30+
/// A clique in G is an independent set in the complement, so the configuration is the same.
31+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
32+
target_solution.to_vec()
33+
}
34+
}
35+
36+
/// Build the complement graph: edges between all non-adjacent vertex pairs.
37+
fn complement_edges(graph: &SimpleGraph) -> Vec<(usize, usize)> {
38+
let n = graph.num_vertices();
39+
let mut edges = Vec::new();
40+
for u in 0..n {
41+
for v in (u + 1)..n {
42+
if !graph.has_edge(u, v) {
43+
edges.push((u, v));
44+
}
45+
}
46+
}
47+
edges
48+
}
49+
50+
#[reduction(
51+
overhead = {
52+
num_vertices = "num_vertices",
53+
num_edges = "num_vertices * (num_vertices - 1) / 2 - num_edges",
54+
}
55+
)]
56+
impl ReduceTo<MaximumIndependentSet<SimpleGraph, i32>> for MaximumClique<SimpleGraph, i32> {
57+
type Result = ReductionCliqueToIS<i32>;
58+
59+
fn reduce_to(&self) -> Self::Result {
60+
let comp_edges = complement_edges(self.graph());
61+
let target = MaximumIndependentSet::new(
62+
SimpleGraph::new(self.graph().num_vertices(), comp_edges),
63+
self.weights().to_vec(),
64+
);
65+
ReductionCliqueToIS { target }
66+
}
67+
}
68+
69+
#[cfg(test)]
70+
#[path = "../unit_tests/rules/maximumclique_maximumindependentset.rs"]
71+
mod tests;

src/rules/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod kcoloring_casts;
1414
mod ksatisfiability_casts;
1515
mod ksatisfiability_qubo;
1616
mod ksatisfiability_subsetsum;
17+
mod maximumclique_maximumindependentset;
1718
mod maximumindependentset_casts;
1819
mod maximumindependentset_gridgraph;
1920
mod maximumindependentset_maximumclique;
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use super::*;
2+
use crate::solvers::BruteForce;
3+
use crate::topology::Graph;
4+
use crate::traits::Problem;
5+
use crate::types::SolutionSize;
6+
use std::collections::HashSet;
7+
8+
#[test]
9+
fn test_maximumclique_to_maximumindependentset_closed_loop() {
10+
// Path graph P4: vertices {0,1,2,3}, edges {(0,1),(1,2),(2,3)}
11+
// Maximum clique is any edge, size 2.
12+
// Complement has edges {(0,2),(0,3),(1,3)}, MIS of size 2.
13+
let source = MaximumClique::new(
14+
SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]),
15+
vec![1i32; 4],
16+
);
17+
let reduction = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&source);
18+
let target = reduction.target_problem();
19+
20+
// Verify complement graph structure
21+
assert_eq!(target.graph().num_vertices(), 4);
22+
assert_eq!(target.graph().num_edges(), 3); // 4*3/2 - 3 = 3
23+
24+
let solver = BruteForce::new();
25+
26+
// Solve target (MIS on complement graph)
27+
let target_solutions = solver.find_all_best(target);
28+
assert!(!target_solutions.is_empty());
29+
30+
// Solve source directly
31+
let source_solutions: HashSet<Vec<usize>> = solver.find_all_best(&source).into_iter().collect();
32+
assert!(!source_solutions.is_empty());
33+
34+
// Extract solutions and verify they are optimal for source
35+
for target_sol in &target_solutions {
36+
let source_sol = reduction.extract_solution(target_sol);
37+
assert!(source_solutions.contains(&source_sol));
38+
}
39+
}
40+
41+
#[test]
42+
fn test_maximumclique_to_maximumindependentset_triangle() {
43+
// Complete graph K3: all 3 edges present
44+
// Complement is empty graph (no edges)
45+
// MIS on empty graph = all vertices
46+
let source = MaximumClique::new(
47+
SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]),
48+
vec![1i32; 3],
49+
);
50+
let reduction = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&source);
51+
let target = reduction.target_problem();
52+
53+
// Complement of K3 has no edges
54+
assert_eq!(target.graph().num_edges(), 0);
55+
56+
let solver = BruteForce::new();
57+
let target_solutions = solver.find_all_best(target);
58+
59+
// MIS on empty graph is all vertices selected
60+
assert!(target_solutions
61+
.iter()
62+
.any(|s| s.iter().sum::<usize>() == 3));
63+
64+
// Extract solution: should be the full clique {0,1,2}
65+
let source_sol = reduction.extract_solution(&target_solutions[0]);
66+
assert!(matches!(
67+
source.evaluate(&source_sol),
68+
SolutionSize::Valid(3)
69+
));
70+
}
71+
72+
#[test]
73+
fn test_maximumclique_to_maximumindependentset_weights_preserved() {
74+
let source = MaximumClique::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![10, 20, 30]);
75+
let reduction = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&source);
76+
let target = reduction.target_problem();
77+
78+
assert_eq!(target.weights().to_vec(), vec![10, 20, 30]);
79+
}
80+
81+
#[test]
82+
fn test_maximumclique_to_maximumindependentset_empty_graph() {
83+
// Empty graph (no edges): complement is complete graph
84+
// Max clique in empty graph = any single vertex
85+
let source = MaximumClique::new(SimpleGraph::new(3, vec![]), vec![1i32; 3]);
86+
let reduction = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&source);
87+
let target = reduction.target_problem();
88+
89+
// Complement of empty graph is K3
90+
assert_eq!(target.graph().num_edges(), 3);
91+
92+
let solver = BruteForce::new();
93+
let target_solutions = solver.find_all_best(target);
94+
95+
// MIS on K3 is any single vertex
96+
assert!(target_solutions
97+
.iter()
98+
.all(|s| s.iter().sum::<usize>() == 1));
99+
}
100+
101+
#[test]
102+
fn test_maximumclique_to_maximumindependentset_overhead() {
103+
// Verify overhead formula: complement edges = n*(n-1)/2 - m
104+
let source = MaximumClique::new(
105+
SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
106+
vec![1i32; 5],
107+
);
108+
let reduction = ReduceTo::<MaximumIndependentSet<SimpleGraph, i32>>::reduce_to(&source);
109+
let target = reduction.target_problem();
110+
111+
assert_eq!(target.graph().num_vertices(), 5);
112+
// 5*4/2 - 4 = 6
113+
assert_eq!(target.graph().num_edges(), 6);
114+
}

tests/suites/examples.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ example_test!(reduction_ksatisfiability_to_subsetsum);
2525
example_test!(reduction_ksatisfiability_to_satisfiability);
2626
example_test!(reduction_maxcut_to_spinglass);
2727
example_test!(reduction_maximumclique_to_ilp);
28+
example_test!(reduction_maximumclique_to_maximumindependentset);
2829
example_test!(reduction_maximumindependentset_to_ilp);
2930
example_test!(reduction_maximumindependentset_to_maximumclique);
3031
example_test!(reduction_maximumindependentset_to_maximumsetpacking);
@@ -99,6 +100,10 @@ example_fn!(
99100
);
100101
example_fn!(test_maxcut_to_spinglass, reduction_maxcut_to_spinglass);
101102
example_fn!(test_maximumclique_to_ilp, reduction_maximumclique_to_ilp);
103+
example_fn!(
104+
test_maximumclique_to_maximumindependentset,
105+
reduction_maximumclique_to_maximumindependentset
106+
);
102107
example_fn!(
103108
test_maximumindependentset_to_ilp,
104109
reduction_maximumindependentset_to_ilp

0 commit comments

Comments
 (0)