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
61 changes: 61 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"OptimalLinearArrangement": [Optimal Linear Arrangement],
"RuralPostman": [Rural Postman],
"MixedChinesePostman": [Mixed Chinese Postman],
"StackerCrane": [Stacker Crane],
"LongestCommonSubsequence": [Longest Common Subsequence],
"ExactCoverBy3Sets": [Exact Cover by 3-Sets],
"SubsetSum": [Subset Sum],
Expand Down Expand Up @@ -3662,6 +3663,66 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("StackerCrane")
let arcs = x.instance.arcs.map(a => (a.at(0), a.at(1)))
let edges = x.instance.edges.map(e => (e.at(0), e.at(1)))
let B = x.instance.bound
let config = x.optimal_config
let positions = (
(-2.0, 0.9),
(-2.0, -0.9),
(0.0, -1.5),
(2.0, -0.9),
(0.0, 1.5),
(2.0, 0.9),
)
[
#problem-def("StackerCrane")[
Given a mixed graph $G = (V, A, E)$ with directed arcs $A$, undirected edges $E$, nonnegative lengths $l: A union E -> ZZ_(gt.eq 0)$, and a bound $B in ZZ^+$, determine whether there exists a closed walk in $G$ that traverses every arc in $A$ in its prescribed direction and has total length at most $B$.
][
Stacker Crane is the mixed-graph arc-routing problem listed as ND26 in Garey and Johnson @garey1979. Frederickson, Hecht, and Kim prove the problem NP-complete via reduction from Hamiltonian Circuit and give the classical $9 slash 5$-approximation for the metric case @frederickson1978routing. The problem stays difficult even on trees @fredericksonguan1993. The standard Held-Karp-style dynamic program over (current vertex, covered-arc subset) runs in $O(|V|^2 dot 2^|A|)$ time#footnote[Included as a straightforward exact dynamic-programming baseline over subsets of required arcs; no sharper exact bound was independently verified while preparing this entry.].

A configuration is a permutation of the required arcs, interpreted as the order in which those arcs are forced into the tour. The verifier traverses each chosen arc, then inserts the shortest available connector path from that arc's head to the tail of the next arc, wrapping around at the end to close the walk.

*Example.* The canonical instance has 6 vertices, 5 required arcs, 7 undirected edges, and bound $B = #B$. The witness configuration $[#config.map(str).join(", ")]$ orders the required arcs as $a_0, a_2, a_1, a_4, a_3$. Traversing those arcs contributes 17 units of required-arc length, and the shortest connector paths contribute $1 + 1 + 1 + 0 + 0 = 3$, so the resulting closed walk has total length $20 = B$. Reducing the bound to 19 makes the same instance unsatisfiable.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o stacker-crane.json",
"pred solve stacker-crane.json --solver brute-force",
"pred evaluate stacker-crane.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 1cm, {
import draw: *
let blue = graph-colors.at(0)
let gray = luma(200)

for (u, v) in edges {
line(positions.at(u), positions.at(v), stroke: (paint: gray, thickness: 0.7pt))
}

for (i, (u, v)) in arcs.enumerate() {
line(positions.at(u), positions.at(v), stroke: (paint: blue, thickness: 1.7pt))
let mid = (
(positions.at(u).at(0) + positions.at(v).at(0)) / 2,
(positions.at(u).at(1) + positions.at(v).at(1)) / 2,
)
content(mid, text(6pt, fill: blue)[$a_#i$], fill: white, frame: "rect", padding: 0.05, stroke: none)
}

for (i, pos) in positions.enumerate() {
circle(pos, radius: 0.18, fill: white, stroke: 0.6pt + black)
content(pos, text(7pt)[$v_#i$])
}
}),
caption: [Stacker Crane hourglass instance. Required directed arcs are shown in blue and labeled $a_0$ through $a_4$; undirected connector edges are gray. The satisfying order $a_0, a_2, a_1, a_4, a_3$ yields total length 20.],
) <fig:stacker-crane>
]
]
}

#{
let x = load-model-example("SubgraphIsomorphism")
let nv-host = x.instance.host_graph.num_vertices
Expand Down
21 changes: 21 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,27 @@ @article{frederickson1979
doi = {10.1145/322139.322150}
}

@article{frederickson1978routing,
author = {Greg N. Frederickson and Matthew S. Hecht and Chul E. Kim},
title = {Approximation Algorithms for Some Routing Problems},
journal = {SIAM Journal on Computing},
volume = {7},
number = {2},
pages = {178--193},
year = {1978},
doi = {10.1137/0207017}
}

@article{fredericksonguan1993,
author = {Greg N. Frederickson and Da-Wei Guan},
title = {Nonpreemptive Ensemble Motion Planning on a Tree},
journal = {Journal of Algorithms},
volume = {15},
number = {1},
pages = {29--60},
year = {1993}
}

