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
10 changes: 1 addition & 9 deletions rustfmt.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,9 @@ tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Default"

# Imports
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
# Stable import options
reorder_imports = true

# Comments and documentation
format_code_in_doc_comments = true
normalize_comments = true
wrap_comments = true
comment_width = 100

# Formatting style
use_field_init_shorthand = true
use_try_shorthand = true
Expand Down
14 changes: 14 additions & 0 deletions src/models/graph/kclique.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,20 @@ impl<G: Graph> KClique<G> {
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
is_kclique_config(&self.graph, config, self.k)
}

/// Build a binary selection config from the listed vertices.
pub fn config_from_vertices(num_vertices: usize, selected_vertices: &[usize]) -> Vec<usize> {
let mut config = vec![0; num_vertices];
for &vertex in selected_vertices {
config[vertex] = 1;
}
config
}

/// Convenience wrapper around [`Self::config_from_vertices`] using `self.num_vertices()`.
pub fn config_from_selected_vertices(&self, selected_vertices: &[usize]) -> Vec<usize> {
Self::config_from_vertices(self.num_vertices(), selected_vertices)
}
}

impl<G> Problem for KClique<G>
Expand Down
1 change: 0 additions & 1 deletion src/rules/consecutiveonesmatrixaugmentation_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
// Row 0: [1,0,1] needs 1 flip (the middle 0), cost=1
// Row 1: [0,1,1] needs 0 flips, cost=0
// Total = 1 <= 1
let source_config = vec![0, 1, 2];
let reduction: ReductionCOMAToILP = ReduceTo::<ILP<bool>>::reduce_to(&source);
let ilp_solver = crate::solvers::ILPSolver::new();
let target_config = ilp_solver
Expand Down
110 changes: 110 additions & 0 deletions src/rules/exactcoverby3sets_staffscheduling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
//! Reduction from ExactCoverBy3Sets to StaffScheduling.
//!
//! Given an X3C instance with universe X (|X| = 3q) and collection C of
//! 3-element subsets, construct a StaffScheduling instance where:
//! - Each universe element becomes a period (m = 3q periods)
//! - Each subset S_j = {a, b, c} becomes a schedule with shifts at positions a, b, c
//! - All requirements are 1 (each period needs exactly 1 worker)
//! - The worker budget is q (an exact cover uses exactly q subsets)
//! - shifts_per_schedule = 3 (each schedule has exactly 3 active periods)
//!
//! An exact cover in X3C corresponds to a feasible staff assignment.

use crate::models::misc::StaffScheduling;
use crate::models::set::ExactCoverBy3Sets;
use crate::reduction;
use crate::rules::traits::{ReduceTo, ReductionResult};

/// Result of reducing ExactCoverBy3Sets to StaffScheduling.
#[derive(Debug, Clone)]
pub struct ReductionXC3SToStaffScheduling {
target: StaffScheduling,
}

impl ReductionResult for ReductionXC3SToStaffScheduling {
type Source = ExactCoverBy3Sets;
type Target = StaffScheduling;

fn target_problem(&self) -> &StaffScheduling {
&self.target
}

/// Extract XC3S solution from StaffScheduling solution.
///
/// StaffScheduling config[j] = number of workers assigned to schedule j.
/// XC3S config[j] = 1 if subset j is selected, 0 otherwise.
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
target_solution
.iter()
.map(|&count| if count > 0 { 1 } else { 0 })
.collect()
}
}

