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

#{
let x = load-model-example("MixedChinesePostman", variant: (weight: "i32"))
let nv = x.instance.graph.num_vertices
let arcs = x.instance.graph.arcs
let edges = x.instance.graph.edges
let arc-weights = x.instance.arc_weights
let edge-weights = x.instance.edge_weights
let B = x.instance.bound
let config = x.optimal_config
let oriented = edges.enumerate().map(((i, e)) => if config.at(i) == 0 { e } else { (e.at(1), e.at(0)) })
let base-cost = arc-weights.sum() + edge-weights.sum()
let total-cost = 22
[
#problem-def("MixedChinesePostman")[
Given a mixed graph $G = (V, A, E)$ with directed arcs $A$, undirected edges $E$, integer lengths $l(e) >= 0$ for every $e in A union E$, and a bound $B in ZZ^+$, determine whether there exists a closed walk in $G$ that traverses every arc in its prescribed direction and every undirected edge at least once in some direction with total length at most $B$.
][
Mixed Chinese Postman is the mixed-graph arc-routing problem ND25 in Garey and Johnson @garey1979. Papadimitriou proved the mixed case NP-complete even when all lengths are 1, the graph is planar, and the maximum degree is 3 @papadimitriou1976edge. In contrast, the pure undirected and pure directed cases are polynomial-time solvable via matching / circulation machinery @edmondsjohnson1973. The implementation here uses one binary variable per undirected edge orientation, so the search space contributes the $2^|E|$ factor visible in the registered exact bound.

*Example.* Consider the instance on #nv vertices with directed arcs $(v_0, v_1)$, $(v_1, v_2)$, $(v_2, v_3)$, $(v_3, v_0)$ of lengths $2, 3, 1, 4$ and undirected edges $\{v_0, v_2\}$, $\{v_1, v_3\}$, $\{v_0, v_4\}$, $\{v_4, v_2\}$ of lengths $2, 3, 1, 2$. The config $(1, 1, 0, 0)$ orients those edges as $(v_2, v_0)$, $(v_3, v_1)$, $(v_0, v_4)$, and $(v_4, v_2)$, producing a strongly connected digraph. The base traversal cost is #base-cost, and duplicating the shortest path $v_1 arrow v_2 arrow v_3$ adds 4 more, so the total cost is $#total-cost <= B = #B$, proving the answer is YES.

#pred-commands(
"pred create --example MixedChinesePostman/i32 -o mixed-chinese-postman.json",
"pred solve mixed-chinese-postman.json --solver brute-force",
"pred evaluate mixed-chinese-postman.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 1cm, {
import draw: *
let positions = (
(-1.25, 0.85),
(1.25, 0.85),
(1.25, -0.85),
(-1.25, -0.85),
(0.25, 0.0),
)

for (idx, (u, v)) in arcs.enumerate() {
line(
positions.at(u),
positions.at(v),
stroke: 0.8pt + luma(80),
mark: (end: "straight", scale: 0.45),
)
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: luma(40))[#arc-weights.at(idx)],
fill: white,
frame: "rect",
padding: 0.04,
stroke: none,
)
}

for (idx, (u, v)) in oriented.enumerate() {
line(
positions.at(u),
positions.at(v),
stroke: 1.3pt + graph-colors.at(0),
mark: (end: "straight", scale: 0.5),
)
let mid = (
(positions.at(u).at(0) + positions.at(v).at(0)) / 2,
(positions.at(u).at(1) + positions.at(v).at(1)) / 2,
)
let offset = if idx == 0 { (-0.18, 0.12) } else if idx == 1 { (0.18, 0.12) } else if idx == 2 { (-0.12, -0.1) } else { (0.12, -0.1) }
content(
(mid.at(0) + offset.at(0), mid.at(1) + offset.at(1)),
text(6pt, fill: graph-colors.at(0))[#edge-weights.at(idx)],
fill: white,
frame: "rect",
padding: 0.04,
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: [Mixed Chinese Postman example. Gray arrows are the original directed arcs, while blue arrows are the chosen orientations of the former undirected edges under config $(1, 1, 0, 0)$. Duplicating the path $v_1 arrow v_2 arrow v_3$ yields total cost #total-cost.],
) <fig:mixed-chinese-postman>
]
]
}

