Skip to content

Commit e652ba6

Browse files
zazabapclaudeGiggleLiu
authored
Fix #248: Add RuralPostman model (#608)
* Add plan for #248: [Model] RuralPostman Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Implement #248: Add RuralPostman model Add the Rural Postman Problem as a satisfaction problem: given a graph with edge lengths, a required subset of edges, and a bound B, determine if a circuit exists covering all required edges within the bound. - Model: src/models/graph/rural_postman.rs (SatisfactionProblem, Metric=bool) - 16 unit tests covering valid/invalid circuits, brute force, serialization - CLI: dispatch, alias (RPP), create handler with --required-edges/--bound - Paper: display-name + problem-def entry Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan file after implementation * Fix Copilot review comments on RuralPostman 1. Rename schema field edge_lengths → edge_weights for CLI consistency 2. Fix subset symbol inconsistency in paper (subset → subset.eq) 3. Allow edge multiplicity {0,1,2} instead of binary selection to correctly model circuits that traverse edges multiple times (RPP semantics) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 6401e7d commit e652ba6

11 files changed

Lines changed: 586 additions & 2 deletions

File tree

docs/paper/reductions.typ

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"BicliqueCover": [Biclique Cover],
5454
"BinPacking": [Bin Packing],
5555
"ClosestVectorProblem": [Closest Vector Problem],
56+
"RuralPostman": [Rural Postman],
5657
"LongestCommonSubsequence": [Longest Common Subsequence],
5758
"SubsetSum": [Subset Sum],
5859
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
@@ -981,6 +982,14 @@ Biclique Cover is equivalent to factoring the biadjacency matrix $M$ of the bipa
981982
*Example.* Let $n = 4$ items with weights $(2, 3, 4, 5)$, values $(3, 4, 5, 7)$, and capacity $C = 7$. Selecting $S = {1, 2}$ (items with weights 3 and 4) gives total weight $3 + 4 = 7 lt.eq C$ and total value $4 + 5 = 9$. Selecting $S = {0, 3}$ (weights 2 and 5) gives weight $2 + 5 = 7 lt.eq C$ and value $3 + 7 = 10$, which is optimal.
982983
]
983984

