Skip to content

Commit 3c23636

Browse files
zazabapclaude
andauthored
Fix #165: Add MaximumIndependentSet to MaximumClique reduction (#606)
* feat: add MaximumIndependentSet to MaximumClique reduction (#165) Implement Karp's classical complement graph reduction: an independent set in G is a clique in the complement graph Ḡ. Includes unit tests, example, and paper documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: update MIS→ILP path test to expect MaxClique intermediate Adding the MIS→MaxClique reduction created a shorter path through the reduction graph, changing MIS→ILP from [MIS, MaxSetPacking, ILP] to [MIS, MaxClique, ILP]. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: regenerate reduction_graph.json with MIS→MaxClique edge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge main, regenerate reduction graph, fix formatting - Merge latest main to pick up new models/rules - Regenerate reduction_graph.json with MIS→MaxClique edge - Fix rustfmt formatting in detect_isolated_problems.rs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: make MIS→ILP path test accept either MaxClique or MaxSetPacking Both are valid 2-step paths with the same cost. The tie-breaking depends on hash map iteration order which differs across platforms. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: regenerate example JSON with consistent key ordering Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b65f176 commit 3c23636

11 files changed

Lines changed: 465 additions & 5 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
{
2+
"source": {
3+
"problem": "MaximumIndependentSet",
4+
"variant": {
5+
"graph": "SimpleGraph",
6+
"weight": "i32"
7+
},
8+
"instance": {
9+
"edges": [
10+
[
11+
0,
12+
1
13+
],
14+
[
15+
1,
16+
2
17+
],
18+
[
19+
2,
20+
3
21+
],
22+
[
23+
3,
24+
4
25+
]
26+
],
27+
"num_edges": 4,
28+
"num_vertices": 5
29+
}
30+
},
31+
"target": {
32+
"problem": "MaximumClique",
33+
"variant": {
34+
"weight": "i32",
35+
"graph": "SimpleGraph"
36+
},
37+
"instance": {
38+
"edges": [
39+
[
40+
0,
41+
2
42+
],
43+
[
44+
0,
45+
3
46+
],
47+
[
48+
0,
49+
4
50+
],
51+
[
52+
1,
53+
3
54+
],
55+
[
56+
1,
57+
4
58+
],
59+
[
60+
2,
61+
4
62+
]
63+
],
64+
"num_edges": 6,
65+
"num_vertices": 5
66+
}
67+
},
68+
"overhead": [
69+
{
70+
"field": "num_vertices",
71+
"expr": {
72+
"Var": "num_vertices"
73+
},
74+
"formula": "num_vertices"
75+
},
76+
{
77+
"field": "num_edges",
78+
"expr": {
79+
"Add": [
80+
{
81+
"Mul": [
82+
{
83+
"Mul": [
84+
{
85+
"Var": "num_vertices"
86+
},
87+
{
88+
"Add": [
89+
{
90+
"Var": "num_vertices"
91+
},
92+
{
93+
"Mul": [
94+
{
95+
"Const": -1.0
96+
},
97+
{
98+
"Const": 1.0
99+
}
100+
]
101+
}
102+
]
103+
}
104+
]
105+
},
106+
{
107+
"Pow": [
108+
{
109+
"Const": 2.0
110+
},
111+
{
112+
"Const": -1.0
113+
}
114+
]
115+
}
116+
]
117+
},
118+
{
119+
"Mul": [
120+
{
121+
"Const": -1.0
122+
},
123+
{
124+
"Var": "num_edges"
125+
}
126+
]
127+
}
128+
]
129+
},
130+
"formula": "num_vertices * (num_vertices + -1 * 1) * 2^-1 + -1 * num_edges"
131+
}
132+
]
133+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"solutions": [
3+
{
4+
"source_config": [
5+
1,
6+
0,
7+
1,
8+
0,
9+
1
10+
],
11+
"target_config": [
12+
1,
13+
0,
14+
1,
15+
0,
16+
1
17+
]
18+
}
19+
]
20+
}

docs/paper/reductions.typ

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,6 +1043,27 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead
10431043
_Solution extraction._ For VC solution $C$, return $S = V backslash C$, i.e.\ flip each variable: $s_v = 1 - c_v$.
10441044
]
10451045

