1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
//! The meet-in-the-middle pair search behind `shortest_path`,
//! `shortest_path_directed` and `shortest_path_cost{,_with}`.
//!
//! Deliberately graph-agnostic: it knows only `u32` vertex ids, two neighbour
//! producers and a per-node predicate. `graph_algorithms::bidirectional_path`
//! binds it to the live graph; the split keeps the termination proof — the
//! subtle part — readable next to the loop it constrains, and testable against
//! hand-built adjacency without a `DirGraph`.
use Interrupt;
use FxHashMap;
/// One side's BFS tree: `node -> (parent, hops from that side's root)`. The
/// root is recorded as its own parent, which is what terminates a
/// reconstruction walk.
///
/// A `HashMap` rather than two `node_bound`-sized `Vec`s, deliberately: the
/// 0.9.53 fix measured that the flat arrays cost 500 KB + 2 MB of allocate +
/// zero per call on a 500 K-node graph *regardless of how far the search
/// actually walked*, and that this alone dominated a shallow lookup (37 µs →
/// 4 µs when it went away). The Cypher `shortestPath()` executor runs one of
/// these per row, so the per-call floor is the number that matters, not the
/// per-node constant. The same reasoning is why [`bidirectional_bfs`] does not
/// borrow S3's [`BfsScratch`], whose generation stamps pay for themselves only
/// across many searches over one fixed vertex space.
type BfsTree = ;
/// The four references one round of [`bidirectional_bfs`] works on: the
/// frontier to drain, the tree to grow, the *opposite* tree to test meetings
/// against, and the neighbour producer for this side's direction. Which side
/// they name is chosen per round, by whichever frontier is smaller.
type BidirSide<'a> = ;
/// Shortest path between two anchored endpoints by **meeting in the middle**:
/// one BFS grows from `source` along the edges as given, a second grows from
/// `target` along their reverse, and the answer is stitched together where the
/// two trees first touch.
///
/// A one-sided BFS explores every node within the full path radius `d`; two
/// searches meeting in the middle explore two balls of radius `d/2`, which on
/// any graph with meaningful branching is a small fraction of the work — the
/// difference between `b^d` and `2·b^(d/2)`.
///
/// # Termination — the rule that makes the length right
///
/// A round expands **one** side, the one with the smaller frontier, through a
/// **whole level**, and stops the instant that level touches the other tree.
/// The two sides are never interleaved inside a level: a side only starts a
/// new level once the other side's last one finished.
///
/// That invariant — *each tree holds exactly the nodes within `df` (resp.
/// `db`) hops of its root* — is what makes the first touch optimal, and it is
/// worth spelling out because the plausible-looking alternatives are wrong.
/// Suppose the trees are disjoint at the top of a round (they are: any node
/// entering both would have been detected by whichever insertion came second).
/// A source→target path of length `L ≤ df + db` would put its node at offset
/// `df` in *both* trees, so disjointness proves `L ≥ df + db + 1`. Now expand
/// the forward side to `df + 1` and find `w` already in the backward tree, at
/// `dist_backward(w) = k ≤ db`. Splicing gives a real path of `df + 1 + k`, so
/// `L ≤ df + 1 + k ≤ df + db + 1 ≤ L` — every inequality collapses, and `w` is
/// on a shortest path whichever `w` the level happened to reach first. Because
/// the round returns immediately, the half-drained level is never used for
/// anything.
///
/// What breaks the argument is advancing one side while the other's level is
/// half-expanded — the textbook shape being "expand both frontiers a level,
/// then compare, and report `forward_level + backward_level`". That variant is
/// right on even-length shortest paths and one hop too long on odd-length
/// ones, where the two halves meet on the middle *edge* and a per-round
/// counter cannot express the half-round.
/// `test_bidirectional_beats_the_naive_level_counter_off_by_one` pins the
/// asymmetry against a deliberately-naive reference, and
/// `test_bidirectional_matches_one_sided_on_random_graphs` cross-checks every
/// length against a one-sided BFS over thousands of random pairs.
///
/// # Arguments
///
/// * `expand_fwd(u, sink)` feeds `sink` every node reachable from `u` in the
/// query's direction; `expand_bwd(u, sink)` does the same for the reverse
/// direction. Duplicates are harmless — the trees double as visited sets.
/// * `via(w)` gates a node's use as an **intermediate** hop, identically on
/// both frontiers. `source` and `target` are exempt (checked here, so a
/// caller's predicate need not special-case them), matching the rest of the
/// family: `via_types` restricts the middle of a path, never its ends.
/// * `deadline` is polled every 1000 frontier nodes; expiry answers `None`,
/// the same "no answer" a disconnected pair gets.
///
/// Returns the node sequence from `source` to `target` inclusive, or `None`.
/// When several shortest paths tie, *which* one comes back is arbitrary — as
/// it always was for the one-sided search, but a different arbitrary choice.
pub ,
mut expand_bwd:
/// Splice the two half-paths meeting at `node` into one `source → target`
/// sequence: the forward tree walked back to its root and reversed, then the
/// backward tree walked forward to *its* root, which is the target.