#[reduction(
overhead = {
num_periods = "universe_size",
num_schedules = "num_subsets",
num_workers = "universe_size / 3",
}
)]
impl ReduceTo<StaffScheduling> for ExactCoverBy3Sets {
type Result = ReductionXC3SToStaffScheduling;

fn reduce_to(&self) -> Self::Result {
let universe_size = self.universe_size();
let q = universe_size / 3;

// Build schedule patterns: one per subset
let schedules: Vec<Vec<bool>> = self
.subsets()
.iter()
.map(|subset| {
let mut schedule = vec![false; universe_size];
for &elem in subset {
schedule[elem] = true;
}
schedule
})
.collect();

// Each period requires exactly 1 worker
let requirements = vec![1u64; universe_size];

let target = StaffScheduling::new(
3, // shifts_per_schedule
schedules,
requirements,
q as u64, // num_workers = q
);

ReductionXC3SToStaffScheduling { 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: "exactcoverby3sets_to_staffscheduling",
build: || {
// Universe {0,1,2,3,4,5}, subsets [{0,1,2}, {3,4,5}, {0,3,4}, {1,2,5}]
// Exact cover: S0 + S1
let source =
ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]);
// In StaffScheduling, assigning 1 worker to schedule 0 and 1 worker to schedule 1
crate::example_db::specs::rule_example_with_witness::<_, StaffScheduling>(
source,
SolutionPair {
source_config: vec![1, 1, 0, 0],
target_config: vec![1, 1, 0, 0],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/exactcoverby3sets_staffscheduling.rs"]
mod tests;
59 changes: 59 additions & 0 deletions src/rules/graph_helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! Shared helpers for graph-based reductions.

use crate::topology::Graph;

/// Extract a Hamiltonian cycle vertex ordering from edge-selection configs on complete graphs.
///
/// Given a graph and a binary `target_solution` over its edges (1 = selected),
/// walks the selected edges to produce a vertex permutation representing the cycle.
/// Returns `vec![0; n]` if the selection does not form a valid Hamiltonian cycle.
pub(crate) fn edges_to_cycle_order<G: Graph>(graph: &G, target_solution: &[usize]) -> Vec<usize> {
let n = graph.num_vertices();
if n == 0 {
return vec![];
}

let edges = graph.edges();
if target_solution.len() != edges.len() {
return vec![0; n];
}

let mut adjacency = vec![Vec::new(); n];
let mut selected_count = 0usize;
for (idx, &selected) in target_solution.iter().enumerate() {
if selected != 1 {
continue;
}
let (u, v) = edges[idx];
adjacency[u].push(v);
adjacency[v].push(u);
selected_count += 1;
}

if selected_count != n || adjacency.iter().any(|neighbors| neighbors.len() != 2) {
return vec![0; n];
}

let mut order = Vec::with_capacity(n);
let mut prev = None;
let mut current = 0usize;

for _ in 0..n {
order.push(current);
let neighbors = &adjacency[current];
let next = match prev {
Some(previous) => {
if neighbors[0] == previous {
neighbors[1]
} else {
neighbors[0]
}
}
None => neighbors[0],
};
prev = Some(current);
current = next;
}

order
}
75 changes: 75 additions & 0 deletions src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//! Reduction from HamiltonianCircuit to BottleneckTravelingSalesman.
//!
//! The standard construction embeds the source graph into the complete graph on the
//! same vertex set, assigning weight 1 to source edges and weight 2 to non-edges.
//! The optimal bottleneck tour equals 1 iff the source graph contains a Hamiltonian circuit.

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

/// Result of reducing HamiltonianCircuit to BottleneckTravelingSalesman.
#[derive(Debug, Clone)]
pub struct ReductionHamiltonianCircuitToBottleneckTravelingSalesman {
target: BottleneckTravelingSalesman,
}

impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesman {
type Source = HamiltonianCircuit<SimpleGraph>;
type Target = BottleneckTravelingSalesman;

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

fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution)
}
}

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

fn reduce_to(&self) -> Self::Result {
let num_vertices = self.num_vertices();
let target_graph = SimpleGraph::complete(num_vertices);
let weights = target_graph
.edges()
.into_iter()
.map(|(u, v)| if self.graph().has_edge(u, v) { 1 } else { 2 })
.collect();
let target = BottleneckTravelingSalesman::new(target_graph, weights);

ReductionHamiltonianCircuitToBottleneckTravelingSalesman { 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: "hamiltoniancircuit_to_bottlenecktravelingsalesman",
build: || {
let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
crate::example_db::specs::rule_example_with_witness::<_, BottleneckTravelingSalesman>(
source,
SolutionPair {
source_config: vec![0, 1, 2, 3],
target_config: vec![1, 0, 1, 1, 0, 1],
},
)
},
}]
}

#[cfg(test)]
#[path = "../unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs"]
mod tests;
Loading
Loading