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/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,142 @@
|
|
|
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
|
+
/**
|
|
46
|
+
* Lexicographic comparison of two composite keys.
|
|
47
|
+
* @param {[number, number, *]} a - [length, hops, id]
|
|
48
|
+
* @param {[number, number, *]} b - [length, hops, id]
|
|
49
|
+
* @returns {number} Negative when a < b, positive when a > b, 0 when equal
|
|
50
|
+
*/
|
|
51
|
+
function compareKeys(a, b) {
|
|
52
|
+
if (a[0] !== b[0]) return a[0] < b[0] ? -1 : 1;
|
|
53
|
+
if (a[1] !== b[1]) return a[1] < b[1] ? -1 : 1;
|
|
54
|
+
if (a[2] !== b[2]) return a[2] < b[2] ? -1 : 1;
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Normalize a bound to a composite key. A scalar B becomes the infimum of
|
|
60
|
+
* all keys of length B, preserving the strict "distance < B" contract;
|
|
61
|
+
* a composite bound (an array) passes through unchanged.
|
|
62
|
+
* @param {number|[number, number, *]} B
|
|
63
|
+
* @returns {[number, number, *]}
|
|
64
|
+
*/
|
|
65
|
+
function toBound(B) {
|
|
66
|
+
return typeof B === "number" ? [B, -Infinity, -Infinity] : B;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Bundle (or create) the tie-break label maps that accompany d̂.
|
|
71
|
+
* @param {Map<*, number>} [hops] - Canonical path edge counts
|
|
72
|
+
* @param {Map<*, *>} [preds] - Canonical predecessor of each vertex
|
|
73
|
+
* @returns {{ hops: Map<*, number>, preds: Map<*, *> }}
|
|
74
|
+
*/
|
|
75
|
+
function makeTies(hops = new Map(), preds = new Map()) {
|
|
76
|
+
return { hops, preds };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* The frontier-ordering key of a vertex under its current labels.
|
|
81
|
+
* @param {*} v - Vertex id
|
|
82
|
+
* @param {Map<*, number>} dHat - Distance estimates d̂[·]
|
|
83
|
+
* @param {{ hops: Map<*, number> }} ties - Tie-break labels
|
|
84
|
+
* @returns {[number, number, *]} [d̂[v], hops[v], v]
|
|
85
|
+
*/
|
|
86
|
+
function orderKey(v, dHat, ties) {
|
|
87
|
+
return [dHat.get(v) ?? Infinity, ties.hops.get(v) ?? 0, v];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Attempt the canonical relaxation of edge (u, v). The candidate path
|
|
92
|
+
* (canonical path to u, then this edge) has path key
|
|
93
|
+
* [d̂[u] + w, hops[u] + 1, u]; it wins iff it is strictly smaller than v's
|
|
94
|
+
* current path key [d̂[v], hops[v], preds[v]] — so d̂, hops and preds move
|
|
95
|
+
* together and the chosen predecessor is deterministic regardless of edge
|
|
96
|
+
* or iteration order. Improvements strictly decrease v's path key, which is
|
|
97
|
+
* bounded below, so chains of relaxations always terminate (no zero-weight
|
|
98
|
+
* cycle can loop).
|
|
99
|
+
*
|
|
100
|
+
* The exact-equality case — the candidate IS v's current canonical label —
|
|
101
|
+
* is reported separately as `improved: false`. It is the paper's `≤`
|
|
102
|
+
* re-relaxation made canonical: when a caller completes u and relaxes its
|
|
103
|
+
* edges, an equal result on (u, v) means u is v's recorded label-setter and
|
|
104
|
+
* v may need to be re-enqueued at this level (it was labeled by a deeper
|
|
105
|
+
* call that did not complete it). Only the one canonical predecessor
|
|
106
|
+
* triggers this, so re-enqueues are deterministic and never duplicated by
|
|
107
|
+
* tied alternative parents; callers filter already-completed vertices to
|
|
108
|
+
* keep the re-enqueue finite, exactly like the paper.
|
|
109
|
+
*
|
|
110
|
+
* @param {*} u - Edge tail (its labels are read)
|
|
111
|
+
* @param {*} v - Edge head (its labels may be updated)
|
|
112
|
+
* @param {number} weight - Edge weight, >= 0
|
|
113
|
+
* @param {Map<*, number>} dHat - Distance estimates d̂[·], updated in place
|
|
114
|
+
* @param {{ hops: Map<*, number>, preds: Map<*, *> }} ties - Updated in place
|
|
115
|
+
* @param {[number, number, *]} [bound] - Optional gate: skip (no d̂ update)
|
|
116
|
+
* unless the resulting order key would be strictly below this bound
|
|
117
|
+
* @returns {{ key: [number, number, *], improved: boolean }|null} v's order
|
|
118
|
+
* key [d̂[v], hops[v], v] with `improved: true` when the labels were
|
|
119
|
+
* updated, `improved: false` when the candidate exactly matches v's
|
|
120
|
+
* canonical label; null when the candidate loses (or the bound gates it)
|
|
121
|
+
*/
|
|
122
|
+
function relaxEdge(u, v, weight, dHat, ties, bound) {
|
|
123
|
+
const length = dHat.get(u) + weight;
|
|
124
|
+
const hopCount = (ties.hops.get(u) ?? 0) + 1;
|
|
125
|
+
if (bound !== undefined && compareKeys([length, hopCount, v], bound) >= 0) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
const currentPathKey = [
|
|
129
|
+
dHat.get(v) ?? Infinity,
|
|
130
|
+
ties.hops.get(v) ?? 0,
|
|
131
|
+
ties.preds.has(v) ? ties.preds.get(v) : NO_PRED,
|
|
132
|
+
];
|
|
133
|
+
const cmp = compareKeys([length, hopCount, u], currentPathKey);
|
|
134
|
+
if (cmp > 0) return null;
|
|
135
|
+
if (cmp === 0) return { key: [length, hopCount, v], improved: false };
|
|
136
|
+
dHat.set(v, length);
|
|
137
|
+
ties.hops.set(v, hopCount);
|
|
138
|
+
ties.preds.set(v, u);
|
|
139
|
+
return { key: [length, hopCount, v], improved: true };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { compareKeys, toBound, makeTies, orderKey, relaxEdge, NO_PRED };
|