|
71 | 71 | "HamiltonianCircuit": [Hamiltonian Circuit], |
72 | 72 | "BiconnectivityAugmentation": [Biconnectivity Augmentation], |
73 | 73 | "HamiltonianPath": [Hamiltonian Path], |
| 74 | + "ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path], |
74 | 75 | "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], |
75 | 76 | "LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths], |
76 | 77 | "IsomorphicSpanningTree": [Isomorphic Spanning Tree], |
@@ -1020,44 +1021,56 @@ is feasible: each set induces a connected subgraph, the component weights are $2 |
1020 | 1021 | ] |
1021 | 1022 | } |
1022 | 1023 | #{ |
1023 | | - let x = load-model-example("KthBestSpanningTree") |
| 1024 | + let x = load-model-example("ShortestWeightConstrainedPath") |
| 1025 | + let nv = graph-num-vertices(x.instance) |
1024 | 1026 | 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) |
1031 | 1036 | [ |
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$. |
1034 | 1039 | ][ |
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. |
1036 | 1041 |
|
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. |
1038 | 1043 |
|
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$. |
1040 | 1045 |
|
1041 | 1046 | #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)) |
1042 | 1050 | canvas(length: 1cm, { |
1043 | 1051 | import draw: * |
1044 | | - let pos = ((0.0, 1.8), (2.4, 1.8), (2.4, 0.0), (0.0, 0.0)) |
1045 | 1052 | 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 | + ) |
1053 | 1063 | } |
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$] }) |
1056 | 1069 | } |
1057 | 1070 | }) |
1058 | 1071 | }, |
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> |
1061 | 1074 | ] |
1062 | 1075 | ] |
1063 | 1076 | } |
|
0 commit comments