Skip to content

Commit 704ed6a

Browse files
zazabapclaudeisPANNGiggleLiu
authored
feat: add 8 Tier 1a reduction rules (#770) (#777)
* feat: add 8 Tier 1a reduction rules (#770) Implement 8 simple, verified reduction rules from Garey & Johnson: - HamiltonianCircuit → HamiltonianPath (#199): vertex duplication + pendants - KClique → SubgraphIsomorphism (#201): pattern = K_k - Partition → MultiprocessorScheduling (#203): m=2, D=S/2 - HamiltonianPath → IsomorphicSpanningTree (#234): T = P_n - HamiltonianCircuit → BottleneckTravelingSalesman (#259): {1,2}-distances - KClique → ConjunctiveBooleanQuery (#464): k-clique as Boolean CQ - ExactCoverBy3Sets → StaffScheduling (#487): subset-to-schedule encoding - Satisfiability → NAESatisfiability (#753): sentinel variable construction Each rule includes full test coverage (closed-loop, edge cases, extraction). Fixes #199, Fixes #201, Fixes #203, Fixes #234, Fixes #259, Fixes #464, Fixes #487, Fixes #753 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR #777 review comments - HC→HP: special-case n < 3 to avoid panic on empty/tiny graphs - SAT→NAESAT: handle empty clauses (map to fixed unsatisfiable NAE clause) - SAT→NAESAT: guard extract_solution against short configs with assert - SAT→NAESAT test: fix comment (variable 3 → variable 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: extract shared cycle-walking helper, simplify reductions - Extract duplicated edges_to_cycle_order logic from HC→TSP and HC→BTSP into graph_helpers module (-110 lines) - Use SimpleGraph::complete(k) in KClique→SubgraphIsomorphism - Use .edges() directly in KClique→CBQ instead of O(n²) has_edge loop - Replace assert! with graceful fallback in SAT→NAE-SAT extract_solution Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Refine tier1a rule implementations - simplify new graph reductions by using shared graph constructors and a shared KClique selection helper - reduce control-flow and repeated setup in HamiltonianCircuit/Partition-related rule code and tests - add regression coverage for HamiltonianCircuit small-n and Satisfiability empty-clause edge cases - remove the unused example variable warning and align rustfmt config with the stable toolchain * Fix duplicate docstring on KClique::config_from_selected_vertices Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent a69b719 commit 704ed6a

23 files changed

+1892
-63
lines changed

rustfmt.toml

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,9 @@ tab_spaces = 4
88
newline_style = "Unix"
99
use_small_heuristics = "Default"
1010

11-
# Imports
12-
imports_granularity = "Crate"
13-
group_imports = "StdExternalCrate"
11+
# Stable import options
1412
reorder_imports = true
1513

16-
# Comments and documentation
17-
format_code_in_doc_comments = true
18-
normalize_comments = true
19-
wrap_comments = true
20-
comment_width = 100
21-
2214
# Formatting style
2315
use_field_init_shorthand = true
2416
use_try_shorthand = true

src/models/graph/kclique.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,20 @@ impl<G: Graph> KClique<G> {
6666
pub fn is_valid_solution(&self, config: &[usize]) -> bool {
6767
is_kclique_config(&self.graph, config, self.k)
6868
}
69+
70+
/// Build a binary selection config from the listed vertices.
71+
pub fn config_from_vertices(num_vertices: usize, selected_vertices: &[usize]) -> Vec<usize> {
72+
let mut config = vec![0; num_vertices];
73+
for &vertex in selected_vertices {
74+
config[vertex] = 1;
75+
}
76+
config
77+
}
78+
79+
/// Convenience wrapper around [`Self::config_from_vertices`] using `self.num_vertices()`.
80+
pub fn config_from_selected_vertices(&self, selected_vertices: &[usize]) -> Vec<usize> {
81+
Self::config_from_vertices(self.num_vertices(), selected_vertices)
82+
}
6983
}
7084

7185
impl<G> Problem for KClique<G>

src/rules/consecutiveonesmatrixaugmentation_ilp.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::Ru
182182
// Row 0: [1,0,1] needs 1 flip (the middle 0), cost=1
183183
// Row 1: [0,1,1] needs 0 flips, cost=0
184184
// Total = 1 <= 1
185-
let source_config = vec![0, 1, 2];
186185
let reduction: ReductionCOMAToILP = ReduceTo::<ILP<bool>>::reduce_to(&source);
187186
let ilp_solver = crate::solvers::ILPSolver::new();
188187
let target_config = ilp_solver
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! Reduction from ExactCoverBy3Sets to StaffScheduling.
2+
//!
3+
//! Given an X3C instance with universe X (|X| = 3q) and collection C of
4+
//! 3-element subsets, construct a StaffScheduling instance where:
5+
//! - Each universe element becomes a period (m = 3q periods)
6+
//! - Each subset S_j = {a, b, c} becomes a schedule with shifts at positions a, b, c
7+
//! - All requirements are 1 (each period needs exactly 1 worker)
8+
//! - The worker budget is q (an exact cover uses exactly q subsets)
9+
//! - shifts_per_schedule = 3 (each schedule has exactly 3 active periods)
10+
//!
11+
//! An exact cover in X3C corresponds to a feasible staff assignment.
12+
13+
use crate::models::misc::StaffScheduling;
14+
use crate::models::set::ExactCoverBy3Sets;
15+
use crate::reduction;
16+
use crate::rules::traits::{ReduceTo, ReductionResult};
17+
18+
/// Result of reducing ExactCoverBy3Sets to StaffScheduling.
19+
#[derive(Debug, Clone)]
20+
pub struct ReductionXC3SToStaffScheduling {
21+
target: StaffScheduling,
22+
}
23+
24+
impl ReductionResult for ReductionXC3SToStaffScheduling {
25+
type Source = ExactCoverBy3Sets;
26+
type Target = StaffScheduling;
27+
28+
fn target_problem(&self) -> &StaffScheduling {
29+
&self.target
30+
}
31+
32+
/// Extract XC3S solution from StaffScheduling solution.
33+
///
34+
/// StaffScheduling config[j] = number of workers assigned to schedule j.
35+
/// XC3S config[j] = 1 if subset j is selected, 0 otherwise.
36+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
37+
target_solution
38+
.iter()
39+
.map(|&count| if count > 0 { 1 } else { 0 })
40+
.collect()
41+
}
42+
}
43+
44+
#[reduction(
45+
overhead = {
46+
num_periods = "universe_size",
47+
num_schedules = "num_subsets",
48+
num_workers = "universe_size / 3",
49+
}
50+
)]
51+
impl ReduceTo<StaffScheduling> for ExactCoverBy3Sets {
52+
type Result = ReductionXC3SToStaffScheduling;
53+
54+
fn reduce_to(&self) -> Self::Result {
55+
let universe_size = self.universe_size();
56+
let q = universe_size / 3;
57+
58+
// Build schedule patterns: one per subset
59+
let schedules: Vec<Vec<bool>> = self
60+
.subsets()
61+
.iter()
62+
.map(|subset| {
63+
let mut schedule = vec![false; universe_size];
64+
for &elem in subset {
65+
schedule[elem] = true;
66+
}
67+
schedule
68+
})
69+
.collect();
70+
71+
// Each period requires exactly 1 worker
72+
let requirements = vec![1u64; universe_size];
73+
74+
let target = StaffScheduling::new(
75+
3, // shifts_per_schedule
76+
schedules,
77+
requirements,
78+
q as u64, // num_workers = q
79+
);
80+
81+
ReductionXC3SToStaffScheduling { target }
82+
}
83+
}
84+
85+
#[cfg(feature = "example-db")]
86+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
87+
use crate::export::SolutionPair;
88+
89+
vec![crate::example_db::specs::RuleExampleSpec {
90+
id: "exactcoverby3sets_to_staffscheduling",
91+
build: || {
92+
// Universe {0,1,2,3,4,5}, subsets [{0,1,2}, {3,4,5}, {0,3,4}, {1,2,5}]
93+
// Exact cover: S0 + S1
94+
let source =
95+
ExactCoverBy3Sets::new(6, vec![[0, 1, 2], [3, 4, 5], [0, 3, 4], [1, 2, 5]]);
96+
// In StaffScheduling, assigning 1 worker to schedule 0 and 1 worker to schedule 1
97+
crate::example_db::specs::rule_example_with_witness::<_, StaffScheduling>(
98+
source,
99+
SolutionPair {
100+
source_config: vec![1, 1, 0, 0],
101+
target_config: vec![1, 1, 0, 0],
102+
},
103+
)
104+
},
105+
}]
106+
}
107+
108+
#[cfg(test)]
109+
#[path = "../unit_tests/rules/exactcoverby3sets_staffscheduling.rs"]
110+
mod tests;

