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
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
11 changes: 10 additions & 1 deletion docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@
"SubsetSum": [Subset Sum],
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"MultipleChoiceBranching": [Multiple Choice Branching],
"PartitionIntoPathsOfLength2": [Partition into Paths of Length 2],
"ResourceConstrainedScheduling": [Resource Constrained Scheduling],
"QuadraticAssignment": [Quadratic Assignment],
"SequencingWithReleaseTimesAndDeadlines": [Sequencing with Release Times and Deadlines],
"MultipleChoiceBranching": [Multiple Choice Branching],
"ShortestCommonSupersequence": [Shortest Common Supersequence],
"MinimumSumMulticenter": [Minimum Sum Multicenter],
"SteinerTree": [Steiner Tree],
Expand Down Expand Up @@ -1409,6 +1410,14 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
]
}

#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}$.
]

#{
let x = load-model-example("MinimumSumMulticenter")
let nv = graph-num-vertices(x.instance)
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 @@ -259,6 +259,7 @@ Flags by problem type:
LCS --strings, --bound [--alphabet-size]
FAS --arcs [--weights] [--num-vertices]
FVS --arcs [--weights] [--num-vertices]
PartitionIntoPathsOfLength2 --graph
ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences
QAP --matrix (cost), --distance-matrix
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 @@ -2149,6 +2149,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(),
)
}

// ConjunctiveBooleanQuery
"ConjunctiveBooleanQuery" => {
let usage = "Usage: pred create CBQ --domain-size 6 --relations \"2:0,3|1,3;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\"";
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ pub mod prelude {
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut,
MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching,
OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, TravelingSalesman,
UndirectedTwoCommodityIntegralFlow,
OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles,
RuralPostman, TravelingSalesman, UndirectedTwoCommodityIntegralFlow,
};
pub use crate::models::misc::{
BinPacking, CbqRelation, ConjunctiveBooleanQuery, Factoring, FlowShopScheduling, Knapsack,
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 @@ -19,6 +19,7 @@
//! - [`SpinGlass`]: Ising model Hamiltonian
//! - [`MinimumMultiwayCut`]: Minimum weight multiway cut
//! - [`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
//! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem
//! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges
Expand Down Expand Up @@ -60,6 +61,7 @@ pub(crate) mod minimum_sum_multicenter;
pub(crate) mod minimum_vertex_cover;
pub(crate) mod multiple_choice_branching;
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 Down Expand Up @@ -94,6 +96,7 @@ pub use minimum_sum_multicenter::MinimumSumMulticenter;
pub use minimum_vertex_cover::MinimumVertexCover;
pub use multiple_choice_branching::MultipleChoiceBranching;
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 Down Expand Up @@ -130,6 +133,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(biconnectivity_augmentation::canonical_model_example_specs());
specs.extend(bounded_component_spanning_forest::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.extend(steiner_tree::canonical_model_example_specs());
specs.extend(directed_two_commodity_integral_flow::canonical_model_example_specs());
specs.extend(undirected_two_commodity_integral_flow::canonical_model_example_specs());
Expand Down
188 changes: 188 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,188 @@
//! 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",
instance: Box::new(PartitionIntoPathsOfLength2::new(SimpleGraph::new(
6,
vec![(0, 1), (1, 2), (3, 4), (4, 5)],
))),
optimal_config: vec![0, 0, 0, 1, 1, 1],
optimal_value: serde_json::json!(true),
}]
}

#[cfg(test)]
#[path = "../../unit_tests/models/graph/partition_into_paths_of_length_2.rs"]
mod tests;
6 changes: 3 additions & 3 deletions src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ pub use graph::{
LengthBoundedDisjointPaths, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet,
MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching,
OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SteinerTree,
StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman,
UndirectedTwoCommodityIntegralFlow,
OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman,
SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism,
TravelingSalesman, UndirectedTwoCommodityIntegralFlow,
};
pub use misc::PartiallyOrderedKnapsack;
pub use misc::{
Expand Down
Loading
Loading