bmssp 1.1.1 → 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 +113 -22
- package/docs/index.html +82 -15
- package/index.mjs +19 -9
- package/package.json +1 -1
- package/src/baseCase.mjs +41 -33
- package/src/blockList.mjs +133 -68
- package/src/bmssp.mjs +370 -66
- package/src/boundIndex.mjs +243 -0
- package/src/findPivots.mjs +40 -26
- package/src/graph.mjs +174 -0
- package/src/select.mjs +144 -0
- package/src/tieBreak.mjs +111 -29
package/src/bmssp.mjs
CHANGED
|
@@ -1,19 +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,
|
|
7
|
+
compareKeyParts,
|
|
6
8
|
toBound,
|
|
7
9
|
makeTies,
|
|
8
|
-
|
|
10
|
+
makeLabels,
|
|
11
|
+
labelKey,
|
|
9
12
|
relaxEdge,
|
|
13
|
+
NO_PRED,
|
|
14
|
+
RELAX_LOST,
|
|
10
15
|
} from "./tieBreak.mjs";
|
|
11
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
|
+
*/
|
|
12
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
|
+
*/
|
|
13
49
|
constructor(inputGraph) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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);
|
|
17
58
|
|
|
18
59
|
// Main graph represented as an array of edges
|
|
19
60
|
this.graph = [];
|
|
@@ -22,18 +63,19 @@ class BMSSP {
|
|
|
22
63
|
// Map to store shortest paths
|
|
23
64
|
this.shortestPaths = new Map();
|
|
24
65
|
// Adjacency map: nodeId -> array of [to, weight] outgoing edges.
|
|
25
|
-
//
|
|
26
|
-
// 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.
|
|
27
68
|
this.adjacency = new Map();
|
|
28
69
|
// Canonical tie-break labels (#163): hops = edge count of the canonical
|
|
29
|
-
// shortest path, preds = its predecessor pointer.
|
|
30
|
-
//
|
|
31
|
-
//
|
|
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.
|
|
32
74
|
this.hops = new Map();
|
|
33
75
|
this.preds = new Map();
|
|
34
76
|
this.ties = makeTies(this.hops, this.preds);
|
|
35
77
|
|
|
36
|
-
for (let [index, edge] of
|
|
78
|
+
for (let [index, edge] of inputEdges.entries()) {
|
|
37
79
|
if (!Array.isArray(edge) || edge.length !== 3) {
|
|
38
80
|
throw new Error(`Edge at index ${index} must be [from, to, weight]`);
|
|
39
81
|
}
|
|
@@ -56,9 +98,23 @@ class BMSSP {
|
|
|
56
98
|
this.nodeIDs.add(edge[1]);
|
|
57
99
|
}
|
|
58
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
|
+
|
|
59
112
|
// Build the adjacency map from the copied edges
|
|
60
113
|
this.buildAdjacency();
|
|
61
114
|
|
|
115
|
+
// Build the dense-index engine state: sorted-id index + CSR + labels
|
|
116
|
+
this.buildIndex();
|
|
117
|
+
|
|
62
118
|
// Initialize shortest paths map
|
|
63
119
|
this.initializeShortestPaths();
|
|
64
120
|
|
|
@@ -83,20 +139,71 @@ class BMSSP {
|
|
|
83
139
|
}
|
|
84
140
|
}
|
|
85
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
|
+
|
|
86
190
|
// Return the outgoing edges of a node as an array of [to, weight].
|
|
87
191
|
// Unknown nodes return an empty array.
|
|
88
192
|
getEdges(nodeId) {
|
|
89
193
|
return this.adjacency.get(nodeId) ?? [];
|
|
90
194
|
}
|
|
91
195
|
|
|
92
|
-
// Method to initialize the shortest paths map
|
|
93
|
-
//
|
|
196
|
+
// Method to initialize the shortest paths map, its #163 tie-break mirror
|
|
197
|
+
// maps, and the #205 engine arrays behind them
|
|
94
198
|
initializeShortestPaths() {
|
|
95
199
|
for (let nodeId of this.nodeIDs) {
|
|
96
200
|
this.shortestPaths.set(nodeId, Infinity);
|
|
97
201
|
}
|
|
98
202
|
this.hops.clear();
|
|
99
203
|
this.preds.clear();
|
|
204
|
+
this.labels.dist.fill(Infinity);
|
|
205
|
+
this.labels.hops.fill(0);
|
|
206
|
+
this.labels.preds.fill(NO_PRED);
|
|
100
207
|
}
|
|
101
208
|
|
|
102
209
|
/**
|
|
@@ -108,6 +215,8 @@ class BMSSP {
|
|
|
108
215
|
* - topLevel = max(1, ⌈log₂ n / t⌉) — level of the top BMSSP call.
|
|
109
216
|
* Everything is clamped to >= 1 so tiny graphs stay out of degenerate
|
|
110
217
|
* regimes: correctness never depends on the asymptotics.
|
|
218
|
+
*
|
|
219
|
+
* @internal
|
|
111
220
|
*/
|
|
112
221
|
deriveParameters() {
|
|
113
222
|
const logn = Math.log2(Math.max(2, this.nodeIDs.size));
|
|
@@ -116,26 +225,74 @@ class BMSSP {
|
|
|
116
225
|
this.topLevel = Math.max(1, Math.ceil(logn / this.t));
|
|
117
226
|
}
|
|
118
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
|
+
|
|
119
279
|
/**
|
|
120
280
|
* BMSSP(l, B, S) — Algorithm 3 of "Breaking the Sorting Barrier for
|
|
121
281
|
* Directed Single-Source Shortest Paths": the main bounded multi-source
|
|
122
282
|
* recursion, wiring FindPivots (Algorithm 1), the Lemma 3.3 BlockList and
|
|
123
283
|
* BaseCase (Algorithm 2) together.
|
|
124
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
|
+
*
|
|
125
291
|
* Preconditions (the top-level call satisfies them trivially):
|
|
126
292
|
* - Every vertex in S is complete (this.shortestPaths holds its true
|
|
127
293
|
* distance), and every incomplete vertex v with d(v) < B has a shortest
|
|
128
294
|
* path through some complete vertex of S.
|
|
129
295
|
*
|
|
130
|
-
* Distance estimates live in this.shortestPaths (the paper's d̂[·]) and
|
|
131
|
-
* are relaxed in place at every level, together with the canonical hops
|
|
132
|
-
* and preds labels (#163). All internal ordering uses the composite
|
|
133
|
-
* [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
|
|
134
|
-
* Assumption 2.1: pull separators are strict, no key ever ties a bound,
|
|
135
|
-
* and the pre-#163 degenerate-tie guards (out-of-scope pivots,
|
|
136
|
-
* boundary-tied batch members, the empty-child stall escape hatch) are
|
|
137
|
-
* unnecessary by construction.
|
|
138
|
-
*
|
|
139
296
|
* Two outcomes (Lemma 3.1, strict under the composite order):
|
|
140
297
|
* - Successful execution: the block list emptied — boundKey === B's key
|
|
141
298
|
* and vertices holds every v with key(v) < B reachable through S.
|
|
@@ -145,48 +302,60 @@ class BMSSP {
|
|
|
145
302
|
* the returned bound's length (never exceed it).
|
|
146
303
|
* Every returned vertex is complete either way.
|
|
147
304
|
*
|
|
305
|
+
* @public
|
|
148
306
|
* @param {number} l - Recursion level; 0 delegates to baseCase
|
|
149
307
|
* @param {number|[number, number, *]} B - Strict upper bound on the keys
|
|
150
308
|
* in scope: a number (Infinity is allowed) or a composite bound
|
|
151
|
-
* @param {Set<*>} S - Non-empty set of complete frontier sources
|
|
309
|
+
* @param {Set<*>} S - Non-empty set of complete frontier sources (ids)
|
|
152
310
|
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
|
|
153
311
|
* The boundary B' <= B (same kind as the B passed in: scalar callers
|
|
154
312
|
* get a scalar), its composite key, and the set U of vertices
|
|
155
|
-
* completed below it
|
|
313
|
+
* completed below it — all in original-id space
|
|
156
314
|
*/
|
|
157
315
|
bmssp(l, B, S) {
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
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);
|
|
162
333
|
// Project the composite result back to the caller's kind: a successful
|
|
163
334
|
// execution echoes B itself, a partial one reports the separator (whose
|
|
164
335
|
// length is strictly below a scalar B by construction)
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
? compareKeys(
|
|
336
|
+
const bound =
|
|
337
|
+
typeof B === "number"
|
|
338
|
+
? compareKeys(result.boundKey, boundKey) === 0
|
|
168
339
|
? B
|
|
169
340
|
: finalKey[0]
|
|
170
|
-
: finalKey
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
174
350
|
|
|
175
351
|
if (l === 0) {
|
|
176
|
-
const result = baseCase(boundKey, S,
|
|
177
|
-
return
|
|
352
|
+
const result = baseCase(boundKey, S, labels, this.csr, this.k);
|
|
353
|
+
return { boundKey: result.boundKey, vertices: result.vertices };
|
|
178
354
|
}
|
|
179
355
|
|
|
180
356
|
// Shrink the frontier: only the pivots are worth recursing on, and W
|
|
181
357
|
// is a batch of already-completed vertices folded in at the end
|
|
182
|
-
const { pivots, W } = findPivots(
|
|
183
|
-
boundKey,
|
|
184
|
-
S,
|
|
185
|
-
dHat,
|
|
186
|
-
this.adjacency,
|
|
187
|
-
this.k,
|
|
188
|
-
ties,
|
|
189
|
-
);
|
|
358
|
+
const { pivots, W } = findPivots(boundKey, S, labels, this.csr, this.k);
|
|
190
359
|
|
|
191
360
|
// Seed the Lemma 3.3 block list with the pivots. lastBoundKey tracks
|
|
192
361
|
// the paper's Bi': B when P is empty, min key over P before the first
|
|
@@ -197,7 +366,7 @@ class BMSSP {
|
|
|
197
366
|
const D = new BlockList(2 ** ((l - 1) * this.t), boundKey, compareKeys);
|
|
198
367
|
let lastBoundKey = boundKey;
|
|
199
368
|
for (const x of pivots) {
|
|
200
|
-
const key =
|
|
369
|
+
const key = labelKey(x, labels);
|
|
201
370
|
if (compareKeys(key, boundKey) < 0) {
|
|
202
371
|
D.insert(x, key);
|
|
203
372
|
if (compareKeys(key, lastBoundKey) < 0) lastBoundKey = key;
|
|
@@ -206,11 +375,12 @@ class BMSSP {
|
|
|
206
375
|
|
|
207
376
|
const U = new Set();
|
|
208
377
|
const workloadCap = this.k * 2 ** (l * this.t);
|
|
378
|
+
const { offsets, targets, weights } = this.csr;
|
|
209
379
|
|
|
210
380
|
while (U.size < workloadCap && !D.isEmpty()) {
|
|
211
381
|
// Bi, Si <- D.Pull(): the next-closest small batch and its separator
|
|
212
382
|
const { keys: Si, bound: BiKey } = D.pull();
|
|
213
|
-
const child = this.
|
|
383
|
+
const child = this.bmsspIndex(l - 1, BiKey, Si);
|
|
214
384
|
const BiPrimeKey = child.boundKey;
|
|
215
385
|
const Ui = child.vertices;
|
|
216
386
|
|
|
@@ -224,19 +394,23 @@ class BMSSP {
|
|
|
224
394
|
// without completing (the paper's `≤` relaxation, deterministic here:
|
|
225
395
|
// only the recorded label-setter triggers it); vertices already
|
|
226
396
|
// completed at this level are filtered so re-enqueues stay finite.
|
|
397
|
+
// A non-lost relaxation leaves v's stored label equal to the
|
|
398
|
+
// candidate, so the band tests compare the unpacked stored components
|
|
399
|
+
// and a key array is built only for the enqueue itself (#168).
|
|
227
400
|
const K = [];
|
|
228
401
|
for (const u of Ui) {
|
|
229
|
-
for (
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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);
|
|
405
|
+
if (result === RELAX_LOST || U.has(v)) continue;
|
|
406
|
+
const length = labels.dist[v];
|
|
407
|
+
const hopCount = labels.hops[v];
|
|
408
|
+
if (compareKeyParts(length, hopCount, v, BiKey) >= 0) {
|
|
409
|
+
if (compareKeyParts(length, hopCount, v, boundKey) < 0) {
|
|
410
|
+
D.insert(v, [length, hopCount, v]);
|
|
411
|
+
}
|
|
412
|
+
} else if (compareKeyParts(length, hopCount, v, BiPrimeKey) >= 0) {
|
|
413
|
+
K.push([v, [length, hopCount, v]]);
|
|
240
414
|
}
|
|
241
415
|
}
|
|
242
416
|
}
|
|
@@ -244,9 +418,13 @@ class BMSSP {
|
|
|
244
418
|
// go back in front of everything else
|
|
245
419
|
for (const x of Si) {
|
|
246
420
|
if (U.has(x)) continue;
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
421
|
+
const length = labels.dist[x];
|
|
422
|
+
const hopCount = labels.hops[x];
|
|
423
|
+
if (
|
|
424
|
+
compareKeyParts(length, hopCount, x, BiKey) < 0 &&
|
|
425
|
+
compareKeyParts(length, hopCount, x, BiPrimeKey) >= 0
|
|
426
|
+
) {
|
|
427
|
+
K.push([x, [length, hopCount, x]]);
|
|
250
428
|
}
|
|
251
429
|
}
|
|
252
430
|
if (K.length > 0) D.batchPrepend(K);
|
|
@@ -256,9 +434,11 @@ class BMSSP {
|
|
|
256
434
|
const finalKey =
|
|
257
435
|
compareKeys(lastBoundKey, boundKey) < 0 ? lastBoundKey : boundKey;
|
|
258
436
|
for (const x of W) {
|
|
259
|
-
if (
|
|
437
|
+
if (compareKeyParts(labels.dist[x], labels.hops[x], x, finalKey) < 0) {
|
|
438
|
+
U.add(x);
|
|
439
|
+
}
|
|
260
440
|
}
|
|
261
|
-
return
|
|
441
|
+
return { boundKey: finalKey, vertices: U };
|
|
262
442
|
}
|
|
263
443
|
|
|
264
444
|
// Method to calculate shortest paths from startNode via the BMSSP
|
|
@@ -266,7 +446,8 @@ class BMSSP {
|
|
|
266
446
|
// is always a successful execution, so it completes every reachable
|
|
267
447
|
// vertex; unreachable ones keep their Infinity estimate. Distances, hop
|
|
268
448
|
// counts and predecessor pointers all end at their canonical values —
|
|
269
|
-
// 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).
|
|
270
451
|
calculateShortestPaths(startNode) {
|
|
271
452
|
// To clean the state before calculation
|
|
272
453
|
this.initializeShortestPaths();
|
|
@@ -278,9 +459,131 @@ class BMSSP {
|
|
|
278
459
|
|
|
279
460
|
// The source is complete at distance 0 with zero hops and no
|
|
280
461
|
// predecessor; everything else is Infinity
|
|
281
|
-
this.
|
|
282
|
-
this.
|
|
283
|
-
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
|
+
}
|
|
284
587
|
}
|
|
285
588
|
|
|
286
589
|
/**
|
|
@@ -288,6 +591,7 @@ class BMSSP {
|
|
|
288
591
|
* target. Returns an empty array when target is unreachable or no shortest
|
|
289
592
|
* path run has completed yet.
|
|
290
593
|
*
|
|
594
|
+
* @public
|
|
291
595
|
* @param {number} target - A node in the graph
|
|
292
596
|
* @returns {number[]} Node IDs from the source through target
|
|
293
597
|
* @throws {Error} If target is not in the graph
|