bmssp 1.0.1 → 1.1.1
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 +87 -25
- package/docs/index.html +186 -36
- package/index.mjs +39 -0
- package/package.json +2 -1
- package/src/baseCase.mjs +56 -32
- package/src/blockList.mjs +35 -21
- package/src/bmssp.mjs +157 -84
- package/src/constantDegree.mjs +136 -0
- package/src/findPivots.mjs +39 -30
- package/src/heap.mjs +25 -11
- package/src/tieBreak.mjs +174 -0
package/src/baseCase.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { MinHeap } from "./heap.mjs";
|
|
2
|
+
import {
|
|
3
|
+
compareKeys,
|
|
4
|
+
toBound,
|
|
5
|
+
makeTies,
|
|
6
|
+
orderKey,
|
|
7
|
+
relaxEdge,
|
|
8
|
+
} from "./tieBreak.mjs";
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* BaseCase(B, S) — Algorithm 2 of "Breaking the Sorting Barrier for Directed
|
|
@@ -12,26 +19,40 @@ import { MinHeap } from "./heap.mjs";
|
|
|
12
19
|
*
|
|
13
20
|
* Distance estimates are read from and written into dHat in place — exactly
|
|
14
21
|
* like the paper's global d̂[·] labels, so improvements made here are visible
|
|
15
|
-
* to the levels above.
|
|
22
|
+
* to the levels above. Since #163 the heap is ordered by the composite
|
|
23
|
+
* [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
|
|
24
|
+
* Assumption 2.1 (distinct path lengths): a settled vertex carries its
|
|
25
|
+
* canonical (lexicographically minimal) label, which no later relaxation can
|
|
26
|
+
* improve. The settled filter below only skips canonical re-enqueue signals
|
|
27
|
+
* (exact label equality from the recorded predecessor), which is what keeps
|
|
28
|
+
* zero-weight plateaus quiescent instead of looping.
|
|
16
29
|
*
|
|
17
30
|
* Two outcomes:
|
|
18
31
|
* - Full success (fewer than k + 1 vertices exist under B): every vertex
|
|
19
|
-
* with
|
|
20
|
-
* - Partial (the k + 1 cap was hit):
|
|
21
|
-
* B'
|
|
32
|
+
* with key < B is settled and returned, with bound === B.
|
|
33
|
+
* - Partial (the k + 1 cap was hit): boundKey = the largest settled key
|
|
34
|
+
* B' < B, and exactly the k strictly-closer vertices are returned. Under
|
|
35
|
+
* the composite order the strictly-closer filter is exact — scalar-length
|
|
36
|
+
* ties with the boundary vertex are broken by (hops, id), and a returned
|
|
37
|
+
* vertex may therefore share the boundary's scalar length (the documented
|
|
38
|
+
* d(v) <= bound caveat of the scalar view).
|
|
22
39
|
* Every returned vertex is complete either way.
|
|
23
40
|
*
|
|
24
|
-
* @param {number} B - Strict upper bound on the
|
|
41
|
+
* @param {number|[number, number, *]} B - Strict upper bound on the keys to
|
|
42
|
+
* settle: a number (Infinity is allowed) or a composite bound
|
|
25
43
|
* @param {Set<*>|Iterable<*>} S - Singleton set holding the complete source x
|
|
26
44
|
* @param {Map<*, number>} dHat - Global distance estimates d̂[·], updated in place
|
|
27
45
|
* @param {Map<*, Array<[*, number]>>} adjacency - nodeId -> outgoing [to, weight] edges
|
|
28
46
|
* @param {number} k - Settle cap parameter, >= 1 (floored); the paper's ⌊log^(1/3) n⌋
|
|
29
|
-
* @
|
|
30
|
-
*
|
|
47
|
+
* @param {{ hops: Map<*, number>, preds: Map<*, *> }} [ties] - Canonical
|
|
48
|
+
* tie-break labels updated alongside dHat; fresh throwaway maps by default
|
|
49
|
+
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
|
|
50
|
+
* The boundary B' <= B (same kind as the B passed in), its composite key,
|
|
51
|
+
* and the set U of vertices settled below it
|
|
31
52
|
* @throws {Error} If k is not a number >= 1, S is not a singleton, or the
|
|
32
53
|
* source has no finite distance estimate
|
|
33
54
|
*/
|
|
34
|
-
function baseCase(B, S, dHat, adjacency, k) {
|
|
55
|
+
function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
|
|
35
56
|
if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
|
|
36
57
|
throw new Error("k must be a number >= 1");
|
|
37
58
|
}
|
|
@@ -45,50 +66,53 @@ function baseCase(B, S, dHat, adjacency, k) {
|
|
|
45
66
|
if (typeof sourceDistance !== "number" || !Number.isFinite(sourceDistance)) {
|
|
46
67
|
throw new Error("the source must have a finite distance estimate");
|
|
47
68
|
}
|
|
69
|
+
const boundKey = toBound(B);
|
|
48
70
|
|
|
49
71
|
// U0 in the paper: the vertices settled by this call, seeded with x
|
|
50
72
|
const settled = new Set([x]);
|
|
51
|
-
const heap = new MinHeap();
|
|
52
|
-
heap.insert(x,
|
|
73
|
+
const heap = new MinHeap(compareKeys);
|
|
74
|
+
heap.insert(x, orderKey(x, dHat, ties));
|
|
53
75
|
|
|
54
76
|
while (!heap.isEmpty() && settled.size < cap + 1) {
|
|
55
|
-
const { key: u
|
|
77
|
+
const { key: u } = heap.extractMin();
|
|
56
78
|
settled.add(u);
|
|
57
79
|
for (const [v, weight] of adjacency.get(u) ?? []) {
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
heap.insert(v, candidate);
|
|
70
|
-
}
|
|
80
|
+
// Canonical relaxation, gated by the bound (the paper's `< B`). An
|
|
81
|
+
// exact-equality result means u is v's recorded label-setter (v was
|
|
82
|
+
// labeled by an earlier phase without being completed) and v must be
|
|
83
|
+
// (re-)enqueued — unless this very call already settled it, which is
|
|
84
|
+
// what keeps zero-weight plateaus finite.
|
|
85
|
+
const relaxed = relaxEdge(u, v, weight, dHat, ties, boundKey);
|
|
86
|
+
if (relaxed === null || settled.has(v)) continue;
|
|
87
|
+
if (heap.has(v)) {
|
|
88
|
+
heap.decreaseKey(v, relaxed.key);
|
|
89
|
+
} else {
|
|
90
|
+
heap.insert(v, relaxed.key);
|
|
71
91
|
}
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
if (settled.size <= cap) {
|
|
76
96
|
// Exhausted before the cap: everything under B is settled (B' = B)
|
|
77
|
-
return { bound: B, vertices: settled };
|
|
97
|
+
return { bound: B, boundKey, vertices: settled };
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
// Hit the k + 1 cap: report B' = the largest settled
|
|
81
|
-
//
|
|
82
|
-
let boundary =
|
|
100
|
+
// Hit the k + 1 cap: report B' = the largest settled key and return the
|
|
101
|
+
// vertices strictly below it — exactly k of them, keys being distinct
|
|
102
|
+
let boundary = null;
|
|
83
103
|
for (const v of settled) {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
104
|
+
const key = orderKey(v, dHat, ties);
|
|
105
|
+
if (boundary === null || compareKeys(key, boundary) > 0) boundary = key;
|
|
86
106
|
}
|
|
87
107
|
const vertices = new Set();
|
|
88
108
|
for (const v of settled) {
|
|
89
|
-
if (
|
|
109
|
+
if (compareKeys(orderKey(v, dHat, ties), boundary) < 0) vertices.add(v);
|
|
90
110
|
}
|
|
91
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
bound: typeof B === "number" ? boundary[0] : boundary,
|
|
113
|
+
boundKey: boundary,
|
|
114
|
+
vertices,
|
|
115
|
+
};
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
export { baseCase };
|
package/src/blockList.mjs
CHANGED
|
@@ -22,15 +22,19 @@ class BlockList {
|
|
|
22
22
|
/**
|
|
23
23
|
* Initialize the structure (Lemma 3.3 Initialize).
|
|
24
24
|
* @param {number} M - Block size / pull batch size, >= 1. At recursion level l this is 2^((l-1)·t).
|
|
25
|
-
* @param {
|
|
25
|
+
* @param {*} B - Strict upper bound on every value ever stored (Infinity is allowed)
|
|
26
|
+
* @param {(a: *, b: *) => number} [compare] - Value comparator (negative
|
|
27
|
+
* when a < b). Defaults to numeric order; #163 passes composite
|
|
28
|
+
* [length, hops, id] keys with their lexicographic comparator.
|
|
26
29
|
* @throws {Error} If M is not a number >= 1
|
|
27
30
|
*/
|
|
28
|
-
constructor(M, B) {
|
|
31
|
+
constructor(M, B, compare) {
|
|
29
32
|
if (typeof M !== "number" || Number.isNaN(M) || M < 1) {
|
|
30
33
|
throw new Error("M must be a number >= 1");
|
|
31
34
|
}
|
|
32
35
|
this.M = Math.floor(M);
|
|
33
36
|
this.B = B;
|
|
37
|
+
this.compare = compare ?? ((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
34
38
|
// d1 starts as a single empty block with upper bound B
|
|
35
39
|
this.d1 = [this.makeBlock(B)];
|
|
36
40
|
this.d0 = [];
|
|
@@ -61,12 +65,12 @@ class BlockList {
|
|
|
61
65
|
* @throws {Error} If value >= B
|
|
62
66
|
*/
|
|
63
67
|
insert(key, value) {
|
|
64
|
-
if (!(value
|
|
68
|
+
if (!(this.compare(value, this.B) < 0)) {
|
|
65
69
|
throw new Error("value must be < B");
|
|
66
70
|
}
|
|
67
71
|
const holder = this.locator.get(key);
|
|
68
72
|
if (holder !== undefined) {
|
|
69
|
-
if (holder.entries.get(key) <=
|
|
73
|
+
if (this.compare(holder.entries.get(key), value) <= 0) return;
|
|
70
74
|
this.removeKey(key, holder);
|
|
71
75
|
}
|
|
72
76
|
// Binary-search d1 for the first block whose bound covers the value
|
|
@@ -74,7 +78,7 @@ class BlockList {
|
|
|
74
78
|
let hi = this.d1.length - 1;
|
|
75
79
|
while (lo < hi) {
|
|
76
80
|
const mid = (lo + hi) >> 1;
|
|
77
|
-
if (this.d1[mid].bound >=
|
|
81
|
+
if (this.compare(this.d1[mid].bound, value) >= 0) {
|
|
78
82
|
hi = mid;
|
|
79
83
|
} else {
|
|
80
84
|
lo = mid + 1;
|
|
@@ -96,7 +100,7 @@ class BlockList {
|
|
|
96
100
|
// simpler — acceptable for this correctness-first implementation.)
|
|
97
101
|
splitBlock(index) {
|
|
98
102
|
const block = this.d1[index];
|
|
99
|
-
const sorted = [...block.entries].sort((a, b) => a[1]
|
|
103
|
+
const sorted = [...block.entries].sort((a, b) => this.compare(a[1], b[1]));
|
|
100
104
|
const half = sorted.length >> 1;
|
|
101
105
|
const lower = this.makeBlock(sorted[half - 1][1]);
|
|
102
106
|
for (let i = 0; i < half; i += 1) {
|
|
@@ -121,11 +125,11 @@ class BlockList {
|
|
|
121
125
|
// Dedupe the batch, keeping the smallest value per key
|
|
122
126
|
const best = new Map();
|
|
123
127
|
for (const [key, value] of pairs) {
|
|
124
|
-
if (!(value
|
|
128
|
+
if (!(this.compare(value, this.B) < 0)) {
|
|
125
129
|
throw new Error("value must be < B");
|
|
126
130
|
}
|
|
127
131
|
const seen = best.get(key);
|
|
128
|
-
if (seen === undefined || value <
|
|
132
|
+
if (seen === undefined || this.compare(value, seen) < 0) {
|
|
129
133
|
best.set(key, value);
|
|
130
134
|
}
|
|
131
135
|
}
|
|
@@ -134,7 +138,7 @@ class BlockList {
|
|
|
134
138
|
for (const [key, value] of best) {
|
|
135
139
|
const holder = this.locator.get(key);
|
|
136
140
|
if (holder !== undefined) {
|
|
137
|
-
if (holder.entries.get(key) <=
|
|
141
|
+
if (this.compare(holder.entries.get(key), value) <= 0) continue;
|
|
138
142
|
this.removeKey(key, holder);
|
|
139
143
|
}
|
|
140
144
|
fresh.push([key, value]);
|
|
@@ -145,25 +149,32 @@ class BlockList {
|
|
|
145
149
|
if (fresh.length <= this.M) {
|
|
146
150
|
chunks = [fresh];
|
|
147
151
|
} else {
|
|
148
|
-
fresh.sort((a, b) => a[1]
|
|
152
|
+
fresh.sort((a, b) => this.compare(a[1], b[1]));
|
|
149
153
|
const chunkSize = Math.ceil(this.M / 2);
|
|
150
154
|
chunks = [];
|
|
151
155
|
for (let i = 0; i < fresh.length; i += chunkSize) {
|
|
152
156
|
chunks.push(fresh.slice(i, i + chunkSize));
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
+
// Materialize the chunks as blocks (ascending, so chunk 0 — the smallest
|
|
160
|
+
// values — ends up frontmost) and prepend them all in one concat. A
|
|
161
|
+
// per-chunk unshift here re-shifts the whole d0 array every time: with a
|
|
162
|
+
// small M the chunk count approaches |L| and the loop turns quadratic —
|
|
163
|
+
// the #182 star-graph blowup (~n single-entry chunks at M = 1).
|
|
164
|
+
const blocks = [];
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
// Seed the block bound with the first value, then max-update — avoids
|
|
167
|
+
// needing a -Infinity sentinel that a custom comparator can't order
|
|
168
|
+
const block = this.makeBlock(chunk[0][1]);
|
|
159
169
|
for (const [key, value] of chunk) {
|
|
160
170
|
block.entries.set(key, value);
|
|
161
171
|
this.locator.set(key, block);
|
|
162
|
-
if (value
|
|
172
|
+
if (this.compare(value, block.bound) > 0) block.bound = value;
|
|
163
173
|
this.count += 1;
|
|
164
174
|
}
|
|
165
|
-
|
|
175
|
+
blocks.push(block);
|
|
166
176
|
}
|
|
177
|
+
this.d0 = blocks.concat(this.d0);
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
/**
|
|
@@ -198,21 +209,24 @@ class BlockList {
|
|
|
198
209
|
}
|
|
199
210
|
}
|
|
200
211
|
// Take the M smallest candidates out of the structure
|
|
201
|
-
candidates.sort((a, b) => a[1]
|
|
212
|
+
candidates.sort((a, b) => this.compare(a[1], b[1]));
|
|
202
213
|
const keys = new Set();
|
|
203
214
|
for (let i = 0; i < this.M; i += 1) {
|
|
204
215
|
const [key, , block] = candidates[i];
|
|
205
216
|
keys.add(key);
|
|
206
217
|
this.removeKey(key, block);
|
|
207
218
|
}
|
|
208
|
-
// Separator = smallest value still stored
|
|
209
|
-
//
|
|
210
|
-
|
|
219
|
+
// Separator = smallest value still stored (non-null here: this branch
|
|
220
|
+
// only runs when more than M values were present). Thanks to the
|
|
221
|
+
// inter-block ordering it lives in the first non-empty block of d0/d1.
|
|
222
|
+
// Under a strict total order (#163's composite keys) this separator is
|
|
223
|
+
// strictly above every pulled value — no boundary ties.
|
|
224
|
+
let bound = null;
|
|
211
225
|
for (const seq of [this.d0, this.d1]) {
|
|
212
226
|
const block = seq.find((b) => b.entries.size > 0);
|
|
213
227
|
if (block) {
|
|
214
228
|
for (const value of block.entries.values()) {
|
|
215
|
-
if (value <
|
|
229
|
+
if (bound === null || this.compare(value, bound) < 0) bound = value;
|
|
216
230
|
}
|
|
217
231
|
}
|
|
218
232
|
}
|
package/src/bmssp.mjs
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { baseCase } from "./baseCase.mjs";
|
|
2
2
|
import { findPivots } from "./findPivots.mjs";
|
|
3
3
|
import { BlockList } from "./blockList.mjs";
|
|
4
|
+
import {
|
|
5
|
+
compareKeys,
|
|
6
|
+
toBound,
|
|
7
|
+
makeTies,
|
|
8
|
+
orderKey,
|
|
9
|
+
relaxEdge,
|
|
10
|
+
} from "./tieBreak.mjs";
|
|
4
11
|
|
|
5
12
|
class BMSSP {
|
|
6
13
|
constructor(inputGraph) {
|
|
14
|
+
if (!Array.isArray(inputGraph)) {
|
|
15
|
+
throw new Error("Input graph must be an array of edges");
|
|
16
|
+
}
|
|
17
|
+
|
|
7
18
|
// Main graph represented as an array of edges
|
|
8
19
|
this.graph = [];
|
|
9
20
|
// Set to store unique node IDs
|
|
@@ -14,8 +25,29 @@ class BMSSP {
|
|
|
14
25
|
// Lets the algorithm fetch a node's edges in O(1) instead of scanning
|
|
15
26
|
// the whole edge list on every lookup.
|
|
16
27
|
this.adjacency = new Map();
|
|
28
|
+
// Canonical tie-break labels (#163): hops = edge count of the canonical
|
|
29
|
+
// shortest path, preds = its predecessor pointer. Updated in lockstep
|
|
30
|
+
// with shortestPaths by relaxEdge; together they realize the paper's
|
|
31
|
+
// Assumption 2.1 (distinct path lengths) via [length, hops, id] keys.
|
|
32
|
+
this.hops = new Map();
|
|
33
|
+
this.preds = new Map();
|
|
34
|
+
this.ties = makeTies(this.hops, this.preds);
|
|
35
|
+
|
|
36
|
+
for (let [index, edge] of inputGraph.entries()) {
|
|
37
|
+
if (!Array.isArray(edge) || edge.length !== 3) {
|
|
38
|
+
throw new Error(`Edge at index ${index} must be [from, to, weight]`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [from, to, weight] = edge;
|
|
42
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) {
|
|
43
|
+
throw new Error(`Edge at index ${index} must have numeric node IDs`);
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isFinite(weight) || weight < 0) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Edge at index ${index} must have a non-negative numeric weight`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
17
50
|
|
|
18
|
-
for (let edge of inputGraph) {
|
|
19
51
|
// Create a deep copy of each edge array
|
|
20
52
|
this.graph.push([...edge]);
|
|
21
53
|
|
|
@@ -57,11 +89,14 @@ class BMSSP {
|
|
|
57
89
|
return this.adjacency.get(nodeId) ?? [];
|
|
58
90
|
}
|
|
59
91
|
|
|
60
|
-
// Method to initialize the shortest paths map
|
|
92
|
+
// Method to initialize the shortest paths map (and the #163 tie-break
|
|
93
|
+
// labels that travel with it)
|
|
61
94
|
initializeShortestPaths() {
|
|
62
95
|
for (let nodeId of this.nodeIDs) {
|
|
63
96
|
this.shortestPaths.set(nodeId, Infinity);
|
|
64
97
|
}
|
|
98
|
+
this.hops.clear();
|
|
99
|
+
this.preds.clear();
|
|
65
100
|
}
|
|
66
101
|
|
|
67
102
|
/**
|
|
@@ -93,45 +128,79 @@ class BMSSP {
|
|
|
93
128
|
* path through some complete vertex of S.
|
|
94
129
|
*
|
|
95
130
|
* Distance estimates live in this.shortestPaths (the paper's d̂[·]) and
|
|
96
|
-
* are relaxed in place at every level
|
|
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.
|
|
97
138
|
*
|
|
98
|
-
* Two outcomes (Lemma 3.1):
|
|
99
|
-
* - Successful execution: the block list emptied —
|
|
100
|
-
* vertices holds every v with
|
|
101
|
-
* - Partial execution: the k·2^(l·t) workload guard tripped —
|
|
102
|
-
* and vertices holds exactly the v with
|
|
139
|
+
* Two outcomes (Lemma 3.1, strict under the composite order):
|
|
140
|
+
* - Successful execution: the block list emptied — boundKey === B's key
|
|
141
|
+
* and vertices holds every v with key(v) < B reachable through S.
|
|
142
|
+
* - Partial execution: the k·2^(l·t) workload guard tripped —
|
|
143
|
+
* boundKey < B's key and vertices holds exactly the v with
|
|
144
|
+
* key(v) < boundKey. In the scalar projection a returned vertex may tie
|
|
145
|
+
* the returned bound's length (never exceed it).
|
|
103
146
|
* Every returned vertex is complete either way.
|
|
104
147
|
*
|
|
105
148
|
* @param {number} l - Recursion level; 0 delegates to baseCase
|
|
106
|
-
* @param {number} B - Strict upper bound on the
|
|
149
|
+
* @param {number|[number, number, *]} B - Strict upper bound on the keys
|
|
150
|
+
* in scope: a number (Infinity is allowed) or a composite bound
|
|
107
151
|
* @param {Set<*>} S - Non-empty set of complete frontier sources
|
|
108
|
-
* @returns {{ bound: number, vertices: Set<*> }}
|
|
109
|
-
*
|
|
152
|
+
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
|
|
153
|
+
* The boundary B' <= B (same kind as the B passed in: scalar callers
|
|
154
|
+
* get a scalar), its composite key, and the set U of vertices
|
|
155
|
+
* completed below it
|
|
110
156
|
*/
|
|
111
157
|
bmssp(l, B, S) {
|
|
112
158
|
const dHat = this.shortestPaths;
|
|
159
|
+
const ties = this.ties;
|
|
160
|
+
const boundKey = toBound(B);
|
|
161
|
+
const scalarB = typeof B === "number";
|
|
162
|
+
// Project the composite result back to the caller's kind: a successful
|
|
163
|
+
// execution echoes B itself, a partial one reports the separator (whose
|
|
164
|
+
// length is strictly below a scalar B by construction)
|
|
165
|
+
const finish = (finalKey, vertices) => ({
|
|
166
|
+
bound: scalarB
|
|
167
|
+
? compareKeys(finalKey, boundKey) === 0
|
|
168
|
+
? B
|
|
169
|
+
: finalKey[0]
|
|
170
|
+
: finalKey,
|
|
171
|
+
boundKey: finalKey,
|
|
172
|
+
vertices,
|
|
173
|
+
});
|
|
174
|
+
|
|
113
175
|
if (l === 0) {
|
|
114
|
-
|
|
176
|
+
const result = baseCase(boundKey, S, dHat, this.adjacency, this.k, ties);
|
|
177
|
+
return finish(result.boundKey, result.vertices);
|
|
115
178
|
}
|
|
116
179
|
|
|
117
180
|
// Shrink the frontier: only the pivots are worth recursing on, and W
|
|
118
181
|
// is a batch of already-completed vertices folded in at the end
|
|
119
|
-
const { pivots, W } = findPivots(
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
182
|
+
const { pivots, W } = findPivots(
|
|
183
|
+
boundKey,
|
|
184
|
+
S,
|
|
185
|
+
dHat,
|
|
186
|
+
this.adjacency,
|
|
187
|
+
this.k,
|
|
188
|
+
ties,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// Seed the Lemma 3.3 block list with the pivots. lastBoundKey tracks
|
|
192
|
+
// the paper's Bi': B when P is empty, min key over P before the first
|
|
193
|
+
// pull, then the boundary returned by the latest recursive call.
|
|
194
|
+
// The scope filter is for direct multi-source callers who may pass
|
|
195
|
+
// sources at or above B — internal calls can't produce one, because
|
|
196
|
+
// every pull separator is strict under the composite order.
|
|
197
|
+
const D = new BlockList(2 ** ((l - 1) * this.t), boundKey, compareKeys);
|
|
198
|
+
let lastBoundKey = boundKey;
|
|
130
199
|
for (const x of pivots) {
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
D.insert(x,
|
|
134
|
-
if (
|
|
200
|
+
const key = orderKey(x, dHat, ties);
|
|
201
|
+
if (compareKeys(key, boundKey) < 0) {
|
|
202
|
+
D.insert(x, key);
|
|
203
|
+
if (compareKeys(key, lastBoundKey) < 0) lastBoundKey = key;
|
|
135
204
|
}
|
|
136
205
|
}
|
|
137
206
|
|
|
@@ -140,86 +209,64 @@ class BMSSP {
|
|
|
140
209
|
|
|
141
210
|
while (U.size < workloadCap && !D.isEmpty()) {
|
|
142
211
|
// Bi, Si <- D.Pull(): the next-closest small batch and its separator
|
|
143
|
-
const { keys: Si, bound:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Degenerate tie stall: the child settled only vertices tied
|
|
148
|
-
// exactly at its boundary (possible only through zero-weight
|
|
149
|
-
// paths, which violate the paper's Assumption 2.1 — see #163),
|
|
150
|
-
// so its strict d̂ < B' filter returned nothing and this batch
|
|
151
|
-
// would be re-pulled forever. Escape hatch: settle everything
|
|
152
|
-
// below Bi reachable through the batch with an uncapped bounded
|
|
153
|
-
// Dijkstra per member — correct, just not sublinear.
|
|
154
|
-
Ui = new Set();
|
|
155
|
-
for (const x of Si) {
|
|
156
|
-
const { vertices } = baseCase(
|
|
157
|
-
Bi,
|
|
158
|
-
new Set([x]),
|
|
159
|
-
dHat,
|
|
160
|
-
this.adjacency,
|
|
161
|
-
Math.max(1, this.nodeIDs.size),
|
|
162
|
-
);
|
|
163
|
-
for (const v of vertices) Ui.add(v);
|
|
164
|
-
}
|
|
165
|
-
BiPrime = Bi;
|
|
166
|
-
}
|
|
212
|
+
const { keys: Si, bound: BiKey } = D.pull();
|
|
213
|
+
const child = this.bmssp(l - 1, BiKey, Si);
|
|
214
|
+
const BiPrimeKey = child.boundKey;
|
|
215
|
+
const Ui = child.vertices;
|
|
167
216
|
|
|
168
|
-
|
|
217
|
+
lastBoundKey = BiPrimeKey;
|
|
169
218
|
for (const v of Ui) U.add(v);
|
|
170
219
|
|
|
171
|
-
// Relax out of the newly-completed Ui, routing improved neighbors
|
|
172
|
-
//
|
|
173
|
-
//
|
|
220
|
+
// Relax out of the newly-completed Ui, routing improved neighbors by
|
|
221
|
+
// key band: [Bi, B) re-enters the block list, [Bi', Bi) is staged for
|
|
222
|
+
// a batch prepend (closer than the current batch's floor). An exact
|
|
223
|
+
// canonical equality re-enqueues a vertex that a deeper call labeled
|
|
224
|
+
// without completing (the paper's `≤` relaxation, deterministic here:
|
|
225
|
+
// only the recorded label-setter triggers it); vertices already
|
|
226
|
+
// completed at this level are filtered so re-enqueues stay finite.
|
|
174
227
|
const K = [];
|
|
175
228
|
for (const u of Ui) {
|
|
176
|
-
const du = dHat.get(u);
|
|
177
229
|
for (const [v, weight] of this.adjacency.get(u) ?? []) {
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
D.insert(v, candidate);
|
|
189
|
-
} else if (candidate >= BiPrime && candidate < Bi) {
|
|
190
|
-
K.push([v, candidate]);
|
|
191
|
-
}
|
|
230
|
+
const relaxed = relaxEdge(u, v, weight, dHat, ties);
|
|
231
|
+
if (relaxed === null || U.has(v)) continue;
|
|
232
|
+
const key = relaxed.key;
|
|
233
|
+
if (compareKeys(key, BiKey) >= 0 && compareKeys(key, boundKey) < 0) {
|
|
234
|
+
D.insert(v, key);
|
|
235
|
+
} else if (
|
|
236
|
+
compareKeys(key, BiPrimeKey) >= 0 &&
|
|
237
|
+
compareKeys(key, BiKey) < 0
|
|
238
|
+
) {
|
|
239
|
+
K.push([v, key]);
|
|
192
240
|
}
|
|
193
241
|
}
|
|
194
242
|
}
|
|
195
|
-
// Batch members the child did not complete (
|
|
196
|
-
// go back in front of everything else
|
|
197
|
-
// separator (d̂ == Bi < B, an Assumption 2.1 violation) is still in
|
|
198
|
-
// scope at this level and re-enters through a regular insert.
|
|
243
|
+
// Batch members the child did not complete (key still in [Bi', Bi))
|
|
244
|
+
// go back in front of everything else
|
|
199
245
|
for (const x of Si) {
|
|
200
246
|
if (U.has(x)) continue;
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
K.push([x,
|
|
204
|
-
} else if (dx === Bi && Bi < B) {
|
|
205
|
-
D.insert(x, dx);
|
|
247
|
+
const key = orderKey(x, dHat, ties);
|
|
248
|
+
if (compareKeys(key, BiPrimeKey) >= 0 && compareKeys(key, BiKey) < 0) {
|
|
249
|
+
K.push([x, key]);
|
|
206
250
|
}
|
|
207
251
|
}
|
|
208
252
|
if (K.length > 0) D.batchPrepend(K);
|
|
209
253
|
}
|
|
210
254
|
|
|
211
255
|
// B' <- min(last Bi', B); fold in the FindPivots batch below it
|
|
212
|
-
const
|
|
256
|
+
const finalKey =
|
|
257
|
+
compareKeys(lastBoundKey, boundKey) < 0 ? lastBoundKey : boundKey;
|
|
213
258
|
for (const x of W) {
|
|
214
|
-
if (
|
|
259
|
+
if (compareKeys(orderKey(x, dHat, ties), finalKey) < 0) U.add(x);
|
|
215
260
|
}
|
|
216
|
-
return
|
|
261
|
+
return finish(finalKey, U);
|
|
217
262
|
}
|
|
218
263
|
|
|
219
264
|
// Method to calculate shortest paths from startNode via the BMSSP
|
|
220
265
|
// recursion (Algorithm 3). The top-level call BMSSP(topLevel, ∞, {start})
|
|
221
266
|
// is always a successful execution, so it completes every reachable
|
|
222
|
-
// vertex; unreachable ones keep their Infinity estimate.
|
|
267
|
+
// vertex; unreachable ones keep their Infinity estimate. Distances, hop
|
|
268
|
+
// counts and predecessor pointers all end at their canonical values —
|
|
269
|
+
// independent of edge or iteration order (#163).
|
|
223
270
|
calculateShortestPaths(startNode) {
|
|
224
271
|
// To clean the state before calculation
|
|
225
272
|
this.initializeShortestPaths();
|
|
@@ -229,10 +276,36 @@ class BMSSP {
|
|
|
229
276
|
throw new Error("Start node not found in the graph");
|
|
230
277
|
}
|
|
231
278
|
|
|
232
|
-
// The source is complete at distance 0
|
|
279
|
+
// The source is complete at distance 0 with zero hops and no
|
|
280
|
+
// predecessor; everything else is Infinity
|
|
233
281
|
this.shortestPaths.set(startNode, 0);
|
|
282
|
+
this.hops.set(startNode, 0);
|
|
234
283
|
this.bmssp(this.topLevel, Infinity, new Set([startNode]));
|
|
235
284
|
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Reconstruct the canonical shortest path from the most recent source to
|
|
288
|
+
* target. Returns an empty array when target is unreachable or no shortest
|
|
289
|
+
* path run has completed yet.
|
|
290
|
+
*
|
|
291
|
+
* @param {number} target - A node in the graph
|
|
292
|
+
* @returns {number[]} Node IDs from the source through target
|
|
293
|
+
* @throws {Error} If target is not in the graph
|
|
294
|
+
*/
|
|
295
|
+
reconstructPath(target) {
|
|
296
|
+
if (!this.nodeIDs.has(target)) {
|
|
297
|
+
throw new Error("Target node not found in the graph");
|
|
298
|
+
}
|
|
299
|
+
if (this.shortestPaths.get(target) === Infinity) return [];
|
|
300
|
+
|
|
301
|
+
const path = [target];
|
|
302
|
+
let current = target;
|
|
303
|
+
while (this.preds.has(current)) {
|
|
304
|
+
current = this.preds.get(current);
|
|
305
|
+
path.push(current);
|
|
306
|
+
}
|
|
307
|
+
return path.reverse();
|
|
308
|
+
}
|
|
236
309
|
}
|
|
237
310
|
|
|
238
311
|
export { BMSSP };
|