1046+
#let mis_clique = load-example("maximumindependentset_to_maximumclique")
1047+
#let mis_clique_r = load-results("maximumindependentset_to_maximumclique")
1048+
#let mis_clique_sol = mis_clique_r.solutions.at(0)
1049+
#reduction-rule("MaximumIndependentSet", "MaximumClique",
1050+
example: true,
1051+
example-caption: [Path graph $P_5$: IS $arrow.r$ Clique via complement],
1052+
extra: [
1053+
Source IS: $S = {#mis_clique_sol.source_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #mis_clique_sol.source_config.filter(x => x == 1).len()) #h(1em)
1054+
Target Clique: $C = {#mis_clique_sol.target_config.enumerate().filter(((i, x)) => x == 1).map(((i, x)) => str(i)).join(", ")}$ (size #mis_clique_sol.target_config.filter(x => x == 1).len()) \
1055+
Source $|E| = #mis_clique.source.instance.num_edges$, complement $|overline(E)| = #mis_clique.target.instance.num_edges$ #sym.checkmark
1056+
],
1057+
)[
1058+
An independent set in $G$ is exactly a clique in the complement graph $overline(G)$: vertices with no edges between them in $G$ are pairwise adjacent in $overline(G)$. Both problems maximize total vertex weight, so optimal values are preserved. This is Karp's classical complement graph reduction.
1059+
][
1060+
_Construction._ Given IS instance $(G = (V, E), bold(w))$, build $overline(G) = (V, overline(E))$ where $overline(E) = {(u, v) : u != v, (u, v) in.not E}$. Create MaxClique instance $(overline(G), bold(w))$ with the same weights. Variables correspond one-to-one: vertex $v$ in the source maps to vertex $v$ in the target.
1061+
1062+
_Correctness._ ($arrow.r.double$) If $S$ is independent in $G$, then for any $u, v in S$, $(u, v) in.not E$, so $(u, v) in overline(E)$ — all pairs in $S$ are adjacent in $overline(G)$, making $S$ a clique. ($arrow.l.double$) If $C$ is a clique in $overline(G)$, then for any $u, v in C$, $(u, v) in overline(E)$, so $(u, v) in.not E$ — no pair in $C$ is adjacent in $G$, making $C$ independent. Weight sums are identical, so optimality is preserved.
1063+
1064+
_Solution extraction._ For clique solution $C$ in $overline(G)$, return IS $= C$ (identity mapping: $s_v = c_v$).
1065+
]
1066+
10461067
#reduction-rule("MaximumIndependentSet", "MaximumSetPacking")[
10471068
The key insight is that two vertices are adjacent if and only if they share an edge. By representing each vertex $v$ as the set of its incident edges $S_v$, adjacency becomes set overlap: $S_u inter S_v != emptyset$ iff $(u,v) in E$. Thus an independent set (no two adjacent) maps exactly to a packing (no two overlapping).
10481069
][

docs/src/reductions/reduction_graph.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,21 @@
756756
],
757757
"doc_path": "rules/maximumindependentset_maximumsetpacking/index.html"
758758
},
759+
{
760+
"source": 26,
761+
"target": 22,
762+
"overhead": [
763+
{
764+
"field": "num_vertices",
765+
"formula": "num_vertices"
766+
},
767+
{
768+
"field": "num_edges",
769+
"formula": "num_vertices * (num_vertices + -1 * 1) * 2^-1 + -1 * num_edges"
770+
}
771+
],
772+
"doc_path": "rules/maximumindependentset_maximumclique/index.html"
773+
},
759774
{
760775
"source": 26,
761776
"target": 33,

examples/detect_isolated_problems.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,7 @@ fn main() {
107107
let label = if v.is_empty() {
108108
name.to_string()
109109
} else {
110-
let parts: Vec<String> =
111-
v.iter().map(|(k, val)| format!("{k}: {val}")).collect();
110+
let parts: Vec<String> = v.iter().map(|(k, val)| format!("{k}: {val}")).collect();
112111
format!("{name} {{{}}}", parts.join(", "))
113112
};
114113
if let Some(c) = graph.variant_complexity(name, v) {
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// # Independent Set to Clique Reduction
2+
//
3+
// ## Mathematical Equivalence
4+
// S is an independent set in G iff S is a clique in the complement graph Ḡ.
5+
// The reduction builds Ḡ by taking edges not in G. Solution extraction is
6+
// identity: the same vertex set works for both problems.
7+
//
8+
// ## This Example
9+
// - Instance: Path graph P5 (5 vertices, 4 edges)
10+
// - Source MIS: max size 3 (e.g., {0, 2, 4})
11+
// - Target MaxClique on complement: max clique size 3
12+
//
13+
// ## Output
14+
// Exports `docs/paper/examples/maximumindependentset_to_maximumclique.json` and `.result.json`.
15+
16+
use problemreductions::export::*;
17+
use problemreductions::prelude::*;
18+
use problemreductions::topology::{Graph, SimpleGraph};
19+
20+
pub fn run() {
21+
// Path graph: 0-1-2-3-4
22+
let source = MaximumIndependentSet::new(
23+
SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
24+
vec![1i32; 5],
25+
);
26+
27+
let reduction = ReduceTo::<MaximumClique<SimpleGraph, i32>>::reduce_to(&source);
28+
let target = reduction.target_problem();
29+
30+
println!("\n=== Problem Transformation ===");
31+
println!(
32+
"Source: MaximumIndependentSet with {} vertices, {} edges",
33+
source.graph().num_vertices(),
34+
source.graph().num_edges()
35+
);
36+
println!(
37+
"Target: MaximumClique with {} vertices, {} edges (complement graph)",
38+
target.num_vertices(),
39+
target.num_edges()
40+
);
41+
42+
let solver = BruteForce::new();
43+
let target_solutions = solver.find_all_best(target);
44+
println!("\n=== Solution ===");
45+
println!("Target solutions found: {}", target_solutions.len());
46+
47+
let mut solutions = Vec::new();
48+
for target_sol in &target_solutions {
49+
let source_sol = reduction.extract_solution(target_sol);
50+
let size = source.evaluate(&source_sol);
51+
assert!(size.is_valid());
52+
solutions.push(SolutionPair {
53+
source_config: source_sol.clone(),
54+
target_config: target_sol.clone(),
55+
});
56+
}
57+
58+
let source_sol = reduction.extract_solution(&target_solutions[0]);
59+
println!("Source IS solution: {:?}", source_sol);
60+
let size = source.evaluate(&source_sol);
61+
println!("Solution size: {:?}", size);
62+
assert!(size.is_valid());
63+
println!("\nReduction verified successfully");
64+
65+
// Export JSON
66+
let source_edges = source.graph().edges();
67+
let target_edges = target.graph().edges();
68+
let source_variant = variant_to_map(MaximumIndependentSet::<SimpleGraph, i32>::variant());
69+
let target_variant = variant_to_map(MaximumClique::<SimpleGraph, i32>::variant());
70+
let overhead = lookup_overhead(
71+
"MaximumIndependentSet",
72+
&source_variant,
73+
"MaximumClique",
74+
&target_variant,
75+
)
76+
.expect("MaximumIndependentSet -> MaximumClique overhead not found");
77+
78+
let data = ReductionData {
79+
source: ProblemSide {
80+
problem: MaximumIndependentSet::<SimpleGraph, i32>::NAME.to_string(),
81+
variant: source_variant,
82+
instance: serde_json::json!({
83+
"num_vertices": source.graph().num_vertices(),
84+
"num_edges": source.graph().num_edges(),
85+
"edges": source_edges,
86+
}),
87+
},
88+
target: ProblemSide {
89+
problem: MaximumClique::<SimpleGraph, i32>::NAME.to_string(),
90+
variant: target_variant,
91+
instance: serde_json::json!({
92+
"num_vertices": target.num_vertices(),
93+
"num_edges": target.num_edges(),
94+
"edges": target_edges,
95+
}),
96+
},
97+
overhead: overhead_to_json(&overhead),
98+
};
99+
100+
let results = ResultData { solutions };
101+
let name = "maximumindependentset_to_maximumclique";
102+
write_example(name, &data, &results);
103+
}
104+
105+
fn main() {
106+
run()
107+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//! Reduction from MaximumIndependentSet to MaximumClique via complement graph.
2+
//!
3+
//! An independent set in G corresponds to a clique in the complement graph Ḡ.
4+
//! This is Karp's classical complement graph reduction.
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 MaximumIndependentSet to MaximumClique.
13+
#[derive(Debug, Clone)]
14+
pub struct ReductionISToClique<W> {
15+
target: MaximumClique<SimpleGraph, W>,
16+
}
17+
18+
impl<W> ReductionResult for ReductionISToClique<W>
19+
where
20+
W: WeightElement + crate::variant::VariantParam,
21+
{
22+
type Source = MaximumIndependentSet<SimpleGraph, W>;
23+
type Target = MaximumClique<SimpleGraph, W>;
24+
25+
fn target_problem(&self) -> &Self::Target {
26+
&self.target
27+
}
28+
29+
/// Solution extraction: identity mapping.
30+
/// A vertex selected in the clique (target) is also selected in the independent set (source).
31+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
32+
target_solution.to_vec()
33+
}
34+
}
35+
36+
#[reduction(
37+
overhead = {
38+
num_vertices = "num_vertices",
39+
num_edges = "num_vertices * (num_vertices - 1) / 2 - num_edges",
40+
}
41+
)]
42+
impl ReduceTo<MaximumClique<SimpleGraph, i32>> for MaximumIndependentSet<SimpleGraph, i32> {
43+
type Result = ReductionISToClique<i32>;
44+
45+
fn reduce_to(&self) -> Self::Result {
46+
let n = self.graph().num_vertices();
47+
// Build complement graph edges
48+
let mut complement_edges = Vec::new();
49+
for u in 0..n {
50+
for v in (u + 1)..n {
51+
if !self.graph().has_edge(u, v) {
52+
complement_edges.push((u, v));
53+
}
54+
}
55+
}
56+
let target = MaximumClique::new(
57+
SimpleGraph::new(n, complement_edges),
58+
self.weights().to_vec(),
59+
);
60+
ReductionISToClique { target }
61+
}
62+
}
63+
64+
#[cfg(test)]
65+
#[path = "../unit_tests/rules/maximumindependentset_maximumclique.rs"]
66+
mod tests;

src/rules/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ mod ksatisfiability_qubo;
1616
mod ksatisfiability_subsetsum;
1717
mod maximumindependentset_casts;
1818
mod maximumindependentset_gridgraph;
19+
mod maximumindependentset_maximumclique;
1920
mod maximumindependentset_maximumsetpacking;
2021
mod maximumindependentset_triangular;
2122
mod maximummatching_maximumsetpacking;

0 commit comments

Comments
 (0)