bmssp 1.1.1 → 2.0.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 +113 -22
- package/docs/index.html +82 -15
- package/index.mjs +19 -9
- package/package.json +1 -1
- package/src/baseCase.mjs +41 -33
- package/src/blockList.mjs +133 -68
- package/src/bmssp.mjs +370 -66
- package/src/boundIndex.mjs +243 -0
- package/src/findPivots.mjs +40 -26
- package/src/graph.mjs +174 -0
- package/src/select.mjs +144 -0
- package/src/tieBreak.mjs +111 -29
package/src/blockList.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { BoundIndex } from "./boundIndex.mjs";
|
|
2
|
+
import { partitionByRank } from "./select.mjs";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Block-based "partial-sort" structure D from Lemma 3.3 of the BMSSP paper
|
|
3
6
|
* ("Breaking the Sorting Barrier for Directed Single-Source Shortest Paths").
|
|
@@ -11,12 +14,16 @@
|
|
|
11
14
|
* Internal layout:
|
|
12
15
|
* - d1: blocks filled by insert(). Each block carries an upper bound on its
|
|
13
16
|
* values; bounds are non-decreasing across blocks, and the last block
|
|
14
|
-
* always has bound B so every value < B has a home.
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
17
|
+
* always has bound B so every value < B has a home. The sequence lives in
|
|
18
|
+
* a self-balancing BST (BoundIndex) searched through the monotone bounds.
|
|
19
|
+
* - d0: blocks filled by batchPrepend() only, kept as a doubly-linked list;
|
|
20
|
+
* they conceptually sit in front of d1 (their values are smaller than
|
|
21
|
+
* everything inserted so far).
|
|
17
22
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
23
|
+
* Since #167 the structure meets Lemma 3.3's exact bounds: the bound index
|
|
24
|
+
* is a balanced BST (O(log #blocks) search/split/drop instead of O(#blocks)
|
|
25
|
+
* array splices) and splits/chunking/pulls use deterministic linear-time
|
|
26
|
+
* selection (partitionByRank) instead of an O(M log M) sort.
|
|
20
27
|
*/
|
|
21
28
|
class BlockList {
|
|
22
29
|
/**
|
|
@@ -35,17 +42,26 @@ class BlockList {
|
|
|
35
42
|
this.M = Math.floor(M);
|
|
36
43
|
this.B = B;
|
|
37
44
|
this.compare = compare ?? ((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
this.
|
|
45
|
+
// Shared [key, value]-pair comparator for splits/chunking/pulls, hoisted
|
|
46
|
+
// so the hot paths don't allocate a closure per call
|
|
47
|
+
this.compareBySecond = (a, b) => this.compare(a[1], b[1]);
|
|
48
|
+
// d1 starts as a single empty block with upper bound B; that block is
|
|
49
|
+
// kept as the sequence's last forever so every value < B has a home
|
|
50
|
+
this.d1 = new BoundIndex();
|
|
51
|
+
this.lastD1Block = this.makeBlock(this.B);
|
|
52
|
+
this.lastD1Block.node = this.d1.append(this.lastD1Block);
|
|
53
|
+
// d0 is a doubly-linked list of blocks; the head holds the smallest values
|
|
54
|
+
this.d0Head = null;
|
|
41
55
|
// key -> block currently holding that key, for O(1) duplicate handling
|
|
42
56
|
this.locator = new Map();
|
|
43
57
|
this.count = 0;
|
|
44
58
|
}
|
|
45
59
|
|
|
46
|
-
// Internal: create an empty block with the given value upper bound
|
|
60
|
+
// Internal: create an empty block with the given value upper bound.
|
|
61
|
+
// node is the handle in the d1 BoundIndex (null for d0 blocks);
|
|
62
|
+
// prev/next thread the d0 linked list (null for d1 blocks).
|
|
47
63
|
makeBlock(bound) {
|
|
48
|
-
return { bound, entries: new Map() };
|
|
64
|
+
return { bound, entries: new Map(), node: null, prev: null, next: null };
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
// Number of pairs currently stored
|
|
@@ -73,43 +89,37 @@ class BlockList {
|
|
|
73
89
|
if (this.compare(holder.entries.get(key), value) <= 0) return;
|
|
74
90
|
this.removeKey(key, holder);
|
|
75
91
|
}
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
} else {
|
|
84
|
-
lo = mid + 1;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const block = this.d1[lo];
|
|
92
|
+
// First d1 block whose bound covers the value: bounds are monotone along
|
|
93
|
+
// the sequence, so this predicate search is the paper's O(log #blocks)
|
|
94
|
+
// BST lookup. It always succeeds — the last block's bound is B > value.
|
|
95
|
+
const node = this.d1.findFirst(
|
|
96
|
+
(candidate) => this.compare(candidate.bound, value) >= 0,
|
|
97
|
+
);
|
|
98
|
+
const block = node.item;
|
|
88
99
|
block.entries.set(key, value);
|
|
89
100
|
this.locator.set(key, block);
|
|
90
101
|
this.count += 1;
|
|
91
102
|
if (block.entries.size > this.M) {
|
|
92
|
-
this.splitBlock(
|
|
103
|
+
this.splitBlock(block);
|
|
93
104
|
}
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
// Internal: split an overfull d1 block into two halves around its median
|
|
97
|
-
// value
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const lower = this.makeBlock(sorted[half - 1][1]);
|
|
108
|
+
// value (deterministic linear-time selection, as Lemma 3.3 prescribes).
|
|
109
|
+
// The lower half gets bound = the median (its own max value); the upper
|
|
110
|
+
// half keeps the original bound, so inter-block ordering is preserved.
|
|
111
|
+
splitBlock(block) {
|
|
112
|
+
const pairs = [...block.entries];
|
|
113
|
+
const half = pairs.length >> 1;
|
|
114
|
+
partitionByRank(pairs, half - 1, this.compareBySecond);
|
|
115
|
+
const lower = this.makeBlock(pairs[half - 1][1]);
|
|
106
116
|
for (let i = 0; i < half; i += 1) {
|
|
107
|
-
const [key, value] =
|
|
117
|
+
const [key, value] = pairs[i];
|
|
108
118
|
block.entries.delete(key);
|
|
109
119
|
lower.entries.set(key, value);
|
|
110
120
|
this.locator.set(key, lower);
|
|
111
121
|
}
|
|
112
|
-
this.d1.
|
|
122
|
+
lower.node = this.d1.insertBefore(block.node, lower);
|
|
113
123
|
}
|
|
114
124
|
|
|
115
125
|
/**
|
|
@@ -144,23 +154,34 @@ class BlockList {
|
|
|
144
154
|
fresh.push([key, value]);
|
|
145
155
|
}
|
|
146
156
|
if (fresh.length === 0) return;
|
|
147
|
-
// One block if the batch fits
|
|
157
|
+
// One block if the batch fits; otherwise value-ordered chunks of
|
|
158
|
+
// <= ceil(M/2) — O(|L|/M) blocks built with O(|L|·max{1, log(|L|/M)})
|
|
159
|
+
// comparisons, the Lemma 3.3 bound
|
|
148
160
|
let chunks;
|
|
149
161
|
if (fresh.length <= this.M) {
|
|
150
162
|
chunks = [fresh];
|
|
151
163
|
} else {
|
|
152
|
-
fresh.sort((a, b) => this.compare(a[1], b[1]));
|
|
153
164
|
const chunkSize = Math.ceil(this.M / 2);
|
|
154
165
|
chunks = [];
|
|
155
|
-
|
|
156
|
-
chunks
|
|
166
|
+
if (fresh.length >= chunkSize * chunkSize) {
|
|
167
|
+
// Many chunks (|L| >= chunkSize², e.g. the M = 1 star regime, where
|
|
168
|
+
// |L|/M ~ |L|): sorting's O(|L| log |L|) is within 2x of the
|
|
169
|
+
// O(|L| log(|L|/chunkSize)) target — |L| >= c² gives
|
|
170
|
+
// log |L| <= 2 log(|L|/c) — and a single sort is much faster than
|
|
171
|
+
// log(|L|/M) rounds of median selection
|
|
172
|
+
fresh.sort(this.compareBySecond);
|
|
173
|
+
for (let i = 0; i < fresh.length; i += chunkSize) {
|
|
174
|
+
chunks.push(fresh.slice(i, i + chunkSize));
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// Few chunks (|L| < chunkSize²): repeated median splitting — here
|
|
178
|
+
// the recursion is at most log2(chunkSize) levels deep, and a sort
|
|
179
|
+
// would overshoot the Lemma bound (up to O(|L| log |L|) for
|
|
180
|
+
// O(|L| log(|L|/M)) with |L| close to M)
|
|
181
|
+
this.chunkByMedian(fresh, chunkSize, chunks);
|
|
157
182
|
}
|
|
158
183
|
}
|
|
159
|
-
// Materialize the chunks as blocks (ascending
|
|
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).
|
|
184
|
+
// Materialize the chunks as blocks (ascending order)...
|
|
164
185
|
const blocks = [];
|
|
165
186
|
for (const chunk of chunks) {
|
|
166
187
|
// Seed the block bound with the first value, then max-update — avoids
|
|
@@ -174,7 +195,28 @@ class BlockList {
|
|
|
174
195
|
}
|
|
175
196
|
blocks.push(block);
|
|
176
197
|
}
|
|
177
|
-
|
|
198
|
+
// ...and link them in front of d0, smallest chunk becoming the new head
|
|
199
|
+
let head = this.d0Head;
|
|
200
|
+
for (let i = blocks.length - 1; i >= 0; i -= 1) {
|
|
201
|
+
const block = blocks[i];
|
|
202
|
+
block.next = head;
|
|
203
|
+
if (head !== null) head.prev = block;
|
|
204
|
+
head = block;
|
|
205
|
+
}
|
|
206
|
+
this.d0Head = head;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Internal: recursively median-split pairs (in place / via slices) into
|
|
210
|
+
// value-ordered chunks of at most maxSize, appended to out ascending
|
|
211
|
+
chunkByMedian(pairs, maxSize, out) {
|
|
212
|
+
if (pairs.length <= maxSize) {
|
|
213
|
+
out.push(pairs);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const half = pairs.length >> 1;
|
|
217
|
+
partitionByRank(pairs, half - 1, this.compareBySecond);
|
|
218
|
+
this.chunkByMedian(pairs.slice(0, half), maxSize, out);
|
|
219
|
+
this.chunkByMedian(pairs.slice(half), maxSize, out);
|
|
178
220
|
}
|
|
179
221
|
|
|
180
222
|
/**
|
|
@@ -189,8 +231,10 @@ class BlockList {
|
|
|
189
231
|
if (this.count <= this.M) {
|
|
190
232
|
// Everything fits in one batch: drain the structure and reset it
|
|
191
233
|
const keys = new Set(this.locator.keys());
|
|
192
|
-
this.
|
|
193
|
-
this.d1
|
|
234
|
+
this.d0Head = null;
|
|
235
|
+
this.d1.clear();
|
|
236
|
+
this.lastD1Block = this.makeBlock(this.B);
|
|
237
|
+
this.lastD1Block.node = this.d1.append(this.lastD1Block);
|
|
194
238
|
this.locator.clear();
|
|
195
239
|
this.count = 0;
|
|
196
240
|
return { keys, bound: this.B };
|
|
@@ -198,18 +242,31 @@ class BlockList {
|
|
|
198
242
|
// Collect prefix blocks from each sequence until that side holds >= M
|
|
199
243
|
// candidate elements (or runs out). The M smallest overall are in there.
|
|
200
244
|
const candidates = [];
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
245
|
+
let collected = 0;
|
|
246
|
+
for (
|
|
247
|
+
let block = this.d0Head;
|
|
248
|
+
block !== null && collected < this.M;
|
|
249
|
+
block = block.next
|
|
250
|
+
) {
|
|
251
|
+
for (const [key, value] of block.entries) {
|
|
252
|
+
candidates.push([key, value, block]);
|
|
209
253
|
}
|
|
254
|
+
collected += block.entries.size;
|
|
210
255
|
}
|
|
211
|
-
|
|
212
|
-
|
|
256
|
+
collected = 0;
|
|
257
|
+
for (
|
|
258
|
+
let node = this.d1.first();
|
|
259
|
+
node !== null && collected < this.M;
|
|
260
|
+
node = this.d1.next(node)
|
|
261
|
+
) {
|
|
262
|
+
for (const [key, value] of node.item.entries) {
|
|
263
|
+
candidates.push([key, value, node.item]);
|
|
264
|
+
}
|
|
265
|
+
collected += node.item.entries.size;
|
|
266
|
+
}
|
|
267
|
+
// Move the M smallest candidates to the front (linear-time selection,
|
|
268
|
+
// the Lemma 3.3 O(M) pull) and take them out of the structure
|
|
269
|
+
partitionByRank(candidates, this.M - 1, this.compareBySecond);
|
|
213
270
|
const keys = new Set();
|
|
214
271
|
for (let i = 0; i < this.M; i += 1) {
|
|
215
272
|
const [key, , block] = candidates[i];
|
|
@@ -222,12 +279,20 @@ class BlockList {
|
|
|
222
279
|
// Under a strict total order (#163's composite keys) this separator is
|
|
223
280
|
// strictly above every pulled value — no boundary ties.
|
|
224
281
|
let bound = null;
|
|
225
|
-
for (
|
|
226
|
-
|
|
227
|
-
if (block) {
|
|
282
|
+
for (let block = this.d0Head; block !== null; block = block.next) {
|
|
283
|
+
if (block.entries.size > 0) {
|
|
228
284
|
for (const value of block.entries.values()) {
|
|
229
285
|
if (bound === null || this.compare(value, bound) < 0) bound = value;
|
|
230
286
|
}
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (let node = this.d1.first(); node !== null; node = this.d1.next(node)) {
|
|
291
|
+
if (node.item.entries.size > 0) {
|
|
292
|
+
for (const value of node.item.entries.values()) {
|
|
293
|
+
if (bound === null || this.compare(value, bound) < 0) bound = value;
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
231
296
|
}
|
|
232
297
|
}
|
|
233
298
|
return { keys, bound };
|
|
@@ -244,18 +309,18 @@ class BlockList {
|
|
|
244
309
|
}
|
|
245
310
|
}
|
|
246
311
|
|
|
247
|
-
// Internal: physically remove an emptied block
|
|
248
|
-
//
|
|
312
|
+
// Internal: physically remove an emptied block from its sequence — an
|
|
313
|
+
// O(log #blocks) BST removal for d1, an O(1) unlink for d0. The last d1
|
|
314
|
+
// block (bound B) is kept even when empty so insert always finds a home.
|
|
249
315
|
dropIfEmpty(block) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
this.
|
|
316
|
+
if (block.node !== null) {
|
|
317
|
+
if (block === this.lastD1Block) return;
|
|
318
|
+
this.d1.remove(block.node);
|
|
253
319
|
return;
|
|
254
320
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
}
|
|
321
|
+
if (block.prev !== null) block.prev.next = block.next;
|
|
322
|
+
else this.d0Head = block.next;
|
|
323
|
+
if (block.next !== null) block.next.prev = block.prev;
|
|
259
324
|
}
|
|
260
325
|
}
|
|
261
326
|
|