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: 37 additions & 24 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"HamiltonianCircuit": [Hamiltonian Circuit],
"BiconnectivityAugmentation": [Biconnectivity Augmentation],
"HamiltonianPath": [Hamiltonian Path],
"ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path],
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
Expand Down Expand Up @@ -1020,44 +1021,56 @@ is feasible: each set induces a connected subgraph, the component weights are $2
]
}
#{
let x = load-model-example("KthBestSpanningTree")
let x = load-model-example("ShortestWeightConstrainedPath")
let nv = graph-num-vertices(x.instance)
let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1)))
let weights = x.instance.weights
let m = edges.len()
let sol = x.optimal_config
let tree1 = sol.enumerate().filter(((i, v)) => i < m and v == 1).map(((i, _)) => edges.at(i))
let blue = graph-colors.at(0)
let gray = luma(190)
let lengths = x.instance.edge_lengths
let weights = x.instance.edge_weights
let s = x.instance.source_vertex
let t = x.instance.target_vertex
let K = x.instance.length_bound
let W = x.instance.weight_bound
let path-config = x.optimal_config
let path-edges = edges.enumerate().filter(((idx, _)) => path-config.at(idx) == 1).map(((idx, e)) => e)
let path-order = (0, 2, 3, 5)
[
#problem-def("KthBestSpanningTree")[
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$.
#problem-def("ShortestWeightConstrainedPath")[
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$.
][
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.
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.

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.
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.

*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.
*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$.

#figure({
let blue = graph-colors.at(0)
let gray = luma(200)
let verts = ((0, 1), (1.5, 1.8), (1.5, 0.2), (3, 1.8), (3, 0.2), (4.5, 1))
canvas(length: 1cm, {
import draw: *
let pos = ((0.0, 1.8), (2.4, 1.8), (2.4, 0.0), (0.0, 0.0))
for (idx, (u, v)) in edges.enumerate() {
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))
g-edge(pos.at(u), pos.at(v), stroke: if in-tree1 { 2pt + blue } else { 1pt + gray })
let mid-x = (pos.at(u).at(0) + pos.at(v).at(0)) / 2
let mid-y = (pos.at(u).at(1) + pos.at(v).at(1)) / 2
// Offset diagonal edge labels to avoid overlap at center
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) }
content((mid-x + ox, mid-y + oy), text(7pt)[#weights.at(idx)], fill: white, frame: "rect", padding: .06, stroke: none)
let on-path = path-config.at(idx) == 1
g-edge(verts.at(u), verts.at(v), stroke: if on-path { 2pt + blue } else { 1pt + gray })
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
let dx = if idx == 7 { -0.25 } else if idx == 5 or idx == 6 { 0.15 } else { 0 }
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 }
draw.content(
(mx + dx, my + dy),
text(7pt, fill: luma(80))[#("(" + str(int(lengths.at(idx))) + ", " + str(int(weights.at(idx))) + ")")]
)
}
for (idx, p) in pos.enumerate() {
g-node(p, name: "v" + str(idx), fill: white, label: $v_#idx$)
for (k, pos) in verts.enumerate() {
let on-path = path-order.any(v => v == k)
g-node(pos, name: "v" + str(k),
fill: if on-path { blue } else { white },
label: if on-path { text(fill: white)[$v_#k$] } else { [$v_#k$] })
}
})
},
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$.],
) <fig:kth-best-spanning-tree>
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.],
) <fig:shortest-weight-constrained-path>
]
]
}
Expand Down
33 changes: 33 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,39 @@ @article{eswarantarjan1976
doi = {10.1137/0205044}
}

@article{hassin1992,
author = {Refael Hassin},
title = {Approximation Schemes for the Restricted Shortest Path Problem},
journal = {Mathematics of Operations Research},
volume = {17},
number = {1},
pages = {36--42},
year = {1992},
doi = {10.1287/moor.17.1.36}
}

@article{joksch1966,
author = {Hans C. Joksch},
title = {The Shortest Route Problem with Constraints},
journal = {Journal of Mathematical Analysis and Applications},
volume = {14},
number = {2},
pages = {191--197},
year = {1966},
doi = {10.1016/0022-247X(66)90002-6}
}

@article{lorenzraz2001,
author = {Daniel H. Lorenz and Danny Raz},
title = {A Simple Efficient Approximation Scheme for the Restricted Shortest Path Problem},
journal = {Operations Research Letters},
volume = {28},
number = {5},
pages = {213--219},
year = {2001},
doi = {10.1016/S0167-6377(01)00079-6}
}

@article{gareyJohnsonStockmeyer1976,
author = {Michael R. Garey and David S. Johnson and Larry Stockmeyer},
title = {Some Simplified {NP}-Complete Graph Problems},
Expand Down
1 change: 1 addition & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 -
pred create SpinGlass --graph 0-1,1-2 -o sg.json
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
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
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
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
pred solve rpc.json --solver brute-force
pred create MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json
Expand Down
16 changes: 16 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ TIP: Run `pred create <PROBLEM>` (no other flags) to see problem-specific help.
Flags by problem type:
MIS, MVC, MaxClique, MinDomSet --graph, --weights
MaxCut, MaxMatching, TSP --graph, --edge-weights
ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --length-bound, --weight-bound
MaximalIS --graph, --weights
SAT, KSAT --num-vars, --clauses [--k]
QUBO --matrix
Expand Down Expand Up @@ -338,6 +339,9 @@ pub struct CreateArgs {
/// Edge weights (e.g., 2,3,1) [default: all 1s]
#[arg(long)]
pub edge_weights: Option<String>,
/// Edge lengths (e.g., 2,3,1) [default: all 1s]
#[arg(long)]
pub edge_lengths: Option<String>,
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
#[arg(long)]
pub capacities: Option<String>,
Expand Down Expand Up @@ -375,6 +379,12 @@ pub struct CreateArgs {
/// Number of vertices for random graph generation
#[arg(long)]
pub num_vertices: Option<usize>,
/// Source vertex for path problems
#[arg(long)]
pub source_vertex: Option<usize>,
/// Target vertex for path problems
#[arg(long)]
pub target_vertex: Option<usize>,
/// Edge probability for random graph generation (0.0 to 1.0) [default: 0.5]
#[arg(long)]
pub edge_prob: Option<f64>,
Expand Down Expand Up @@ -483,6 +493,12 @@ pub struct CreateArgs {
/// Upper bound or length bound (for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
#[arg(long, allow_hyphen_values = true)]
pub bound: Option<i64>,
/// Upper bound on total path length
#[arg(long)]
pub length_bound: Option<i32>,
/// Upper bound on total path weight
#[arg(long)]
pub weight_bound: Option<i32>,
/// Pattern graph edge list for SubgraphIsomorphism (e.g., 0-1,1-2,2-0)
#[arg(long)]
pub pattern: Option<String>,
Expand Down
Loading
Loading