Skip to content
46 changes: 46 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -4262,6 +4262,52 @@ 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 gp_mc = load-example("GraphPartitioning", "MaxCut")
#let gp_mc_sol = gp_mc.solutions.at(0)
#let gp_mc_source_edges = gp_mc.source.instance.graph.edges.map(e => (e.at(0), e.at(1)))
#let gp_mc_target_edges = gp_mc.target.instance.graph.edges.map(e => (e.at(0), e.at(1)))
#let gp_mc_weights = gp_mc.target.instance.edge_weights
#let gp_mc_nv = gp_mc.source.instance.graph.num_vertices
#let gp_mc_ne = gp_mc_source_edges.len()
#let gp_mc_penalty = gp_mc_ne + 1
#let gp_mc_side_a = range(gp_mc_nv).filter(i => gp_mc_sol.source_config.at(i) == 0)
#let gp_mc_side_b = range(gp_mc_nv).filter(i => gp_mc_sol.source_config.at(i) == 1)
#let gp_mc_weight_lo = gp_mc_target_edges.enumerate().filter(((i, e)) => gp_mc_weights.at(i) == gp_mc_penalty - 1).map(((i, e)) => e)
#let gp_mc_weight_hi = gp_mc_target_edges.enumerate().filter(((i, e)) => gp_mc_weights.at(i) == gp_mc_penalty).map(((i, e)) => e)
#let gp_mc_source_cross = gp_mc_source_edges.filter(e => gp_mc_sol.source_config.at(e.at(0)) != gp_mc_sol.source_config.at(e.at(1)))
#let gp_mc_cut_lo = gp_mc_target_edges.enumerate().filter(((i, e)) =>
gp_mc_weights.at(i) == gp_mc_penalty - 1 and
gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1))
).map(((i, e)) => e)
#let gp_mc_cut_hi = gp_mc_target_edges.enumerate().filter(((i, e)) =>
gp_mc_weights.at(i) == gp_mc_penalty and
gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1))
).map(((i, e)) => e)
#let gp_mc_cut_value = gp_mc_target_edges.enumerate().filter(((i, e)) =>
gp_mc_sol.target_config.at(e.at(0)) != gp_mc_sol.target_config.at(e.at(1))
).map(((i, e)) => gp_mc_weights.at(i)).sum(default: 0)
#reduction-rule("GraphPartitioning", "MaxCut",
example: true,
example-caption: [6-vertex minimum bisection to weighted Max-Cut],
extra: [
Here $m = #gp_mc_ne$, so $P = m + 1 = #gp_mc_penalty$ \
Weight $#(gp_mc_penalty - 1)$ edges (original edges): {#gp_mc_weight_lo.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")} \
Weight $#gp_mc_penalty$ edges (non-edges): {#gp_mc_weight_hi.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")} \
Canonical witness $A = {#gp_mc_side_a.map(i => $v_#i$).join(", ")}$, $B = {#gp_mc_side_b.map(i => $v_#i$).join(", ")}$ cuts source edges {#gp_mc_source_cross.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")} and attains weighted cut $#gp_mc_cut_lo.len() * #(gp_mc_penalty - 1) + #gp_mc_cut_hi.len() * #gp_mc_penalty = #gp_mc_cut_value$ #sym.checkmark
],
)[
@garey1976 Graph Partitioning minimizes cut edges subject to a perfect-balance constraint, while Max-Cut maximizes a weighted cut without any balance constraint. A standard folklore construction in combinatorial optimization removes that constraint by rewarding every cross-pair equally and then subtracting one unit on original edges. The resulting weighted complete graph forces every optimum to be balanced first, and among balanced cuts it exactly minimizes the original bisection width.
][
_Construction._ Given a Graph Partitioning instance $G = (V, E)$ with $n = |V|$ and $m = |E|$, set $P = m + 1$. Build the complete graph $G' = (V, E')$ on the same vertex set, where $E'$ contains every unordered pair $\{u, v\}$ with $u != v$. Assign weight $w'_(u, v) = P - 1$ when $(u, v) in E$, and $w'_(u, v) = P$ otherwise. For any partition $(A, B)$ of $V$, the weighted cut in $G'$ is
$ "cut"_(G')(A, B) = P |A| |B| - "cut"_G(A, B). $

_Correctness._ ($arrow.r.double$) Let $(A, B)$ be a maximum cut of $G'$. If it were unbalanced, then $|A| |B|$ would be at least one smaller than for a balanced partition $(A', B')$. Hence
$ "cut"_(G')(A', B') - "cut"_(G')(A, B) >= P - ("cut"_G(A', B') - "cut"_G(A, B)) >= P - m > 0, $
because $0 <= "cut"_G(·, ·) <= m$ and $P = m + 1$. Therefore every maximum cut of $G'$ is balanced. Among balanced partitions, $P |A| |B| = P (n slash 2)^2$ is constant, so maximizing $"cut"_(G')(A, B)$ is equivalent to minimizing $"cut"_G(A, B)$. ($arrow.l.double$) Conversely, every minimum bisection of $G$ is balanced and therefore maximizes $P |A| |B| - "cut"_G(A, B)$ in $G'$.

