Skip to content

Commit 2e08ed1

Browse files
GiggleLiuclaude
andauthored
Fix #117: [Model] GraphPartitioning (#570)
* Add plan for #117: [Model] GraphPartitioning Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Implement #117: Add GraphPartitioning model - Add GraphPartitioning (Minimum Bisection) problem model - Single type param G (unweighted, counts crossing edges) - Balanced bisection: requires |A| = |B| = n/2 (even n) - Direction: Minimize - Register in CLI (dispatch, aliases, create command) - Add unit tests (11 tests covering all cases) - Add problem-def entry in paper with CeTZ visualization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Review fixes: strengthen test assertion, warn on odd vertex count - Assert exact cut value in unbalanced_invalid test - Warn and round up when random generation gets odd vertex count Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add config length check and binary validation in GraphPartitioning::evaluate Address Copilot review comments: validate config length matches vertex count and use filter-based counting instead of sum for correct binary detection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add GraphPartitioning to CLI help "Flags by problem type" table Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: use is_multiple_of() to satisfy clippy lint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4d8fd93 commit 2e08ed1

10 files changed

Lines changed: 410 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"MaximumIndependentSet": [Maximum Independent Set],
3030
"MinimumVertexCover": [Minimum Vertex Cover],
3131
"MaxCut": [Max-Cut],
32+
"GraphPartitioning": [Graph Partitioning],
3233
"KColoring": [$k$-Coloring],
3334
"MinimumDominatingSet": [Minimum Dominating Set],
3435
"MaximumMatching": [Maximum Matching],
@@ -379,6 +380,57 @@ Max-Cut is NP-hard on general graphs @barahona1982 but polynomial-time solvable
379380
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.],
380381
) <fig:house-maxcut>
381382
]
383+
#problem-def("GraphPartitioning")[
384+
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:
385+
$ "cut"(A, B) = |{(u, v) in E : u in A, v in B}|. $
386+
][
387+
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.
388+
389+
*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.
390+
391+
#figure(
392+
canvas(length: 1cm, {
393+
// 6-vertex layout: two columns of 3
394+
let verts = (
395+
(0, 2), // v0: top-left
396+
(0, 1), // v1: mid-left
397+
(0, 0), // v2: bottom-left
398+
(2.5, 2), // v3: top-right
399+
(2.5, 1), // v4: mid-right
400+
(2.5, 0), // v5: bottom-right
401+
)
402+
let edges = ((0,1),(0,2),(1,2),(1,3),(2,3),(2,4),(3,4),(3,5),(4,5))
403+
let side-a = (0, 1, 2)
404+
let cut-edges = edges.filter(e => side-a.contains(e.at(0)) != side-a.contains(e.at(1)))
405+
// Draw edges
406+
for (u, v) in edges {
407+
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))
408+
g-edge(verts.at(u), verts.at(v),
409+
stroke: if crossing { 2pt + graph-colors.at(1) } else { 1pt + luma(180) })
410+
}
411+
// Draw partition regions
412+
import draw: *
413+
on-layer(-1, {
414+
rect((-0.5, -0.5), (0.5, 2.5),
415+
fill: graph-colors.at(0).transparentize(90%),
416+
stroke: (dash: "dashed", paint: graph-colors.at(0), thickness: 0.8pt))
417+
content((0, 2.8), text(8pt, fill: graph-colors.at(0))[$A$])
418+
rect((2.0, -0.5), (3.0, 2.5),
419+
fill: graph-colors.at(1).transparentize(90%),
420+
stroke: (dash: "dashed", paint: graph-colors.at(1), thickness: 0.8pt))
421+
content((2.5, 2.8), text(8pt, fill: graph-colors.at(1))[$B$])
422+
})
423+
// Draw nodes
424+
for (k, pos) in verts.enumerate() {
425+
let in-a = side-a.contains(k)
426+
g-node(pos, name: "v" + str(k),
427+
fill: if in-a { graph-colors.at(0) } else { graph-colors.at(1) },
428+
label: text(fill: white)[$v_#k$])
429+
}
430+
}),
431+
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.],
432+
) <fig:graph-partitioning>
433+
]
382434
#problem-def("KColoring")[
383435
Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$.
384436
][

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ Flags by problem type:
208208
QUBO --matrix
209209
SpinGlass --graph, --couplings, --fields
210210
KColoring --graph, --k
211+
GraphPartitioning --graph
211212
Factoring --target, --m, --n
212213
BinPacking --sizes, --capacity
213214
PaintShop --sequence

problemreductions-cli/src/commands/create.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::problem_name::{parse_problem_spec, resolve_variant};
55
use crate::util;
66
use anyhow::{bail, Context, Result};
77
use problemreductions::models::algebraic::{ClosestVectorProblem, BMF};
8+
use problemreductions::models::graph::GraphPartitioning;
89
use problemreductions::models::misc::{BinPacking, PaintShop};
910
use problemreductions::prelude::*;
1011
use problemreductions::registry::collect_schemas;
@@ -74,6 +75,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
7475
Some("UnitDiskGraph") => "--positions \"0,0;1,0;0.5,0.8\" --radius 1.5",
7576
_ => "--graph 0-1,1-2,2-3 --weights 1,1,1,1",
7677
},
78+
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
7779
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
7880
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
7981
}
@@ -192,6 +194,19 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
192194
create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)?
193195
}
194196

