bmssp 1.0.1 → 1.1.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 +66 -17
- package/docs/index.html +186 -36
- package/index.mjs +39 -0
- package/package.json +1 -1
- package/src/baseCase.mjs +56 -32
- package/src/blockList.mjs +26 -17
- 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 +142 -0
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,7 +149,7 @@ 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) {
|
|
@@ -155,11 +159,13 @@ class BlockList {
|
|
|
155
159
|
// Prepend in reverse chunk order so the smallest chunk lands at the front
|
|
156
160
|
for (let c = chunks.length - 1; c >= 0; c -= 1) {
|
|
157
161
|
const chunk = chunks[c];
|
|
158
|
-
|
|
162
|
+
// Seed the block bound with the first value, then max-update — avoids
|
|
163
|
+
// needing a -Infinity sentinel that a custom comparator can't order
|
|
164
|
+
const block = this.makeBlock(chunk[0][1]);
|
|
159
165
|
for (const [key, value] of chunk) {
|
|
160
166
|
block.entries.set(key, value);
|
|
161
167
|
this.locator.set(key, block);
|
|
162
|
-
if (value
|
|
168
|
+
if (this.compare(value, block.bound) > 0) block.bound = value;
|
|
163
169
|
this.count += 1;
|
|
164
170
|
}
|
|
165
171
|
this.d0.unshift(block);
|
|
@@ -198,21 +204,24 @@ class BlockList {
|
|
|
198
204
|
}
|
|
199
205
|
}
|
|
200
206
|
// Take the M smallest candidates out of the structure
|
|
201
|
-
candidates.sort((a, b) => a[1]
|
|
207
|
+
candidates.sort((a, b) => this.compare(a[1], b[1]));
|
|
202
208
|
const keys = new Set();
|
|
203
209
|
for (let i = 0; i < this.M; i += 1) {
|
|
204
210
|
const [key, , block] = candidates[i];
|
|
205
211
|
keys.add(key);
|
|
206
212
|
this.removeKey(key, block);
|
|
207
213
|
}
|
|
208
|
-
// Separator = smallest value still stored
|
|
209
|
-
//
|
|
210
|
-
|
|
214
|
+
// Separator = smallest value still stored (non-null here: this branch
|
|
215
|
+
// only runs when more than M values were present). Thanks to the
|
|
216
|
+
// inter-block ordering it lives in the first non-empty block of d0/d1.
|
|
217
|
+
// Under a strict total order (#163's composite keys) this separator is
|
|
218
|
+
// strictly above every pulled value — no boundary ties.
|
|
219
|
+
let bound = null;
|
|
211
220
|
for (const seq of [this.d0, this.d1]) {
|
|
212
221
|
const block = seq.find((b) => b.entries.size > 0);
|
|
213
222
|
if (block) {
|
|
214
223
|
for (const value of block.entries.values()) {
|
|
215
|
-
if (value <
|
|
224
|
+
if (bound === null || this.compare(value, bound) < 0) bound = value;
|
|
216
225
|
}
|
|
217
226
|
}
|
|
218
227
|
}
|
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 };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Constant-degree transform — the paper's Preliminaries (knowledge/01 §
|
|
2
|
+
// "Preliminaries / model assumptions"; term in knowledge/07). The
|
|
3
|
+
// O(m·log^(2/3) n) bound is argued on a graph whose vertices all have
|
|
4
|
+
// in-degree AND out-degree <= 2. This module rewrites any graph into that
|
|
5
|
+
// shape while preserving every distance.
|
|
6
|
+
//
|
|
7
|
+
// OPT-IN, by design. Nothing in the core algorithm calls this: BMSSP is
|
|
8
|
+
// correct on the untransformed graph (validated node-by-node against the
|
|
9
|
+
// Dijkstra oracle). The transform only realizes the paper's structural
|
|
10
|
+
// precondition for callers who want it — it changes constants, never
|
|
11
|
+
// correctness (issue #164, milestone 1.1.0).
|
|
12
|
+
|
|
13
|
+
// How it works: split each vertex into one "port" copy per incident edge
|
|
14
|
+
// endpoint and thread those copies onto a zero-weight directed cycle. A cycle
|
|
15
|
+
// gives every copy exactly one incoming and one outgoing cycle edge; a copy
|
|
16
|
+
// that additionally hosts a single original endpoint therefore reaches
|
|
17
|
+
// degree 2 on that one side and stays at 1 on the other — never more. Because
|
|
18
|
+
// the cycle costs nothing to traverse, all copies of a vertex are mutually
|
|
19
|
+
// reachable at zero added distance, so each copy's shortest distance equals
|
|
20
|
+
// the original vertex's: the rewrite is distance-preserving.
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Rewrite a directed graph so every vertex has in-degree and out-degree <= 2,
|
|
24
|
+
* preserving all shortest-path distances.
|
|
25
|
+
*
|
|
26
|
+
* @param {Array<[number, number, number]>} graph - [from, to, weight] edges,
|
|
27
|
+
* the same input contract as the BMSSP constructor: an array of exact
|
|
28
|
+
* three-element arrays with finite numeric node IDs and finite, non-negative
|
|
29
|
+
* weights. An empty array is valid.
|
|
30
|
+
* @returns {{
|
|
31
|
+
* edges: Array<[number, number, number]>,
|
|
32
|
+
* copiesOf: Map<number, number[]>,
|
|
33
|
+
* originalOf: Map<number, number>,
|
|
34
|
+
* sourceCopy: (original: number) => number,
|
|
35
|
+
* collapse: (transformedDistances: Map<number, number>) => Map<number, number>
|
|
36
|
+
* }}
|
|
37
|
+
* - `edges`: the transformed graph (fresh integer copy IDs from 0).
|
|
38
|
+
* - `copiesOf`: original node ID -> its copy IDs, in allocation order.
|
|
39
|
+
* - `originalOf`: copy ID -> the original node ID it belongs to.
|
|
40
|
+
* - `sourceCopy(original)`: a canonical copy to start a run from (any copy
|
|
41
|
+
* works — the zero-weight cycle equalizes them); throws for an unknown
|
|
42
|
+
* node.
|
|
43
|
+
* - `collapse(distances)`: fold a transformed-graph distance map back onto
|
|
44
|
+
* the original node IDs (the min over each vertex's copies; all copies
|
|
45
|
+
* share the same value, so the min is exact).
|
|
46
|
+
*/
|
|
47
|
+
function constantDegreeTransform(graph) {
|
|
48
|
+
if (!Array.isArray(graph)) {
|
|
49
|
+
throw new Error("Input graph must be an array of edges");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let nextCopyId = 0;
|
|
53
|
+
// original node ID -> its copy IDs, in allocation order
|
|
54
|
+
const copiesOf = new Map();
|
|
55
|
+
// copy ID -> the original node ID it belongs to
|
|
56
|
+
const originalOf = new Map();
|
|
57
|
+
// per original edge, the out-port copy of `from` and the in-port copy of `to`
|
|
58
|
+
const outCopyForEdge = new Array(graph.length);
|
|
59
|
+
const inCopyForEdge = new Array(graph.length);
|
|
60
|
+
|
|
61
|
+
const allocateCopy = (originalId) => {
|
|
62
|
+
const id = nextCopyId;
|
|
63
|
+
nextCopyId += 1;
|
|
64
|
+
originalOf.set(id, originalId);
|
|
65
|
+
let copies = copiesOf.get(originalId);
|
|
66
|
+
if (copies === undefined) {
|
|
67
|
+
copies = [];
|
|
68
|
+
copiesOf.set(originalId, copies);
|
|
69
|
+
}
|
|
70
|
+
copies.push(id);
|
|
71
|
+
return id;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// One copy per edge endpoint, allocated in edge order so the whole transform
|
|
75
|
+
// is deterministic: the out-port at `from`, the in-port at `to`. Validate
|
|
76
|
+
// exactly like the BMSSP constructor so the transform can front any graph.
|
|
77
|
+
graph.forEach((edge, index) => {
|
|
78
|
+
if (!Array.isArray(edge) || edge.length !== 3) {
|
|
79
|
+
throw new Error(`Edge at index ${index} must be [from, to, weight]`);
|
|
80
|
+
}
|
|
81
|
+
const [from, to, weight] = edge;
|
|
82
|
+
if (!Number.isFinite(from) || !Number.isFinite(to)) {
|
|
83
|
+
throw new Error(`Edge at index ${index} must have numeric node IDs`);
|
|
84
|
+
}
|
|
85
|
+
if (!Number.isFinite(weight) || weight < 0) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Edge at index ${index} must have a non-negative numeric weight`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
outCopyForEdge[index] = allocateCopy(from);
|
|
91
|
+
inCopyForEdge[index] = allocateCopy(to);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const edges = [];
|
|
95
|
+
|
|
96
|
+
// Zero-weight directed cycle through each vertex's copies. A vertex with a
|
|
97
|
+
// single incident endpoint has one copy and needs no cycle.
|
|
98
|
+
for (const copies of copiesOf.values()) {
|
|
99
|
+
if (copies.length < 2) continue;
|
|
100
|
+
for (let i = 0; i < copies.length; i += 1) {
|
|
101
|
+
const next = copies[(i + 1) % copies.length];
|
|
102
|
+
edges.push([copies[i], next, 0]);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Each original edge connects its own out-copy to its own in-copy, weight
|
|
107
|
+
// unchanged.
|
|
108
|
+
graph.forEach(([, , weight], index) => {
|
|
109
|
+
edges.push([outCopyForEdge[index], inCopyForEdge[index], weight]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const sourceCopy = (original) => {
|
|
113
|
+
const copies = copiesOf.get(original);
|
|
114
|
+
if (copies === undefined) {
|
|
115
|
+
throw new Error(`Node ${original} is not in the graph`);
|
|
116
|
+
}
|
|
117
|
+
return copies[0];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const collapse = (transformedDistances) => {
|
|
121
|
+
const result = new Map();
|
|
122
|
+
for (const [original, copies] of copiesOf) {
|
|
123
|
+
let best = Infinity;
|
|
124
|
+
for (const copy of copies) {
|
|
125
|
+
const d = transformedDistances.get(copy);
|
|
126
|
+
if (d !== undefined && d < best) best = d;
|
|
127
|
+
}
|
|
128
|
+
result.set(original, best);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return { edges, copiesOf, originalOf, sourceCopy, collapse };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export { constantDegreeTransform };
|