bmssp 1.0.0 โ 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 +81 -15
- 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/README.md
CHANGED
|
@@ -17,8 +17,8 @@ first to beat Dijkstra's O(m + nยทlog n) bound on sparse directed graphs.
|
|
|
17
17
|
|
|
18
18
|
**The algorithm is functional end-to-end as of `1.0.0`** ๐ โ `calculateShortestPaths()`
|
|
19
19
|
computes distances via the paper's method (FindPivots โ block list โ recursive BMSSP with
|
|
20
|
-
the bounded base case), validated node-by-node against a Dijkstra oracle
|
|
21
|
-
|
|
20
|
+
the bounded base case), validated node-by-node against a Dijkstra oracle on thousands of
|
|
21
|
+
seeded graphs โ up to 2 million nodes. The project was built piece by piece against the paper,
|
|
22
22
|
with every building block shipped, tested, and released individually:
|
|
23
23
|
|
|
24
24
|
| Building block (paper) | Where | Status |
|
|
@@ -30,14 +30,24 @@ with every building block shipped, tested, and released individually:
|
|
|
30
30
|
| `BaseCase(B, S)` โ Algorithm 2, bounded mini-Dijkstra ([#40](https://github.com/Sirivasv/bmssp-js/issues/40)) | `src/baseCase.mjs` | โ
done |
|
|
31
31
|
| `FindPivots(B, S)` โ Algorithm 1, frontier shrinking ([#44](https://github.com/Sirivasv/bmssp-js/issues/44)) | `src/findPivots.mjs` | โ
done |
|
|
32
32
|
| `BMSSP(l, B, S)` โ Algorithm 3, the main recursion ([#43](https://github.com/Sirivasv/bmssp-js/issues/43)) | `src/bmssp.mjs` | โ
done โ **1.0.0** |
|
|
33
|
+
| Deterministic tie-breaking โ Assumption 2.1 realized ([#163](https://github.com/Sirivasv/bmssp-js/issues/163)) | `src/tieBreak.mjs` | โ
done |
|
|
34
|
+
| Shortest-path reconstruction ([#169](https://github.com/Sirivasv/bmssp-js/issues/169)) | `BMSSP.reconstructPath()` | โ
done |
|
|
35
|
+
| Constructor input validation ([#165](https://github.com/Sirivasv/bmssp-js/issues/165)) | `BMSSP` constructor | โ
done |
|
|
36
|
+
| Opt-in constant-degree transform, in/out-degree โค 2 ([#164](https://github.com/Sirivasv/bmssp-js/issues/164)) | `constantDegreeTransform()` | โ
done |
|
|
33
37
|
|
|
34
38
|
> **Honest note:** the paper's win is asymptotic, and this repo optimizes for correctness
|
|
35
|
-
> and readability, not raw speed.
|
|
36
|
-
>
|
|
37
|
-
> Dijkstra
|
|
38
|
-
>
|
|
39
|
-
>
|
|
40
|
-
> (
|
|
39
|
+
> and readability, not raw speed. Measured head-to-head (algorithm time only, graph
|
|
40
|
+
> loading excluded โ see [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)):
|
|
41
|
+
> Dijkstra still wins on wall-clock at every practical size, though the gap narrows as
|
|
42
|
+
> sparse graphs grow (2.5ร at 50k nodes โ 1.57ร at 2M). But in the paper's own metric โ
|
|
43
|
+
> **comparisons between path lengths** โ BMSSP does **fewer comparisons than Dijkstra
|
|
44
|
+
> from about n = 1M on sparse graphs** (0.91ร at n = 2M), and the advantage grows with
|
|
45
|
+
> size: the "sorting barrier" is measurably broken; what remains is JS constant factors.
|
|
46
|
+
> Where inputs violate the paper's distinct-path-lengths assumption (e.g. zero-weight
|
|
47
|
+
> edges), a principled deterministic tie-break
|
|
48
|
+
> ([#163](https://github.com/Sirivasv/bmssp-js/issues/163)) realizes that assumption in
|
|
49
|
+
> code: paths are ranked by composite `(length, hops, id)` keys, so distances, predecessor
|
|
50
|
+
> pointers and completed sets are identical no matter how the edge list is ordered.
|
|
41
51
|
|
|
42
52
|
## How it works (in two ideas)
|
|
43
53
|
|
|
@@ -49,7 +59,9 @@ with every building block shipped, tested, and released individually:
|
|
|
49
59
|
ordered *between* blocks but unsorted *within* them โ enough to repeatedly pull the next
|
|
50
60
|
closest batch without paying the ฮ(log n)-per-vertex "sorting barrier."
|
|
51
61
|
|
|
52
|
-
The
|
|
62
|
+
The wall-clock crossover point is astronomically large, but the asymptotics are real: in
|
|
63
|
+
measured comparison counts this implementation already beats Dijkstra past ~1M nodes on
|
|
64
|
+
sparse graphs ([benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)). This repo
|
|
53
65
|
optimizes for a **correct, readable, well-tested** implementation, validated line-by-line
|
|
54
66
|
against a Dijkstra oracle โ not for raw speed.
|
|
55
67
|
|
|
@@ -62,7 +74,8 @@ npm install bmssp
|
|
|
62
74
|
## Usage
|
|
63
75
|
|
|
64
76
|
The package is ESM-only (`.mjs`). Graphs are arrays of `[from, to, weight]` edges with
|
|
65
|
-
numeric node IDs and non-negative weights
|
|
77
|
+
finite numeric node IDs and finite, non-negative weights. The constructor rejects malformed
|
|
78
|
+
edges with an error that identifies their array index; an empty edge array remains valid:
|
|
66
79
|
|
|
67
80
|
```javascript
|
|
68
81
|
import { BMSSP } from "bmssp";
|
|
@@ -75,10 +88,41 @@ const graph = new BMSSP([
|
|
|
75
88
|
|
|
76
89
|
graph.calculateShortestPaths(0);
|
|
77
90
|
console.log(graph.shortestPaths); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
|
|
91
|
+
console.log(graph.reconstructPath(1)); // [0, 1]
|
|
78
92
|
```
|
|
79
93
|
|
|
94
|
+
`reconstructPath(target)` uses the latest `calculateShortestPaths()` run. It returns `[]`
|
|
95
|
+
when the target is unreachable (or before any run) and throws for a node outside the graph.
|
|
96
|
+
|
|
80
97
|
A reference `dijkstra` implementation is also exported. See the `examples/` directory for
|
|
81
|
-
more.
|
|
98
|
+
more, or the [public API reference](https://sirivasv.github.io/bmssp-js/) for the complete
|
|
99
|
+
documented surface.
|
|
100
|
+
|
|
101
|
+
### Optional: constant-degree transform
|
|
102
|
+
|
|
103
|
+
The paper's bound assumes every vertex has in-degree and out-degree โค 2. That preprocessing
|
|
104
|
+
is **not** required for correctness here โ BMSSP is validated on arbitrary graphs โ but it is
|
|
105
|
+
available opt-in via `constantDegreeTransform`, which rewrites any graph into that shape by
|
|
106
|
+
splitting each vertex into a zero-weight cycle of "port" copies. The rewrite is
|
|
107
|
+
distance-preserving, so you run on the transformed graph and fold the result back onto your
|
|
108
|
+
original node IDs:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
import { BMSSP, constantDegreeTransform } from "bmssp";
|
|
112
|
+
|
|
113
|
+
const t = constantDegreeTransform([
|
|
114
|
+
[0, 1, 50],
|
|
115
|
+
[0, 2, 25],
|
|
116
|
+
[1, 2, 75],
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const g = new BMSSP(t.edges); // every node now has in/out-degree โค 2
|
|
120
|
+
g.calculateShortestPaths(t.sourceCopy(0)); // start from a copy of original node 0
|
|
121
|
+
console.log(t.collapse(g.shortestPaths)); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`collapse` maps the transformed graph's distances back to the original node IDs; `sourceCopy`
|
|
125
|
+
picks a valid start copy for an original node.
|
|
82
126
|
|
|
83
127
|
### Using the Docker image
|
|
84
128
|
|
|
@@ -101,26 +145,48 @@ Other image versions are on [Docker Hub](https://hub.docker.com/r/sirivasv/bmssp
|
|
|
101
145
|
|
|
102
146
|
```bash
|
|
103
147
|
npm install
|
|
104
|
-
npm test # Jest suite
|
|
148
|
+
npm test # Jest suite โ every graph seeded, every failure reproducible (~3 s)
|
|
105
149
|
npm run lint # Prettier + ESLint
|
|
106
150
|
npm run bench # seeded micro-benchmarks (see benchmarks/README.md)
|
|
107
151
|
```
|
|
108
152
|
|
|
153
|
+
The measured BMSSP-vs-Dijkstra head-to-head โ wall-clock and comparison counts, with
|
|
154
|
+
methodology โ lives in [benchmarks/HEAD-TO-HEAD.md](benchmarks/HEAD-TO-HEAD.md)
|
|
155
|
+
([#170](https://github.com/Sirivasv/bmssp-js/issues/170) tracks harness integration).
|
|
156
|
+
|
|
109
157
|
The test suite's core contract: for every node, BMSSP's distances must equal the Dijkstra
|
|
110
|
-
oracle's
|
|
158
|
+
oracle's. A seeded property/fuzz suite (`test/fuzz.test.mjs`) hammers that contract across
|
|
159
|
+
eight graph shapes (sparse/dense random, grids, chains, stars, DAGs, disconnected forests,
|
|
160
|
+
multigraphs), extreme weight regimes (all-zero, mixed zero/huge, exact floats), direct
|
|
161
|
+
multi-source bounded calls checked against per-source oracles, and seeded scale runs up to
|
|
162
|
+
150k nodes. It runs in `npm test` by default; crank the volume with the `FUZZ_ROUNDS`
|
|
163
|
+
multiplier (e.g. `FUZZ_ROUNDS=25 npm test -- test/fuzz.test.mjs` checks several thousand
|
|
164
|
+
graphs in a few seconds), and set `FUZZ_XL=1` for an additional 2-million-node round
|
|
165
|
+
(~30 s). Failures always report the seed that produced them.
|
|
111
166
|
|
|
112
167
|
## Roadmap
|
|
113
168
|
|
|
114
169
|
| Milestone | Theme | Status |
|
|
115
170
|
| --- | --- | --- |
|
|
116
171
|
| [`1.0.0`](https://github.com/Sirivasv/bmssp-js/milestones) | First end-to-end functional BMSSP (issues #40โ#45) | โ
done |
|
|
117
|
-
| `1.1.0` | Correctness hardening โ fuzz tests, edge cases, tie-breaking, input validation
|
|
118
|
-
| `1.2.0` | Performance & ergonomics โ exact Lemma 3.3 asymptotics,
|
|
172
|
+
| `1.1.0` | Correctness hardening โ fuzz tests, edge cases, tie-breaking, input validation, constant-degree transform, API docs | โ
done |
|
|
173
|
+
| `1.2.0` | Performance & ergonomics โ exact Lemma 3.3 asymptotics, BMSSP-vs-Dijkstra benchmarks, performance-cliff investigation | ๐จ current focus |
|
|
119
174
|
| `2.0.0` | API generalization โ public multi-source/bounded entry point, flexible inputs | planned |
|
|
120
175
|
|
|
176
|
+
## Contributing (humans and AI agents welcome)
|
|
177
|
+
|
|
121
178
|
Contributions are welcome โ see [CONTRIBUTING.md](CONTRIBUTING.md) and the
|
|
122
179
|
[`help wanted` / `good first issue` labels](https://github.com/Sirivasv/bmssp-js/issues).
|
|
123
180
|
|
|
181
|
+
This repo is built to be **agent-friendly, model-agnostic**: everything a coding assistant
|
|
182
|
+
needs to contribute is checked in, so you can point **any AI running any model** at the
|
|
183
|
+
project and it can start working **without ever reading the source paper**. Have your agent
|
|
184
|
+
read [`.claude/CLAUDE.md`](.claude/CLAUDE.md) first โ it lays out the full working lifecycle
|
|
185
|
+
as literal, follow-along checklists โ backed by a self-contained knowledge base under
|
|
186
|
+
[`.claude/knowledge/`](.claude/knowledge/) (a verified transcription of the paper, the
|
|
187
|
+
current codebase map, the roadmap, and a glossary). Prefer to work by hand? The same
|
|
188
|
+
knowledge base reads as plain documentation.
|
|
189
|
+
|
|
124
190
|
## Other implementations on GitHub
|
|
125
191
|
|
|
126
192
|
<https://github.com/search?q=bmssp&type=repositories>
|
package/docs/index.html
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
|
-
<title>
|
|
5
|
+
<title>bmssp โ BMSSP shortest paths for JavaScript</title>
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<meta name="description" content="JavaScript implementation of BMSSP, the O(m log^(2/3) n) single-source shortest-paths algorithm โ public API documentation.">
|
|
7
8
|
<style>
|
|
8
9
|
body {
|
|
9
10
|
margin: 0;
|
|
@@ -14,35 +15,63 @@
|
|
|
14
15
|
display: flex;
|
|
15
16
|
flex-direction: column;
|
|
16
17
|
align-items: center;
|
|
17
|
-
justify-content: center;
|
|
18
18
|
}
|
|
19
19
|
.container {
|
|
20
20
|
background: #fff;
|
|
21
21
|
border-radius: 18px;
|
|
22
22
|
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
|
23
23
|
padding: 2.5rem 2rem;
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
margin: 2rem 1rem;
|
|
25
|
+
max-width: 760px;
|
|
26
|
+
width: calc(100% - 2rem);
|
|
27
|
+
box-sizing: border-box;
|
|
26
28
|
}
|
|
29
|
+
header { text-align: center; }
|
|
27
30
|
h1 {
|
|
28
31
|
font-size: 2.2rem;
|
|
29
32
|
margin-bottom: 0.5rem;
|
|
30
33
|
color: #3a0ca3;
|
|
31
34
|
}
|
|
32
|
-
|
|
33
|
-
font-size: 1.
|
|
34
|
-
|
|
35
|
+
h2 {
|
|
36
|
+
font-size: 1.4rem;
|
|
37
|
+
color: #3a0ca3;
|
|
38
|
+
border-bottom: 2px solid #e0e7ff;
|
|
39
|
+
padding-bottom: 0.3rem;
|
|
40
|
+
margin-top: 2.2rem;
|
|
41
|
+
}
|
|
42
|
+
h3 {
|
|
43
|
+
font-size: 1.05rem;
|
|
44
|
+
color: #22223b;
|
|
45
|
+
margin-bottom: 0.3rem;
|
|
46
|
+
}
|
|
47
|
+
p, li {
|
|
48
|
+
font-size: 1rem;
|
|
49
|
+
line-height: 1.55;
|
|
35
50
|
color: #4a4e69;
|
|
36
51
|
}
|
|
52
|
+
.tagline { font-size: 1.1rem; }
|
|
53
|
+
a { color: #4361ee; }
|
|
54
|
+
code {
|
|
55
|
+
font-family: 'Cascadia Code', Consolas, Menlo, monospace;
|
|
56
|
+
font-size: 0.92em;
|
|
57
|
+
background: #eef1ff;
|
|
58
|
+
border-radius: 4px;
|
|
59
|
+
padding: 0.08em 0.35em;
|
|
60
|
+
}
|
|
61
|
+
pre {
|
|
62
|
+
background: #22223b;
|
|
63
|
+
color: #f0fdfa;
|
|
64
|
+
border-radius: 10px;
|
|
65
|
+
padding: 1rem 1.2rem;
|
|
66
|
+
overflow-x: auto;
|
|
67
|
+
}
|
|
68
|
+
pre code { background: none; color: inherit; padding: 0; }
|
|
37
69
|
.graphics {
|
|
38
|
-
margin: 1.
|
|
70
|
+
margin: 1.2rem 0 0.4rem;
|
|
39
71
|
display: flex;
|
|
40
72
|
justify-content: center;
|
|
41
73
|
}
|
|
42
|
-
.node-graph {
|
|
43
|
-
width: 120px;
|
|
44
|
-
height: 120px;
|
|
45
|
-
}
|
|
74
|
+
.node-graph { width: 110px; height: 110px; }
|
|
46
75
|
.btn {
|
|
47
76
|
display: inline-block;
|
|
48
77
|
padding: 0.7em 1.5em;
|
|
@@ -54,43 +83,164 @@
|
|
|
54
83
|
text-decoration: none;
|
|
55
84
|
transition: background 0.2s;
|
|
56
85
|
cursor: pointer;
|
|
86
|
+
margin: 0.3rem 0.35rem;
|
|
57
87
|
}
|
|
58
|
-
.btn:hover {
|
|
59
|
-
|
|
88
|
+
.btn:hover { background: #3a0ca3; }
|
|
89
|
+
.btn.secondary { background: #4cc9f0; color: #22223b; }
|
|
90
|
+
.btn.secondary:hover { background: #3ab8de; }
|
|
91
|
+
.note {
|
|
92
|
+
background: #f0fdfa;
|
|
93
|
+
border-left: 4px solid #4cc9f0;
|
|
94
|
+
border-radius: 0 8px 8px 0;
|
|
95
|
+
padding: 0.7rem 1rem;
|
|
96
|
+
margin: 1rem 0;
|
|
60
97
|
}
|
|
61
98
|
footer {
|
|
62
|
-
margin
|
|
99
|
+
margin: 0 0 2rem;
|
|
63
100
|
font-size: 0.95rem;
|
|
64
|
-
color: #
|
|
101
|
+
color: #6c757d;
|
|
102
|
+
text-align: center;
|
|
65
103
|
}
|
|
104
|
+
footer a { color: #6c757d; }
|
|
66
105
|
</style>
|
|
67
106
|
</head>
|
|
68
107
|
<body>
|
|
69
108
|
<div class="container">
|
|
70
|
-
<
|
|
109
|
+
<header>
|
|
110
|
+
<h1>bmssp</h1>
|
|
111
|
+
<p class="tagline">
|
|
112
|
+
A JavaScript (ESM) implementation of <strong>BMSSP</strong> โ
|
|
113
|
+
<strong>B</strong>ounded <strong>M</strong>ulti-<strong>S</strong>ource
|
|
114
|
+
<strong>S</strong>hortest <strong>P</strong>aths โ the deterministic
|
|
115
|
+
O(mยทlog<sup>2/3</sup> n) single-source shortest-paths algorithm from
|
|
116
|
+
<a href="https://dl.acm.org/doi/10.1145/3717823.3718179" target="_blank" rel="noopener">
|
|
117
|
+
โBreaking the Sorting Barrier for Directed Single-Source Shortest Pathsโ</a>
|
|
118
|
+
(Duan, Mao, Mao, Shu, Yin โ 2025), the first to beat Dijkstra on sparse directed graphs.
|
|
119
|
+
</p>
|
|
120
|
+
<div class="graphics">
|
|
121
|
+
<!-- SVG: stylized graph with nodes and edges -->
|
|
122
|
+
<svg class="node-graph" viewBox="0 0 120 120" role="img" aria-label="Stylized graph">
|
|
123
|
+
<line x1="60" y1="20" x2="30" y2="60" stroke="#adb5bd" stroke-width="3"/>
|
|
124
|
+
<line x1="60" y1="20" x2="90" y2="60" stroke="#adb5bd" stroke-width="3"/>
|
|
125
|
+
<line x1="30" y1="60" x2="60" y2="100" stroke="#adb5bd" stroke-width="3"/>
|
|
126
|
+
<line x1="90" y1="60" x2="60" y2="100" stroke="#adb5bd" stroke-width="3"/>
|
|
127
|
+
<circle cx="60" cy="20" r="12" fill="#4361ee" />
|
|
128
|
+
<circle cx="30" cy="60" r="10" fill="#4cc9f0" />
|
|
129
|
+
<circle cx="90" cy="60" r="10" fill="#4cc9f0" />
|
|
130
|
+
<circle cx="60" cy="100" r="12" fill="#3a0ca3" />
|
|
131
|
+
</svg>
|
|
132
|
+
</div>
|
|
133
|
+
<p>
|
|
134
|
+
<a class="btn" href="https://github.com/Sirivasv/bmssp-js" target="_blank" rel="noopener">GitHub</a>
|
|
135
|
+
<a class="btn secondary" href="https://www.npmjs.com/package/bmssp" target="_blank" rel="noopener">npm</a>
|
|
136
|
+
<a class="btn secondary" href="https://hub.docker.com/r/sirivasv/bmssp-js" target="_blank" rel="noopener">Docker Hub</a>
|
|
137
|
+
</p>
|
|
138
|
+
</header>
|
|
139
|
+
|
|
140
|
+
<h2>Install</h2>
|
|
141
|
+
<pre><code>npm install bmssp</code></pre>
|
|
71
142
|
<p>
|
|
72
|
-
|
|
143
|
+
The package is ESM-only. Graphs are arrays of <code>[from, to, weight]</code>
|
|
144
|
+
edges with finite numeric node IDs and finite, non-negative weights.
|
|
73
145
|
</p>
|
|
146
|
+
|
|
147
|
+
<h2>Quick start</h2>
|
|
148
|
+
<pre><code>import { BMSSP } from "bmssp";
|
|
149
|
+
|
|
150
|
+
const graph = new BMSSP([
|
|
151
|
+
[0, 1, 50],
|
|
152
|
+
[1, 2, 75],
|
|
153
|
+
[0, 2, 25],
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
graph.calculateShortestPaths(0);
|
|
157
|
+
graph.shortestPaths; // Map(3) { 0 => 0, 1 => 50, 2 => 25 }
|
|
158
|
+
graph.reconstructPath(1); // [0, 1]</code></pre>
|
|
159
|
+
|
|
160
|
+
<h2>Public API</h2>
|
|
161
|
+
<p>The package has exactly three exports: <code>BMSSP</code>, <code>dijkstra</code>,
|
|
162
|
+
and <code>constantDegreeTransform</code>.</p>
|
|
163
|
+
|
|
164
|
+
<h3><code>new BMSSP(graph)</code></h3>
|
|
74
165
|
<p>
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
166
|
+
Validates and stores the edge list. Every entry must be an exact three-element
|
|
167
|
+
<code>[from, to, weight]</code> array; node IDs must be finite numbers and weights
|
|
168
|
+
finite and non-negative. Malformed input throws an <code>Error</code> identifying
|
|
169
|
+
the offending edge index. An empty edge array is valid (the graph then has no
|
|
170
|
+
nodes, so any start node is rejected later).
|
|
79
171
|
</p>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
172
|
+
|
|
173
|
+
<h3><code>graph.calculateShortestPaths(source)</code></h3>
|
|
174
|
+
<p>
|
|
175
|
+
Computes shortest distances from <code>source</code> via the paper's method
|
|
176
|
+
(FindPivots โ block list โ recursive BMSSP with the bounded base case โ not by
|
|
177
|
+
calling Dijkstra). Throws if <code>source</code> is not a node of the graph.
|
|
178
|
+
Results land in <code>graph.shortestPaths</code>, a
|
|
179
|
+
<code>Map<nodeId, distance></code> holding a value for every node โ
|
|
180
|
+
<code>Infinity</code> where unreachable. Distances and predecessor pointers are
|
|
181
|
+
canonical: deterministic regardless of edge-list order, even with ties and
|
|
182
|
+
zero-weight edges. Calling it again with a different source resets state and
|
|
183
|
+
recomputes.
|
|
184
|
+
</p>
|
|
185
|
+
|
|
186
|
+
<h3><code>graph.reconstructPath(target)</code></h3>
|
|
187
|
+
<p>
|
|
188
|
+
Returns the canonical shortest path from the most recent source to
|
|
189
|
+
<code>target</code> as an array of node IDs (<code>[source, โฆ, target]</code>).
|
|
190
|
+
Returns <code>[]</code> when <code>target</code> is unreachable or no run has
|
|
191
|
+
completed yet; throws for a node that is not in the graph.
|
|
192
|
+
</p>
|
|
193
|
+
|
|
194
|
+
<h3><code>dijkstra(graph, nodeIDs, source)</code></h3>
|
|
195
|
+
<p>
|
|
196
|
+
The reference Dijkstra implementation used as the test suite's ground-truth
|
|
197
|
+
oracle, exported for comparison and independent checking. Takes the raw edge
|
|
198
|
+
array, a <code>Set</code> of node IDs, and a source; returns a
|
|
199
|
+
<code>Map<nodeId, distance></code> (<code>Infinity</code> if unreachable).
|
|
200
|
+
</p>
|
|
201
|
+
|
|
202
|
+
<h3><code>constantDegreeTransform(graph)</code></h3>
|
|
203
|
+
<p>
|
|
204
|
+
Opt-in preprocessing that rewrites any graph so every vertex has in-degree and
|
|
205
|
+
out-degree โค 2 (the paper's preliminary assumption) by splitting each vertex into
|
|
206
|
+
a zero-weight cycle of โportโ copies. Distance-preserving and never required for
|
|
207
|
+
correctness โ BMSSP is validated on arbitrary graphs. Returns
|
|
208
|
+
<code>{ edges, copiesOf, originalOf, sourceCopy, collapse }</code>:
|
|
209
|
+
</p>
|
|
210
|
+
<pre><code>import { BMSSP, constantDegreeTransform } from "bmssp";
|
|
211
|
+
|
|
212
|
+
const t = constantDegreeTransform([
|
|
213
|
+
[0, 1, 50],
|
|
214
|
+
[0, 2, 25],
|
|
215
|
+
[1, 2, 75],
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
const g = new BMSSP(t.edges); // in/out-degree โค 2 everywhere
|
|
219
|
+
g.calculateShortestPaths(t.sourceCopy(0)); // start from a copy of node 0
|
|
220
|
+
t.collapse(g.shortestPaths); // Map(3) { 0 => 0, 1 => 50, 2 => 25 }</code></pre>
|
|
221
|
+
|
|
222
|
+
<div class="note">
|
|
223
|
+
Algorithm internals โ the Lemma 3.3 block list, the indexed min-heap,
|
|
224
|
+
<code>BaseCase</code>, <code>FindPivots</code>, and the tie-break helpers โ are
|
|
225
|
+
deliberately <em>not</em> exported. They are implementation details, documented
|
|
226
|
+
in-source for contributors.
|
|
92
227
|
</div>
|
|
93
|
-
|
|
228
|
+
|
|
229
|
+
<h2>Correctness & performance</h2>
|
|
230
|
+
<p>
|
|
231
|
+
Every release is validated node-by-node against the Dijkstra oracle on thousands
|
|
232
|
+
of seeded graphs (up to 2 million nodes). The paper's win is asymptotic: measured
|
|
233
|
+
head-to-head, Dijkstra still wins wall-clock at practical sizes, but in the
|
|
234
|
+
paper's own metric โ comparisons between path lengths โ this implementation does
|
|
235
|
+
fewer comparisons than Dijkstra from about n = 1M on sparse graphs. See
|
|
236
|
+
<a href="https://github.com/Sirivasv/bmssp-js/blob/main/benchmarks/HEAD-TO-HEAD.md"
|
|
237
|
+
target="_blank" rel="noopener">benchmarks/HEAD-TO-HEAD.md</a> for the data and
|
|
238
|
+
methodology.
|
|
239
|
+
</p>
|
|
94
240
|
</div>
|
|
241
|
+
<footer>
|
|
242
|
+
<a href="https://github.com/Sirivasv/bmssp-js" target="_blank" rel="noopener">bmssp-js</a>
|
|
243
|
+
ยท MPL-2.0 ยท Saul Ivan Rivas Vega
|
|
244
|
+
</footer>
|
|
95
245
|
</body>
|
|
96
|
-
</html
|
|
246
|
+
</html>
|
package/index.mjs
CHANGED
|
@@ -1,2 +1,41 @@
|
|
|
1
|
+
// Public API of the `bmssp` package (ESM-only). Exactly three exports:
|
|
2
|
+
// the BMSSP class, the reference `dijkstra`, and the opt-in
|
|
3
|
+
// `constantDegreeTransform`. Algorithm-internal modules (the block list,
|
|
4
|
+
// the indexed heap, BaseCase, FindPivots, the tie-break helpers) are
|
|
5
|
+
// deliberately NOT re-exported โ they are implementation details of the
|
|
6
|
+
// recursion, not supported public API.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* BMSSP โ the paper's Algorithm 3 behind a small class API.
|
|
10
|
+
*
|
|
11
|
+
* `new BMSSP(graph)` takes an array of `[from, to, weight]` edges with
|
|
12
|
+
* finite numeric node IDs and finite, non-negative weights (an empty array
|
|
13
|
+
* is valid; malformed edges throw with the offending index).
|
|
14
|
+
* `calculateShortestPaths(source)` computes canonical distances into the
|
|
15
|
+
* `shortestPaths` Map (`Infinity` for unreachable nodes), and
|
|
16
|
+
* `reconstructPath(target)` returns the canonical shortest path as an
|
|
17
|
+
* array of node IDs. Full JSDoc lives on the class in `src/bmssp.mjs`.
|
|
18
|
+
*/
|
|
1
19
|
export { BMSSP } from "./src/bmssp.mjs";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Reference Dijkstra implementation โ the oracle the BMSSP test suite is
|
|
23
|
+
* validated against, exported for comparison and independent checking.
|
|
24
|
+
*
|
|
25
|
+
* `dijkstra(graph, nodeIDs, source)` returns a `Map` from node ID to
|
|
26
|
+
* shortest distance (`Infinity` if unreachable). See `src/dijkstra.mjs`.
|
|
27
|
+
*/
|
|
2
28
|
export { dijkstra } from "./src/dijkstra.mjs";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Opt-in constant-degree transform โ rewrites any graph so every vertex
|
|
32
|
+
* has in-degree and out-degree <= 2 (the paper's preliminary assumption)
|
|
33
|
+
* by splitting each vertex into a zero-weight cycle of port copies. The
|
|
34
|
+
* rewrite is distance-preserving and never required for correctness.
|
|
35
|
+
*
|
|
36
|
+
* `constantDegreeTransform(graph)` returns
|
|
37
|
+
* `{ edges, copiesOf, originalOf, sourceCopy, collapse }`; run BMSSP on
|
|
38
|
+
* `edges` from `sourceCopy(source)` and `collapse()` the resulting
|
|
39
|
+
* distances back onto original node IDs. See `src/constantDegree.mjs`.
|
|
40
|
+
*/
|
|
41
|
+
export { constantDegreeTransform } from "./src/constantDegree.mjs";
|
package/package.json
CHANGED
package/src/baseCase.mjs
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { MinHeap } from "./heap.mjs";
|
|
2
|
+
import {
|
|
3
|
+
compareKeys,
|
|
4
|
+
toBound,
|
|
5
|
+
makeTies,
|
|
6
|
+
orderKey,
|
|
7
|
+
relaxEdge,
|
|
8
|
+
} from "./tieBreak.mjs";
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* BaseCase(B, S) โ Algorithm 2 of "Breaking the Sorting Barrier for Directed
|
|
@@ -12,26 +19,40 @@ import { MinHeap } from "./heap.mjs";
|
|
|
12
19
|
*
|
|
13
20
|
* Distance estimates are read from and written into dHat in place โ exactly
|
|
14
21
|
* like the paper's global dฬ[ยท] labels, so improvements made here are visible
|
|
15
|
-
* to the levels above.
|
|
22
|
+
* to the levels above. Since #163 the heap is ordered by the composite
|
|
23
|
+
* [length, hops, id] keys of src/tieBreak.mjs, which realize the paper's
|
|
24
|
+
* Assumption 2.1 (distinct path lengths): a settled vertex carries its
|
|
25
|
+
* canonical (lexicographically minimal) label, which no later relaxation can
|
|
26
|
+
* improve. The settled filter below only skips canonical re-enqueue signals
|
|
27
|
+
* (exact label equality from the recorded predecessor), which is what keeps
|
|
28
|
+
* zero-weight plateaus quiescent instead of looping.
|
|
16
29
|
*
|
|
17
30
|
* Two outcomes:
|
|
18
31
|
* - Full success (fewer than k + 1 vertices exist under B): every vertex
|
|
19
|
-
* with
|
|
20
|
-
* - Partial (the k + 1 cap was hit):
|
|
21
|
-
* B'
|
|
32
|
+
* with key < B is settled and returned, with bound === B.
|
|
33
|
+
* - Partial (the k + 1 cap was hit): boundKey = the largest settled key
|
|
34
|
+
* B' < B, and exactly the k strictly-closer vertices are returned. Under
|
|
35
|
+
* the composite order the strictly-closer filter is exact โ scalar-length
|
|
36
|
+
* ties with the boundary vertex are broken by (hops, id), and a returned
|
|
37
|
+
* vertex may therefore share the boundary's scalar length (the documented
|
|
38
|
+
* d(v) <= bound caveat of the scalar view).
|
|
22
39
|
* Every returned vertex is complete either way.
|
|
23
40
|
*
|
|
24
|
-
* @param {number} B - Strict upper bound on the
|
|
41
|
+
* @param {number|[number, number, *]} B - Strict upper bound on the keys to
|
|
42
|
+
* settle: a number (Infinity is allowed) or a composite bound
|
|
25
43
|
* @param {Set<*>|Iterable<*>} S - Singleton set holding the complete source x
|
|
26
44
|
* @param {Map<*, number>} dHat - Global distance estimates dฬ[ยท], updated in place
|
|
27
45
|
* @param {Map<*, Array<[*, number]>>} adjacency - nodeId -> outgoing [to, weight] edges
|
|
28
46
|
* @param {number} k - Settle cap parameter, >= 1 (floored); the paper's โlog^(1/3) nโ
|
|
29
|
-
* @
|
|
30
|
-
*
|
|
47
|
+
* @param {{ hops: Map<*, number>, preds: Map<*, *> }} [ties] - Canonical
|
|
48
|
+
* tie-break labels updated alongside dHat; fresh throwaway maps by default
|
|
49
|
+
* @returns {{ bound: number|[number, number, *], boundKey: [number, number, *], vertices: Set<*> }}
|
|
50
|
+
* The boundary B' <= B (same kind as the B passed in), its composite key,
|
|
51
|
+
* and the set U of vertices settled below it
|
|
31
52
|
* @throws {Error} If k is not a number >= 1, S is not a singleton, or the
|
|
32
53
|
* source has no finite distance estimate
|
|
33
54
|
*/
|
|
34
|
-
function baseCase(B, S, dHat, adjacency, k) {
|
|
55
|
+
function baseCase(B, S, dHat, adjacency, k, ties = makeTies()) {
|
|
35
56
|
if (typeof k !== "number" || Number.isNaN(k) || k < 1) {
|
|
36
57
|
throw new Error("k must be a number >= 1");
|
|
37
58
|
}
|
|
@@ -45,50 +66,53 @@ function baseCase(B, S, dHat, adjacency, k) {
|
|
|
45
66
|
if (typeof sourceDistance !== "number" || !Number.isFinite(sourceDistance)) {
|
|
46
67
|
throw new Error("the source must have a finite distance estimate");
|
|
47
68
|
}
|
|
69
|
+
const boundKey = toBound(B);
|
|
48
70
|
|
|
49
71
|
// U0 in the paper: the vertices settled by this call, seeded with x
|
|
50
72
|
const settled = new Set([x]);
|
|
51
|
-
const heap = new MinHeap();
|
|
52
|
-
heap.insert(x,
|
|
73
|
+
const heap = new MinHeap(compareKeys);
|
|
74
|
+
heap.insert(x, orderKey(x, dHat, ties));
|
|
53
75
|
|
|
54
76
|
while (!heap.isEmpty() && settled.size < cap + 1) {
|
|
55
|
-
const { key: u
|
|
77
|
+
const { key: u } = heap.extractMin();
|
|
56
78
|
settled.add(u);
|
|
57
79
|
for (const [v, weight] of adjacency.get(u) ?? []) {
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
heap.insert(v, candidate);
|
|
70
|
-
}
|
|
80
|
+
// Canonical relaxation, gated by the bound (the paper's `< B`). An
|
|
81
|
+
// exact-equality result means u is v's recorded label-setter (v was
|
|
82
|
+
// labeled by an earlier phase without being completed) and v must be
|
|
83
|
+
// (re-)enqueued โ unless this very call already settled it, which is
|
|
84
|
+
// what keeps zero-weight plateaus finite.
|
|
85
|
+
const relaxed = relaxEdge(u, v, weight, dHat, ties, boundKey);
|
|
86
|
+
if (relaxed === null || settled.has(v)) continue;
|
|
87
|
+
if (heap.has(v)) {
|
|
88
|
+
heap.decreaseKey(v, relaxed.key);
|
|
89
|
+
} else {
|
|
90
|
+
heap.insert(v, relaxed.key);
|
|
71
91
|
}
|
|
72
92
|
}
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
if (settled.size <= cap) {
|
|
76
96
|
// Exhausted before the cap: everything under B is settled (B' = B)
|
|
77
|
-
return { bound: B, vertices: settled };
|
|
97
|
+
return { bound: B, boundKey, vertices: settled };
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
// Hit the k + 1 cap: report B' = the largest settled
|
|
81
|
-
//
|
|
82
|
-
let boundary =
|
|
100
|
+
// Hit the k + 1 cap: report B' = the largest settled key and return the
|
|
101
|
+
// vertices strictly below it โ exactly k of them, keys being distinct
|
|
102
|
+
let boundary = null;
|
|
83
103
|
for (const v of settled) {
|
|
84
|
-
const
|
|
85
|
-
if (
|
|
104
|
+
const key = orderKey(v, dHat, ties);
|
|
105
|
+
if (boundary === null || compareKeys(key, boundary) > 0) boundary = key;
|
|
86
106
|
}
|
|
87
107
|
const vertices = new Set();
|
|
88
108
|
for (const v of settled) {
|
|
89
|
-
if (
|
|
109
|
+
if (compareKeys(orderKey(v, dHat, ties), boundary) < 0) vertices.add(v);
|
|
90
110
|
}
|
|
91
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
bound: typeof B === "number" ? boundary[0] : boundary,
|
|
113
|
+
boundKey: boundary,
|
|
114
|
+
vertices,
|
|
115
|
+
};
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
export { baseCase };
|