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
53 changes: 40 additions & 13 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -1088,36 +1088,63 @@ 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")[
Given a directed graph $G = (V, A)$, a set $C subset.eq (V times V backslash A) times ZZ_(> 0)$ of weighted candidate arcs, and a bound $B in ZZ_(>= 0)$, determine whether there exists a subset $C' subset.eq C$ such that $sum_((u, v, w) in C') w <= B$ and the augmented digraph $(V, A union {(u, v) : (u, v, w) in C'})$ is strongly connected.
][
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$.],
) <fig:strong-connectivity-augmentation>
]
]
Expand Down
2 changes: 1 addition & 1 deletion src/example_db/fixtures/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}]}
],
Expand Down
52 changes: 18 additions & 34 deletions src/models/graph/strong_connectivity_augmentation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,48 +229,32 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
vec![crate::example_db::specs::ModelExampleSpec {
id: "strong_connectivity_augmentation_i32",
build: || {
// Path digraph 0→1→2→3→4 (not strongly connected — no back-edges).
// Nine candidate arcs are all individually affordable, but only the
// pair (4→1, w=3) + (1→0, w=5) = 8 = B achieves strong connectivity.
Comment on lines +233 to +234
// Other 4-escapes (4→3, 4→2) land on vertices from which reaching 0
// within the remaining budget is impossible.
let problem = StrongConnectivityAugmentation::new(
DirectedGraph::new(
6,
vec![
(0, 1),
(1, 2),
(2, 0),
(3, 4),
(4, 3),
(2, 3),
(4, 5),
(5, 3),
],
),
DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
vec![
(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),
(4, 0, 10), // direct fix, too expensive
(4, 3, 3), // 4-escape to dead end
(4, 2, 3), // 4-escape to dead end
(4, 1, 3), // correct 4-escape
(3, 0, 7), // too expensive to combine
(3, 1, 3), // dead-end intermediate
(2, 0, 7), // too expensive to combine
(2, 1, 3), // dead-end intermediate
(1, 0, 5), // the closing arc
],
1,
8,
);

crate::example_db::specs::satisfaction_example(
problem,
vec![
vec![0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
vec![0; 18],
vec![0, 0, 0, 1, 0, 0, 0, 0, 1], // unique: (4→1)+(1→0), w=8
vec![0, 0, 0, 0, 0, 0, 0, 0, 0], // no arcs: not connected
],
)
},
Expand Down
Loading