bmssp 1.2.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -19
- package/docs/index.html +82 -15
- package/index.mjs +19 -9
- package/package.json +1 -1
- package/src/baseCase.mjs +35 -35
- package/src/bmssp.mjs +351 -63
- package/src/findPivots.mjs +28 -29
- package/src/graph.mjs +174 -0
- package/src/tieBreak.mjs +55 -19
package/src/bmssp.mjs
CHANGED
|
@@ -1,21 +1,60 @@
|
|
|
1
1
|
import { baseCase } from "./baseCase.mjs";
|
|
2
2
|
import { findPivots } from "./findPivots.mjs";
|
|
3
3
|
import { BlockList } from "./blockList.mjs";
|
|
4
|
+
import { normalizeGraphInput } from "./graph.mjs";
|
|
4
5
|
import {
|
|
5
6
|
compareKeys,
|
|
6
7
|
compareKeyParts,
|
|
7
8
|
toBound,
|
|
8
9
|
makeTies,
|
|
9
|
-
|
|
10
|
+
makeLabels,
|
|
11
|
+
labelKey,
|
|
10
12
|
relaxEdge,
|
|
13
|
+
NO_PRED,
|
|
11
14
|
RELAX_LOST,
|
|
12
15
|
} from "./tieBreak.mjs";
|
|
13
16
|
|
|
17
|
+
/**
|
|
18
|
+
* BMSSP — the paper's Algorithm 3 behind a small class API.
|
|
19
|
+
*
|
|
20
|
+
* **Supported public API (stable as of 2.0.0 — see `MIGRATION.md`):**
|
|
21
|
+
* - `new BMSSP(input)` — construct from an edge array, an adjacency `Map`/object,
|
|
22
|
+
* or a {@link Graph} builder (see the constructor).
|
|
23
|
+
* - {@link BMSSP#calculateShortestPaths} — single-source SSSP.
|
|
24
|
+
* - {@link BMSSP#calculateShortestPathsFrom} — multi-source / bounded run (the
|
|
25
|
+
* ergonomic front door to the paper's `BMSSP(l, B, S)` generalization).
|
|
26
|
+
* - {@link BMSSP#bmssp} — the low-level bounded multi-source primitive (advanced;
|
|
27
|
+
* composite keys, returns `{ bound, boundKey, vertices }`).
|
|
28
|
+
* - {@link BMSSP#reconstructPath} — canonical path for the latest run.
|
|
29
|
+
* - {@link BMSSP#getEdges} — O(1) outgoing-edge lookup.
|
|
30
|
+
* - Public fields: `shortestPaths`, `nodeIDs`, `hops`, `preds`, `adjacency`,
|
|
31
|
+
* `graph` (all documented in the constructor).
|
|
32
|
+
*
|
|
33
|
+
* **Everything else on this class is `@internal`** — the dense-index engine
|
|
34
|
+
* (`csr`, `labels`, `ids`, `indexOf`, `bmsspIndex`, `syncLabelsIn/Out`,
|
|
35
|
+
* `boundToEngine`, `keyToPublic`, `buildIndex`, …) and the derived parameters
|
|
36
|
+
* (`k`, `t`, `topLevel`, `ties`). It is not part of the public contract and may
|
|
37
|
+
* change in a minor release. The algorithm-internal modules (`BlockList`,
|
|
38
|
+
* `MinHeap`, `baseCase`, `findPivots`, `BoundIndex`, `select`, `tieBreak`) are
|
|
39
|
+
* likewise not re-exported from `index.mjs`.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
14
43
|
class BMSSP {
|
|
44
|
+
/**
|
|
45
|
+
* @public
|
|
46
|
+
* @param {Array<[number,number,number]>|Map|Object|Graph} inputGraph - Any
|
|
47
|
+
* #172 input shape: an edge array, an adjacency `Map`/object, or a `Graph`.
|
|
48
|
+
*/
|
|
15
49
|
constructor(inputGraph) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
50
|
+
// #172: accept several input shapes (edge array, adjacency map/object, or
|
|
51
|
+
// a Graph builder) and reduce them to a canonical { edges, vertices }.
|
|
52
|
+
// `vertices` is the EXPLICIT vertex universe — declared nodes, including
|
|
53
|
+
// isolated ones; edge endpoints are folded in below. An edge-array input
|
|
54
|
+
// yields an empty `vertices`, reproducing the pre-#172 "infer from edges"
|
|
55
|
+
// behavior exactly.
|
|
56
|
+
const { edges: inputEdges, vertices: declaredVertices } =
|
|
57
|
+
normalizeGraphInput(inputGraph);
|
|
19
58
|
|
|
20
59
|
// Main graph represented as an array of edges
|
|
21
60
|
this.graph = [];
|
|
@@ -24,18 +63,19 @@ class BMSSP {
|
|
|
24
63
|
// Map to store shortest paths
|
|
25
64
|
this.shortestPaths = new Map();
|
|
26
65
|
// Adjacency map: nodeId -> array of [to, weight] outgoing edges.
|
|
27
|
-
//
|
|
28
|
-
// the
|
|
66
|
+
// The public O(1) edge-lookup view (getEdges); since #205 the algorithm
|
|
67
|
+
// itself runs on the CSR arrays built by buildIndex below.
|
|
29
68
|
this.adjacency = new Map();
|
|
30
69
|
// Canonical tie-break labels (#163): hops = edge count of the canonical
|
|
31
|
-
// shortest path, preds = its predecessor pointer.
|
|
32
|
-
//
|
|
33
|
-
//
|
|
70
|
+
// shortest path, preds = its predecessor pointer. Since #205 these Maps
|
|
71
|
+
// are the PUBLIC mirror of the engine's typed-array labels, refreshed
|
|
72
|
+
// whenever a run finishes; together with shortestPaths they realize the
|
|
73
|
+
// paper's Assumption 2.1 via [length, hops, id] keys.
|
|
34
74
|
this.hops = new Map();
|
|
35
75
|
this.preds = new Map();
|
|
36
76
|
this.ties = makeTies(this.hops, this.preds);
|
|
37
77
|
|
|
38
|
-
for (let [index, edge] of
|
|
78
|
+
for (let [index, edge] of inputEdges.entries()) {
|
|
39
79
|
if (!Array.isArray(edge) || edge.length !== 3) {
|
|
40
80
|
throw new Error(`Edge at index ${index} must be [from, to, weight]`);
|
|
41
81
|
}
|
|
@@ -58,9 +98,23 @@ class BMSSP {
|
|
|
58
98
|
this.nodeIDs.add(edge[1]);
|
|
59
99
|
}
|
|
60
100
|
|
|
101
|
+
// #172: fold in explicitly declared vertices (isolated nodes included).
|
|
102
|
+
// Same finiteness contract as edge endpoints; buildIndex/buildAdjacency
|
|
103
|
+
// then give every declared node an index, an empty CSR range and an
|
|
104
|
+
// empty neighbor list.
|
|
105
|
+
for (const id of declaredVertices) {
|
|
106
|
+
if (!Number.isFinite(id)) {
|
|
107
|
+
throw new Error("Declared vertex IDs must be finite numbers");
|
|
108
|
+
}
|
|
109
|
+
this.nodeIDs.add(id);
|
|
110
|
+
}
|
|
111
|
+
|
|
61
112
|
// Build the adjacency map from the copied edges
|
|
62
113
|
this.buildAdjacency();
|
|
63
114
|
|
|
115
|
+
// Build the dense-index engine state: sorted-id index + CSR + labels
|
|
116
|
+
this.buildIndex();
|
|
117
|
+
|
|
64
118
|
// Initialize shortest paths map
|
|
65
119
|
this.initializeShortestPaths();
|
|
66
120
|
|
|
@@ -85,20 +139,71 @@ class BMSSP {
|
|
|
85
139
|
}
|
|
86
140
|
}
|
|
87
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Build the dense-index engine state (#205): assign every node ID a dense
|
|
144
|
+
* index, lay the graph out in CSR form over those indices, and allocate
|
|
145
|
+
* the typed-array labels.
|
|
146
|
+
*
|
|
147
|
+
* Indices are assigned in ASCENDING NUMERIC ID ORDER. That makes index
|
|
148
|
+
* order equal id order, so the composite-key id tie-break (#163) picks the
|
|
149
|
+
* same canonical labels the id-keyed engine did — and, because the
|
|
150
|
+
* assignment depends only on the node-ID set, results stay invariant
|
|
151
|
+
* under edge-list permutation.
|
|
152
|
+
*
|
|
153
|
+
* @internal
|
|
154
|
+
*/
|
|
155
|
+
buildIndex() {
|
|
156
|
+
const sorted = [...this.nodeIDs].sort((a, b) => a - b);
|
|
157
|
+
const n = sorted.length;
|
|
158
|
+
const m = this.graph.length;
|
|
159
|
+
// ids: index -> original node id; indexOf: original node id -> index
|
|
160
|
+
this.ids = Float64Array.from(sorted);
|
|
161
|
+
this.indexOf = new Map();
|
|
162
|
+
for (let i = 0; i < n; i += 1) {
|
|
163
|
+
this.indexOf.set(sorted[i], i);
|
|
164
|
+
}
|
|
165
|
+
// CSR: offsets[u]..offsets[u+1] delimit u's outgoing edges in
|
|
166
|
+
// targets/weights (edge order within a node follows this.graph; the
|
|
167
|
+
// #163 canonical labels are iteration-order independent anyway)
|
|
168
|
+
const offsets = new Uint32Array(n + 1);
|
|
169
|
+
for (const [from] of this.graph) {
|
|
170
|
+
offsets[this.indexOf.get(from) + 1] += 1;
|
|
171
|
+
}
|
|
172
|
+
for (let i = 0; i < n; i += 1) {
|
|
173
|
+
offsets[i + 1] += offsets[i];
|
|
174
|
+
}
|
|
175
|
+
const targets = new Uint32Array(m);
|
|
176
|
+
const weights = new Float64Array(m);
|
|
177
|
+
const cursor = offsets.slice(0, n);
|
|
178
|
+
for (const [from, to, weight] of this.graph) {
|
|
179
|
+
const u = this.indexOf.get(from);
|
|
180
|
+
const e = cursor[u];
|
|
181
|
+
cursor[u] += 1;
|
|
182
|
+
targets[e] = this.indexOf.get(to);
|
|
183
|
+
weights[e] = weight;
|
|
184
|
+
}
|
|
185
|
+
this.csr = { offsets, targets, weights };
|
|
186
|
+
// Engine labels: d̂ / hops / canonical preds over dense indices
|
|
187
|
+
this.labels = makeLabels(n);
|
|
188
|
+
}
|
|
189
|
+
|
|
88
190
|
// Return the outgoing edges of a node as an array of [to, weight].
|
|
89
191
|
// Unknown nodes return an empty array.
|
|
90
192
|
getEdges(nodeId) {
|
|
91
193
|
return this.adjacency.get(nodeId) ?? [];
|
|
92
194
|
}
|
|
93
195
|
|
|
94
|
-
// Method to initialize the shortest paths map
|
|
95
|
-
//
|
|
196
|
+
// Method to initialize the shortest paths map, its #163 tie-break mirror
|
|
197
|
+
// maps, and the #205 engine arrays behind them
|
|
96
198
|
initializeShortestPaths() {
|
|
97
199
|
for (let nodeId of this.nodeIDs) {
|
|
98
200
|
this.shortestPaths.set(nodeId, Infinity);
|
|
99
201
|
}
|
|
100
202
|
this.hops.clear();
|
|
101
203
|
this.preds.clear();
|
|
204
|
+
this.labels.dist.fill(Infinity);
|
|
205
|
+
this.labels.hops.fill(0);
|
|
206
|
+
this.labels.preds.fill(NO_PRED);
|
|
102
207
|
}
|
|
103
208
|
|
|
104
209
|
/**
|
|
@@ -110,6 +215,8 @@ class BMSSP {
|
|
|
110
215
|
* - topLevel = max(1, ⌈log₂ n / t⌉) — level of the top BMSSP call.
|
|
111
216
|
* Everything is clamped to >= 1 so tiny graphs stay out of degenerate
|
|
112
217
|
* regimes: correctness never depends on the asymptotics.
|
|
218
|
+
*
|
|
219
|
+
* @internal
|
|
113
220
|
*/
|
|
114
221
|
deriveParameters() {
|
|
115
222
|
const logn = Math.log2(Math.max(2, this.nodeIDs.size));
|
|
@@ -118,26 +225,74 @@ class BMSSP {
|
|
|
118
225
|
this.topLevel = Math.max(1, Math.ceil(logn / this.t));
|
|
119
226
|
}
|
|
120
227
|
|
|
228
|
+
// Internal (#205): load the engine arrays from the public Maps. A direct
|
|
229
|
+
// multi-source caller seeds initial state by writing this.shortestPaths
|
|
230
|
+
// (the documented contract), so the wrapper snapshots those distances into
|
|
231
|
+
// the engine. Seeded sources are roots — hop 0, no predecessor — matching
|
|
232
|
+
// both the paper and the pre-#205 behavior (the hops/preds Maps are empty
|
|
233
|
+
// after initializeShortestPaths, so they contributed nothing there either).
|
|
234
|
+
syncLabelsIn() {
|
|
235
|
+
const { dist, hops, preds } = this.labels;
|
|
236
|
+
dist.fill(Infinity);
|
|
237
|
+
hops.fill(0);
|
|
238
|
+
preds.fill(NO_PRED);
|
|
239
|
+
for (const [id, d] of this.shortestPaths) {
|
|
240
|
+
if (Number.isFinite(d)) dist[this.indexOf.get(id)] = d;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Internal (#205): mirror the engine arrays back into the public Maps
|
|
245
|
+
// (shortestPaths / hops / preds, keyed by original ids). Unreached
|
|
246
|
+
// vertices keep their Infinity entries; sources keep no preds entry
|
|
247
|
+
// (their stored pred is the NO_PRED sentinel), which reconstructPath
|
|
248
|
+
// relies on to terminate.
|
|
249
|
+
syncLabelsOut() {
|
|
250
|
+
const { dist, hops, preds } = this.labels;
|
|
251
|
+
const ids = this.ids;
|
|
252
|
+
for (let i = 0; i < ids.length; i += 1) {
|
|
253
|
+
if (dist[i] === Infinity) continue;
|
|
254
|
+
const id = ids[i];
|
|
255
|
+
this.shortestPaths.set(id, dist[i]);
|
|
256
|
+
this.hops.set(id, hops[i]);
|
|
257
|
+
if (preds[i] !== NO_PRED) this.preds.set(id, ids[preds[i]]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Internal (#205): translate a caller's bound into the engine's index
|
|
262
|
+
// space. Scalar bounds become the usual [B, -Inf, -Inf] infimum key; a
|
|
263
|
+
// composite bound's id component is mapped to its index (ids not in the
|
|
264
|
+
// graph — including the -Infinity sentinel — pass through unchanged).
|
|
265
|
+
boundToEngine(B) {
|
|
266
|
+
if (typeof B === "number") return toBound(B);
|
|
267
|
+
const idx = this.indexOf.get(B[2]);
|
|
268
|
+
return [B[0], B[1], idx === undefined ? B[2] : idx];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Internal (#205): translate an engine key's index component back to the
|
|
272
|
+
// original node id (sentinel components pass through).
|
|
273
|
+
keyToPublic(key) {
|
|
274
|
+
const idx = key[2];
|
|
275
|
+
const isIndex = Number.isInteger(idx) && idx >= 0 && idx < this.ids.length;
|
|
276
|
+
return [key[0], key[1], isIndex ? this.ids[idx] : idx];
|
|
277
|
+
}
|
|
278
|
+
|
|
121
279
|
/**
|
|
122
280
|
* BMSSP(l, B, S) — Algorithm 3 of "Breaking the Sorting Barrier for
|
|
123
281
|
* Directed Single-Source Shortest Paths": the main bounded multi-source
|
|
124
282
|
* recursion, wiring FindPivots (Algorithm 1), the Lemma 3.3 BlockList and
|
|
125
283
|
* BaseCase (Algorithm 2) together.
|
|
126
284
|
*
|
|
285
|
+
* Public boundary (#205): S holds original node ids and the returned
|
|
286
|
+
* vertices/boundKey are in id space; initial multi-source state is seeded
|
|
287
|
+
* by writing this.shortestPaths (the pre-#205 contract). Internally the
|
|
288
|
+
* call runs on the dense-index engine — this wrapper snapshots the Maps
|
|
289
|
+
* into the typed arrays, runs bmsspIndex, and mirrors the arrays back.
|
|
290
|
+
*
|
|
127
291
|
* Preconditions (the top-level call satisfies them trivially):
|
|
128
292
|
* - Every vertex in S is complete (this.shortestPaths holds its true
|
|
129
293
|
* distance), and every incomplete vertex v with d(v) < B has a shortest
|
|
130
294
|
* path through some complete vertex of S.
|
|
131
295
|
*
|
|
132
|
-
* Distance estimates live in this.shortestPaths (the paper's d̂[·]) and
|
|
133
|
-
* are relaxed in place at every level, together with the canonical hops
|
|
134
|
-
* and preds labels (#163). All internal ordering uses the composite
|
|
135
|
-
* [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
|
|
136
|
-
* Assumption 2.1: pull separators are strict, no key ever ties a bound,
|
|
137
|
-
* and the pre-#163 degenerate-tie guards (out-of-scope pivots,
|
|
138
|
-
* boundary-tied batch members, the empty-child stall escape hatch) are
|
|
139
|
-
* unnecessary by construction.
|
|
140
|
-
*
|
|
141
296
|
* Two outcomes (Lemma 3.1, strict under the composite order):
|
|
142
297
|
* - Successful execution: the block list emptied — boundKey === B's key
|
|
143
298
|
* and vertices holds every v with key(v) < B reachable through S.
|
|
@@ -147,48 +302,60 @@ class BMSSP {
|
|
|
147
302
|
* the returned bound's length (never exceed it).
|
|
148
303
|
* Every returned vertex is complete either way.
|
|
149
304
|
*
|
|
305
|
+
* @public
|
|
150
306
|
* @param {number} l - Recursion level; 0 delegates to baseCase
|
|
151
307
|
* @param {number|[number, number, *]} B - Strict upper bound on the keys
|
|
152
308
|
* in scope: a number (Infinity is allowed) or a composite bound
|
|
153
|
-
* @param {Set<*>} S - Non-empty set of complete frontier sources
|
|
309
|
+
* @param {Set<*>} S - Non-empty set of complete frontier sources (ids)
|
|
154
310
|
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
|
|
155
311
|
* The boundary B' <= B (same kind as the B passed in: scalar callers
|
|
156
312
|
* get a scalar), its composite key, and the set U of vertices
|
|
157
|
-
* completed below it
|
|
313
|
+
* completed below it — all in original-id space
|
|
158
314
|
*/
|
|
159
315
|
bmssp(l, B, S) {
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
316
|
+
this.syncLabelsIn();
|
|
317
|
+
const boundKey = this.boundToEngine(B);
|
|
318
|
+
const SIdx = new Set();
|
|
319
|
+
for (const id of S) {
|
|
320
|
+
const idx = this.indexOf.get(id);
|
|
321
|
+
// Unknown sources keep their raw id: the engine then reads an
|
|
322
|
+
// undefined label and fails the finite-distance precondition, the
|
|
323
|
+
// same error the id-keyed engine raised
|
|
324
|
+
SIdx.add(idx === undefined ? id : idx);
|
|
325
|
+
}
|
|
326
|
+
const result = this.bmsspIndex(l, boundKey, SIdx);
|
|
327
|
+
this.syncLabelsOut();
|
|
328
|
+
const vertices = new Set();
|
|
329
|
+
for (const idx of result.vertices) {
|
|
330
|
+
vertices.add(this.ids[idx]);
|
|
331
|
+
}
|
|
332
|
+
const finalKey = this.keyToPublic(result.boundKey);
|
|
164
333
|
// Project the composite result back to the caller's kind: a successful
|
|
165
334
|
// execution echoes B itself, a partial one reports the separator (whose
|
|
166
335
|
// length is strictly below a scalar B by construction)
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
? compareKeys(
|
|
336
|
+
const bound =
|
|
337
|
+
typeof B === "number"
|
|
338
|
+
? compareKeys(result.boundKey, boundKey) === 0
|
|
170
339
|
? B
|
|
171
340
|
: finalKey[0]
|
|
172
|
-
: finalKey
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
341
|
+
: finalKey;
|
|
342
|
+
return { bound, boundKey: finalKey, vertices };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Internal (#205): the actual Algorithm 3 recursion, entirely in dense
|
|
346
|
+
// index space — S/vertices hold indices, keys are [length, hops, index],
|
|
347
|
+
// the graph is CSR and the labels are the shared typed arrays.
|
|
348
|
+
bmsspIndex(l, boundKey, S) {
|
|
349
|
+
const labels = this.labels;
|
|
176
350
|
|
|
177
351
|
if (l === 0) {
|
|
178
|
-
const result = baseCase(boundKey, S,
|
|
179
|
-
return
|
|
352
|
+
const result = baseCase(boundKey, S, labels, this.csr, this.k);
|
|
353
|
+
return { boundKey: result.boundKey, vertices: result.vertices };
|
|
180
354
|
}
|
|
181
355
|
|
|
182
356
|
// Shrink the frontier: only the pivots are worth recursing on, and W
|
|
183
357
|
// is a batch of already-completed vertices folded in at the end
|
|
184
|
-
const { pivots, W } = findPivots(
|
|
185
|
-
boundKey,
|
|
186
|
-
S,
|
|
187
|
-
dHat,
|
|
188
|
-
this.adjacency,
|
|
189
|
-
this.k,
|
|
190
|
-
ties,
|
|
191
|
-
);
|
|
358
|
+
const { pivots, W } = findPivots(boundKey, S, labels, this.csr, this.k);
|
|
192
359
|
|
|
193
360
|
// Seed the Lemma 3.3 block list with the pivots. lastBoundKey tracks
|
|
194
361
|
// the paper's Bi': B when P is empty, min key over P before the first
|
|
@@ -199,7 +366,7 @@ class BMSSP {
|
|
|
199
366
|
const D = new BlockList(2 ** ((l - 1) * this.t), boundKey, compareKeys);
|
|
200
367
|
let lastBoundKey = boundKey;
|
|
201
368
|
for (const x of pivots) {
|
|
202
|
-
const key =
|
|
369
|
+
const key = labelKey(x, labels);
|
|
203
370
|
if (compareKeys(key, boundKey) < 0) {
|
|
204
371
|
D.insert(x, key);
|
|
205
372
|
if (compareKeys(key, lastBoundKey) < 0) lastBoundKey = key;
|
|
@@ -208,11 +375,12 @@ class BMSSP {
|
|
|
208
375
|
|
|
209
376
|
const U = new Set();
|
|
210
377
|
const workloadCap = this.k * 2 ** (l * this.t);
|
|
378
|
+
const { offsets, targets, weights } = this.csr;
|
|
211
379
|
|
|
212
380
|
while (U.size < workloadCap && !D.isEmpty()) {
|
|
213
381
|
// Bi, Si <- D.Pull(): the next-closest small batch and its separator
|
|
214
382
|
const { keys: Si, bound: BiKey } = D.pull();
|
|
215
|
-
const child = this.
|
|
383
|
+
const child = this.bmsspIndex(l - 1, BiKey, Si);
|
|
216
384
|
const BiPrimeKey = child.boundKey;
|
|
217
385
|
const Ui = child.vertices;
|
|
218
386
|
|
|
@@ -231,15 +399,12 @@ class BMSSP {
|
|
|
231
399
|
// and a key array is built only for the enqueue itself (#168).
|
|
232
400
|
const K = [];
|
|
233
401
|
for (const u of Ui) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const edge = edges[i];
|
|
238
|
-
const v = edge[0];
|
|
239
|
-
const result = relaxEdge(u, v, edge[1], dHat, ties);
|
|
402
|
+
for (let e = offsets[u]; e < offsets[u + 1]; e += 1) {
|
|
403
|
+
const v = targets[e];
|
|
404
|
+
const result = relaxEdge(u, v, weights[e], labels);
|
|
240
405
|
if (result === RELAX_LOST || U.has(v)) continue;
|
|
241
|
-
const length =
|
|
242
|
-
const hopCount =
|
|
406
|
+
const length = labels.dist[v];
|
|
407
|
+
const hopCount = labels.hops[v];
|
|
243
408
|
if (compareKeyParts(length, hopCount, v, BiKey) >= 0) {
|
|
244
409
|
if (compareKeyParts(length, hopCount, v, boundKey) < 0) {
|
|
245
410
|
D.insert(v, [length, hopCount, v]);
|
|
@@ -253,8 +418,8 @@ class BMSSP {
|
|
|
253
418
|
// go back in front of everything else
|
|
254
419
|
for (const x of Si) {
|
|
255
420
|
if (U.has(x)) continue;
|
|
256
|
-
const length =
|
|
257
|
-
const hopCount =
|
|
421
|
+
const length = labels.dist[x];
|
|
422
|
+
const hopCount = labels.hops[x];
|
|
258
423
|
if (
|
|
259
424
|
compareKeyParts(length, hopCount, x, BiKey) < 0 &&
|
|
260
425
|
compareKeyParts(length, hopCount, x, BiPrimeKey) >= 0
|
|
@@ -269,12 +434,11 @@ class BMSSP {
|
|
|
269
434
|
const finalKey =
|
|
270
435
|
compareKeys(lastBoundKey, boundKey) < 0 ? lastBoundKey : boundKey;
|
|
271
436
|
for (const x of W) {
|
|
272
|
-
|
|
273
|
-
if (compareKeyParts(length, ties.hops.get(x) ?? 0, x, finalKey) < 0) {
|
|
437
|
+
if (compareKeyParts(labels.dist[x], labels.hops[x], x, finalKey) < 0) {
|
|
274
438
|
U.add(x);
|
|
275
439
|
}
|
|
276
440
|
}
|
|
277
|
-
return
|
|
441
|
+
return { boundKey: finalKey, vertices: U };
|
|
278
442
|
}
|
|
279
443
|
|
|
280
444
|
// Method to calculate shortest paths from startNode via the BMSSP
|
|
@@ -282,7 +446,8 @@ class BMSSP {
|
|
|
282
446
|
// is always a successful execution, so it completes every reachable
|
|
283
447
|
// vertex; unreachable ones keep their Infinity estimate. Distances, hop
|
|
284
448
|
// counts and predecessor pointers all end at their canonical values —
|
|
285
|
-
// independent of edge or iteration order (#163)
|
|
449
|
+
// independent of edge or iteration order (#163) — and are mirrored into
|
|
450
|
+
// the public Maps when the run finishes (#205).
|
|
286
451
|
calculateShortestPaths(startNode) {
|
|
287
452
|
// To clean the state before calculation
|
|
288
453
|
this.initializeShortestPaths();
|
|
@@ -294,9 +459,131 @@ class BMSSP {
|
|
|
294
459
|
|
|
295
460
|
// The source is complete at distance 0 with zero hops and no
|
|
296
461
|
// predecessor; everything else is Infinity
|
|
297
|
-
this.
|
|
298
|
-
this.
|
|
299
|
-
this.
|
|
462
|
+
const s = this.indexOf.get(startNode);
|
|
463
|
+
this.labels.dist[s] = 0;
|
|
464
|
+
this.bmsspIndex(this.topLevel, toBound(Infinity), new Set([s]));
|
|
465
|
+
this.syncLabelsOut();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Internal (#171): reduce the flexible `sources` argument of
|
|
469
|
+
// calculateShortestPathsFrom to a Map<id, initialDistance>. Accepts a
|
|
470
|
+
// Map<id, dist>, a plain object { id: dist } (numeric-string keys coerced
|
|
471
|
+
// like #172's adjacency objects), an array of [id, dist] pairs, or a bare
|
|
472
|
+
// array of ids (each seeded at distance 0). Every id must be a known node
|
|
473
|
+
// and every initial distance a finite non-negative number; a repeated
|
|
474
|
+
// source keeps its smallest initial distance.
|
|
475
|
+
normalizeSources(sources) {
|
|
476
|
+
const out = new Map();
|
|
477
|
+
const add = (id, dist) => {
|
|
478
|
+
if (!this.nodeIDs.has(id)) {
|
|
479
|
+
throw new Error(`Source ${id} not found in the graph`);
|
|
480
|
+
}
|
|
481
|
+
if (!Number.isFinite(dist) || dist < 0) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`Source ${id} must have a non-negative finite initial distance`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const prev = out.get(id);
|
|
487
|
+
out.set(id, prev === undefined ? dist : Math.min(prev, dist));
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
if (sources instanceof Map) {
|
|
491
|
+
for (const [id, dist] of sources) add(id, dist);
|
|
492
|
+
} else if (Array.isArray(sources)) {
|
|
493
|
+
for (const entry of sources) {
|
|
494
|
+
if (Array.isArray(entry)) {
|
|
495
|
+
if (entry.length !== 2) {
|
|
496
|
+
throw new Error("Each source pair must be [id, initialDistance]");
|
|
497
|
+
}
|
|
498
|
+
add(entry[0], entry[1]);
|
|
499
|
+
} else {
|
|
500
|
+
add(entry, 0);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
} else if (sources && typeof sources === "object") {
|
|
504
|
+
for (const key of Object.keys(sources)) {
|
|
505
|
+
add(Number(key), sources[key]);
|
|
506
|
+
}
|
|
507
|
+
} else {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"sources must be a Map, object, array of [id, dist] pairs, or array of ids",
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
return out;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Multi-source, optionally-bounded shortest paths — the public form of the
|
|
517
|
+
* paper's BMSSP(l, B, S) generalization (§04). Runs from a SET of sources,
|
|
518
|
+
* each with an initial distance, optionally under a strict distance bound B.
|
|
519
|
+
* Single-source SSSP is exactly the special case sources = [start],
|
|
520
|
+
* bound = Infinity — what calculateShortestPaths does.
|
|
521
|
+
*
|
|
522
|
+
* Results land in this.shortestPaths (and the hops/preds mirror), like
|
|
523
|
+
* calculateShortestPaths: every completed vertex holds its distance;
|
|
524
|
+
* vertices not reached — or, in a bounded run, not completed below B — keep
|
|
525
|
+
* Infinity. Read the answer from this.shortestPaths after the call. Under a
|
|
526
|
+
* finite bound only the completed set U is exposed: BMSSP may relax vertices
|
|
527
|
+
* above B without completing them, so those leftover estimates are pruned
|
|
528
|
+
* back to Infinity, leaving exactly the vertices with distance < B.
|
|
529
|
+
*
|
|
530
|
+
* The initial distances are treated as the sources' TRUE (complete)
|
|
531
|
+
* distances — the paper's precondition on S. With the common all-zero
|
|
532
|
+
* seeding this is trivially satisfied (nearest-of-many). Passing custom
|
|
533
|
+
* initial distances where one source's shortest path undercuts another's
|
|
534
|
+
* declared distance violates the precondition; the multi-source ground
|
|
535
|
+
* truth is trueDist(v) = min over sources s of (d0[s] + dist_s(v)).
|
|
536
|
+
*
|
|
537
|
+
* @public
|
|
538
|
+
* @param {Map<number,number>|Object<string,number>|Array<[number,number]>|number[]} sources
|
|
539
|
+
* The source set. A Map<id, dist>, a plain object { id: dist } (numeric
|
|
540
|
+
* string keys coerced), an array of [id, dist] pairs, or a bare array of
|
|
541
|
+
* ids (each seeded at distance 0).
|
|
542
|
+
* @param {object} [options]
|
|
543
|
+
* @param {number} [options.bound=Infinity] - Strict distance upper bound B;
|
|
544
|
+
* only vertices with distance < B are completed. Infinity runs unbounded.
|
|
545
|
+
* @throws {Error} If sources is empty or an unrecognized shape, a source id
|
|
546
|
+
* is not in the graph, an initial distance is not a non-negative finite
|
|
547
|
+
* number, or bound is not a non-negative number / Infinity.
|
|
548
|
+
*/
|
|
549
|
+
calculateShortestPathsFrom(sources, { bound = Infinity } = {}) {
|
|
550
|
+
this.initializeShortestPaths();
|
|
551
|
+
|
|
552
|
+
const seeded = this.normalizeSources(sources);
|
|
553
|
+
if (seeded.size === 0) {
|
|
554
|
+
throw new Error("At least one source is required");
|
|
555
|
+
}
|
|
556
|
+
if (typeof bound !== "number" || Number.isNaN(bound) || bound < 0) {
|
|
557
|
+
throw new Error("bound must be a non-negative number or Infinity");
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Seed each source's initial distance into the public estimate; the
|
|
561
|
+
// bmssp() wrapper's syncLabelsIn snapshots these finite distances into the
|
|
562
|
+
// engine as complete roots (hop 0, no predecessor).
|
|
563
|
+
for (const [id, dist] of seeded) {
|
|
564
|
+
this.shortestPaths.set(id, dist);
|
|
565
|
+
}
|
|
566
|
+
const { vertices } = this.bmssp(
|
|
567
|
+
this.topLevel,
|
|
568
|
+
bound,
|
|
569
|
+
new Set(seeded.keys()),
|
|
570
|
+
);
|
|
571
|
+
|
|
572
|
+
// Under a finite bound the top-level call is a successful execution whose
|
|
573
|
+
// returned set U is exactly the vertices completed below B. BMSSP relaxes
|
|
574
|
+
// (but does not complete) some vertices at or above B, leaving over-
|
|
575
|
+
// estimates in the mirror; prune everything outside U so the public Maps
|
|
576
|
+
// report only the exact in-bound distances (matching an unbounded run,
|
|
577
|
+
// whose U already equals the reachable set — no pruning needed there).
|
|
578
|
+
if (bound !== Infinity) {
|
|
579
|
+
for (const id of this.nodeIDs) {
|
|
580
|
+
if (!vertices.has(id)) {
|
|
581
|
+
this.shortestPaths.set(id, Infinity);
|
|
582
|
+
this.hops.delete(id);
|
|
583
|
+
this.preds.delete(id);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
300
587
|
}
|
|
301
588
|
|
|
302
589
|
/**
|
|
@@ -304,6 +591,7 @@ class BMSSP {
|
|
|
304
591
|
* target. Returns an empty array when target is unreachable or no shortest
|
|
305
592
|
* path run has completed yet.
|
|
306
593
|
*
|
|
594
|
+
* @public
|
|
307
595
|
* @param {number} target - A node in the graph
|
|
308
596
|
* @returns {number[]} Node IDs from the source through target
|
|
309
597
|
* @throws {Error} If target is not in the graph
|