Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"MaximumIndependentSet": [Maximum Independent Set],
"MinimumVertexCover": [Minimum Vertex Cover],
"MaxCut": [Max-Cut],
"GraphPartitioning": [Graph Partitioning],
"KColoring": [$k$-Coloring],
"MinimumDominatingSet": [Minimum Dominating Set],
"MaximumMatching": [Maximum Matching],
Expand Down Expand Up @@ -379,6 +380,57 @@ Max-Cut is NP-hard on general graphs @barahona1982 but polynomial-time solvable
caption: [The house graph with max cut $S = {v_0, v_3}$ (blue) vs $overline(S) = {v_1, v_2, v_4}$ (white). Cut edges shown in bold blue; 5 of 6 edges are cut.],
) <fig:house-maxcut>
]
#problem-def("GraphPartitioning")[
Given an undirected graph $G = (V, E)$ with $|V| = n$ (even), find a partition of $V$ into two disjoint sets $A$ and $B$ with $|A| = |B| = n slash 2$ that minimizes the number of edges crossing the partition:
$ "cut"(A, B) = |{(u, v) in E : u in A, v in B}|. $
][
Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel computing, and scientific simulation, where balanced workload distribution with minimal communication is essential. Closely related to Max-Cut (which _maximizes_ rather than _minimizes_ the cut) and to the Ising Spin Glass model. NP-completeness was proved by Garey, Johnson and Stockmeyer (1976). Arora, Rao and Vazirani (2009) gave an $O(sqrt(log n))$-approximation algorithm. Standard partitioning tools include METIS, KaHIP, and Scotch.

*Example.* Consider the graph $G$ with $n = 6$ vertices and 9 edges: $(v_0, v_1)$, $(v_0, v_2)$, $(v_1, v_2)$, $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$, $(v_3, v_4)$, $(v_3, v_5)$, $(v_4, v_5)$. The optimal balanced partition is $A = {v_0, v_1, v_2}$, $B = {v_3, v_4, v_5}$, with cut value 3: the crossing edges are $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$. All other balanced partitions yield a cut of at least 3.

#figure(
canvas(length: 1cm, {
// 6-vertex layout: two columns of 3
let verts = (
(0, 2), // v0: top-left
(0, 1), // v1: mid-left
(0, 0), // v2: bottom-left
(2.5, 2), // v3: top-right
(2.5, 1), // v4: mid-right
(2.5, 0), // v5: bottom-right
)
let edges = ((0,1),(0,2),(1,2),(1,3),(2,3),(2,4),(3,4),(3,5),(4,5))
let side-a = (0, 1, 2)
let cut-edges = edges.filter(e => side-a.contains(e.at(0)) != side-a.contains(e.at(1)))
// Draw edges
for (u, v) in edges {
let crossing = cut-edges.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u))
g-edge(verts.at(u), verts.at(v),
stroke: if crossing { 2pt + graph-colors.at(1) } else { 1pt + luma(180) })
}
// Draw partition regions
import draw: *
on-layer(-1, {
rect((-0.5, -0.5), (0.5, 2.5),
fill: graph-colors.at(0).transparentize(90%),
stroke: (dash: "dashed", paint: graph-colors.at(0), thickness: 0.8pt))
content((0, 2.8), text(8pt, fill: graph-colors.at(0))[$A$])
rect((2.0, -0.5), (3.0, 2.5),
fill: graph-colors.at(1).transparentize(90%),
stroke: (dash: "dashed", paint: graph-colors.at(1), thickness: 0.8pt))
content((2.5, 2.8), text(8pt, fill: graph-colors.at(1))[$B$])
})
// Draw nodes
for (k, pos) in verts.enumerate() {
let in-a = side-a.contains(k)
g-node(pos, name: "v" + str(k),
fill: if in-a { graph-colors.at(0) } else { graph-colors.at(1) },
label: text(fill: white)[$v_#k$])
}
}),
caption: [Graph with $n = 6$ vertices partitioned into $A = {v_0, v_1, v_2}$ (blue) and $B = {v_3, v_4, v_5}$ (red). The 3 crossing edges $(v_1, v_3)$, $(v_2, v_3)$, $(v_2, v_4)$ are shown in bold red; internal edges are gray.],
) <fig:graph-partitioning>
]
#problem-def("KColoring")[
Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$.
][
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ Flags by problem type:
QUBO --matrix
SpinGlass --graph, --couplings, --fields
KColoring --graph, --k
GraphPartitioning --graph
Factoring --target, --m, --n
BinPacking --sizes, --capacity
PaintShop --sequence
Expand Down
36 changes: 36 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant};
use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
use problemreductions::models::graph::GraphPartitioning;
use problemreductions::models::misc::{BinPacking, PaintShop};
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
Expand Down Expand Up @@ -74,6 +75,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
Some("UnitDiskGraph") => "--positions \"0,0;1,0;0.5,0.8\" --radius 1.5",
_ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1",
},
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
}
Expand Down Expand Up @@ -193,6 +195,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)?
}