197+
// Graph partitioning (graph only, no weights)
198+
"GraphPartitioning" => {
199+
let (graph, _) = parse_graph(args).map_err(|e| {
200+
anyhow::anyhow!(
201+
"{e}\n\nUsage: pred create GraphPartitioning --graph 0-1,1-2,2-3,0-2,1-3,0-3"
202+
)
203+
})?;
204+
(
205+
ser(GraphPartitioning::new(graph))?,
206+
resolved_variant.clone(),
207+
)
208+
}
209+
195210
// Graph problems with edge weights
196211
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
197212
let (graph, _) = parse_graph(args).map_err(|e| {
@@ -887,6 +902,27 @@ fn create_random(
887902
}
888903
}
889904

905+
// GraphPartitioning (graph only, no weights; requires even vertex count)
906+
"GraphPartitioning" => {
907+
let num_vertices = if num_vertices % 2 != 0 {
908+
eprintln!(
909+
"Warning: GraphPartitioning requires even vertex count; rounding {} up to {}",
910+
num_vertices,
911+
num_vertices + 1
912+
);
913+
num_vertices + 1
914+
} else {
915+
num_vertices
916+
};
917+
let edge_prob = args.edge_prob.unwrap_or(0.5);
918+
if !(0.0..=1.0).contains(&edge_prob) {
919+
bail!("--edge-prob must be between 0.0 and 1.0");
920+
}
921+
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
922+
let variant = variant_map(&[("graph", "SimpleGraph")]);
923+
(ser(GraphPartitioning::new(graph))?, variant)
924+
}
925+
890926
// Graph problems with edge weights
891927
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
892928
let edge_prob = args.edge_prob.unwrap_or(0.5);

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ pub fn load_problem(
210210
"MaximumClique" => deser_opt::<MaximumClique<SimpleGraph, i32>>(data),
211211
"MaximumMatching" => deser_opt::<MaximumMatching<SimpleGraph, i32>>(data),
212212
"MinimumDominatingSet" => deser_opt::<MinimumDominatingSet<SimpleGraph, i32>>(data),
213+
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
213214
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
214215
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
215216
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
@@ -267,6 +268,7 @@ pub fn serialize_any_problem(
267268
"MaximumClique" => try_ser::<MaximumClique<SimpleGraph, i32>>(any),
268269
"MaximumMatching" => try_ser::<MaximumMatching<SimpleGraph, i32>>(any),
269270
"MinimumDominatingSet" => try_ser::<MinimumDominatingSet<SimpleGraph, i32>>(any),
271+
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
270272
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
271273
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
272274
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),

problemreductions-cli/src/problem_name.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub fn resolve_alias(input: &str) -> String {
3232
"3sat" => "KSatisfiability".to_string(),
3333
"ksat" | "ksatisfiability" => "KSatisfiability".to_string(),
3434
"qubo" => "QUBO".to_string(),
35+
"graphpartitioning" => "GraphPartitioning".to_string(),
3536
"maxcut" => "MaxCut".to_string(),
3637
"spinglass" => "SpinGlass".to_string(),
3738
"ilp" => "ILP".to_string(),

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub mod prelude {
4040
// Problem types
4141
pub use crate::models::algebraic::{BMF, QUBO};
4242
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
43-
pub use crate::models::graph::{BicliqueCover, SpinGlass};
43+
pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass};
4444
pub use crate::models::graph::{
4545
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
4646
MinimumDominatingSet, MinimumVertexCover, TravelingSalesman,
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
//! GraphPartitioning problem implementation.
2+
//!
3+
//! The Graph Partitioning (Minimum Bisection) problem asks for a balanced partition
4+
//! of vertices into two equal halves minimizing the number of crossing edges.
5+
6+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
7+
use crate::topology::{Graph, SimpleGraph};
8+
use crate::traits::{OptimizationProblem, Problem};
9+
use crate::types::{Direction, SolutionSize};
10+
use serde::{Deserialize, Serialize};
11+
12+
inventory::submit! {
13+
ProblemSchemaEntry {
14+
name: "GraphPartitioning",
15+
module_path: module_path!(),
16+
description: "Find minimum cut balanced bisection of a graph",
17+
fields: &[
18+
FieldInfo { name: "graph", type_name: "G", description: "The undirected graph G=(V,E)" },
19+
],
20+
}
21+
}
22+
23+
/// The Graph Partitioning (Minimum Bisection) problem.
24+
///
25+
/// Given an undirected graph G = (V, E) with |V| = n (even),
26+
/// partition V into two disjoint sets A and B with |A| = |B| = n/2,
27+
/// minimizing the number of edges crossing the partition.
28+
///
29+
/// # Type Parameters
30+
///
31+
/// * `G` - The graph type (e.g., `SimpleGraph`)
32+
///
33+
/// # Example
34+
///
35+
/// ```
36+
/// use problemreductions::models::graph::GraphPartitioning;
37+
/// use problemreductions::topology::SimpleGraph;
38+
/// use problemreductions::types::SolutionSize;
39+
/// use problemreductions::{Problem, Solver, BruteForce};
40+
///
41+
/// // Square graph: 0-1, 1-2, 2-3, 3-0
42+
/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)]);
43+
/// let problem = GraphPartitioning::new(graph);
44+
///
45+
/// let solver = BruteForce::new();
46+
/// let solutions = solver.find_all_best(&problem);
47+
///
48+
/// // Minimum bisection of a 4-cycle: cut = 2
49+
/// for sol in solutions {
50+
/// let size = problem.evaluate(&sol);
51+
/// assert_eq!(size, SolutionSize::Valid(2));
52+
/// }
53+
/// ```
54+
#[derive(Debug, Clone, Serialize, Deserialize)]
55+
pub struct GraphPartitioning<G> {
56+
/// The underlying graph structure.
57+
graph: G,
58+
}
59+
60+
impl<G: Graph> GraphPartitioning<G> {
61+
/// Create a GraphPartitioning problem from a graph.
62+
///
63+
/// # Arguments
64+
/// * `graph` - The undirected graph to partition
65+
pub fn new(graph: G) -> Self {
66+
Self { graph }
67+
}
68+
69+
/// Get a reference to the underlying graph.
70+
pub fn graph(&self) -> &G {
71+
&self.graph
72+
}
73+
74+
/// Get the number of vertices in the underlying graph.
75+
pub fn num_vertices(&self) -> usize {
76+
self.graph.num_vertices()
77+
}
78+
79+
/// Get the number of edges in the underlying graph.
80+
pub fn num_edges(&self) -> usize {
81+
self.graph.num_edges()
82+
}
83+
}
84+
85+
impl<G> Problem for GraphPartitioning<G>
86+
where
87+
G: Graph + crate::variant::VariantParam,
88+
{
89+
const NAME: &'static str = "GraphPartitioning";
90+
type Metric = SolutionSize<i32>;
91+
92+
fn variant() -> Vec<(&'static str, &'static str)> {
93+
crate::variant_params![G]
94+
}
95+
96+
fn dims(&self) -> Vec<usize> {
97+
vec![2; self.graph.num_vertices()]
98+
}
99+
100+
fn evaluate(&self, config: &[usize]) -> SolutionSize<i32> {
101+
let n = self.graph.num_vertices();
102+
if config.len() != n {
103+
return SolutionSize::Invalid;
104+
}
105+
// Balanced bisection requires even n
106+
if !n.is_multiple_of(2) {
107+
return SolutionSize::Invalid;
108+
}
109+
// Check balanced: exactly n/2 vertices in partition 1
110+
let count_ones = config.iter().filter(|&&x| x == 1).count();
111+
if count_ones != n / 2 {
112+
return SolutionSize::Invalid;
113+
}
114+
// Count crossing edges
115+
let mut cut = 0i32;
116+
for (u, v) in self.graph.edges() {
117+
if config[u] != config[v] {
118+
cut += 1;
119+
}
120+
}
121+
SolutionSize::Valid(cut)
122+
}
123+
}
124+
125+
impl<G> OptimizationProblem for GraphPartitioning<G>
126+
where
127+
G: Graph + crate::variant::VariantParam,
128+
{
129+
type Value = i32;
130+
131+
fn direction(&self) -> Direction {
132+
Direction::Minimize
133+
}
134+
}
135+
136+
crate::declare_variants! {
137+
GraphPartitioning<SimpleGraph> => "2^num_vertices",
138+
}
139+
140+
#[cfg(test)]
141+
#[path = "../../unit_tests/models/graph/graph_partitioning.rs"]
142+
mod tests;

src/models/graph/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@
77
//! - [`MinimumDominatingSet`]: Minimum dominating set
88
//! - [`MaximumClique`]: Maximum weight clique
99
//! - [`MaxCut`]: Maximum cut on weighted graphs
10+
//! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning)
1011
//! - [`KColoring`]: K-vertex coloring
1112
//! - [`MaximumMatching`]: Maximum weight matching
1213
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
1314
//! - [`SpinGlass`]: Ising model Hamiltonian
1415
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
1516
1617
pub(crate) mod biclique_cover;
18+
pub(crate) mod graph_partitioning;
1719
pub(crate) mod kcoloring;
1820
pub(crate) mod max_cut;
1921
pub(crate) mod maximal_is;
@@ -26,6 +28,7 @@ pub(crate) mod spin_glass;
2628
pub(crate) mod traveling_salesman;
2729

2830
pub use biclique_cover::BicliqueCover;
31+
pub use graph_partitioning::GraphPartitioning;
2932
pub use kcoloring::KColoring;
3033
pub use max_cut::MaxCut;
3134
pub use maximal_is::MaximalIS;

src/models/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ pub mod set;
1212
pub use algebraic::{ClosestVectorProblem, BMF, ILP, QUBO};
1313
pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
1414
pub use graph::{
15-
BicliqueCover, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
16-
MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman,
15+
BicliqueCover, GraphPartitioning, KColoring, MaxCut, MaximalIS, MaximumClique,
16+
MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumVertexCover, SpinGlass,
17+
TravelingSalesman,
1718
};
1819
pub use misc::{BinPacking, Factoring, Knapsack, PaintShop};
1920
pub use set::{MaximumSetPacking, MinimumSetCovering};

0 commit comments

Comments
 (0)