@article{gottlob2002,
author = {Georg Gottlob and Nicola Leone and Francesco Scarcello},
title = {Hypertree Decompositions and Tractable Queries},
Expand Down
19 changes: 19 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ Flags by problem type:
MinMaxMulticenter (pCenter) --graph, --weights, --edge-weights, --k, --bound
MixedChinesePostman (MCPP) --graph, --arcs, --edge-weights, --arc-costs, --bound [--num-vertices]
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
StackerCrane --arcs, --graph, --arc-costs, --edge-lengths, --bound [--num-vertices]
MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices]
AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys]
ConsistencyOfDatabaseFrequencyTables --num-objects, --attribute-domains, --frequency-tables [--known-values]
Expand Down Expand Up @@ -850,4 +851,22 @@ mod tests {
assert!(help.contains("--potential-edges"));
assert!(help.contains("--budget"));
}

#[test]
fn test_create_help_mentions_stacker_crane_flags() {
let cmd = Cli::command();
let create = cmd.find_subcommand("create").expect("create subcommand");
let help = create
.get_after_help()
.expect("create after_help")
.to_string();

assert!(help.contains("StackerCrane"));
assert!(help.contains("--arcs"));
assert!(help.contains("--graph"));
assert!(help.contains("--arc-costs"));
assert!(help.contains("--edge-lengths"));
assert!(help.contains("--bound"));
assert!(help.contains("--num-vertices"));
}
}
127 changes: 127 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"RuralPostman" => {
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
}
"StackerCrane" => {
"--arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --bound 20 --num-vertices 6"
}
"MultipleChoiceBranching" => {
"--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10"
}
Expand Down Expand Up @@ -671,6 +674,9 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(),
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
("StackerCrane", "edges") => return "graph".to_string(),
("StackerCrane", "arc_lengths") => return "arc-costs".to_string(),
("StackerCrane", "edge_lengths") => return "edge-lengths".to_string(),
("StaffScheduling", "shifts_per_schedule") => return "k".to_string(),
("TimetableDesign", "num_tasks") => return "num-tasks".to_string(),
_ => {}
Expand Down Expand Up @@ -1518,6 +1524,51 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// StackerCrane
"StackerCrane" => {
let usage = "Usage: pred create StackerCrane --arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --bound 20 --num-vertices 6";
let arcs_str = args
.arcs
.as_deref()
.ok_or_else(|| anyhow::anyhow!("StackerCrane requires --arcs\n\n{usage}"))?;
let (arcs_graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?;
let (edges_graph, num_vertices) =
parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
anyhow::ensure!(
edges_graph.num_vertices() == num_vertices,
"internal error: inconsistent graph vertex count"
);
anyhow::ensure!(
num_vertices == arcs_graph.num_vertices(),
"StackerCrane requires the directed and undirected inputs to agree on --num-vertices\n\n{usage}"
);
let arc_lengths = parse_arc_costs(args, num_arcs)?;
let edge_lengths = parse_i32_edge_values(
args.edge_lengths.as_ref(),
edges_graph.num_edges(),
"edge length",
)?;
let bound_raw = args
.bound
.ok_or_else(|| anyhow::anyhow!("StackerCrane requires --bound\n\n{usage}"))?;
let bound = parse_nonnegative_usize_bound(bound_raw, "StackerCrane", usage)?;
let bound = i32::try_from(bound).map_err(|_| {
anyhow::anyhow!("StackerCrane --bound must fit in i32 (got {bound_raw})\n\n{usage}")
})?;
(
ser(StackerCrane::try_new(
num_vertices,
arcs_graph.arcs(),
edges_graph.edges(),
arc_lengths,
edge_lengths,
bound,
)
.map_err(|e| anyhow::anyhow!(e))?)?,
resolved_variant.clone(),
)
}

// MultipleChoiceBranching
"MultipleChoiceBranching" => {
let usage = "Usage: pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10";
Expand Down Expand Up @@ -6016,6 +6067,82 @@ mod tests {
std::fs::remove_file(output_path).ok();
}

#[test]
fn test_create_stacker_crane_json() {
let mut args = empty_args();
args.problem = Some("StackerCrane".to_string());
args.num_vertices = Some(6);
args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string());
args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string());
args.arc_costs = Some("3,4,2,5,3".to_string());
args.edge_lengths = Some("2,1,3,2,1,4,3".to_string());
args.bound = Some(20);

let output_path = std::env::temp_dir().join("pred_test_create_stacker_crane.json");
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let content = std::fs::read_to_string(&output_path).unwrap();
let json: serde_json::Value = serde_json::from_str(&content).unwrap();
assert_eq!(json["type"], "StackerCrane");
assert_eq!(json["data"]["num_vertices"], 6);
assert_eq!(json["data"]["bound"], 20);
assert_eq!(json["data"]["arcs"][0], serde_json::json!([0, 4]));
assert_eq!(json["data"]["edge_lengths"][6], 3);

std::fs::remove_file(output_path).ok();
}

