Skip to content

Commit 2de7770

Browse files
authored
Merge pull request #695 from CodingThrust/fix/strong-connectivity-augmentation-example
Fix StrongConnectivityAugmentation canonical example
2 parents 6bb3e9c + c7a7612 commit 2de7770

3 files changed

Lines changed: 59 additions & 48 deletions

File tree

docs/paper/reductions.typ

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,36 +1088,63 @@ is feasible: each set induces a connected subgraph, the component weights are $2
10881088
let bound = x.instance.bound
10891089
let sol = x.optimal.at(0)
10901090
let chosen = candidates.enumerate().filter(((i, _)) => sol.config.at(i) == 1).map(((i, arc)) => arc)
1091-
let arc = chosen.at(0)
1091+
let total-weight = chosen.map(a => a.at(2)).sum()
10921092
let blue = graph-colors.at(0)
10931093
[
10941094
#problem-def("StrongConnectivityAugmentation")[
10951095
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.
10961096
][
10971097
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.]
10981098

1099-
*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}$.
1099+
*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.
11001100

11011101
#figure({
1102-
let verts = ((0, 1), (1.2, 1.6), (1.2, 0.4), (3.4, 1.0), (4.6, 1.5), (4.6, 0.5))
1102+
let verts = ((0, 0), (1.5, 0), (3.0, 0), (4.5, 0), (6.0, 0))
1103+
let highlighted = chosen.map(a => (a.at(0), a.at(1))).flatten()
11031104
canvas(length: 1cm, {
1105+
// Vertices (drawn first so edges can reference named anchors)
1106+
for (k, pos) in verts.enumerate() {
1107+
g-node(pos, name: "v" + str(k),
1108+
fill: if highlighted.contains(k) { blue.transparentize(65%) } else { white },
1109+
label: [$v_#k$])
1110+
}
1111+
// Base arcs (black, between named nodes)
11041112
for (u, v) in arcs {
1105-
draw.line(verts.at(u), verts.at(v),
1113+
draw.line("v" + str(u), "v" + str(v),
11061114
stroke: 1pt + black,
11071115
mark: (end: "straight", scale: 0.4))
11081116
}
1109-
draw.line(verts.at(arc.at(0)), verts.at(arc.at(1)),
1110-
stroke: 1.6pt + blue,
1111-
mark: (end: "straight", scale: 0.45))
1112-
for (k, pos) in verts.enumerate() {
1113-
let highlighted = k == arc.at(0) or k == arc.at(1)
1114-
g-node(pos, name: "v" + str(k),
1115-
fill: if highlighted { blue.transparentize(65%) } else { white },
1116-
label: [$v_#k$])
1117+
// Chosen augmenting arcs (blue, curved above the path)
1118+
let r = 0.24
1119+
for (idx, arc) in chosen.enumerate() {
1120+
let (u, v, w) = arc
1121+
let pu = verts.at(u)
1122+
let pv = verts.at(v)
1123+
let rise = 0.7 + 0.3 * calc.abs(u - v)
1124+
let ctrl = ((pu.at(0) + pv.at(0)) / 2, rise)
1125+
// Shorten start toward control point
1126+
let dx-s = ctrl.at(0) - pu.at(0)
1127+
let dy-s = ctrl.at(1) - pu.at(1)
1128+
let ds = calc.sqrt(dx-s * dx-s + dy-s * dy-s)
1129+
let p0 = (pu.at(0) + r * dx-s / ds, pu.at(1) + r * dy-s / ds)
1130+
// Shorten end toward control point
1131+
let dx-e = ctrl.at(0) - pv.at(0)
1132+
let dy-e = ctrl.at(1) - pv.at(1)
1133+
let de = calc.sqrt(dx-e * dx-e + dy-e * dy-e)
1134+
let p1 = (pv.at(0) + r * dx-e / de, pv.at(1) + r * dy-e / de)
1135+
draw.bezier(p0, p1, ctrl,
1136+
stroke: 1.6pt + blue,
1137+
mark: (end: "straight", scale: 0.5),
1138+
)
1139+
// Weight label
1140+
draw.content(
1141+
((pu.at(0) + pv.at(0)) / 2, rise + 0.3),
1142+
text(7pt, fill: blue)[$#w$],
1143+
)
11171144
}
11181145
})
11191146
},
1120-
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.],
1147+
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$.],
11211148
) <fig:strong-connectivity-augmentation>
11221149
]
11231150
]

src/example_db/fixtures/examples.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
{"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}]},
4949
{"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}}]},
5050
{"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}]},
51-
{"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}]},
51+
{"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}]},
5252
{"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}}]},
5353
{"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}]}
5454
],

src/models/graph/strong_connectivity_augmentation.rs

Lines changed: 18 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -229,48 +229,32 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
229229
vec![crate::example_db::specs::ModelExampleSpec {
230230
id: "strong_connectivity_augmentation_i32",
231231
build: || {
232+
// Path digraph 0→1→2→3→4 (not strongly connected — no back-edges).
233+
// Nine candidate arcs are all individually affordable, but only the
234+
// pair (4→1, w=3) + (1→0, w=5) = 8 = B achieves strong connectivity.
235+
// Other 4-escapes (4→3, 4→2) land on vertices from which reaching 0
236+
// within the remaining budget is impossible.
232237
let problem = StrongConnectivityAugmentation::new(
233-
DirectedGraph::new(
234-
6,
235-
vec![
236-
(0, 1),
237-
(1, 2),
238-
(2, 0),
239-
(3, 4),
240-
(4, 3),
241-
(2, 3),
242-
(4, 5),
243-
(5, 3),
244-
],
245-
),
238+
DirectedGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]),
246239
vec![
247-
(3, 0, 5),
248-
(3, 1, 3),
249-
(3, 2, 4),
250-
(4, 0, 6),
251-
(4, 1, 2),
252-
(4, 2, 7),
253-
(5, 0, 4),
254-
(5, 1, 3),
255-
(5, 2, 1),
256-
(0, 3, 8),
257-
(0, 4, 3),
258-
(0, 5, 2),
259-
(1, 3, 6),
260-
(1, 4, 4),
261-
(1, 5, 5),
262-
(2, 4, 3),
263-
(2, 5, 7),
264-
(1, 0, 2),
240+
(4, 0, 10), // direct fix, too expensive
241+
(4, 3, 3), // 4-escape to dead end
242+
(4, 2, 3), // 4-escape to dead end
243+
(4, 1, 3), // correct 4-escape
244+
(3, 0, 7), // too expensive to combine
245+
(3, 1, 3), // dead-end intermediate
246+
(2, 0, 7), // too expensive to combine
247+
(2, 1, 3), // dead-end intermediate
248+
(1, 0, 5), // the closing arc
265249
],
266-
1,
250+
8,
267251
);
268252

269253
crate::example_db::specs::satisfaction_example(
270254
problem,
271255
vec![
272-
vec![0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
273-
vec![0; 18],
256+
vec![0, 0, 0, 1, 0, 0, 0, 0, 1], // unique: (4→1)+(1→0), w=8
257+
vec![0, 0, 0, 0, 0, 0, 0, 0, 0], // no arcs: not connected
274258
],
275259
)
276260
},

0 commit comments

Comments
 (0)