Skip to content
133 changes: 133 additions & 0 deletions docs/paper/examples/maximumindependentset_to_maximumclique.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"source": {
"problem": "MaximumIndependentSet",
"variant": {
"graph": "SimpleGraph",
"weight": "i32"
},
"instance": {
"edges": [
[
0,
1
],
[
1,
2
],
[
2,
3
],
[
3,
4
]
],
"num_edges": 4,
"num_vertices": 5
}
},
"target": {
"problem": "MaximumClique",
"variant": {
"weight": "i32",
"graph": "SimpleGraph"
},
"instance": {
"edges": [
[
0,
2
],
[
0,
3
],
[
0,
4
],
[
1,
3
],
[
1,
4
],
[
2,
4
]
],
"num_edges": 6,
"num_vertices": 5
}
},
"overhead": [
{
"field": "num_vertices",
"expr": {
"Var": "num_vertices"
},
"formula": "num_vertices"
},
{
"field": "num_edges",
"expr": {
"Add": [
{
"Mul": [
{
"Mul": [
{
"Var": "num_vertices"
},
{
"Add": [
{
"Var": "num_vertices"
},
{
"Mul": [
{
"Const": -1.0
},
{
"Const": 1.0
}
]
}
]
}
]
},
{
"Pow": [
{
"Const": 2.0
},
{
"Const": -1.0
}
]
}
]
},
{
"Mul": [
{
"Const": -1.0
},
{
"Var": "num_edges"
}
]
}
]
},
"formula": "num_vertices * (num_vertices + -1 * 1) * 2^-1 + -1 * num_edges"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"solutions": [
{
"source_config": [
1,
0,
1,
0,
1
],
"target_config": [
1,
0,
1,
0,
1
]
}
]
}
21 changes: 21 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -1043,6 +1043,27 @@ Each reduction is presented as a *Rule* (with linked problem names and overhead
_Solution extraction._ For VC solution $C$, return $S = V backslash C$, i.e.\ flip each variable: $s_v = 1 - c_v$.
]

#let mis_clique = load-example("maximumindependentset_to_maximumclique")
#let mis_clique_r = load-results("maximumindependentset_to_maximumclique")
#let mis_clique_sol = mis_clique_r.solutions.at(0)
#reduction-rule("MaximumIndependentSet", "MaximumClique",
example: true,
example-caption: [Path graph $P_5$: IS $arrow.r$ Clique via complement],
extra: [
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)
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()) \
Source $|E| = #mis_clique.source.instance.num_edges$, complement $|overline(E)| = #mis_clique.target.instance.num_edges$ #sym.checkmark
],
)[
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.
][
_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.

_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.

_Solution extraction._ For clique solution $C$ in $overline(G)$, return IS $= C$ (identity mapping: $s_v = c_v$).
]