#[test]
fn test_create_stacker_crane_rejects_mismatched_arc_lengths() {
let mut args = empty_args();
args.problem = Some("StackerCrane".to_string());
args.num_vertices = Some(6);
args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string());
args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string());
args.arc_costs = Some("3,4,2,5".to_string());
args.edge_lengths = Some("2,1,3,2,1,4,3".to_string());
args.bound = Some(20);

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("Expected 5 arc costs but got 4"));
}

#[test]
fn test_create_stacker_crane_rejects_out_of_range_vertices() {
let mut args = empty_args();
args.problem = Some("StackerCrane".to_string());
args.num_vertices = Some(5);
args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string());
args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string());
args.arc_costs = Some("3,4,2,5,3".to_string());
args.edge_lengths = Some("2,1,3,2,1,4,3".to_string());
args.bound = Some(20);

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("--num-vertices (5) is too small for the arcs"));
}

#[test]
fn test_create_balanced_complete_bipartite_subgraph() {
use crate::dispatch::ProblemJsonOutput;
Expand Down
18 changes: 18 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,24 @@ fn test_show_balanced_complete_bipartite_subgraph_complexity() {
);
}

#[test]
fn test_create_stacker_crane_schema_help_uses_documented_flags() {
let output = pred().args(["create", "StackerCrane"]).output().unwrap();
assert!(!output.status.success());

let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("StackerCrane"), "stderr: {stderr}");
assert!(stderr.contains("--arcs"), "stderr: {stderr}");
assert!(stderr.contains("--graph"), "stderr: {stderr}");
assert!(stderr.contains("--arc-costs"), "stderr: {stderr}");
assert!(stderr.contains("--edge-lengths"), "stderr: {stderr}");
assert!(stderr.contains("--bound"), "stderr: {stderr}");
assert!(stderr.contains("--num-vertices"), "stderr: {stderr}");
assert!(!stderr.contains("--biedges"), "stderr: {stderr}");
assert!(!stderr.contains("--arc-lengths"), "stderr: {stderr}");
assert!(!stderr.contains("--edge-weights"), "stderr: {stderr}");
}

#[test]
fn test_solve_balanced_complete_bipartite_subgraph_suggests_bruteforce() {
let tmp = std::env::temp_dir().join("pred_test_bcbs_problem.json");
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ pub mod prelude {
SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost,
SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness,
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,
SumOfSquaresPartition, Term, TimetableDesign,
ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection,
SubsetSum, SumOfSquaresPartition, Term, TimetableDesign,
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
Expand Down
4 changes: 4 additions & 0 deletions src/models/misc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
//! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles
//! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints
//! - [`SchedulingWithIndividualDeadlines`]: Meet per-task deadlines on parallel processors
//! - [`StackerCrane`]: Route a crane through required arcs within a length bound
//! - [`SequencingToMinimizeMaximumCumulativeCost`]: Keep every cumulative schedule cost prefix under a bound
//! - [`SequencingToMinimizeWeightedCompletionTime`]: Minimize total weighted completion time
//! - [`SequencingToMinimizeWeightedTardiness`]: Decide whether a schedule meets a weighted tardiness bound
Expand Down Expand Up @@ -57,6 +58,7 @@ mod sequencing_to_minimize_weighted_tardiness;
mod sequencing_with_release_times_and_deadlines;
mod sequencing_within_intervals;
pub(crate) mod shortest_common_supersequence;
mod stacker_crane;
mod staff_scheduling;
pub(crate) mod string_to_string_correction;
mod subset_sum;
Expand Down Expand Up @@ -91,6 +93,7 @@ pub use sequencing_to_minimize_weighted_tardiness::SequencingToMinimizeWeightedT
pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines;
pub use sequencing_within_intervals::SequencingWithinIntervals;
pub use shortest_common_supersequence::ShortestCommonSupersequence;
pub use stacker_crane::StackerCrane;
pub use staff_scheduling::StaffScheduling;
pub use string_to_string_correction::StringToStringCorrection;
pub use subset_sum::SubsetSum;
Expand All @@ -114,6 +117,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(scheduling_with_individual_deadlines::canonical_model_example_specs());
specs.extend(sequencing_within_intervals::canonical_model_example_specs());
specs.extend(staff_scheduling::canonical_model_example_specs());
specs.extend(stacker_crane::canonical_model_example_specs());
specs.extend(timetable_design::canonical_model_example_specs());
specs.extend(shortest_common_supersequence::canonical_model_example_specs());
specs.extend(resource_constrained_scheduling::canonical_model_example_specs());
Expand Down
Loading
Loading