diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 95a25547..a0dafe9c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1088,7 +1088,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 let bound = x.instance.bound let sol = x.optimal.at(0) let chosen = candidates.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc) - let arc = chosen.at(0) + let total-weight = chosen.map(a => a.at(2)).sum() let blue = graph-colors.at(0) [ #problem-def("StrongConnectivityAugmentation")[ @@ -1096,28 +1096,55 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ][ Strong Connectivity Augmentation models network design problems where a partially connected directed communication graph may be repaired by buying additional arcs. Eswaran and Tarjan showed that the unweighted augmentation problem is solvable in linear time, while the weighted variant is substantially harder @eswarantarjan1976. The decision version recorded as ND19 in Garey and Johnson is NP-complete @garey1979. The implementation here uses one binary variable per candidate arc, so brute-force over the candidate set yields a worst-case bound of $O^*(2^m)$ where $m = "num_potential_arcs"$. #footnote[No exact algorithm improving on brute-force is claimed here for the weighted candidate-arc formulation implemented in the codebase.] - *Example.* The canonical instance has $n = #nv$ vertices, $|A| = #ne$ existing arcs, #candidates.len() weighted candidate arcs, and bound $B = #bound$. The base graph already contains the directed 3-cycle $v_0 -> v_1 -> v_2 -> v_0$ and the strongly connected component on ${v_3, v_4, v_5}$, with only the forward bridge $v_2 -> v_3$ between them. The unique satisfying augmentation under this bound selects the single candidate arc $(v_#arc.at(0), v_#arc.at(1)))$ of weight #arc.at(2), closing the cycle $v_2 -> v_3 -> v_4 -> v_5 -> v_2$ and making every vertex reachable from every other. The all-zero configuration is infeasible because no path returns from ${v_3, v_4, v_5}$ to ${v_0, v_1, v_2}$. + *Example.* The canonical instance has $n = #nv$ vertices, $|A| = #ne$ existing arcs, and bound $B = #bound$. The base graph is the directed path $v_0 -> v_1 -> v_2 -> v_3 -> v_4$ — every vertex can reach those ahead of it, but vertex $v_4$ is a sink with no outgoing arcs. The #candidates.len() candidate arcs with weights are: #candidates.map(a => $w(v_#(a.at(0)), v_#(a.at(1))) = #(a.at(2))$).join(", "). All are individually within budget, yet only the pair #chosen.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(" and ") with weights #chosen.map(a => $#(a.at(2))$).join($+$) $= #total-weight = B$ achieves strong connectivity. Alternative escape arcs from $v_4$ (to $v_3$ or $v_2$) are equally cheap but land on vertices from which reaching $v_0$ within the remaining budget is impossible. #figure({ - let verts = ((0, 1), (1.2, 1.6), (1.2, 0.4), (3.4, 1.0), (4.6, 1.5), (4.6, 0.5)) + let verts = ((0, 0), (1.5, 0), (3.0, 0), (4.5, 0), (6.0, 0)) + let highlighted = chosen.map(a => (a.at(0), a.at(1))).flatten() canvas(length: 1cm, { + // Vertices (drawn first so edges can reference named anchors) + for (k, pos) in verts.enumerate() { + g-node(pos, name: "v" + str(k), + fill: if highlighted.contains(k) { blue.transparentize(65%) } else { white }, + label: [$v_#k$]) + } + // Base arcs (black, between named nodes) for (u, v) in arcs { - draw.line(verts.at(u), verts.at(v), + draw.line("v" + str(u), "v" + str(v), stroke: 1pt + black, mark: (end: "straight", scale: 0.4)) } - draw.line(verts.at(arc.at(0)), verts.at(arc.at(1)), - stroke: 1.6pt + blue, - mark: (end: "straight", scale: 0.45)) - for (k, pos) in verts.enumerate() { - let highlighted = k == arc.at(0) or k == arc.at(1) - g-node(pos, name: "v" + str(k), - fill: if highlighted { blue.transparentize(65%) } else { white }, - label: [$v_#k$]) + // Chosen augmenting arcs (blue, curved above the path) + let r = 0.24 + for (idx, arc) in chosen.enumerate() { + let (u, v, w) = arc + let pu = verts.at(u) + let pv = verts.at(v) + let rise = 0.7 + 0.3 * calc.abs(u - v) + let ctrl = ((pu.at(0) + pv.at(0)) / 2, rise) + // Shorten start toward control point + let dx-s = ctrl.at(0) - pu.at(0) + let dy-s = ctrl.at(1) - pu.at(1) + let ds = calc.sqrt(dx-s * dx-s + dy-s * dy-s) + let p0 = (pu.at(0) + r * dx-s / ds, pu.at(1) + r * dy-s / ds) + // Shorten end toward control point + let dx-e = ctrl.at(0) - pv.at(0) + let dy-e = ctrl.at(1) - pv.at(1) + let de = calc.sqrt(dx-e * dx-e + dy-e * dy-e) + let p1 = (pv.at(0) + r * dx-e / de, pv.at(1) + r * dy-e / de) + draw.bezier(p0, p1, ctrl, + stroke: 1.6pt + blue, + mark: (end: "straight", scale: 0.5), + ) + // Weight label + draw.content( + ((pu.at(0) + pv.at(0)) / 2, rise + 0.3), + text(7pt, fill: blue)[$#w$], + ) } }) }, - caption: [Strong Connectivity Augmentation on a #{nv}-vertex digraph. Black arcs are present in $A$; the added candidate arc $(v_#arc.at(0), v_#arc.at(1)))$ is shown in blue. With bound $B = #bound$, this single augmentation makes the digraph strongly connected.], + caption: [Strong Connectivity Augmentation on a #{nv}-vertex path digraph. Black arcs form the base path $A$; blue arcs are the unique augmentation (#chosen.map(a => $(v_#(a.at(0)), v_#(a.at(1)))$).join(", ")) with total weight $#total-weight = B = #bound$.], ) ] ] diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index efff5d71..d954a60d 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -48,7 +48,7 @@ {"problem":"StaffScheduling","variant":{},"instance":{"num_workers":4,"requirements":[2,2,2,3,3,2,1],"schedules":[[true,true,true,true,true,false,false],[false,true,true,true,true,true,false],[false,false,true,true,true,true,true],[true,false,false,true,true,true,true],[true,true,false,false,true,true,true]],"shifts_per_schedule":5},"samples":[{"config":[1,1,1,1,0],"metric":true}],"optimal":[{"config":[0,1,1,1,1],"metric":true},{"config":[0,2,0,1,1],"metric":true},{"config":[0,2,0,2,0],"metric":true},{"config":[1,0,1,1,1],"metric":true},{"config":[1,0,2,0,1],"metric":true},{"config":[1,1,0,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true},{"config":[1,1,0,2,0],"metric":true},{"config":[1,1,1,0,1],"metric":true},{"config":[1,1,1,1,0],"metric":true},{"config":[1,2,0,0,1],"metric":true},{"config":[1,2,0,1,0],"metric":true},{"config":[2,0,0,1,1],"metric":true},{"config":[2,0,0,2,0],"metric":true},{"config":[2,0,1,0,1],"metric":true},{"config":[2,0,1,1,0],"metric":true},{"config":[2,0,2,0,0],"metric":true},{"config":[2,1,0,0,1],"metric":true},{"config":[2,1,0,1,0],"metric":true},{"config":[2,1,1,0,0],"metric":true}]}, {"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]}, {"problem":"StringToStringCorrection","variant":{},"instance":{"alphabet_size":4,"bound":2,"source":[0,1,2,3,1,0],"target":[0,1,3,2,1]},"samples":[{"config":[8,5],"metric":true}],"optimal":[{"config":[5,7],"metric":true},{"config":[8,5],"metric":true}]}, - {"problem":"StrongConnectivityAugmentation","variant":{"weight":"i32"},"instance":{"bound":1,"candidate_arcs":[[3,0,5],[3,1,3],[3,2,4],[4,0,6],[4,1,2],[4,2,7],[5,0,4],[5,1,3],[5,2,1],[0,3,8],[0,4,3],[0,5,2],[1,3,6],[1,4,4],[1,5,5],[2,4,3],[2,5,7],[1,0,2]],"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[3,4,null],[4,3,null],[2,3,null],[4,5,null],[5,3,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true},{"config":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0],"metric":true}]}, + {"problem":"StrongConnectivityAugmentation","variant":{"weight":"i32"},"instance":{"bound":8,"candidate_arcs":[[4,0,10],[4,3,3],[4,2,3],[4,1,3],[3,0,7],[3,1,3],[2,0,7],[2,1,3],[1,0,5]],"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[0,0,0,1,0,0,0,0,1],"metric":true},{"config":[0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[0,0,0,1,0,0,0,0,1],"metric":true}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} ], diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index 39af1cde..5c67e50f 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -229,48 +229,32 @@ pub(crate) fn canonical_model_example_specs() -> Vec