Skip to content

Commit fc1cdf8

Browse files
zazabapclaudeGiggleLiuisPANN
authored
Fix #289: Add ShortestWeightConstrainedPath model (#632)
* Add plan for #289: ShortestWeightConstrainedPath model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add ShortestWeightConstrainedPath model * Remove issue 289 plan * fix: address PR #632 review feedback * fix: harden shortest path input validation * Fix merge conflicts with main and update to current example-db API - Resolve merge conflicts in 11 files (both-sides-added pattern) - Remove generated files deleted in main (problem_schemas.json, reduction_graph.json, examples.json) - Update ModelExampleSpec to use new API (instance/optimal_config/optimal_value instead of build closure) - Fix paper Typst code: use graph.edges instead of graph.inner.edges, use optimal_config instead of samples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix missing fields in empty_args() and remove unreachable pattern - Add edge_lengths, source_vertex, target_vertex, length_bound, weight_bound to empty_args() - Remove duplicate W::Sum pattern already covered by earlier match arm Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com> Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn>
1 parent cb32085 commit fc1cdf8

13 files changed

Lines changed: 1111 additions & 47 deletions

File tree

docs/paper/reductions.typ

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"HamiltonianCircuit": [Hamiltonian Circuit],
7272
"BiconnectivityAugmentation": [Biconnectivity Augmentation],
7373
"HamiltonianPath": [Hamiltonian Path],
74+
"ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path],
7475
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
7576
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
7677
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
@@ -1020,44 +1021,56 @@ is feasible: each set induces a connected subgraph, the component weights are $2
10201021
]
10211022
}
10221023
#{
1023-
let x = load-model-example("KthBestSpanningTree")
1024+
let x = load-model-example("ShortestWeightConstrainedPath")
1025+
let nv = graph-num-vertices(x.instance)
10241026
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
1025-
let weights = x.instance.weights
1026-
let m = edges.len()
1027-
let sol = x.optimal_config
1028-
let tree1 = sol.enumerate().filter(((i, v)) => i < m and v == 1).map(((i, _)) => edges.at(i))
1029-
let blue = graph-colors.at(0)
1030-
let gray = luma(190)
1027+
let lengths = x.instance.edge_lengths
1028+
let weights = x.instance.edge_weights
1029+
let s = x.instance.source_vertex
1030+
let t = x.instance.target_vertex
1031+
let K = x.instance.length_bound
1032+
let W = x.instance.weight_bound
1033+
let path-config = x.optimal_config
1034+
let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx) == 1).map(((idx, e)) => e)
1035+
let path-order = (0, 2, 3, 5)
10311036
[
1032-
#problem-def("KthBestSpanningTree")[
1033-
Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ_(gt.eq 0)$, a positive integer $k$, and a bound $B in ZZ_(gt.eq 0)$, determine whether there exist $k$ distinct spanning trees $T_1, dots, T_k subset.eq E$ such that $sum_(e in T_i) w(e) lt.eq B$ for every $i$.
1037+
#problem-def("ShortestWeightConstrainedPath")[
1038+
Given an undirected graph $G = (V, E)$ with positive edge lengths $l: E -> ZZ^+$, positive edge weights $w: E -> ZZ^+$, designated vertices $s, t in V$, and bounds $K, W in ZZ^+$, determine whether there exists a simple path $P$ from $s$ to $t$ such that $sum_(e in P) l(e) <= K$ and $sum_(e in P) w(e) <= W$.
10341039
][
1035-
Kth Best Spanning Tree is catalogued as ND9 in Garey and Johnson @garey1979 and is marked there with an asterisk because the general problem is NP-hard but not known to lie in NP. For any fixed value of $k$, Lawler's $k$-best enumeration framework gives a polynomial-time algorithm when combined with minimum-spanning-tree subroutines @lawler1972. For output-sensitive enumeration, Eppstein gave an algorithm that lists the $k$ smallest spanning trees of a weighted graph in $O(m log beta(m, n) + k^2)$ time @eppstein1992.
1040+
Also called the _restricted shortest path_ or _resource-constrained shortest path_ problem. Garey and Johnson list it as ND30 and show NP-completeness via transformation from Partition @garey1979. The model captures bicriteria routing: one resource measures path length or delay, while the other captures a second consumable budget such as cost, risk, or bandwidth. Because pseudo-polynomial dynamic programming formulations are known @joksch1966, the hardness is weak rather than strong; approximation schemes were later developed by Hassin @hassin1992 and improved by Lorenz and Raz @lorenzraz2001.
10361041

1037-
Variables: $k |E|$ binary values grouped into $k$ consecutive edge-selection blocks. Entry $x_(i, e) = 1$ means edge $e$ belongs to the $i$-th candidate tree. A configuration is satisfying exactly when each block selects a spanning tree, every selected tree has total weight at most $B$, and the $k$ blocks encode pairwise distinct edge sets.
1042+
The implementation catalog reports the natural brute-force complexity of the edge-subset encoding used here: with $m = |E|$ binary variables, exhaustive search over all candidate subsets costs $O^*(2^m)$. A configuration is satisfying precisely when the selected edges form a single simple $s$-$t$ path and both resource sums stay within their bounds.
10381043

1039-
*Example.* Consider $K_4$ with edge weights $w = {(0,1): 1, (0,2): 1, (0,3): 2, (1,2): 2, (1,3): 2, (2,3): 3}$. With $k = 2$ and $B = 4$, exactly two of the $16$ spanning trees have total weight $lt.eq 4$: the star $T_1 = {(0,1), (0,2), (0,3)}$ with weight $4$ and $T_2 = {(0,1), (0,2), (1,3)}$ with weight $4$. Since two distinct bounded spanning trees exist, this is a YES-instance.
1044+
*Example.* Consider the graph on #nv vertices with source $s = v_#s$, target $t = v_#t$, length bound $K = #K$, and weight bound $W = #W$. Edge labels are written as $(l(e), w(e))$. The highlighted path $#path-order.map(v => $v_#v$).join($arrow$)$ uses edges ${#path-edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ")}$, so its total length is $4 + 1 + 4 = 9 <= #K$ and its total weight is $1 + 3 + 3 = 7 <= #W$. This instance has 2 satisfying edge selections; another feasible path is $v_0 arrow v_1 arrow v_4 arrow v_5$.
10401045

10411046
#figure({
1047+
let blue = graph-colors.at(0)
1048+
let gray = luma(200)
1049+
let verts = ((0, 1), (1.5, 1.8), (1.5, 0.2), (3, 1.8), (3, 0.2), (4.5, 1))
10421050
canvas(length: 1cm, {
10431051
import draw: *
1044-
let pos = ((0.0, 1.8), (2.4, 1.8), (2.4, 0.0), (0.0, 0.0))
10451052
for (idx, (u, v)) in edges.enumerate() {
1046-
let in-tree1 = tree1.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u))
1047-
g-edge(pos.at(u), pos.at(v), stroke: if in-tree1 { 2pt + blue } else { 1pt + gray })
1048-
let mid-x = (pos.at(u).at(0) + pos.at(v).at(0)) / 2
1049-
let mid-y = (pos.at(u).at(1) + pos.at(v).at(1)) / 2
1050-
// Offset diagonal edge labels to avoid overlap at center
1051-
let (ox, oy) = if u == 0 and v == 2 { (0.3, 0) } else if u == 1 and v == 3 { (-0.3, 0) } else { (0, 0) }
1052-
content((mid-x + ox, mid-y + oy), text(7pt)[#weights.at(idx)], fill: white, frame: "rect", padding: .06, stroke: none)
1053+
let on-path = path-config.at(idx) == 1
1054+
g-edge(verts.at(u), verts.at(v), stroke: if on-path { 2pt + blue } else { 1pt + gray })
1055+
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
1056+
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
1057+
let dx = if idx == 7 { -0.25 } else if idx == 5 or idx == 6 { 0.15 } else { 0 }
1058+
let dy = if idx == 0 or idx == 2 or idx == 5 { 0.16 } else if idx == 1 or idx == 4 or idx == 6 { -0.16 } else if idx == 7 { 0.12 } else { 0 }
1059+
draw.content(
1060+
(mx + dx, my + dy),
1061+
text(7pt, fill: luma(80))[#("(" + str(int(lengths.at(idx))) + ", " + str(int(weights.at(idx))) + ")")]
1062+
)
10531063
}
1054-
for (idx, p) in pos.enumerate() {
1055-
g-node(p, name: "v" + str(idx), fill: white, label: $v_#idx$)
1064+
for (k, pos) in verts.enumerate() {
1065+
let on-path = path-order.any(v => v == k)
1066+
g-node(pos, name: "v" + str(k),
1067+
fill: if on-path { blue } else { white },
1068+
label: if on-path { text(fill: white)[$v_#k$] } else { [$v_#k$] })
10561069
}
10571070
})
10581071
},
1059-
caption: [Kth Best Spanning Tree on $K_4$. Blue edges show $T_1 = {(0,1), (0,2), (0,3)}$, one of two spanning trees with weight $lt.eq 4$.],
1060-
) <fig:kth-best-spanning-tree>
1072+
caption: [Shortest Weight-Constrained Path instance with edge labels $(l(e), w(e))$. The highlighted path $v_0 arrow v_2 arrow v_3 arrow v_5$ satisfies both bounds.],
1073+
) <fig:shortest-weight-constrained-path>
10611074
]
10621075
]
10631076
}

docs/paper/references.bib

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,39 @@ @article{eswarantarjan1976
201201
doi = {10.1137/0205044}
202202
}
203203

204+
@article{hassin1992,
205+
author = {Refael Hassin},
206+
title = {Approximation Schemes for the Restricted Shortest Path Problem},
207+
journal = {Mathematics of Operations Research},
208+
volume = {17},
209+
number = {1},
210+
pages = {36--42},
211+
year = {1992},
212+
doi = {10.1287/moor.17.1.36}
213+
}
214+
215+
@article{joksch1966,
216+
author = {Hans C. Joksch},
217+
title = {The Shortest Route Problem with Constraints},
218+
journal = {Journal of Mathematical Analysis and Applications},
219+
volume = {14},
220+
number = {2},
221+
pages = {191--197},
222+
year = {1966},
223+
doi = {10.1016/0022-247X(66)90002-6}
224+
}
225+
226+
@article{lorenzraz2001,
227+
author = {Daniel H. Lorenz and Danny Raz},
228+
title = {A Simple Efficient Approximation Scheme for the Restricted Shortest Path Problem},
229+
journal = {Operations Research Letters},
230+
volume = {28},
231+
number = {5},
232+
pages = {213--219},
233+
year = {2001},
234+
doi = {10.1016/S0167-6377(01)00079-6}
235+
}
236+
204237
@article{gareyJohnsonStockmeyer1976,
205238
author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer},
206239
title = {Some Simplified {NP}-Complete Graph Problems},

docs/src/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,7 @@ pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 -
354354
pred create SpinGlass --graph 0-1,1-2 -o sg.json
355355
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
356356
pred create MinMaxMulticenter --graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2 --bound 1 -o pcenter.json
357+
pred create ShortestWeightConstrainedPath --graph 0-1,0-2,1-3,2-3,2-4,3-5,4-5,1-4 --edge-lengths 2,4,3,1,5,4,2,6 --edge-weights 5,1,2,3,2,3,1,1 --source-vertex 0 --target-vertex 5 --length-bound 10 --weight-bound 8 -o swcp.json
357358
pred create RectilinearPictureCompression --matrix "1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1" --k 2 -o rpc.json
358359
pred solve rpc.json --solver brute-force
359360
pred create MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json

problemreductions-cli/src/cli.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ TIP: Run `pred create <PROBLEM>` (no other flags) to see problem-specific help.
217217
Flags by problem type:
218218
MIS, MVC, MaxClique, MinDomSet --graph, --weights
219219
MaxCut, MaxMatching, TSP --graph, --edge-weights
220+
ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --length-bound, --weight-bound
220221
MaximalIS --graph, --weights
221222
SAT, KSAT --num-vars, --clauses [--k]
222223
QUBO --matrix
@@ -338,6 +339,9 @@ pub struct CreateArgs {
338339
/// Edge weights (e.g., 2,3,1) [default: all 1s]
339340
#[arg(long)]
340341
pub edge_weights: Option<String>,
342+
/// Edge lengths (e.g., 2,3,1) [default: all 1s]
343+
#[arg(long)]
344+
pub edge_lengths: Option<String>,
341345
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
342346
#[arg(long)]
343347
pub capacities: Option<String>,
@@ -375,6 +379,12 @@ pub struct CreateArgs {
375379
/// Number of vertices for random graph generation
376380
#[arg(long)]
377381
pub num_vertices: Option<usize>,
382+
/// Source vertex for path problems
383+
#[arg(long)]
384+
pub source_vertex: Option<usize>,
385+
/// Target vertex for path problems
386+
#[arg(long)]
387+
pub target_vertex: Option<usize>,
378388
/// Edge probability for random graph generation (0.0 to 1.0) [default: 0.5]
379389
#[arg(long)]
380390
pub edge_prob: Option<f64>,
@@ -483,6 +493,12 @@ pub struct CreateArgs {
483493
/// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
484494
#[arg(long, allow_hyphen_values = true)]
485495
pub bound: Option<i64>,
496+
/// Upper bound on total path length
497+
#[arg(long)]
498+
pub length_bound: Option<i32>,
499+
/// Upper bound on total path weight
500+
#[arg(long)]
501+
pub weight_bound: Option<i32>,
486502
/// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0)
487503
#[arg(long)]
488504
pub pattern: Option<String>,

0 commit comments

Comments
 (0)