Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"source": {
"problem": "MaximumIndependentSet",
"variant": {
"graph": "SimpleGraph",
"weight": "i32"
"weight": "i32",
"graph": "SimpleGraph"
},
"instance": {
"edges": [
Expand Down Expand Up @@ -31,8 +31,8 @@
"target": {
"problem": "MaximumClique",
"variant": {
"graph": "SimpleGraph",
"weight": "i32"
"weight": "i32",
"graph": "SimpleGraph"
},
"instance": {
"edges": [
Expand Down
9 changes: 9 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"SubsetSum": [Subset Sum],
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"PartitionIntoPathsOfLength2": [Partition into Paths of Length 2],
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SubgraphIsomorphism": [Subgraph Isomorphism],
Expand Down Expand Up @@ -688,6 +689,14 @@ caption: [A directed graph with FVS $S = {v_0}$ (blue, $w(S) = 1$). Removing $v_
) <fig:fvs-example>
]

#problem-def("PartitionIntoPathsOfLength2")[
Given $G = (V, E)$ with $|V| = 3q$, determine if $V$ can be partitioned into $q$ disjoint sets $V_1, ..., V_q$ of three vertices each, such that each $V_t$ induces at least two edges in $G$.
][
A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], proved hard by reduction from 3-Dimensional Matching. Each triple in the partition must form a path of length 2 (exactly two edges, i.e., a $P_3$ subgraph) or a triangle (all three edges). The problem models constrained grouping scenarios where cluster connectivity is required. The best known exact approach uses subset DP in $O^*(3^n)$ time.

*Example.* Consider the graph $G$ with $n = 9$ vertices and edges ${0,1}, {1,2}, {3,4}, {4,5}, {6,7}, {7,8}$ (plus cross-edges ${0,3}, {2,5}, {3,6}, {5,8}$). Setting $q = 3$, the partition $V_1 = {0,1,2}$, $V_2 = {3,4,5}$, $V_3 = {6,7,8}$ is valid: $V_1$ contains edges ${0,1}, {1,2}$ (path $0 dash.em 1 dash.em 2$), $V_2$ contains ${3,4}, {4,5}$, and $V_3$ contains ${6,7}, {7,8}$.
]

#problem-def("MinimumSumMulticenter")[
Given a graph $G = (V, E)$ with vertex weights $w: V -> ZZ_(>= 0)$, edge lengths $l: E -> ZZ_(>= 0)$, and a positive integer $K <= |V|$, find a set $P subset.eq V$ of $K$ vertices (centers) that minimizes the total weighted distance $sum_(v in V) w(v) dot d(v, P)$, where $d(v, P) = min_(p in P) d(v, p)$ is the shortest-path distance from $v$ to the nearest center in $P$.
][
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 @@ -239,6 +239,7 @@ Flags by problem type:
LCS --strings
FAS --arcs [--weights] [--num-vertices]
FVS --arcs [--weights] [--num-vertices]
PartitionIntoPathsOfLength2 --graph
FlowShopScheduling --task-lengths, --deadline [--num-processors]
SCS --strings, --bound [--alphabet-size]
ILP, CircuitSAT (via reduction only)
Expand Down
19 changes: 19 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,25 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// PartitionIntoPathsOfLength2
"PartitionIntoPathsOfLength2" => {
let (graph, _) = parse_graph(args).map_err(|e| {
anyhow::anyhow!(
"{e}\n\nUsage: pred create PartitionIntoPathsOfLength2 --graph 0-1,1-2,3-4,4-5"
)
})?;
if graph.num_vertices() % 3 != 0 {
bail!(
"PartitionIntoPathsOfLength2 requires vertex count divisible by 3, got {}",
graph.num_vertices()
);
}
(
ser(problemreductions::models::graph::PartitionIntoPathsOfLength2::new(graph))?,
resolved_variant.clone(),
)
Comment on lines +985 to +1001
}

_ => bail!("{}", crate::problem_name::unknown_problem_error(canonical)),
};

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub mod prelude {
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement,
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, TravelingSalesman,
};
pub use crate::models::misc::{
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop,
Expand Down
4 changes: 4 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
//! - [`SpinGlass`]: Ising model Hamiltonian
//! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex)
//! - [`PartitionIntoPathsOfLength2`]: Partition vertices into triples with at least two edges each
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
//! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K)
//! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs
Expand All @@ -39,6 +40,7 @@ pub(crate) mod minimum_feedback_vertex_set;
pub(crate) mod minimum_sum_multicenter;
pub(crate) mod minimum_vertex_cover;
pub(crate) mod optimal_linear_arrangement;
pub(crate) mod partition_into_paths_of_length_2;
pub(crate) mod partition_into_triangles;
pub(crate) mod rural_postman;
pub(crate) mod spin_glass;
Expand All @@ -61,6 +63,7 @@ pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet;
pub use minimum_sum_multicenter::MinimumSumMulticenter;
pub use minimum_vertex_cover::MinimumVertexCover;
pub use optimal_linear_arrangement::OptimalLinearArrangement;
pub use partition_into_paths_of_length_2::PartitionIntoPathsOfLength2;
pub use partition_into_triangles::PartitionIntoTriangles;
pub use rural_postman::RuralPostman;
pub use spin_glass::SpinGlass;
Expand All @@ -86,5 +89,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(spin_glass::canonical_model_example_specs());
specs.extend(biclique_cover::canonical_model_example_specs());
specs.extend(partition_into_triangles::canonical_model_example_specs());
specs.extend(partition_into_paths_of_length_2::canonical_model_example_specs());
specs
}
189 changes: 189 additions & 0 deletions src/models/graph/partition_into_paths_of_length_2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
//! Partition into Paths of Length 2 problem implementation.
//!
//! Given a graph G = (V, E) with |V| = 3q, determine whether V can be partitioned
//! into q disjoint sets of three vertices each, such that each set induces at least
//! two edges (i.e., a path of length 2 or a triangle).
//!
//! This is a classical NP-complete problem from Garey & Johnson, Chapter 3, Section 3.3, p.76.

use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
use crate::topology::{Graph, SimpleGraph};
use crate::traits::{Problem, SatisfactionProblem};
use crate::variant::VariantParam;
use serde::{Deserialize, Serialize};

inventory::submit! {
ProblemSchemaEntry {
name: "PartitionIntoPathsOfLength2",
display_name: "Partition into Paths of Length 2",
aliases: &[],
dimensions: &[
VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]),
],
module_path: module_path!(),
description: "Partition vertices into triples each inducing at least two edges (P3 or triangle)",
fields: &[
FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E) with |V| divisible by 3" },
],
}
}
Comment on lines +15 to +29