985+
#problem-def("RuralPostman")[
986+
Given an undirected graph $G = (V, E)$ with edge lengths $l: E -> ZZ_(gt.eq 0)$, a subset $E' subset.eq E$ of required edges, and a bound $B in ZZ^+$, determine whether there exists a circuit (closed walk) in $G$ that traverses every edge in $E'$ and has total length at most $B$.
987+
][
988+
The Rural Postman Problem (RPP) is a fundamental NP-complete arc-routing problem @lenstra1976 that generalizes the Chinese Postman Problem. When $E' = E$, the problem reduces to finding an Eulerian circuit with minimum augmentation (polynomial-time solvable via $T$-join matching). For general $E' subset.eq E$, exact algorithms use dynamic programming over subsets of required edges in $O(n^2 dot 2^r)$ time, where $r = |E'|$ and $n = |V|$, analogous to the Held-Karp algorithm for TSP. The problem admits a $3 slash 2$-approximation for metric instances @frederickson1979.
989+
990+
*Example.* Consider a hexagonal graph with 6 vertices and 8 edges, where all outer edges have length 1 and two diagonal edges have length 2. The required edges are $E' = {(v_0, v_1), (v_2, v_3), (v_4, v_5)}$ with bound $B = 6$. The outer cycle $v_0 -> v_1 -> v_2 -> v_3 -> v_4 -> v_5 -> v_0$ covers all three required edges with total length $6 times 1 = 6 = B$, so the answer is YES.
991+
]
992+
984993
#problem-def("LongestCommonSubsequence")[
985994
Given $k$ strings $s_1, dots, s_k$ over a finite alphabet $Sigma$, find a longest string $w$ that is a subsequence of every $s_i$. A string $w$ is a _subsequence_ of $s$ if $w$ can be obtained by deleting zero or more characters from $s$ without changing the order of the remaining characters.
986995
][

docs/src/reductions/problem_schemas.json

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,17 @@
195195
}
196196
]
197197
},
198+
{
199+
"name": "LongestCommonSubsequence",
200+
"description": "Find the longest string that is a subsequence of every input string",
201+
"fields": [
202+
{
203+
"name": "strings",
204+
"type_name": "Vec<Vec<u8>>",
205+
"description": "The input strings"
206+
}
207+
]
208+
},
198209
{
199210
"name": "MaxCut",
200211
"description": "Find maximum weight cut in a graph",
@@ -387,6 +398,32 @@
387398
}
388399
]
389400
},
401+
{
402+
"name": "RuralPostman",
403+
"description": "Find a circuit covering required edges with total length at most B (Rural Postman Problem)",
404+
"fields": [
405+
{
406+
"name": "graph",
407+
"type_name": "G",
408+
"description": "The underlying graph G=(V,E)"
409+
},
410+
{
411+
"name": "edge_weights",
412+
"type_name": "Vec<W>",
413+
"description": "Edge lengths l(e) for each e in E"
414+
},
415+
{
416+
"name": "required_edges",
417+
"type_name": "Vec<usize>",
418+
"description": "Edge indices of the required subset E' ⊆ E"
419+
},
420+
{
421+
"name": "bound",
422+
"type_name": "W::Sum",
423+
"description": "Upper bound B on total circuit length"
424+
}
425+
]
426+
},
390427
{
391428
"name": "Satisfiability",
392429
"description": "Find satisfying assignment for CNF formula",

problemreductions-cli/src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ Flags by problem type:
219219
BicliqueCover --left, --right, --biedges, --k
220220
BMF --matrix (0/1), --rank
221221
CVP --basis, --target-vec [--bounds]
222+
RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound
222223
LCS --strings
223224
FVS --arcs [--weights] [--num-vertices]
224225
ILP, CircuitSAT (via reduction only)
@@ -332,6 +333,12 @@ pub struct CreateArgs {
332333
/// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10]
333334
#[arg(long, allow_hyphen_values = true)]
334335
pub bounds: Option<String>,
336+
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
337+
#[arg(long)]
338+
pub required_edges: Option<String>,
339+
/// Upper bound B for RuralPostman
340+
#[arg(long)]
341+
pub bound: Option<i32>,
335342
/// Input strings for LCS (semicolon-separated, e.g., "ABAC;BACA")
336343
#[arg(long)]
337344
pub strings: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
4747
&& args.basis.is_none()
4848
&& args.target_vec.is_none()
4949
&& args.bounds.is_none()
50+
&& args.required_edges.is_none()
51+
&& args.bound.is_none()
5052
&& args.strings.is_none()
5153
&& args.arcs.is_none()
5254
}
@@ -91,6 +93,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
9193
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
9294
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
9395
"Factoring" => "--target 15 --m 4 --n 4",
96+
"RuralPostman" => {
97+
"--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4"
98+
}
9499
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
95100
_ => "",
96101
}
@@ -232,6 +237,38 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
232237
(data, resolved_variant.clone())
233238
}
234239

