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/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
- * - d0: blocks filled by batchPrepend() only; they conceptually sit in front
16
- * of d1 (their values are smaller than everything inserted so far).
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
- * The block-bound index is a plain array searched with binary search instead
19
- * of the paper's balanced BST same behavior, worse constants (issue #167).
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
- // d1 starts as a single empty block with upper bound B
39
- this.d1 = [this.makeBlock(B)];
40
- this.d0 = [];
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
- // Binary-search d1 for the first block whose bound covers the value
77
- let lo = 0;
78
- let hi = this.d1.length - 1;
79
- while (lo < hi) {
80
- const mid = (lo + hi) >> 1;
81
- if (this.compare(this.d1[mid].bound, value) >= 0) {
82
- hi = mid;
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(lo);
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. The lower half gets bound = its own max value; the upper half
98
- // keeps the original bound, so inter-block ordering is preserved.
99
- // (The paper uses linear-time median selection; sorting is O(M log M) but
100
- // simpler — acceptable for this correctness-first implementation.)
101
- splitBlock(index) {
102
- const block = this.d1[index];
103
- const sorted = [...block.entries].sort((a, b) => this.compare(a[1], b[1]));
104
- const half = sorted.length >> 1;
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] = sorted[i];
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.splice(index, 0, lower);
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, otherwise sorted chunks of <= ceil(M/2)
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
- for (let i = 0; i < fresh.length; i += chunkSize) {
156
- chunks.push(fresh.slice(i, i + chunkSize));
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, so chunk 0 — the smallest
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
- this.d0 = blocks.concat(this.d0);
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.d0 = [];
193
- this.d1 = [this.makeBlock(this.B)];
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
- for (const seq of [this.d0, this.d1]) {
202
- let collected = 0;
203
- for (const block of seq) {
204
- if (collected >= this.M) break;
205
- for (const [key, value] of block.entries) {
206
- candidates.push([key, value, block]);
207
- }
208
- collected += block.entries.size;
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
- // Take the M smallest candidates out of the structure
212
- candidates.sort((a, b) => this.compare(a[1], b[1]));
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 (const seq of [this.d0, this.d1]) {
226
- const block = seq.find((b) => b.entries.size > 0);
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. The last d1 block (bound B)
248
- // is kept even when empty so insert always finds a home for any value < B.
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
- const i0 = this.d0.indexOf(block);
251
- if (i0 !== -1) {
252
- this.d0.splice(i0, 1);
316
+ if (block.node !== null) {
317
+ if (block === this.lastD1Block) return;
318
+ this.d1.remove(block.node);
253
319
  return;
254
320
  }
255
- const i1 = this.d1.indexOf(block);
256
- if (i1 !== -1 && i1 < this.d1.length - 1) {
257
- this.d1.splice(i1, 1);
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