_Solution extraction._ Read off the same partition vector on the original vertex set: the Max-Cut bit for vertex $v$ is already the Graph Partitioning bit for $v$.
]

#let mis_clique = load-example("MaximumIndependentSet", "MaximumClique")
#let mis_clique_sol = mis_clique.solutions.at(0)
#reduction-rule("MaximumIndependentSet", "MaximumClique",
Expand Down
1 change: 0 additions & 1 deletion src/models/formula/qbf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ impl QuantifiedBooleanFormulas {
}
}
}

}

impl Problem for QuantifiedBooleanFormulas {
Expand Down
3 changes: 1 addition & 2 deletions src/rules/closestvectorproblem_qubo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,7 @@ impl ReduceTo<QUBO<f64>> for ClosestVectorProblem<i32> {
matrix[u][u] =
gram[var_u][var_u] * weight_u * weight_u + 2.0 * weight_u * g_lo_minus_h[var_u];

for v in (u + 1)..total_bits {
let (var_v, weight_v) = bit_terms[v];
for (v, &(var_v, weight_v)) in bit_terms.iter().enumerate().skip(u + 1) {
matrix[u][v] = 2.0 * gram[var_u][var_v] * weight_u * weight_v;
}
}
Expand Down
108 changes: 108 additions & 0 deletions src/rules/graphpartitioning_maxcut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
//! Reduction from GraphPartitioning to MaxCut on a weighted complete graph.

use crate::models::graph::{GraphPartitioning, MaxCut};
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};
use crate::topology::{Graph, SimpleGraph};

/// Result of reducing GraphPartitioning to MaxCut.
#[derive(Debug, Clone)]
pub struct ReductionGPToMaxCut {
target: MaxCut<SimpleGraph, i32>,
}

#[cfg(any(test, feature = "example-db"))]
const ISSUE_EXAMPLE_WITNESS: [usize; 6] = [0, 0, 0, 1, 1, 1];

impl ReductionResult for ReductionGPToMaxCut {
type Source = GraphPartitioning<SimpleGraph>;
type Target = MaxCut<SimpleGraph, i32>;

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

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
target_solution.to_vec()
}
}

#[cfg(any(test, feature = "example-db"))]
fn issue_example() -> GraphPartitioning<SimpleGraph> {
GraphPartitioning::new(SimpleGraph::new(
6,
vec![
(0, 1),
(0, 2),
(1, 2),
(1, 3),
(2, 3),
(2, 4),
(3, 4),
(3, 5),
(4, 5),
],
))
}

fn complete_graph_edges_and_weights(graph: &SimpleGraph) -> (Vec<(usize, usize)>, Vec<i32>) {
let num_vertices = graph.num_vertices();
let p = penalty_weight(graph.num_edges());
let mut edges = Vec::new();
let mut weights = Vec::new();

for u in 0..num_vertices {
for v in (u + 1)..num_vertices {
edges.push((u, v));
weights.push(if graph.has_edge(u, v) { p - 1 } else { p });
}
}

(edges, weights)
}

fn penalty_weight(num_edges: usize) -> i32 {
i32::try_from(num_edges)
.ok()
.and_then(|num_edges| num_edges.checked_add(1))
.expect("GraphPartitioning -> MaxCut penalty exceeds i32 range")
}