#{
let x = load-model-example("SubgraphIsomorphism")
let nv-host = x.instance.host_graph.num_vertices
Expand Down
19 changes: 19 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -1282,3 +1282,22 @@ @inproceedings{williams2002
pages = {299--307},
year = {2002}
}

@article{papadimitriou1976edge,
author = {Christos H. Papadimitriou},
title = {On the Complexity of Edge Traversing},
journal = {Journal of the ACM},
volume = {23},
number = {3},
pages = {544--554},
year = {1976}
}

@article{edmondsjohnson1973,
author = {Jack Edmonds and Ellis L. Johnson},
title = {Matching, Euler Tours and the Chinese Postman},
journal = {Mathematical Programming},
volume = {5},
pages = {88--124},
year = {1973}
}
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ Flags by problem type:
SequencingWithinIntervals --release-times, --deadlines, --lengths
OptimalLinearArrangement --graph, --bound
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
MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices]
AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys]
Expand Down
98 changes: 86 additions & 12 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use problemreductions::models::algebraic::{
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath,
LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut,
LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut, MixedChinesePostman,
MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
};
use problemreductions::models::misc::{
Expand All @@ -31,8 +31,8 @@ use problemreductions::models::BiconnectivityAugmentation;
use problemreductions::prelude::*;
use problemreductions::registry::collect_schemas;
use problemreductions::topology::{
BipartiteGraph, DirectedGraph, Graph, KingsSubgraph, SimpleGraph, TriangularSubgraph,
UnitDiskGraph,
BipartiteGraph, DirectedGraph, Graph, KingsSubgraph, MixedGraph, SimpleGraph,
TriangularSubgraph, UnitDiskGraph,
};
use serde::Serialize;
use std::collections::{BTreeMap, BTreeSet};
Expand Down Expand Up @@ -584,6 +584,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"StrongConnectivityAugmentation" => {
"--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1"
}
"MixedChinesePostman" => {
"--graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4 --bound 24"
}
"RuralPostman" => {
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
}
Expand Down Expand Up @@ -643,7 +646,12 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
fn uses_edge_weights_flag(canonical: &str) -> bool {
matches!(
canonical,
"KthBestSpanningTree" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" | "RuralPostman"
"KthBestSpanningTree"
| "MaxCut"
| "MaximumMatching"
| "TravelingSalesman"
| "RuralPostman"
| "MixedChinesePostman"
)
}

Expand All @@ -661,6 +669,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
("PrimeAttributeName", "num_attributes") => return "universe".to_string(),
("PrimeAttributeName", "dependencies") => return "deps".to_string(),
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(),
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
("StaffScheduling", "shifts_per_schedule") => return "k".to_string(),
("TimetableDesign", "num_tasks") => return "num-tasks".to_string(),
Expand Down Expand Up @@ -827,6 +836,15 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
// DirectedGraph fields use --arcs, not --graph
let hint = type_format_hint(&field.type_name, graph_type);
eprintln!(" --{:<16} {} ({})", "arcs", field.description, hint);
} else if field.type_name == "MixedGraph" {
eprintln!(
" --{:<16} {} ({})",
"graph", "Undirected edges E of the mixed graph", "edge list: 0-1,1-2,2-3"
);
eprintln!(
" --{:<16} {} ({})",
"arcs", "Directed arcs A of the mixed graph", "directed arcs: 0>1,1>2,2>0"
);
} else if field.type_name == "BipartiteGraph" {
eprintln!(
" --{:<16} {} ({})",
Expand Down Expand Up @@ -876,6 +894,9 @@ fn problem_help_flag_name(
if field_type == "DirectedGraph" {
return "arcs".to_string();
}
if field_type == "MixedGraph" {
return "graph".to_string();
}
if canonical == "LengthBoundedDisjointPaths" && field_name == "max_length" {
return "bound".to_string();
}
Expand Down Expand Up @@ -3003,9 +3024,10 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
// AcyclicPartition
"AcyclicPartition" => {
let usage = "Usage: pred create AcyclicPartition/i32 --arcs \"0>1,0>2,1>3,1>4,2>4,2>5,3>5,4>5\" --weights 2,3,2,1,3,1 --arc-costs 1,1,1,1,1,1,1,1 --weight-bound 5 --cost-bound 5";
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!("AcyclicPartition requires --arcs\n\n{usage}")
})?;
let arcs_str = args
.arcs
.as_deref()
.ok_or_else(|| anyhow::anyhow!("AcyclicPartition requires --arcs\n\n{usage}"))?;
let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?;
let vertex_weights = parse_vertex_weights(args, graph.num_vertices())?;
let arc_costs = parse_arc_costs(args, num_arcs)?;
Expand Down Expand Up @@ -3109,6 +3131,46 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MixedChinesePostman
"MixedChinesePostman" => {
let usage = "Usage: pred create MixedChinesePostman --graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4 --bound 24 [--num-vertices N]";
let graph = parse_mixed_graph(args, usage)?;
let arc_costs = parse_arc_costs(args, graph.num_arcs())?;
let edge_weights = parse_edge_weights(args, graph.num_edges())?;
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!("MixedChinesePostman requires --bound\n\n{usage}")
})?;
let bound = i32::try_from(bound).map_err(|_| {
anyhow::anyhow!(
"MixedChinesePostman --bound must fit in i32 (got {bound})\n\n{usage}"
)
})?;
if arc_costs.iter().any(|&cost| cost < 0) {
bail!("MixedChinesePostman --arc-costs must be non-negative\n\n{usage}");
}
if edge_weights.iter().any(|&weight| weight < 0) {
bail!("MixedChinesePostman --edge-weights must be non-negative\n\n{usage}");
}
if resolved_variant.get("weight").map(|w| w.as_str()) == Some("One")
&& (arc_costs.iter().any(|&cost| cost != 1)
|| edge_weights.iter().any(|&weight| weight != 1))
{
bail!(
"Non-unit lengths are not supported for MixedChinesePostman/One.\n\n\
Use the weighted variant instead:\n pred create MixedChinesePostman/i32 --graph ... --arcs ... --edge-weights ... --arc-costs ..."
);
}
(
ser(MixedChinesePostman::new(
graph,
arc_costs,
edge_weights,
bound,
))?,
resolved_variant.clone(),
)
}

