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
|
@@ -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 };
|
package/src/findPivots.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { compareKeys, toBound, makeTies, relaxEdge } from "./tieBreak.mjs";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* FindPivots(B, S) — Algorithm 1 of "Breaking the Sorting Barrier for Directed
|
|
3
5
|
* Single-Source Shortest Paths". Shrinks the frontier S: runs k rounds of
|
|
@@ -13,30 +15,42 @@
|
|
|
13
15
|
*
|
|
14
16
|
* Distance estimates are read from and written into dHat in place — exactly
|
|
15
17
|
* like the paper's global d̂[·] labels, so improvements made here are visible
|
|
16
|
-
* to the levels above.
|
|
17
|
-
*
|
|
18
|
+
* to the levels above. Relaxation is canonical (#163, src/tieBreak.mjs):
|
|
19
|
+
* d̂, hops and preds move together under the composite [length, hops, id]
|
|
20
|
+
* order, and an exact-equality result re-admits a vertex to W through its
|
|
21
|
+
* one canonical label-setter (the paper's `≤` made deterministic). The `< B`
|
|
22
|
+
* test only gates membership in W; d̂ updates are not gated.
|
|
23
|
+
*
|
|
24
|
+
* The paper's tight-edge forest falls out of the canonical labels: each
|
|
25
|
+
* vertex of W \ S hangs off its recorded canonical predecessor, which is
|
|
26
|
+
* always itself in W. Equal-length paths cannot make this a DAG (one pred
|
|
27
|
+
* per vertex) and tight zero-weight cycles cannot occur (a zero-weight edge
|
|
28
|
+
* strictly increases the hops component), so parent chains always terminate
|
|
29
|
+
* in S — the two #44-era tie ambiguities are gone by construction. Members
|
|
30
|
+
* of S are roots by definition and never count toward another source's tree.
|
|
18
31
|
*
|
|
19
32
|
* Two outcomes:
|
|
20
33
|
* - Early exit (|W| grows past k·|S|): the frontier is already small relative
|
|
21
34
|
* to W, so every vertex of S is a pivot.
|
|
22
35
|
* - Forest case (k rounds complete with |W| <= k·|S|): pivots are the S-roots
|
|
23
|
-
* of
|
|
24
|
-
*
|
|
25
|
-
* parent deterministically (first tight edge in W iteration order) so tree
|
|
26
|
-
* sizes are well-defined. Guarantees |pivots| <= |W| / k either way.
|
|
36
|
+
* of canonical-predecessor trees with >= k vertices.
|
|
37
|
+
* Guarantees |pivots| <= |W| / k either way.
|
|
27
38
|
*
|
|
28
|
-
* @param {number} B - Strict upper bound gating membership
|
|
39
|
+
* @param {number|[number, number, *]} B - Strict upper bound gating membership
|
|
40
|
+
* in W: a number (Infinity is allowed) or a composite bound
|
|
29
41
|
* @param {Set<*>|Iterable<*>} S - Non-empty set of complete frontier sources
|
|
30
42
|
* @param {Map<*, number>} dHat - Global distance estimates d̂[·], updated in place
|
|
31
43
|
* @param {Map<*, Array<[*, number]>>} adjacency - nodeId -> outgoing [to, weight] edges
|
|
32
44
|
* @param {number} k - Relaxation rounds / tree-size threshold, >= 1 (floored);
|
|
33
45
|
* the paper's ⌊log^(1/3) n⌋
|
|
46
|
+
* @param {{ hops: Map<*, number>, preds: Map<*, *> }} [ties] - Canonical
|
|
47
|
+
* tie-break labels updated alongside dHat; fresh throwaway maps by default
|
|
34
48
|
* @returns {{ pivots: Set<*>, W: Set<*> }} The pivot subset of S and the set W
|
|
35
49
|
* of vertices touched below B (W always contains S)
|
|
36
50
|
* @throws {Error} If k is not a number >= 1, S is empty, or any source has no
|
|
37
51
|
* finite distance estimate
|
|
38
52
|
*/
|
|
39
|
-
function findPivots(B, S, dHat, adjacency, k) {
|
|
53
|
+
function findPivots(B, S, dHat, adjacency, k, ties = makeTies()) {
|
|
40
54
|
if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
|
|
41
55
|
throw new Error("k must be a number >= 1");
|
|
42
56
|
}
|
|
@@ -51,8 +65,10 @@ function findPivots(B, S, dHat, adjacency, k) {
|
|
|
51
65
|
throw new Error("every source must have a finite distance estimate");
|
|
52
66
|
}
|
|
53
67
|
}
|
|
68
|
+
const boundKey = toBound(B);
|
|
54
69
|
|
|
55
70
|
// W accumulates everything relaxed below B; layer is the paper's W_{i-1}
|
|
71
|
+
const sourceSet = new Set(sources);
|
|
56
72
|
const W = new Set(sources);
|
|
57
73
|
let layer = new Set(sources);
|
|
58
74
|
|
|
@@ -60,11 +76,12 @@ function findPivots(B, S, dHat, adjacency, k) {
|
|
|
60
76
|
const nextLayer = new Set();
|
|
61
77
|
for (const u of layer) {
|
|
62
78
|
for (const [v, weight] of adjacency.get(u) ?? []) {
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
// Canonical relaxation: d̂ updates are not gated by B (paper), and
|
|
80
|
+
// both improvements and exact canonical equality admit v to the
|
|
81
|
+
// next layer when its key is below B
|
|
82
|
+
const relaxed = relaxEdge(u, v, weight, dHat, ties);
|
|
83
|
+
if (relaxed !== null && compareKeys(relaxed.key, boundKey) < 0) {
|
|
84
|
+
nextLayer.add(v);
|
|
68
85
|
}
|
|
69
86
|
}
|
|
70
87
|
}
|
|
@@ -76,28 +93,20 @@ function findPivots(B, S, dHat, adjacency, k) {
|
|
|
76
93
|
}
|
|
77
94
|
}
|
|
78
95
|
|
|
79
|
-
// Forest
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// with parents, so they belong to no root's tree.
|
|
96
|
+
// Forest of canonical predecessors inside W: every vertex of W \ S was
|
|
97
|
+
// (re-)labeled through some layer member during this call, so its
|
|
98
|
+
// recorded pred is in W and parent chains end in S
|
|
83
99
|
const children = new Map();
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (dHat.get(v) === du + weight) {
|
|
90
|
-
hasParent.add(v);
|
|
91
|
-
if (!children.has(u)) children.set(u, []);
|
|
92
|
-
children.get(u).push(v);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
100
|
+
for (const v of W) {
|
|
101
|
+
if (sourceSet.has(v)) continue;
|
|
102
|
+
const parent = ties.preds.get(v);
|
|
103
|
+
if (!children.has(parent)) children.set(parent, []);
|
|
104
|
+
children.get(parent).push(v);
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
// Pivots: sources that root a
|
|
107
|
+
// Pivots: sources that root a canonical tree with >= k vertices
|
|
98
108
|
const pivots = new Set();
|
|
99
109
|
for (const x of sources) {
|
|
100
|
-
if (hasParent.has(x)) continue;
|
|
101
110
|
let size = 0;
|
|
102
111
|
const stack = [x];
|
|
103
112
|
while (stack.length > 0) {
|
package/src/heap.mjs
CHANGED
|
@@ -14,11 +14,29 @@
|
|
|
14
14
|
* duplicate-and-skip variant used inside src/dijkstra.mjs.
|
|
15
15
|
*/
|
|
16
16
|
class MinHeap {
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* @param {(a: *, b: *) => number} [compare] - Value comparator (negative
|
|
19
|
+
* when a < b). Defaults to numeric order, in which case values must be
|
|
20
|
+
* numbers; with a custom comparator values are opaque (#163 passes
|
|
21
|
+
* composite [length, hops, id] keys).
|
|
22
|
+
*/
|
|
23
|
+
constructor(compare) {
|
|
18
24
|
// entries[i] = [key, value], heap-ordered by value
|
|
19
25
|
this.entries = [];
|
|
20
26
|
// key -> index of that key in entries, for O(1) membership / lookup
|
|
21
27
|
this.position = new Map();
|
|
28
|
+
this.numericValues = compare === undefined;
|
|
29
|
+
this.compare = compare ?? ((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Internal: reject non-number values, but only in default numeric mode
|
|
33
|
+
checkValue(value) {
|
|
34
|
+
if (
|
|
35
|
+
this.numericValues &&
|
|
36
|
+
(typeof value !== "number" || Number.isNaN(value))
|
|
37
|
+
) {
|
|
38
|
+
throw new Error("value must be a number");
|
|
39
|
+
}
|
|
22
40
|
}
|
|
23
41
|
|
|
24
42
|
// Number of pairs currently stored
|
|
@@ -65,9 +83,7 @@ class MinHeap {
|
|
|
65
83
|
* @throws {Error} If the key is already stored, or value is not a number
|
|
66
84
|
*/
|
|
67
85
|
insert(key, value) {
|
|
68
|
-
|
|
69
|
-
throw new Error("value must be a number");
|
|
70
|
-
}
|
|
86
|
+
this.checkValue(value);
|
|
71
87
|
if (this.position.has(key)) {
|
|
72
88
|
throw new Error("key already in heap — use decreaseKey");
|
|
73
89
|
}
|
|
@@ -85,14 +101,12 @@ class MinHeap {
|
|
|
85
101
|
* @throws {Error} If the key is not stored, or value is not a number
|
|
86
102
|
*/
|
|
87
103
|
decreaseKey(key, value) {
|
|
88
|
-
|
|
89
|
-
throw new Error("value must be a number");
|
|
90
|
-
}
|
|
104
|
+
this.checkValue(value);
|
|
91
105
|
const index = this.position.get(key);
|
|
92
106
|
if (index === undefined) {
|
|
93
107
|
throw new Error("key not in heap — use insert");
|
|
94
108
|
}
|
|
95
|
-
if (this.entries[index][1] <=
|
|
109
|
+
if (this.compare(this.entries[index][1], value) <= 0) return;
|
|
96
110
|
this.entries[index][1] = value;
|
|
97
111
|
this.siftUp(index);
|
|
98
112
|
}
|
|
@@ -132,7 +146,7 @@ class MinHeap {
|
|
|
132
146
|
let i = index;
|
|
133
147
|
while (i > 0) {
|
|
134
148
|
const parent = (i - 1) >> 1;
|
|
135
|
-
if (this.entries[parent][1]
|
|
149
|
+
if (this.compare(this.entries[parent][1], this.entries[i][1]) <= 0) break;
|
|
136
150
|
this.swap(parent, i);
|
|
137
151
|
i = parent;
|
|
138
152
|
}
|
|
@@ -147,13 +161,13 @@ class MinHeap {
|
|
|
147
161
|
let smallest = i;
|
|
148
162
|
if (
|
|
149
163
|
left < this.entries.length &&
|
|
150
|
-
this.entries[left][1]
|
|
164
|
+
this.compare(this.entries[left][1], this.entries[smallest][1]) < 0
|
|
151
165
|
) {
|
|
152
166
|
smallest = left;
|
|
153
167
|
}
|
|
154
168
|
if (
|
|
155
169
|
right < this.entries.length &&
|
|
156
|
-
this.entries[right][1]
|
|
170
|
+
this.compare(this.entries[right][1], this.entries[smallest][1]) < 0
|
|
157
171
|
) {
|
|
158
172
|
smallest = right;
|
|
159
173
|
}
|
package/src/tieBreak.mjs
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic tie-breaking for equal-length paths (#163) — the code-level
|
|
3
|
+
* stand-in for the paper's Assumption 2.1 ("all paths have distinct
|
|
4
|
+
* lengths", "Breaking the Sorting Barrier for Directed Single-Source
|
|
5
|
+
* Shortest Paths").
|
|
6
|
+
*
|
|
7
|
+
* Every path is ranked by a composite key `[length, hops, id]`, compared
|
|
8
|
+
* lexicographically:
|
|
9
|
+
*
|
|
10
|
+
* - `length` — the ordinary path length (sum of weights). Always dominates,
|
|
11
|
+
* so plain distances are unaffected.
|
|
12
|
+
* - `hops` — the number of edges on the path (the paper's "#vertices"
|
|
13
|
+
* tie-break component). A zero-weight edge leaves `length` unchanged but
|
|
14
|
+
* always increments `hops`, so extending a path strictly increases its
|
|
15
|
+
* key: tight (zero-weight) cycles cannot tie with themselves and an
|
|
16
|
+
* equal-sum re-relaxation can never loop.
|
|
17
|
+
* - `id` — a final disambiguator standing in for the paper's full
|
|
18
|
+
* "vertex sequence" (O(1) to compare instead of O(path)): inside
|
|
19
|
+
* relaxation it is the candidate predecessor's id (picking the canonical
|
|
20
|
+
* parent among equal-(length, hops) alternatives), while for frontier
|
|
21
|
+
* ordering it is the vertex's own id (making the order on vertices a
|
|
22
|
+
* strict total order).
|
|
23
|
+
*
|
|
24
|
+
* With distinct vertex ids no two frontier keys are ever equal, which is
|
|
25
|
+
* exactly what Assumption 2.1 grants the paper: every BlockList pull
|
|
26
|
+
* separator is strict, no vertex ties with a bound, and Lemma 3.1 holds in
|
|
27
|
+
* its strict form — the guards that #40/#43/#161 had to add for degenerate
|
|
28
|
+
* ties become dead code.
|
|
29
|
+
*
|
|
30
|
+
* Scalar bounds stay expressible: `toBound(B)` maps the number B to
|
|
31
|
+
* `[B, -Infinity, -Infinity]`, the infimum of every real key of length B,
|
|
32
|
+
* so "key < toBound(B)" is exactly the public "distance strictly below B"
|
|
33
|
+
* contract.
|
|
34
|
+
*
|
|
35
|
+
* The canonical labels live in three maps updated together by `relaxEdge`:
|
|
36
|
+
* d̂ (the class's `shortestPaths`), plus `hops` and `preds` bundled as a
|
|
37
|
+
* `ties` object. Missing entries default to hops 0 (an externally seeded
|
|
38
|
+
* source is a hop-0 root) and "no predecessor" (which never loses a tie).
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
// Sentinel predecessor for sources: compares below every real id, so a
|
|
42
|
+
// source's own label never loses an equal-(length, hops) comparison.
|
|
43
|
+
const NO_PRED = -Infinity;
|
|
44
|
+
|
|
45
|
+
// Running count of compareKeys calls — the paper's cost metric ("comparisons
|
|
46
|
+
// between path lengths"). Every BMSSP-side comparison funnels through
|
|
47
|
+
// compareKeys (the heap and BlockList receive it as their comparator, and
|
|
48
|
+
// baseCase/findPivots/bmssp call it directly), so this one counter covers the
|
|
49
|
+
// whole algorithm. The benchmark harness (#170) reads it to measure the
|
|
50
|
+
// sorting barrier; one unconditional increment keeps the hot path branch-free.
|
|
51
|
+
let comparisonCount = 0;
|
|
52
|
+
|
|
53
|
+
/** Reset the compareKeys call counter to zero (benchmark instrumentation). */
|
|
54
|
+
function resetComparisonCount() {
|
|
55
|
+
comparisonCount = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Number of compareKeys calls since the last reset (benchmark
|
|
60
|
+
* instrumentation).
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
function getComparisonCount() {
|
|
64
|
+
return comparisonCount;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Lexicographic comparison of two composite keys.
|
|
69
|
+
* @param {[number, number, *]} a - [length, hops, id]
|
|
70
|
+
* @param {[number, number, *]} b - [length, hops, id]
|
|
71
|
+
* @returns {number} Negative when a < b, positive when a > b, 0 when equal
|
|
72
|
+
*/
|
|
73
|
+
function compareKeys(a, b) {
|
|
74
|
+
comparisonCount += 1;
|
|
75
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
76
|
+
if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
|
|
77
|
+
if (a[2] !== b[2]) return a[2] < b[2] ? -1 : 1;
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Normalize a bound to a composite key. A scalar B becomes the infimum of
|
|
83
|
+
* all keys of length B, preserving the strict "distance < B" contract;
|
|
84
|
+
* a composite bound (an array) passes through unchanged.
|
|
85
|
+
* @param {number|[number, number, *]} B
|
|
86
|
+
* @returns {[number, number, *]}
|
|
87
|
+
*/
|
|
88
|
+
function toBound(B) {
|
|
89
|
+
return typeof B === "number" ? [B, -Infinity, -Infinity] : B;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Bundle (or create) the tie-break label maps that accompany d̂.
|
|
94
|
+
* @param {Map<*, number>} [hops] - Canonical path edge counts
|
|
95
|
+
* @param {Map<*, *>} [preds] - Canonical predecessor of each vertex
|
|
96
|
+
* @returns {{ hops: Map<*, number>, preds: Map<*, *> }}
|
|
97
|
+
*/
|
|
98
|
+
function makeTies(hops = new Map(), preds = new Map()) {
|
|
99
|
+
return { hops, preds };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The frontier-ordering key of a vertex under its current labels.
|
|
104
|
+
* @param {*} v - Vertex id
|
|
105
|
+
* @param {Map<*, number>} dHat - Distance estimates d̂[·]
|
|
106
|
+
* @param {{ hops: Map<*, number> }} ties - Tie-break labels
|
|
107
|
+
* @returns {[number, number, *]} [d̂[v], hops[v], v]
|
|
108
|
+
*/
|
|
109
|
+
function orderKey(v, dHat, ties) {
|
|
110
|
+
return [dHat.get(v) ?? Infinity, ties.hops.get(v) ?? 0, v];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Attempt the canonical relaxation of edge (u, v). The candidate path
|
|
115
|
+
* (canonical path to u, then this edge) has path key
|
|
116
|
+
* [d̂[u] + w, hops[u] + 1, u]; it wins iff it is strictly smaller than v's
|
|
117
|
+
* current path key [d̂[v], hops[v], preds[v]] — so d̂, hops and preds move
|
|
118
|
+
* together and the chosen predecessor is deterministic regardless of edge
|
|
119
|
+
* or iteration order. Improvements strictly decrease v's path key, which is
|
|
120
|
+
* bounded below, so chains of relaxations always terminate (no zero-weight
|
|
121
|
+
* cycle can loop).
|
|
122
|
+
*
|
|
123
|
+
* The exact-equality case — the candidate IS v's current canonical label —
|
|
124
|
+
* is reported separately as `improved: false`. It is the paper's `≤`
|
|
125
|
+
* re-relaxation made canonical: when a caller completes u and relaxes its
|
|
126
|
+
* edges, an equal result on (u, v) means u is v's recorded label-setter and
|
|
127
|
+
* v may need to be re-enqueued at this level (it was labeled by a deeper
|
|
128
|
+
* call that did not complete it). Only the one canonical predecessor
|
|
129
|
+
* triggers this, so re-enqueues are deterministic and never duplicated by
|
|
130
|
+
* tied alternative parents; callers filter already-completed vertices to
|
|
131
|
+
* keep the re-enqueue finite, exactly like the paper.
|
|
132
|
+
*
|
|
133
|
+
* @param {*} u - Edge tail (its labels are read)
|
|
134
|
+
* @param {*} v - Edge head (its labels may be updated)
|
|
135
|
+
* @param {number} weight - Edge weight, >= 0
|
|
136
|
+
* @param {Map<*, number>} dHat - Distance estimates d̂[·], updated in place
|
|
137
|
+
* @param {{ hops: Map<*, number>, preds: Map<*, *> }} ties - Updated in place
|
|
138
|
+
* @param {[number, number, *]} [bound] - Optional gate: skip (no d̂ update)
|
|
139
|
+
* unless the resulting order key would be strictly below this bound
|
|
140
|
+
* @returns {{ key: [number, number, *], improved: boolean }|null} v's order
|
|
141
|
+
* key [d̂[v], hops[v], v] with `improved: true` when the labels were
|
|
142
|
+
* updated, `improved: false` when the candidate exactly matches v's
|
|
143
|
+
* canonical label; null when the candidate loses (or the bound gates it)
|
|
144
|
+
*/
|
|
145
|
+
function relaxEdge(u, v, weight, dHat, ties, bound) {
|
|
146
|
+
const length = dHat.get(u) + weight;
|
|
147
|
+
const hopCount = (ties.hops.get(u) ?? 0) + 1;
|
|
148
|
+
if (bound !== undefined && compareKeys([length, hopCount, v], bound) >= 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
const currentPathKey = [
|
|
152
|
+
dHat.get(v) ?? Infinity,
|
|
153
|
+
ties.hops.get(v) ?? 0,
|
|
154
|
+
ties.preds.has(v) ? ties.preds.get(v) : NO_PRED,
|
|
155
|
+
];
|
|
156
|
+
const cmp = compareKeys([length, hopCount, u], currentPathKey);
|
|
157
|
+
if (cmp > 0) return null;
|
|
158
|
+
if (cmp === 0) return { key: [length, hopCount, v], improved: false };
|
|
159
|
+
dHat.set(v, length);
|
|
160
|
+
ties.hops.set(v, hopCount);
|
|
161
|
+
ties.preds.set(v, u);
|
|
162
|
+
return { key: [length, hopCount, v], improved: true };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export {
|
|
166
|
+
compareKeys,
|
|
167
|
+
toBound,
|
|
168
|
+
makeTies,
|
|
169
|
+
orderKey,
|
|
170
|
+
relaxEdge,
|
|
171
|
+
NO_PRED,
|
|
172
|
+
resetComparisonCount,
|
|
173
|
+
getComparisonCount,
|
|
174
|
+
};
|