// Graph partitioning (graph only, no weights)
"GraphPartitioning" => {
let (graph, _) = parse_graph(args).map_err(|e| {
anyhow::anyhow!(
"{e}\n\nUsage: pred create GraphPartitioning --graph 0-1,1-2,2-3,0-2,1-3,0-3"
)
})?;
(
ser(GraphPartitioning::new(graph))?,
resolved_variant.clone(),
)
}

// Graph problems with edge weights
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
let (graph, _) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -874,6 +889,27 @@ fn create_random(
}
}

// GraphPartitioning (graph only, no weights; requires even vertex count)
"GraphPartitioning" => {
let num_vertices = if num_vertices % 2 != 0 {
eprintln!(
"Warning: GraphPartitioning requires even vertex count; rounding {} up to {}",
num_vertices,
num_vertices + 1
);
num_vertices + 1
} else {
num_vertices
};
let edge_prob = args.edge_prob.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
bail!("--edge-prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
let variant = variant_map(&[("graph", "SimpleGraph")]);
(ser(GraphPartitioning::new(graph))?, variant)
}

// Graph problems with edge weights
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ pub fn load_problem(
"MaximumClique" => deser_opt::<MaximumClique<SimpleGraph, i32>>(data),
"MaximumMatching" => deser_opt::<MaximumMatching<SimpleGraph, i32>>(data),
"MinimumDominatingSet" => deser_opt::<MinimumDominatingSet<SimpleGraph, i32>>(data),
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
Expand Down Expand Up @@ -267,6 +268,7 @@ pub fn serialize_any_problem(
"MaximumClique" => try_ser::<MaximumClique<SimpleGraph, i32>>(any),
"MaximumMatching" => try_ser::<MaximumMatching<SimpleGraph, i32>>(any),
"MinimumDominatingSet" => try_ser::<MinimumDominatingSet<SimpleGraph, i32>>(any),
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub fn resolve_alias(input: &str) -> String {
"3sat" => "KSatisfiability".to_string(),
"ksat" | "ksatisfiability" => "KSatisfiability".to_string(),
"qubo" => "QUBO".to_string(),
"graphpartitioning" => "GraphPartitioning".to_string(),
"maxcut" => "MaxCut".to_string(),
"spinglass" => "SpinGlass".to_string(),
"ilp" => "ILP".to_string(),
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub mod prelude {
// Problem types
pub use crate::models::algebraic::{BMF, QUBO};
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
pub use crate::models::graph::{BicliqueCover, SpinGlass};
pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass};
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumVertexCover, TravelingSalesman,
Expand Down
142 changes: 142 additions & 0 deletions src/models/graph/graph_partitioning.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! GraphPartitioning problem implementation.
//!
//! The Graph Partitioning (Minimum Bisection) problem asks for a balanced partition
//! of vertices into two equal halves minimizing the number of crossing edges.

use crate::registry::{FieldInfo, ProblemSchemaEntry};
use crate::topology::{Graph, SimpleGraph};
use crate::traits::{OptimizationProblem, Problem};
use crate::types::{Direction, SolutionSize};
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "GraphPartitioning",
module_path: module_path!(),
description: "Find minimum cut balanced bisection of a graph",
fields: &[
FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" },
],
}
}
Comment on lines +12 to +21
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.

This repo appears to treat docs/src/reductions/reduction_graph.json as an auto-generated artifact (see examples/export_graph.rs) and it currently does not include GraphPartitioning. After adding a new model, please regenerate and commit the updated JSON so docs/tooling that consume it stay in sync.

Copilot uses AI. Check for mistakes.

/// The Graph Partitioning (Minimum Bisection) problem.
///
/// Given an undirected graph G = (V, E) with |V| = n (even),
/// partition V into two disjoint sets A and B with |A| = |B| = n/2,
/// minimizing the number of edges crossing the partition.
///
/// # Type Parameters
///
/// * `G` - The graph type (e.g., `SimpleGraph`)
///
/// # Example
///
/// ```
/// use problemreductions::models::graph::GraphPartitioning;
/// use problemreductions::topology::SimpleGraph;
/// use problemreductions::types::SolutionSize;
/// use problemreductions::{Problem, Solver, BruteForce};
///
/// // Square graph: 0-1, 1-2, 2-3, 3-0
/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]);
/// let problem = GraphPartitioning::new(graph);
///
/// let solver = BruteForce::new();
/// let solutions = solver.find_all_best(&problem);
///
/// // Minimum bisection of a 4-cycle: cut = 2
/// for sol in solutions {
/// let size = problem.evaluate(&sol);
/// assert_eq!(size, SolutionSize::Valid(2));
/// }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphPartitioning<G> {
/// The underlying graph structure.
graph: G,
}