#[reduction(
overhead = {
num_vertices = "num_vertices",
num_edges = "num_vertices * (num_vertices - 1) / 2",
}
)]
impl ReduceTo<MaxCut<SimpleGraph, i32>> for GraphPartitioning<SimpleGraph> {
type Result = ReductionGPToMaxCut;

fn reduce_to(&self) -> Self::Result {
let (edges, weights) = complete_graph_edges_and_weights(self.graph());
let target = MaxCut::new(SimpleGraph::new(self.num_vertices(), edges), weights);

ReductionGPToMaxCut { target }
}
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
use crate::export::SolutionPair;

vec![crate::example_db::specs::RuleExampleSpec {
id: "graphpartitioning_to_maxcut",
build: || {
crate::example_db::specs::rule_example_with_witness::<_, MaxCut<SimpleGraph, i32>>(
issue_example(),
SolutionPair {
source_config: ISSUE_EXAMPLE_WITNESS.to_vec(),
target_config: ISSUE_EXAMPLE_WITNESS.to_vec(),
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/graphpartitioning_maxcut.rs"]
mod tests;
2 changes: 2 additions & 0 deletions src/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ mod closestvectorproblem_qubo;
pub(crate) mod coloring_qubo;
pub(crate) mod factoring_circuit;
mod graph;
pub(crate) mod graphpartitioning_maxcut;
mod kcoloring_casts;
mod knapsack_qubo;
mod ksatisfiability_casts;
Expand Down Expand Up @@ -93,6 +94,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
specs.extend(closestvectorproblem_qubo::canonical_rule_example_specs());
specs.extend(coloring_qubo::canonical_rule_example_specs());
specs.extend(factoring_circuit::canonical_rule_example_specs());
specs.extend(graphpartitioning_maxcut::canonical_rule_example_specs());
specs.extend(knapsack_qubo::canonical_rule_example_specs());
specs.extend(ksatisfiability_qubo::canonical_rule_example_specs());
specs.extend(ksatisfiability_subsetsum::canonical_rule_example_specs());
Expand Down
7 changes: 2 additions & 5 deletions src/unit_tests/models/formula/qbf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,10 +255,7 @@ fn test_qbf_quantifier_clone() {
#[test]
fn test_qbf_empty_clause() {
// An empty clause (disjunction of zero literals) is always false
let problem = QuantifiedBooleanFormulas::new(
1,
vec![Quantifier::Exists],
vec![CNFClause::new(vec![])],
);
let problem =
QuantifiedBooleanFormulas::new(1, vec![Quantifier::Exists], vec![CNFClause::new(vec![])]);
assert!(!problem.is_true());
}
65 changes: 65 additions & 0 deletions src/unit_tests/rules/graphpartitioning_maxcut.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use crate::models::graph::{GraphPartitioning, MaxCut};
use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target;
use crate::rules::{ReduceTo, ReductionResult};
use crate::topology::{Graph, SimpleGraph};

fn issue_example() -> GraphPartitioning<SimpleGraph> {
super::issue_example()
}

#[test]
fn test_graphpartitioning_to_maxcut_closed_loop() {
let source = issue_example();
let reduction = ReduceTo::<MaxCut<SimpleGraph, i32>>::reduce_to(&source);

assert_optimization_round_trip_from_optimization_target(
&source,
&reduction,
"GraphPartitioning->MaxCut closed loop",
);
}

#[test]
fn test_graphpartitioning_to_maxcut_target_structure() {
let source = issue_example();
let reduction = ReduceTo::<MaxCut<SimpleGraph, i32>>::reduce_to(&source);
let target = reduction.target_problem();
let num_vertices = source.num_vertices();
let penalty = i32::try_from(source.num_edges()).unwrap() + 1;

assert_eq!(target.num_vertices(), num_vertices);
assert_eq!(target.num_edges(), num_vertices * (num_vertices - 1) / 2);

for u in 0..num_vertices {
for v in (u + 1)..num_vertices {
let expected_weight = if source.graph().has_edge(u, v) {
penalty - 1
} else {
penalty
};
assert_eq!(
target.edge_weight(u, v),
Some(&expected_weight),
"unexpected weight on edge ({u}, {v})"
);
}
}
}

#[test]
fn test_graphpartitioning_to_maxcut_extract_solution_identity() {
let source = issue_example();
let reduction = ReduceTo::<MaxCut<SimpleGraph, i32>>::reduce_to(&source);
let target_solution = super::ISSUE_EXAMPLE_WITNESS.to_vec();

assert_eq!(
reduction.extract_solution(&target_solution),
target_solution
);
}

#[test]
fn test_graphpartitioning_to_maxcut_penalty_overflow_panics() {
let result = std::panic::catch_unwind(|| super::penalty_weight(i32::MAX as usize));
assert!(result.is_err());
}
Loading