// MinimumSumMulticenter (p-median)
"MinimumSumMulticenter" => {
let (graph, n) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -4759,6 +4821,22 @@ fn parse_directed_graph(
Ok((DirectedGraph::new(num_v, arcs), num_arcs))
}

fn parse_mixed_graph(args: &CreateArgs, usage: &str) -> Result<MixedGraph> {
let (undirected_graph, num_vertices) =
parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let arcs_str = args
.arcs
.as_deref()
.ok_or_else(|| anyhow::anyhow!("MixedChinesePostman requires --arcs\n\n{usage}"))?;
let (directed_graph, _) = parse_directed_graph(arcs_str, Some(num_vertices))
.map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
Ok(MixedGraph::new(
num_vertices,
directed_graph.arcs(),
undirected_graph.edges(),
))
}

/// Parse `--weights` as arc weights (i32), defaulting to all 1s.
fn parse_arc_weights(args: &CreateArgs, num_arcs: usize) -> Result<Vec<i32>> {
match &args.weights {
Expand Down Expand Up @@ -4789,11 +4867,7 @@ fn parse_arc_costs(args: &CreateArgs, num_arcs: usize) -> Result<Vec<i32>> {
.map(|s| s.trim().parse::<i32>())
.collect::<std::result::Result<Vec<_>, _>>()?;
if parsed.len() != num_arcs {
bail!(
"Expected {} arc costs but got {}",
num_arcs,
parsed.len()
);
bail!("Expected {} arc costs but got {}", num_arcs, parsed.len());
}
Ok(parsed)
}
Expand Down
Loading
Loading