240+
// RuralPostman
241+
"RuralPostman" => {
242+
let (graph, _) = parse_graph(args).map_err(|e| {
243+
anyhow::anyhow!(
244+
"{e}\n\nUsage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6"
245+
)
246+
})?;
247+
let edge_weights = parse_edge_weights(args, graph.num_edges())?;
248+
let required_edges_str = args.required_edges.as_deref().ok_or_else(|| {
249+
anyhow::anyhow!(
250+
"RuralPostman requires --required-edges\n\n\
251+
Usage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6"
252+
)
253+
})?;
254+
let required_edges: Vec<usize> = util::parse_comma_list(required_edges_str)?;
255+
let bound = args.bound.ok_or_else(|| {
256+
anyhow::anyhow!(
257+
"RuralPostman requires --bound\n\n\
258+
Usage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6"
259+
)
260+
})?;
261+
(
262+
ser(RuralPostman::new(
263+
graph,
264+
edge_weights,
265+
required_edges,
266+
bound,
267+
))?,
268+
resolved_variant.clone(),
269+
)
270+
}
271+
235272
// KColoring
236273
"KColoring" => {
237274
let (graph, _) = parse_graph(args).map_err(|e| {

problemreductions-cli/src/dispatch.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ pub fn load_problem(
214214
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
215215
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
216216
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
217+
"RuralPostman" => deser_sat::<RuralPostman<SimpleGraph, i32>>(data),
217218
"KColoring" => match variant.get("k").map(|s| s.as_str()) {
218219
Some("K3") => deser_sat::<KColoring<K3, SimpleGraph>>(data),
219220
_ => deser_sat::<KColoring<KN, SimpleGraph>>(data),
@@ -276,6 +277,7 @@ pub fn serialize_any_problem(
276277
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
277278
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
278279
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),
280+
"RuralPostman" => try_ser::<RuralPostman<SimpleGraph, i32>>(any),
279281
"KColoring" => match variant.get("k").map(|s| s.as_str()) {
280282
Some("K3") => try_ser::<KColoring<K3, SimpleGraph>>(any),
281283
_ => try_ser::<KColoring<KN, SimpleGraph>>(any),

problemreductions-cli/src/problem_name.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub const ALIASES: &[(&str, &str)] = &[
2020
("KSAT", "KSatisfiability"),
2121
("TSP", "TravelingSalesman"),
2222
("CVP", "ClosestVectorProblem"),
23+
("RPP", "RuralPostman"),
2324
("LCS", "LongestCommonSubsequence"),
2425
("MaxMatching", "MaximumMatching"),
2526
("FVS", "MinimumFeedbackVertexSet"),
@@ -49,6 +50,7 @@ pub fn resolve_alias(input: &str) -> String {
4950
"kcoloring" => "KColoring".to_string(),
5051
"maximalis" => "MaximalIS".to_string(),
5152
"travelingsalesman" | "tsp" => "TravelingSalesman".to_string(),
53+
"ruralpostman" | "rpp" => "RuralPostman".to_string(),
5254
"paintshop" => "PaintShop".to_string(),
5355
"bmf" => "BMF".to_string(),
5456
"bicliquecover" => "BicliqueCover".to_string(),

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ pub mod prelude {
4444
pub use crate::models::graph::{
4545
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
4646
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover,
47-
PartitionIntoTriangles, TravelingSalesman,
47+
PartitionIntoTriangles, RuralPostman, TravelingSalesman,
4848
};
4949
pub use crate::models::misc::{
5050
BinPacking, Factoring, Knapsack, LongestCommonSubsequence, PaintShop, SubsetSum,

src/models/graph/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
1616
//! - [`SpinGlass`]: Ising model Hamiltonian
1717
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
18+
//! - [`RuralPostman`]: Rural Postman (circuit covering required edges)
1819
1920
pub(crate) mod biclique_cover;
2021
pub(crate) mod graph_partitioning;
@@ -28,6 +29,7 @@ pub(crate) mod minimum_dominating_set;
2829
pub(crate) mod minimum_feedback_vertex_set;
2930
pub(crate) mod minimum_vertex_cover;
3031
pub(crate) mod partition_into_triangles;
32+
pub(crate) mod rural_postman;
3133
pub(crate) mod spin_glass;
3234
pub(crate) mod traveling_salesman;
3335

@@ -43,5 +45,6 @@ pub use minimum_dominating_set::MinimumDominatingSet;
4345
pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet;
4446
pub use minimum_vertex_cover::MinimumVertexCover;
4547
pub use partition_into_triangles::PartitionIntoTriangles;
48+
pub use rural_postman::RuralPostman;
4649
pub use spin_glass::SpinGlass;
4750
pub use traveling_salesman::TravelingSalesman;

0 commit comments

Comments
 (0)