#reduction-rule("MaximumIndependentSet", "MaximumSetPacking")[
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).
][
Expand Down
15 changes: 15 additions & 0 deletions docs/src/reductions/reduction_graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,21 @@
],
"doc_path": "rules/maximumindependentset_maximumsetpacking/index.html"
},
{
"source": 26,
"target": 22,
"overhead": [
{
"field": "num_vertices",
"formula": "num_vertices"
},
{
"field": "num_edges",
"formula": "num_vertices * (num_vertices + -1 * 1) * 2^-1 + -1 * num_edges"
}
],
"doc_path": "rules/maximumindependentset_maximumclique/index.html"
},
{
"source": 26,
"target": 33,
Expand Down
3 changes: 1 addition & 2 deletions examples/detect_isolated_problems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,7 @@ fn main() {
let label = if v.is_empty() {
name.to_string()
} else {
let parts: Vec<String> =
v.iter().map(|(k, val)| format!("{k}: {val}")).collect();
let parts: Vec<String> = v.iter().map(|(k, val)| format!("{k}: {val}")).collect();
format!("{name} {{{}}}", parts.join(", "))
};
if let Some(c) = graph.variant_complexity(name, v) {
Expand Down
107 changes: 107 additions & 0 deletions examples/reduction_maximumindependentset_to_maximumclique.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// # Independent Set to Clique Reduction
//
// ## Mathematical Equivalence
// S is an independent set in G iff S is a clique in the complement graph Ḡ.
// The reduction builds Ḡ by taking edges not in G. Solution extraction is
// identity: the same vertex set works for both problems.
//
// ## This Example
// - Instance: Path graph P5 (5 vertices, 4 edges)
// - Source MIS: max size 3 (e.g., {0, 2, 4})
// - Target MaxClique on complement: max clique size 3
//
// ## Output
// Exports `docs/paper/examples/maximumindependentset_to_maximumclique.json` and `.result.json`.

use problemreductions::export::*;
use problemreductions::prelude::*;
use problemreductions::topology::{Graph, SimpleGraph};

pub fn run() {
// Path graph: 0-1-2-3-4
let source = MaximumIndependentSet::new(
SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
vec![1i32; 5],
);

let reduction = ReduceTo::<MaximumClique<SimpleGraph, i32>>::reduce_to(&source);
let target = reduction.target_problem();

println!("\n=== Problem Transformation ===");
println!(
"Source: MaximumIndependentSet with {} vertices, {} edges",
source.graph().num_vertices(),
source.graph().num_edges()
);
println!(
"Target: MaximumClique with {} vertices, {} edges (complement graph)",
target.num_vertices(),
target.num_edges()
);

let solver = BruteForce::new();
let target_solutions = solver.find_all_best(target);
println!("\n=== Solution ===");
println!("Target solutions found: {}", target_solutions.len());

let mut solutions = Vec::new();
for target_sol in &target_solutions {
let source_sol = reduction.extract_solution(target_sol);
let size = source.evaluate(&source_sol);
assert!(size.is_valid());
solutions.push(SolutionPair {
source_config: source_sol.clone(),
target_config: target_sol.clone(),
});
}

let source_sol = reduction.extract_solution(&target_solutions[0]);
println!("Source IS solution: {:?}", source_sol);
let size = source.evaluate(&source_sol);
println!("Solution size: {:?}", size);
assert!(size.is_valid());
println!("\nReduction verified successfully");

// Export JSON
let source_edges = source.graph().edges();
let target_edges = target.graph().edges();
let source_variant = variant_to_map(MaximumIndependentSet::<SimpleGraph, i32>::variant());
let target_variant = variant_to_map(MaximumClique::<SimpleGraph, i32>::variant());
let overhead = lookup_overhead(
"MaximumIndependentSet",
&source_variant,
"MaximumClique",
&target_variant,
)
.expect("MaximumIndependentSet -> MaximumClique overhead not found");

let data = ReductionData {
source: ProblemSide {
problem: MaximumIndependentSet::<SimpleGraph, i32>::NAME.to_string(),
variant: source_variant,
instance: serde_json::json!({
"num_vertices": source.graph().num_vertices(),
"num_edges": source.graph().num_edges(),
"edges": source_edges,
}),
},
target: ProblemSide {
problem: MaximumClique::<SimpleGraph, i32>::NAME.to_string(),
variant: target_variant,
instance: serde_json::json!({
"num_vertices": target.num_vertices(),
"num_edges": target.num_edges(),
"edges": target_edges,
}),
},
overhead: overhead_to_json(&overhead),
};

let results = ResultData { solutions };
let name = "maximumindependentset_to_maximumclique";
write_example(name, &data, &results);
}

fn main() {
run()
}
66 changes: 66 additions & 0 deletions src/rules/maximumindependentset_maximumclique.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//! Reduction from MaximumIndependentSet to MaximumClique via complement graph.
//!
//! An independent set in G corresponds to a clique in the complement graph Ḡ.
//! This is Karp's classical complement graph reduction.

use crate::models::graph::{MaximumClique, MaximumIndependentSet};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{Graph, SimpleGraph};
use crate::types::WeightElement;

/// Result of reducing MaximumIndependentSet to MaximumClique.
#[derive(Debug, Clone)]
pub struct ReductionISToClique<W> {
target: MaximumClique<SimpleGraph, W>,
}

impl<W> ReductionResult for ReductionISToClique<W>
where
W: WeightElement + crate::variant::VariantParam,
{
type Source = MaximumIndependentSet<SimpleGraph, W>;
type Target = MaximumClique<SimpleGraph, W>;

fn target_problem(&self) -> &Self::Target {
&self.target
}

/// Solution extraction: identity mapping.
/// A vertex selected in the clique (target) is also selected in the independent set (source).
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
target_solution.to_vec()
}
}

#[reduction(
overhead = {
num_vertices = "num_vertices",
num_edges = "num_vertices * (num_vertices - 1) / 2 - num_edges",
}
)]
Comment on lines +36 to +41
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs/src/reductions/reduction_graph.json file needs to be regenerated to include the new MaximumIndependentSet → MaximumClique reduction edge. Run cargo run --example export_graph to update it.

Copilot uses AI. Check for mistakes.
impl ReduceTo<MaximumClique<SimpleGraph, i32>> for MaximumIndependentSet<SimpleGraph, i32> {
type Result = ReductionISToClique<i32>;

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 }
}
}

#[cfg(test)]
#[path = "../unit_tests/rules/maximumindependentset_maximumclique.rs"]
mod tests;
1 change: 1 addition & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ mod ksatisfiability_qubo;
mod ksatisfiability_subsetsum;
mod maximumindependentset_casts;
mod maximumindependentset_gridgraph;
mod maximumindependentset_maximumclique;
mod maximumindependentset_maximumsetpacking;
mod maximumindependentset_triangular;
mod maximummatching_maximumsetpacking;
Expand Down
Loading
Loading