@graphty/algorithms 1.0.1

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.
Files changed (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/dist/src/algorithms/centrality/betweenness.d.ts +34 -0
  4. package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -0
  5. package/dist/src/algorithms/centrality/betweenness.js +223 -0
  6. package/dist/src/algorithms/centrality/betweenness.js.map +1 -0
  7. package/dist/src/algorithms/centrality/closeness.d.ts +43 -0
  8. package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -0
  9. package/dist/src/algorithms/centrality/closeness.js +202 -0
  10. package/dist/src/algorithms/centrality/closeness.js.map +1 -0
  11. package/dist/src/algorithms/centrality/degree.d.ts +17 -0
  12. package/dist/src/algorithms/centrality/degree.d.ts.map +1 -0
  13. package/dist/src/algorithms/centrality/degree.js +86 -0
  14. package/dist/src/algorithms/centrality/degree.js.map +1 -0
  15. package/dist/src/algorithms/centrality/index.d.ts +11 -0
  16. package/dist/src/algorithms/centrality/index.d.ts.map +1 -0
  17. package/dist/src/algorithms/centrality/index.js +8 -0
  18. package/dist/src/algorithms/centrality/index.js.map +1 -0
  19. package/dist/src/algorithms/centrality/pagerank.d.ts +74 -0
  20. package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -0
  21. package/dist/src/algorithms/centrality/pagerank.js +191 -0
  22. package/dist/src/algorithms/centrality/pagerank.js.map +1 -0
  23. package/dist/src/algorithms/components/connected.d.ts +58 -0
  24. package/dist/src/algorithms/components/connected.d.ts.map +1 -0
  25. package/dist/src/algorithms/components/connected.js +234 -0
  26. package/dist/src/algorithms/components/connected.js.map +1 -0
  27. package/dist/src/algorithms/components/index.d.ts +5 -0
  28. package/dist/src/algorithms/components/index.d.ts.map +1 -0
  29. package/dist/src/algorithms/components/index.js +5 -0
  30. package/dist/src/algorithms/components/index.js.map +1 -0
  31. package/dist/src/algorithms/index.d.ts +8 -0
  32. package/dist/src/algorithms/index.d.ts.map +1 -0
  33. package/dist/src/algorithms/index.js +12 -0
  34. package/dist/src/algorithms/index.js.map +1 -0
  35. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts +52 -0
  36. package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -0
  37. package/dist/src/algorithms/shortest-path/bellman-ford.js +131 -0
  38. package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -0
  39. package/dist/src/algorithms/shortest-path/dijkstra.d.ts +26 -0
  40. package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -0
  41. package/dist/src/algorithms/shortest-path/dijkstra.js +199 -0
  42. package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -0
  43. package/dist/src/algorithms/shortest-path/index.d.ts +7 -0
  44. package/dist/src/algorithms/shortest-path/index.d.ts.map +1 -0
  45. package/dist/src/algorithms/shortest-path/index.js +6 -0
  46. package/dist/src/algorithms/shortest-path/index.js.map +1 -0
  47. package/dist/src/algorithms/traversal/bfs.d.ts +25 -0
  48. package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -0
  49. package/dist/src/algorithms/traversal/bfs.js +189 -0
  50. package/dist/src/algorithms/traversal/bfs.js.map +1 -0
  51. package/dist/src/algorithms/traversal/dfs.d.ts +32 -0
  52. package/dist/src/algorithms/traversal/dfs.d.ts.map +1 -0
  53. package/dist/src/algorithms/traversal/dfs.js +259 -0
  54. package/dist/src/algorithms/traversal/dfs.js.map +1 -0
  55. package/dist/src/algorithms/traversal/index.d.ts +7 -0
  56. package/dist/src/algorithms/traversal/index.d.ts.map +1 -0
  57. package/dist/src/algorithms/traversal/index.js +6 -0
  58. package/dist/src/algorithms/traversal/index.js.map +1 -0
  59. package/dist/src/core/graph.d.ts +100 -0
  60. package/dist/src/core/graph.d.ts.map +1 -0
  61. package/dist/src/core/graph.js +276 -0
  62. package/dist/src/core/graph.js.map +1 -0
  63. package/dist/src/data-structures/index.d.ts +6 -0
  64. package/dist/src/data-structures/index.d.ts.map +1 -0
  65. package/dist/src/data-structures/index.js +6 -0
  66. package/dist/src/data-structures/index.js.map +1 -0
  67. package/dist/src/data-structures/priority-queue.d.ts +60 -0
  68. package/dist/src/data-structures/priority-queue.d.ts.map +1 -0
  69. package/dist/src/data-structures/priority-queue.js +152 -0
  70. package/dist/src/data-structures/priority-queue.js.map +1 -0
  71. package/dist/src/data-structures/union-find.d.ts +54 -0
  72. package/dist/src/data-structures/union-find.d.ts.map +1 -0
  73. package/dist/src/data-structures/union-find.js +145 -0
  74. package/dist/src/data-structures/union-find.js.map +1 -0
  75. package/dist/src/index.d.ts +11 -0
  76. package/dist/src/index.d.ts.map +1 -0
  77. package/dist/src/index.js +13 -0
  78. package/dist/src/index.js.map +1 -0
  79. package/dist/src/types/index.d.ts +74 -0
  80. package/dist/src/types/index.d.ts.map +1 -0
  81. package/dist/src/types/index.js +5 -0
  82. package/dist/src/types/index.js.map +1 -0
  83. package/dist/test/browser/basic.test.d.ts +2 -0
  84. package/dist/test/browser/basic.test.d.ts.map +1 -0
  85. package/dist/test/browser/basic.test.js +70 -0
  86. package/dist/test/browser/basic.test.js.map +1 -0
  87. package/dist/test/unit/bellman-ford.test.d.ts +2 -0
  88. package/dist/test/unit/bellman-ford.test.d.ts.map +1 -0
  89. package/dist/test/unit/bellman-ford.test.js +231 -0
  90. package/dist/test/unit/bellman-ford.test.js.map +1 -0
  91. package/dist/test/unit/betweenness-centrality.test.d.ts +2 -0
  92. package/dist/test/unit/betweenness-centrality.test.d.ts.map +1 -0
  93. package/dist/test/unit/betweenness-centrality.test.js +311 -0
  94. package/dist/test/unit/betweenness-centrality.test.js.map +1 -0
  95. package/dist/test/unit/bfs.test.d.ts +2 -0
  96. package/dist/test/unit/bfs.test.d.ts.map +1 -0
  97. package/dist/test/unit/bfs.test.js +277 -0
  98. package/dist/test/unit/bfs.test.js.map +1 -0
  99. package/dist/test/unit/closeness.test.d.ts +2 -0
  100. package/dist/test/unit/closeness.test.d.ts.map +1 -0
  101. package/dist/test/unit/closeness.test.js +266 -0
  102. package/dist/test/unit/closeness.test.js.map +1 -0
  103. package/dist/test/unit/connected-components.test.d.ts +2 -0
  104. package/dist/test/unit/connected-components.test.d.ts.map +1 -0
  105. package/dist/test/unit/connected-components.test.js +355 -0
  106. package/dist/test/unit/connected-components.test.js.map +1 -0
  107. package/dist/test/unit/degree-centrality.test.d.ts +2 -0
  108. package/dist/test/unit/degree-centrality.test.d.ts.map +1 -0
  109. package/dist/test/unit/degree-centrality.test.js +260 -0
  110. package/dist/test/unit/degree-centrality.test.js.map +1 -0
  111. package/dist/test/unit/dfs.test.d.ts +2 -0
  112. package/dist/test/unit/dfs.test.d.ts.map +1 -0
  113. package/dist/test/unit/dfs.test.js +350 -0
  114. package/dist/test/unit/dfs.test.js.map +1 -0
  115. package/dist/test/unit/dijkstra.test.d.ts +2 -0
  116. package/dist/test/unit/dijkstra.test.d.ts.map +1 -0
  117. package/dist/test/unit/dijkstra.test.js +311 -0
  118. package/dist/test/unit/dijkstra.test.js.map +1 -0
  119. package/dist/test/unit/graph.test.d.ts +2 -0
  120. package/dist/test/unit/graph.test.d.ts.map +1 -0
  121. package/dist/test/unit/graph.test.js +175 -0
  122. package/dist/test/unit/graph.test.js.map +1 -0
  123. package/dist/test/unit/pagerank.test.d.ts +2 -0
  124. package/dist/test/unit/pagerank.test.d.ts.map +1 -0
  125. package/dist/test/unit/pagerank.test.js +281 -0
  126. package/dist/test/unit/pagerank.test.js.map +1 -0
  127. package/dist/test/unit/priority-queue.test.d.ts +2 -0
  128. package/dist/test/unit/priority-queue.test.d.ts.map +1 -0
  129. package/dist/test/unit/priority-queue.test.js +189 -0
  130. package/dist/test/unit/priority-queue.test.js.map +1 -0
  131. package/dist/test/unit/union-find.test.d.ts +2 -0
  132. package/dist/test/unit/union-find.test.d.ts.map +1 -0
  133. package/dist/test/unit/union-find.test.js +245 -0
  134. package/dist/test/unit/union-find.test.js.map +1 -0
  135. package/package.json +100 -0
  136. package/src/algorithms/centrality/betweenness.ts +303 -0
  137. package/src/algorithms/centrality/closeness.ts +296 -0
  138. package/src/algorithms/centrality/degree.ts +105 -0
  139. package/src/algorithms/centrality/index.ts +11 -0
  140. package/src/algorithms/centrality/pagerank.ts +302 -0
  141. package/src/algorithms/components/connected.ts +298 -0
  142. package/src/algorithms/components/index.ts +17 -0
  143. package/src/algorithms/index.ts +15 -0
  144. package/src/algorithms/shortest-path/bellman-ford.ts +214 -0
  145. package/src/algorithms/shortest-path/dijkstra.ts +256 -0
  146. package/src/algorithms/shortest-path/index.ts +7 -0
  147. package/src/algorithms/traversal/bfs.ts +236 -0
  148. package/src/algorithms/traversal/dfs.ts +367 -0
  149. package/src/algorithms/traversal/index.ts +7 -0
  150. package/src/core/graph.ts +326 -0
  151. package/src/data-structures/index.ts +6 -0
  152. package/src/data-structures/priority-queue.ts +180 -0
  153. package/src/data-structures/union-find.ts +170 -0
  154. package/src/index.ts +35 -0
  155. package/src/types/index.ts +95 -0
@@ -0,0 +1,298 @@
1
+ import type {Graph} from "../../core/graph.js";
2
+ import {UnionFind} from "../../data-structures/union-find.js";
3
+ import type {NodeId} from "../../types/index.js";
4
+
5
+ /**
6
+ * Connected components algorithms
7
+ *
8
+ * Finds connected components in undirected graphs and strongly connected
9
+ * components in directed graphs using various efficient algorithms.
10
+ */
11
+
12
+ /**
13
+ * Find connected components in an undirected graph using Union-Find
14
+ */
15
+ export function connectedComponents(graph: Graph): NodeId[][] {
16
+ if (graph.isDirected) {
17
+ throw new Error("Connected components algorithm requires an undirected graph. Use stronglyConnectedComponents for directed graphs.");
18
+ }
19
+
20
+ const nodes = Array.from(graph.nodes()).map((node) => node.id);
21
+
22
+ if (nodes.length === 0) {
23
+ return [];
24
+ }
25
+
26
+ const unionFind = new UnionFind(nodes);
27
+
28
+ // Union connected nodes
29
+ for (const edge of graph.edges()) {
30
+ unionFind.union(edge.source, edge.target);
31
+ }
32
+
33
+ return unionFind.getAllComponents();
34
+ }
35
+
36
+ /**
37
+ * Find connected components using DFS (alternative implementation)
38
+ */
39
+ export function connectedComponentsDFS(graph: Graph): NodeId[][] {
40
+ if (graph.isDirected) {
41
+ throw new Error("Connected components algorithm requires an undirected graph");
42
+ }
43
+
44
+ const visited = new Set<NodeId>();
45
+ const components: NodeId[][] = [];
46
+
47
+ for (const node of graph.nodes()) {
48
+ if (!visited.has(node.id)) {
49
+ const component: NodeId[] = [];
50
+ dfsComponent(graph, node.id, visited, component);
51
+ components.push(component);
52
+ }
53
+ }
54
+
55
+ return components;
56
+ }
57
+
58
+ /**
59
+ * DFS helper for connected components
60
+ */
61
+ function dfsComponent(
62
+ graph: Graph,
63
+ nodeId: NodeId,
64
+ visited: Set<NodeId>,
65
+ component: NodeId[],
66
+ ): void {
67
+ visited.add(nodeId);
68
+ component.push(nodeId);
69
+
70
+ for (const neighbor of graph.neighbors(nodeId)) {
71
+ if (!visited.has(neighbor)) {
72
+ dfsComponent(graph, neighbor, visited, component);
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Find the number of connected components
79
+ */
80
+ export function numberOfConnectedComponents(graph: Graph): number {
81
+ return connectedComponents(graph).length;
82
+ }
83
+
84
+ /**
85
+ * Check if the graph is connected (has exactly one connected component)
86
+ */
87
+ export function isConnected(graph: Graph): boolean {
88
+ return numberOfConnectedComponents(graph) <= 1;
89
+ }
90
+
91
+ /**
92
+ * Find the largest connected component
93
+ */
94
+ export function largestConnectedComponent(graph: Graph): NodeId[] {
95
+ const components = connectedComponents(graph);
96
+
97
+ if (components.length === 0) {
98
+ return [];
99
+ }
100
+
101
+ return components.reduce((largest, current) =>
102
+ current.length > largest.length ? current : largest,
103
+ );
104
+ }
105
+
106
+ /**
107
+ * Get the connected component containing a specific node
108
+ */
109
+ export function getConnectedComponent(graph: Graph, nodeId: NodeId): NodeId[] {
110
+ if (!graph.hasNode(nodeId)) {
111
+ throw new Error(`Node ${String(nodeId)} not found in graph`);
112
+ }
113
+
114
+ if (graph.isDirected) {
115
+ throw new Error("Connected components algorithm requires an undirected graph");
116
+ }
117
+
118
+ const visited = new Set<NodeId>();
119
+ const component: NodeId[] = [];
120
+
121
+ dfsComponent(graph, nodeId, visited, component);
122
+
123
+ return component;
124
+ }
125
+
126
+ /**
127
+ * Find strongly connected components using Tarjan's algorithm
128
+ */
129
+ export function stronglyConnectedComponents(graph: Graph): NodeId[][] {
130
+ if (!graph.isDirected) {
131
+ throw new Error("Strongly connected components require a directed graph");
132
+ }
133
+
134
+ const nodes = Array.from(graph.nodes()).map((node) => node.id);
135
+ const components: NodeId[][] = [];
136
+ const indices = new Map<NodeId, number>();
137
+ const lowLinks = new Map<NodeId, number>();
138
+ const onStack = new Set<NodeId>();
139
+ const stack: NodeId[] = [];
140
+ let index = 0;
141
+
142
+ function tarjanSCC(nodeId: NodeId): void {
143
+ // Set the depth index for this node
144
+ indices.set(nodeId, index);
145
+ lowLinks.set(nodeId, index);
146
+ index++;
147
+ stack.push(nodeId);
148
+ onStack.add(nodeId);
149
+
150
+ // Consider successors
151
+ for (const neighbor of graph.neighbors(nodeId)) {
152
+ if (!indices.has(neighbor)) {
153
+ // Successor has not yet been visited; recurse on it
154
+ tarjanSCC(neighbor);
155
+ const nodeLL = lowLinks.get(nodeId) ?? 0;
156
+ const neighborLL = lowLinks.get(neighbor) ?? 0;
157
+ lowLinks.set(nodeId, Math.min(nodeLL, neighborLL));
158
+ } else if (onStack.has(neighbor)) {
159
+ // Successor is in stack and hence in the current SCC
160
+ const nodeLL = lowLinks.get(nodeId) ?? 0;
161
+ const neighborIndex = indices.get(neighbor) ?? 0;
162
+ lowLinks.set(nodeId, Math.min(nodeLL, neighborIndex));
163
+ }
164
+ }
165
+
166
+ // If nodeId is a root node, pop the stack and create an SCC
167
+ const nodeIndex = indices.get(nodeId) ?? 0;
168
+ const nodeLowLink = lowLinks.get(nodeId) ?? 0;
169
+
170
+ if (nodeLowLink === nodeIndex) {
171
+ const component: NodeId[] = [];
172
+ let w: NodeId;
173
+
174
+ do {
175
+ const popped = stack.pop();
176
+ if (popped === undefined) {
177
+ break;
178
+ }
179
+
180
+ w = popped;
181
+
182
+ onStack.delete(w);
183
+ component.push(w);
184
+ } while (w !== nodeId);
185
+
186
+ components.push(component);
187
+ }
188
+ }
189
+
190
+ for (const nodeId of nodes) {
191
+ if (!indices.has(nodeId)) {
192
+ tarjanSCC(nodeId);
193
+ }
194
+ }
195
+
196
+ return components;
197
+ }
198
+
199
+ /**
200
+ * Check if a directed graph is strongly connected
201
+ */
202
+ export function isStronglyConnected(graph: Graph): boolean {
203
+ if (!graph.isDirected) {
204
+ throw new Error("Strong connectivity check requires a directed graph");
205
+ }
206
+
207
+ const components = stronglyConnectedComponents(graph);
208
+ return components.length <= 1;
209
+ }
210
+
211
+ /**
212
+ * Find weakly connected components in a directed graph
213
+ * (treat the graph as undirected for connectivity)
214
+ */
215
+ export function weaklyConnectedComponents(graph: Graph): NodeId[][] {
216
+ if (!graph.isDirected) {
217
+ throw new Error("Weakly connected components are for directed graphs. Use connectedComponents for undirected graphs.");
218
+ }
219
+
220
+ const nodes = Array.from(graph.nodes()).map((node) => node.id);
221
+
222
+ if (nodes.length === 0) {
223
+ return [];
224
+ }
225
+
226
+ const unionFind = new UnionFind(nodes);
227
+
228
+ // Union nodes connected by edges (ignore direction)
229
+ for (const edge of graph.edges()) {
230
+ unionFind.union(edge.source, edge.target);
231
+ }
232
+
233
+ return unionFind.getAllComponents();
234
+ }
235
+
236
+ /**
237
+ * Check if a directed graph is weakly connected
238
+ */
239
+ export function isWeaklyConnected(graph: Graph): boolean {
240
+ if (!graph.isDirected) {
241
+ throw new Error("Weak connectivity check requires a directed graph");
242
+ }
243
+
244
+ return weaklyConnectedComponents(graph).length <= 1;
245
+ }
246
+
247
+ /**
248
+ * Find condensation graph (quotient graph of strongly connected components)
249
+ */
250
+ export function condensationGraph(graph: Graph): {
251
+ condensedGraph: Graph;
252
+ componentMap: Map<NodeId, number>;
253
+ components: NodeId[][];
254
+ } {
255
+ if (!graph.isDirected) {
256
+ throw new Error("Condensation graph requires a directed graph");
257
+ }
258
+
259
+ const components = stronglyConnectedComponents(graph);
260
+ const componentMap = new Map<NodeId, number>();
261
+ const condensedGraph = new (graph.constructor as new (config: {directed: boolean}) => Graph)({directed: true});
262
+
263
+ // Map each node to its component index
264
+ for (let i = 0; i < components.length; i++) {
265
+ const component = components[i];
266
+ if (component) {
267
+ for (const nodeId of component) {
268
+ componentMap.set(nodeId, i);
269
+ }
270
+ // Add component as a node in condensed graph
271
+ condensedGraph.addNode(i);
272
+ }
273
+ }
274
+
275
+ // Add edges between components
276
+ const addedEdges = new Set<string>();
277
+
278
+ for (const edge of graph.edges()) {
279
+ const sourceComponent = componentMap.get(edge.source);
280
+ const targetComponent = componentMap.get(edge.target);
281
+
282
+ if (sourceComponent !== undefined && targetComponent !== undefined &&
283
+ sourceComponent !== targetComponent) {
284
+ const edgeKey = `${sourceComponent}-${targetComponent}`;
285
+
286
+ if (!addedEdges.has(edgeKey)) {
287
+ condensedGraph.addEdge(sourceComponent, targetComponent);
288
+ addedEdges.add(edgeKey);
289
+ }
290
+ }
291
+ }
292
+
293
+ return {
294
+ condensedGraph,
295
+ componentMap,
296
+ components,
297
+ };
298
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Connected components algorithms
3
+ */
4
+
5
+ export {
6
+ condensationGraph,
7
+ connectedComponents,
8
+ connectedComponentsDFS,
9
+ getConnectedComponent,
10
+ isConnected,
11
+ isStronglyConnected,
12
+ isWeaklyConnected,
13
+ largestConnectedComponent,
14
+ numberOfConnectedComponents,
15
+ stronglyConnectedComponents,
16
+ weaklyConnectedComponents,
17
+ } from "./connected.js";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Graph algorithms collection
3
+ */
4
+
5
+ // Traversal algorithms
6
+ export * from "./traversal/index.js";
7
+
8
+ // Shortest path algorithms
9
+ export * from "./shortest-path/index.js";
10
+
11
+ // Centrality algorithms
12
+ export * from "./centrality/index.js";
13
+
14
+ // Connected components algorithms
15
+ export * from "./components/index.js";
@@ -0,0 +1,214 @@
1
+ import type {Graph} from "../../core/graph.js";
2
+ import type {NodeId, ShortestPathResult} from "../../types/index.js";
3
+
4
+ /**
5
+ * Bellman-Ford algorithm implementation for single-source shortest paths
6
+ *
7
+ * Finds shortest paths from a source node to all other nodes in a weighted graph.
8
+ * Unlike Dijkstra's algorithm, it can handle negative edge weights and detect
9
+ * negative cycles.
10
+ */
11
+
12
+ /**
13
+ * Bellman-Ford algorithm options
14
+ */
15
+ export interface BellmanFordOptions {
16
+ /**
17
+ * Target node for early termination (optional)
18
+ */
19
+ target?: NodeId;
20
+ }
21
+
22
+ /**
23
+ * Result of Bellman-Ford algorithm
24
+ */
25
+ export interface BellmanFordResult {
26
+ /**
27
+ * Distance from source to each reachable node
28
+ */
29
+ distances: Map<NodeId, number>;
30
+ /**
31
+ * Predecessor of each node in shortest path tree
32
+ */
33
+ predecessors: Map<NodeId, NodeId | null>;
34
+ /**
35
+ * Whether a negative cycle was detected
36
+ */
37
+ hasNegativeCycle: boolean;
38
+ /**
39
+ * Nodes involved in negative cycle (if any)
40
+ */
41
+ negativeCycleNodes: NodeId[];
42
+ }
43
+
44
+ /**
45
+ * Find shortest paths from source using Bellman-Ford algorithm
46
+ */
47
+ export function bellmanFord(
48
+ graph: Graph,
49
+ source: NodeId,
50
+ options: BellmanFordOptions = {},
51
+ ): BellmanFordResult {
52
+ if (!graph.hasNode(source)) {
53
+ throw new Error(`Source node ${String(source)} not found in graph`);
54
+ }
55
+
56
+ const distances = new Map<NodeId, number>();
57
+ const predecessors = new Map<NodeId, NodeId | null>();
58
+ const nodes = Array.from(graph.nodes()).map((node) => node.id);
59
+
60
+ // Initialize distances
61
+ for (const nodeId of nodes) {
62
+ distances.set(nodeId, nodeId === source ? 0 : Infinity);
63
+ predecessors.set(nodeId, null);
64
+ }
65
+
66
+ // Relax edges |V| - 1 times
67
+ for (let i = 0; i < nodes.length - 1; i++) {
68
+ let updated = false;
69
+
70
+ for (const edge of graph.edges()) {
71
+ const u = edge.source;
72
+ const v = edge.target;
73
+ const weight = edge.weight ?? 1;
74
+
75
+ const distanceU = distances.get(u);
76
+ const distanceV = distances.get(v);
77
+
78
+ if (distanceU !== undefined && distanceV !== undefined && distanceU !== Infinity) {
79
+ const newDistance = distanceU + weight;
80
+
81
+ if (newDistance < distanceV) {
82
+ distances.set(v, newDistance);
83
+ predecessors.set(v, u);
84
+ updated = true;
85
+
86
+ // Early termination if target reached
87
+ if (options.target && v === options.target) {
88
+ break;
89
+ }
90
+ }
91
+ }
92
+ }
93
+
94
+ // If no update in this iteration, we can stop early
95
+ if (!updated) {
96
+ break;
97
+ }
98
+ }
99
+
100
+ // Check for negative cycles
101
+ const negativeCycleNodes: NodeId[] = [];
102
+ let hasNegativeCycle = false;
103
+
104
+ for (const edge of graph.edges()) {
105
+ const u = edge.source;
106
+ const v = edge.target;
107
+ const weight = edge.weight ?? 1;
108
+
109
+ const distanceU = distances.get(u);
110
+ const distanceV = distances.get(v);
111
+
112
+ if (distanceU !== undefined && distanceV !== undefined && distanceU !== Infinity) {
113
+ const newDistance = distanceU + weight;
114
+
115
+ if (newDistance < distanceV) {
116
+ hasNegativeCycle = true;
117
+ negativeCycleNodes.push(v);
118
+ }
119
+ }
120
+ }
121
+
122
+ return {
123
+ distances,
124
+ predecessors,
125
+ hasNegativeCycle,
126
+ negativeCycleNodes,
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Find shortest path between two specific nodes using Bellman-Ford
132
+ */
133
+ export function bellmanFordPath(
134
+ graph: Graph,
135
+ source: NodeId,
136
+ target: NodeId,
137
+ ): ShortestPathResult | null {
138
+ if (!graph.hasNode(source)) {
139
+ throw new Error(`Source node ${String(source)} not found in graph`);
140
+ }
141
+
142
+ if (!graph.hasNode(target)) {
143
+ throw new Error(`Target node ${String(target)} not found in graph`);
144
+ }
145
+
146
+ const result = bellmanFord(graph, source, {target});
147
+
148
+ if (result.hasNegativeCycle) {
149
+ throw new Error("Graph contains a negative cycle");
150
+ }
151
+
152
+ const distance = result.distances.get(target);
153
+
154
+ if (distance === undefined || distance === Infinity) {
155
+ return null; // No path exists
156
+ }
157
+
158
+ // Reconstruct path
159
+ const path = reconstructPath(target, result.predecessors);
160
+
161
+ return {
162
+ distance,
163
+ path,
164
+ predecessor: result.predecessors,
165
+ };
166
+ }
167
+
168
+ /**
169
+ * Check if graph has negative cycles using Bellman-Ford
170
+ */
171
+ export function hasNegativeCycle(graph: Graph): boolean {
172
+ const nodes = Array.from(graph.nodes());
173
+
174
+ if (nodes.length === 0) {
175
+ return false;
176
+ }
177
+
178
+ // Need to check from multiple sources in case of disconnected components
179
+ const checked = new Set<NodeId>();
180
+
181
+ for (const node of nodes) {
182
+ if (!checked.has(node.id)) {
183
+ const result = bellmanFord(graph, node.id);
184
+
185
+ if (result.hasNegativeCycle) {
186
+ return true;
187
+ }
188
+
189
+ // Mark all reachable nodes as checked
190
+ for (const [nodeId, distance] of result.distances) {
191
+ if (distance !== Infinity) {
192
+ checked.add(nodeId);
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ return false;
199
+ }
200
+
201
+ /**
202
+ * Reconstruct path from predecessor map
203
+ */
204
+ function reconstructPath(target: NodeId, predecessors: Map<NodeId, NodeId | null>): NodeId[] {
205
+ const path: NodeId[] = [];
206
+ let current: NodeId | null = target;
207
+
208
+ while (current !== null) {
209
+ path.unshift(current);
210
+ current = predecessors.get(current) ?? null;
211
+ }
212
+
213
+ return path;
214
+ }