src/rules/graph_helpers.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//! Shared helpers for graph-based reductions.
2+
3+
use crate::topology::Graph;
4+
5+
/// Extract a Hamiltonian cycle vertex ordering from edge-selection configs on complete graphs.
6+
///
7+
/// Given a graph and a binary `target_solution` over its edges (1 = selected),
8+
/// walks the selected edges to produce a vertex permutation representing the cycle.
9+
/// Returns `vec![0; n]` if the selection does not form a valid Hamiltonian cycle.
10+
pub(crate) fn edges_to_cycle_order<G: Graph>(graph: &G, target_solution: &[usize]) -> Vec<usize> {
11+
let n = graph.num_vertices();
12+
if n == 0 {
13+
return vec![];
14+
}
15+
16+
let edges = graph.edges();
17+
if target_solution.len() != edges.len() {
18+
return vec![0; n];
19+
}
20+
21+
let mut adjacency = vec![Vec::new(); n];
22+
let mut selected_count = 0usize;
23+
for (idx, &selected) in target_solution.iter().enumerate() {
24+
if selected != 1 {
25+
continue;
26+
}
27+
let (u, v) = edges[idx];
28+
adjacency[u].push(v);
29+
adjacency[v].push(u);
30+
selected_count += 1;
31+
}
32+
33+
if selected_count != n || adjacency.iter().any(|neighbors| neighbors.len() != 2) {
34+
return vec![0; n];
35+
}
36+
37+
let mut order = Vec::with_capacity(n);
38+
let mut prev = None;
39+
let mut current = 0usize;
40+
41+
for _ in 0..n {
42+
order.push(current);
43+
let neighbors = &adjacency[current];
44+
let next = match prev {
45+
Some(previous) => {
46+
if neighbors[0] == previous {
47+
neighbors[1]
48+
} else {
49+
neighbors[0]
50+
}
51+
}
52+
None => neighbors[0],
53+
};
54+
prev = Some(current);
55+
current = next;
56+
}
57+
58+
order
59+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
//! Reduction from HamiltonianCircuit to BottleneckTravelingSalesman.
2+
//!
3+
//! The standard construction embeds the source graph into the complete graph on the
4+
//! same vertex set, assigning weight 1 to source edges and weight 2 to non-edges.
5+
//! The optimal bottleneck tour equals 1 iff the source graph contains a Hamiltonian circuit.
6+
7+
use crate::models::graph::{BottleneckTravelingSalesman, HamiltonianCircuit};
8+
use crate::reduction;
9+
use crate::rules::traits::{ReduceTo, ReductionResult};
10+
use crate::topology::{Graph, SimpleGraph};
11+
12+
/// Result of reducing HamiltonianCircuit to BottleneckTravelingSalesman.
13+
#[derive(Debug, Clone)]
14+
pub struct ReductionHamiltonianCircuitToBottleneckTravelingSalesman {
15+
target: BottleneckTravelingSalesman,
16+
}
17+
18+
impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesman {
19+
type Source = HamiltonianCircuit<SimpleGraph>;
20+
type Target = BottleneckTravelingSalesman;
21+
22+
fn target_problem(&self) -> &Self::Target {
23+
&self.target
24+
}
25+
26+
fn extract_solution(&self, target_solution: &[usize]) -> Vec<usize> {
27+
crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution)
28+
}
29+
}
30+
31+
#[reduction(
32+
overhead = {
33+
num_vertices = "num_vertices",
34+
num_edges = "num_vertices * (num_vertices - 1) / 2",
35+
}
36+
)]
37+
impl ReduceTo<BottleneckTravelingSalesman> for HamiltonianCircuit<SimpleGraph> {
38+
type Result = ReductionHamiltonianCircuitToBottleneckTravelingSalesman;
39+
40+
fn reduce_to(&self) -> Self::Result {
41+
let num_vertices = self.num_vertices();
42+
let target_graph = SimpleGraph::complete(num_vertices);
43+
let weights = target_graph
44+
.edges()
45+
.into_iter()
46+
.map(|(u, v)| if self.graph().has_edge(u, v) { 1 } else { 2 })
47+
.collect();
48+
let target = BottleneckTravelingSalesman::new(target_graph, weights);
49+
50+
ReductionHamiltonianCircuitToBottleneckTravelingSalesman { target }
51+
}
52+
}
53+
54+
#[cfg(feature = "example-db")]
55+
pub(crate) fn canonical_rule_example_specs() -> Vec<crate::example_db::specs::RuleExampleSpec> {
56+
use crate::export::SolutionPair;
57+
58+
vec![crate::example_db::specs::RuleExampleSpec {
59+
id: "hamiltoniancircuit_to_bottlenecktravelingsalesman",
60+
build: || {
61+
let source = HamiltonianCircuit::new(SimpleGraph::cycle(4));
62+
crate::example_db::specs::rule_example_with_witness::<_, BottleneckTravelingSalesman>(
63+
source,
64+
SolutionPair {
65+
source_config: vec![0, 1, 2, 3],
66+
target_config: vec![1, 0, 1, 1, 0, 1],
67+
},
68+
)
69+
},
70+
}]
71+
}
72+
73+
#[cfg(test)]
74+
#[path = "../unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs"]
75+
mod tests;

0 commit comments

Comments
 (0)