bmssp 0.16.0 → 0.18.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/package.json +1 -1
- package/src/baseCase.mjs +94 -0
- package/src/heap.mjs +167 -0
package/package.json
CHANGED
package/src/baseCase.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { MinHeap } from "./heap.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* BaseCase(B, S) — Algorithm 2 of "Breaking the Sorting Barrier for Directed
|
|
5
|
+
* Single-Source Shortest Paths". The level-0 case of the BMSSP recursion: a
|
|
6
|
+
* mini Dijkstra from the single complete source x in S, bounded above by B,
|
|
7
|
+
* that stops after settling k + 1 vertices.
|
|
8
|
+
*
|
|
9
|
+
* Preconditions (guaranteed by the caller, Algorithm 3):
|
|
10
|
+
* - S is a singleton {x} and x is complete (dHat holds its true distance).
|
|
11
|
+
* - Every incomplete vertex v with d(v) < B has a shortest path through x.
|
|
12
|
+
*
|
|
13
|
+
* Distance estimates are read from and written into dHat in place — exactly
|
|
14
|
+
* like the paper's global d̂[·] labels, so improvements made here are visible
|
|
15
|
+
* to the levels above.
|
|
16
|
+
*
|
|
17
|
+
* Two outcomes:
|
|
18
|
+
* - Full success (fewer than k + 1 vertices exist under B): every vertex
|
|
19
|
+
* with d(v) < B is settled and returned, with bound === B.
|
|
20
|
+
* - Partial (the k + 1 cap was hit): bound = the largest settled distance
|
|
21
|
+
* B' <= B, and only the strictly-closer vertices (d̂ < B') are returned.
|
|
22
|
+
* Every returned vertex is complete either way.
|
|
23
|
+
*
|
|
24
|
+
* @param {number} B - Strict upper bound on the distances to settle (Infinity is allowed)
|
|
25
|
+
* @param {Set<*>|Iterable<*>} S - Singleton set holding the complete source x
|
|
26
|
+
* @param {Map<*, number>} dHat - Global distance estimates d̂[·], updated in place
|
|
27
|
+
* @param {Map<*, Array<[*, number]>>} adjacency - nodeId -> outgoing [to, weight] edges
|
|
28
|
+
* @param {number} k - Settle cap parameter, >= 1 (floored); the paper's ⌊log^(1/3) n⌋
|
|
29
|
+
* @returns {{ bound: number, vertices: Set<*> }} The boundary B' <= B and the
|
|
30
|
+
* set U of vertices settled below it
|
|
31
|
+
* @throws {Error} If k is not a number >= 1, S is not a singleton, or the
|
|
32
|
+
* source has no finite distance estimate
|
|
33
|
+
*/
|
|
34
|
+
function baseCase(B, S, dHat, adjacency, k) {
|
|
35
|
+
if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
|
|
36
|
+
throw new Error("k must be a number >= 1");
|
|
37
|
+
}
|
|
38
|
+
const cap = Math.floor(k);
|
|
39
|
+
const sources = [...S];
|
|
40
|
+
if (sources.length !== 1) {
|
|
41
|
+
throw new Error("S must contain exactly one source node");
|
|
42
|
+
}
|
|
43
|
+
const [x] = sources;
|
|
44
|
+
const sourceDistance = dHat.get(x);
|
|
45
|
+
if (typeof sourceDistance !== "number" || !Number.isFinite(sourceDistance)) {
|
|
46
|
+
throw new Error("the source must have a finite distance estimate");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// U0 in the paper: the vertices settled by this call, seeded with x
|
|
50
|
+
const settled = new Set([x]);
|
|
51
|
+
const heap = new MinHeap();
|
|
52
|
+
heap.insert(x, sourceDistance);
|
|
53
|
+
|
|
54
|
+
while (!heap.isEmpty() && settled.size < cap + 1) {
|
|
55
|
+
const { key: u, value: du } = heap.extractMin();
|
|
56
|
+
settled.add(u);
|
|
57
|
+
for (const [v, weight] of adjacency.get(u) ?? []) {
|
|
58
|
+
const candidate = du + weight;
|
|
59
|
+
// Paper relaxation: d̂[u] + w(u,v) <= d̂[v], and below the bound B
|
|
60
|
+
if (candidate <= (dHat.get(v) ?? Infinity) && candidate < B) {
|
|
61
|
+
dHat.set(v, candidate);
|
|
62
|
+
// With non-negative weights a vertex settled in this call cannot
|
|
63
|
+
// strictly improve, so an equal-sum relaxation (which the <= allows,
|
|
64
|
+
// e.g. via a zero-weight cycle) must not re-enter the heap.
|
|
65
|
+
if (settled.has(v)) continue;
|
|
66
|
+
if (heap.has(v)) {
|
|
67
|
+
heap.decreaseKey(v, candidate);
|
|
68
|
+
} else {
|
|
69
|
+
heap.insert(v, candidate);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (settled.size <= cap) {
|
|
76
|
+
// Exhausted before the cap: everything under B is settled (B' = B)
|
|
77
|
+
return { bound: B, vertices: settled };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Hit the k + 1 cap: report B' = the largest settled distance and return
|
|
81
|
+
// only the vertices strictly below it
|
|
82
|
+
let boundary = -Infinity;
|
|
83
|
+
for (const v of settled) {
|
|
84
|
+
const distance = dHat.get(v);
|
|
85
|
+
if (distance > boundary) boundary = distance;
|
|
86
|
+
}
|
|
87
|
+
const vertices = new Set();
|
|
88
|
+
for (const v of settled) {
|
|
89
|
+
if (dHat.get(v) < boundary) vertices.add(v);
|
|
90
|
+
}
|
|
91
|
+
return { bound: boundary, vertices };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { baseCase };
|
package/src/heap.mjs
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array-backed indexed binary min-heap for the BMSSP base case (Algorithm 2
|
|
3
|
+
* of "Breaking the Sorting Barrier for Directed Single-Source Shortest
|
|
4
|
+
* Paths"). Holds <key, value> pairs (vertex, distance estimate) ordered by
|
|
5
|
+
* value and supports exactly the operations BaseCase(B, S) needs:
|
|
6
|
+
*
|
|
7
|
+
* insert(key, value) — add a key not currently stored
|
|
8
|
+
* extractMin() — remove and return the smallest-valued pair
|
|
9
|
+
* decreaseKey(key, value) — lower the value of a stored key
|
|
10
|
+
* has(key) — membership test ("is v in H?")
|
|
11
|
+
*
|
|
12
|
+
* A position map (key -> array index) makes decreaseKey and has O(log n) /
|
|
13
|
+
* O(1), matching the paper's heap literally instead of the lazy
|
|
14
|
+
* duplicate-and-skip variant used inside src/dijkstra.mjs.
|
|
15
|
+
*/
|
|
16
|
+
class MinHeap {
|
|
17
|
+
constructor() {
|
|
18
|
+
// entries[i] = [key, value], heap-ordered by value
|
|
19
|
+
this.entries = [];
|
|
20
|
+
// key -> index of that key in entries, for O(1) membership / lookup
|
|
21
|
+
this.position = new Map();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Number of pairs currently stored
|
|
25
|
+
get size() {
|
|
26
|
+
return this.entries.length;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
isEmpty() {
|
|
30
|
+
return this.entries.length === 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
has(key) {
|
|
34
|
+
return this.position.has(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Current value of a stored key, or undefined if the key is not present.
|
|
39
|
+
* @param {*} key - Typically a node ID
|
|
40
|
+
* @returns {number|undefined}
|
|
41
|
+
*/
|
|
42
|
+
getValue(key) {
|
|
43
|
+
const index = this.position.get(key);
|
|
44
|
+
return index === undefined ? undefined : this.entries[index][1];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Smallest-valued pair without removing it.
|
|
49
|
+
* @returns {{ key: *, value: number }}
|
|
50
|
+
* @throws {Error} If the heap is empty
|
|
51
|
+
*/
|
|
52
|
+
peekMin() {
|
|
53
|
+
if (this.entries.length === 0) {
|
|
54
|
+
throw new Error("heap is empty");
|
|
55
|
+
}
|
|
56
|
+
const [key, value] = this.entries[0];
|
|
57
|
+
return { key, value };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a pair for a key that is not currently stored (Algorithm 2 Insert).
|
|
62
|
+
* A key that was extracted earlier may be inserted again.
|
|
63
|
+
* @param {*} key - Typically a node ID
|
|
64
|
+
* @param {number} value - Priority; smaller comes out first
|
|
65
|
+
* @throws {Error} If the key is already stored, or value is not a number
|
|
66
|
+
*/
|
|
67
|
+
insert(key, value) {
|
|
68
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
69
|
+
throw new Error("value must be a number");
|
|
70
|
+
}
|
|
71
|
+
if (this.position.has(key)) {
|
|
72
|
+
throw new Error("key already in heap — use decreaseKey");
|
|
73
|
+
}
|
|
74
|
+
this.entries.push([key, value]);
|
|
75
|
+
this.position.set(key, this.entries.length - 1);
|
|
76
|
+
this.siftUp(this.entries.length - 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Lower the value of a stored key (Algorithm 2 DecreaseKey). A value that
|
|
81
|
+
* would not decrease the stored one is ignored — the smallest value wins,
|
|
82
|
+
* mirroring the `<=` edge relaxation which re-relaxes with equal sums.
|
|
83
|
+
* @param {*} key - Must be currently stored
|
|
84
|
+
* @param {number} value - New priority; applied only when smaller
|
|
85
|
+
* @throws {Error} If the key is not stored, or value is not a number
|
|
86
|
+
*/
|
|
87
|
+
decreaseKey(key, value) {
|
|
88
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
89
|
+
throw new Error("value must be a number");
|
|
90
|
+
}
|
|
91
|
+
const index = this.position.get(key);
|
|
92
|
+
if (index === undefined) {
|
|
93
|
+
throw new Error("key not in heap — use insert");
|
|
94
|
+
}
|
|
95
|
+
if (this.entries[index][1] <= value) return;
|
|
96
|
+
this.entries[index][1] = value;
|
|
97
|
+
this.siftUp(index);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Remove and return the smallest-valued pair (Algorithm 2 ExtractMin).
|
|
102
|
+
* @returns {{ key: *, value: number }}
|
|
103
|
+
* @throws {Error} If the heap is empty
|
|
104
|
+
*/
|
|
105
|
+
extractMin() {
|
|
106
|
+
if (this.entries.length === 0) {
|
|
107
|
+
throw new Error("heap is empty");
|
|
108
|
+
}
|
|
109
|
+
const [key, value] = this.entries[0];
|
|
110
|
+
this.position.delete(key);
|
|
111
|
+
const last = this.entries.pop();
|
|
112
|
+
if (this.entries.length > 0) {
|
|
113
|
+
this.entries[0] = last;
|
|
114
|
+
this.position.set(last[0], 0);
|
|
115
|
+
this.siftDown(0);
|
|
116
|
+
}
|
|
117
|
+
return { key, value };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Internal: swap two entries and keep the position map in sync
|
|
121
|
+
swap(i, j) {
|
|
122
|
+
const a = this.entries[i];
|
|
123
|
+
const b = this.entries[j];
|
|
124
|
+
this.entries[i] = b;
|
|
125
|
+
this.entries[j] = a;
|
|
126
|
+
this.position.set(b[0], i);
|
|
127
|
+
this.position.set(a[0], j);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Internal: move the entry at index up until its parent is not larger
|
|
131
|
+
siftUp(index) {
|
|
132
|
+
let i = index;
|
|
133
|
+
while (i > 0) {
|
|
134
|
+
const parent = (i - 1) >> 1;
|
|
135
|
+
if (this.entries[parent][1] <= this.entries[i][1]) break;
|
|
136
|
+
this.swap(parent, i);
|
|
137
|
+
i = parent;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Internal: move the entry at index down until no child is smaller
|
|
142
|
+
siftDown(index) {
|
|
143
|
+
let i = index;
|
|
144
|
+
for (;;) {
|
|
145
|
+
const left = 2 * i + 1;
|
|
146
|
+
const right = left + 1;
|
|
147
|
+
let smallest = i;
|
|
148
|
+
if (
|
|
149
|
+
left < this.entries.length &&
|
|
150
|
+
this.entries[left][1] < this.entries[smallest][1]
|
|
151
|
+
) {
|
|
152
|
+
smallest = left;
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
right < this.entries.length &&
|
|
156
|
+
this.entries[right][1] < this.entries[smallest][1]
|
|
157
|
+
) {
|
|
158
|
+
smallest = right;
|
|
159
|
+
}
|
|
160
|
+
if (smallest === i) break;
|
|
161
|
+
this.swap(smallest, i);
|
|
162
|
+
i = smallest;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { MinHeap };
|