/// Partition into Paths of Length 2 problem.
///
/// Given a graph G = (V, E) with |V| = 3q for a positive integer q,
/// determine whether V can be partitioned into q disjoint sets
/// V_1, V_2, ..., V_q of three vertices each, such that each V_t
/// induces at least two edges in G.
///
/// Each triple must form either a path of length 2 (exactly 2 edges)
/// or a triangle (all 3 edges).
///
/// # Type Parameters
///
/// * `G` - Graph type (e.g., SimpleGraph)
///
/// # Example
///
/// ```
/// use problemreductions::models::graph::PartitionIntoPathsOfLength2;
/// use problemreductions::topology::SimpleGraph;
/// use problemreductions::{Problem, Solver, BruteForce};
///
/// // 6-vertex graph with two P3 paths: 0-1-2 and 3-4-5
/// let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4), (4, 5)]);
/// let problem = PartitionIntoPathsOfLength2::new(graph);
///
/// let solver = BruteForce::new();
/// let solution = solver.find_satisfying(&problem);
/// assert!(solution.is_some());
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))]
pub struct PartitionIntoPathsOfLength2<G> {
/// The underlying graph.
graph: G,
}

impl<G: Graph> PartitionIntoPathsOfLength2<G> {
/// Create a new PartitionIntoPathsOfLength2 problem from a graph.
///
/// # Panics
/// Panics if `graph.num_vertices()` is not divisible by 3.
pub fn new(graph: G) -> Self {
assert_eq!(
graph.num_vertices() % 3,
0,
"Number of vertices ({}) must be divisible by 3",
graph.num_vertices()
);
Self { graph }
}

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

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

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

/// Get q = |V| / 3, the number of groups in the partition.
pub fn num_groups(&self) -> usize {
self.graph.num_vertices() / 3
}

/// Check if a configuration represents a valid partition.
///
/// A valid configuration assigns each vertex to a group (0..q-1) such that:
/// 1. Each group contains exactly 3 vertices.
/// 2. Each group induces at least 2 edges.
pub fn is_valid_partition(&self, config: &[usize]) -> bool {
let n = self.graph.num_vertices();
let q = self.num_groups();

if config.len() != n {
return false;
}

// Check all assignments are in range
if config.iter().any(|&g| g >= q) {
return false;
}

// Count vertices per group
let mut group_sizes = vec![0usize; q];
for &g in config {
group_sizes[g] += 1;
}

// Each group must have exactly 3 vertices
if group_sizes.iter().any(|&s| s != 3) {
return false;
}

// Check each group induces at least 2 edges (single pass over edges)
let mut group_edge_counts = vec![0usize; q];
for (u, v) in self.graph.edges() {
if config[u] == config[v] {
group_edge_counts[config[u]] += 1;
}
}
if group_edge_counts.iter().any(|&c| c < 2) {
return false;
}

true
}
}

impl<G> Problem for PartitionIntoPathsOfLength2<G>
where
G: Graph + VariantParam,
{
const NAME: &'static str = "PartitionIntoPathsOfLength2";
type Metric = bool;

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

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

fn evaluate(&self, config: &[usize]) -> bool {
self.is_valid_partition(config)
}
}

impl<G: Graph + VariantParam> SatisfactionProblem for PartitionIntoPathsOfLength2<G> {}

crate::declare_variants! {
default sat PartitionIntoPathsOfLength2<SimpleGraph> => "3^num_vertices",
}

#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
vec![crate::example_db::specs::ModelExampleSpec {
id: "partition_into_paths_of_length_2_simplegraph",
build: || {
let problem = PartitionIntoPathsOfLength2::new(SimpleGraph::new(
6,
vec![(0, 1), (1, 2), (3, 4), (4, 5)],
));
crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 0, 1, 1, 1]])
},
}]
}

#[cfg(test)]
#[path = "../../unit_tests/models/graph/partition_into_paths_of_length_2.rs"]
mod tests;
4 changes: 2 additions & 2 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ pub use graph::{
BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, KColoring, MaxCut,
MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet,
MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover,
OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism,
TravelingSalesman,
OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman,
SpinGlass, SubgraphIsomorphism, TravelingSalesman,
};
pub use misc::{
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop,
Expand Down
Loading
Loading