impl<G: Graph> GraphPartitioning<G> {
/// Create a GraphPartitioning problem from a graph.
///
/// # Arguments
/// * `graph` - The undirected graph to partition
pub fn new(graph: G) -> Self {
Self { graph }
}

/// Get a reference to the underlying graph.
pub fn graph(&self) -> &G {
&self.graph
}

/// Get the number of vertices in the underlying graph.
pub fn num_vertices(&self) -> usize {
self.graph.num_vertices()
}

/// Get the number of edges in the underlying graph.
pub fn num_edges(&self) -> usize {
self.graph.num_edges()
}
}

impl<G> Problem for GraphPartitioning<G>
where
G: Graph + crate::variant::VariantParam,
{
const NAME: &'static str = "GraphPartitioning";
type Metric = SolutionSize<i32>;

fn variant() -> Vec<(&'static str, &'static str)> {
crate::variant_params![G]
}

fn dims(&self) -> Vec<usize> {
vec![2; self.graph.num_vertices()]
}

fn evaluate(&self, config: &[usize]) -> SolutionSize<i32> {
let n = self.graph.num_vertices();
if config.len() != n {
return SolutionSize::Invalid;
}
// Balanced bisection requires even n
if !n.is_multiple_of(2) {
return SolutionSize::Invalid;
}
// Check balanced: exactly n/2 vertices in partition 1
let count_ones = config.iter().filter(|&&x| x == 1).count();
if count_ones != n / 2 {
return SolutionSize::Invalid;
}
// Count crossing edges
let mut cut = 0i32;
for (u, v) in self.graph.edges() {
if config[u] != config[v] {
cut += 1;
}
Comment on lines +115 to +119
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.

evaluate indexes config[u]/config[v] while iterating edges, but it never checks config.len() == num_vertices(). For shorter configs this will panic at runtime instead of returning SolutionSize::Invalid. Add a length check (and consider using get() like other graph models) before indexing.

Copilot uses AI. Check for mistakes.
}
SolutionSize::Valid(cut)
}
}

impl<G> OptimizationProblem for GraphPartitioning<G>
where
G: Graph + crate::variant::VariantParam,
{
type Value = i32;

fn direction(&self) -> Direction {
Direction::Minimize
}
}

crate::declare_variants! {
GraphPartitioning<SimpleGraph> => "2^num_vertices",
}

#[cfg(test)]
#[path = "../../unit_tests/models/graph/graph_partitioning.rs"]
mod tests;
3 changes: 3 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
//! - [`MinimumDominatingSet`]: Minimum dominating set
//! - [`MaximumClique`]: Maximum weight clique
//! - [`MaxCut`]: Maximum cut on weighted graphs
//! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning)
//! - [`KColoring`]: K-vertex coloring
//! - [`MaximumMatching`]: Maximum weight matching
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
//! - [`SpinGlass`]: Ising model Hamiltonian
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs

pub(crate) mod biclique_cover;
pub(crate) mod graph_partitioning;
pub(crate) mod kcoloring;
pub(crate) mod max_cut;
pub(crate) mod maximal_is;
Expand All @@ -26,6 +28,7 @@ pub(crate) mod spin_glass;
pub(crate) mod traveling_salesman;

pub use biclique_cover::BicliqueCover;
pub use graph_partitioning::GraphPartitioning;
pub use kcoloring::KColoring;
pub use max_cut::MaxCut;
pub use maximal_is::MaximalIS;
Expand Down
5 changes: 3 additions & 2 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ pub mod set;
pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO};
pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
pub use graph::{
BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman,
BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique,
MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass,
TravelingSalesman,
};
pub use misc::{BinPacking, Factoring, Knapsack, PaintShop};
pub use set::{MaximumSetPacking, MinimumSetCovering};
Loading
Loading