@graphty/algorithms 1.2.0 → 1.3.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.
- package/README.md +490 -336
- package/dist/algorithms.d.ts +42 -2
- package/dist/algorithms.js +11099 -2
- package/dist/algorithms.js.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.d.ts +4 -0
- package/dist/src/algorithms/centrality/betweenness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/betweenness.js +124 -170
- package/dist/src/algorithms/centrality/betweenness.js.map +1 -1
- package/dist/src/algorithms/centrality/closeness.d.ts +26 -1
- package/dist/src/algorithms/centrality/closeness.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/closeness.js +56 -149
- package/dist/src/algorithms/centrality/closeness.js.map +1 -1
- package/dist/src/algorithms/centrality/degree.js +3 -3
- package/dist/src/algorithms/centrality/degree.js.map +1 -1
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts +39 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js +286 -0
- package/dist/src/algorithms/centrality/delta-pagerank-simple.js.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts +80 -0
- package/dist/src/algorithms/centrality/delta-pagerank.d.ts.map +1 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js +294 -0
- package/dist/src/algorithms/centrality/delta-pagerank.js.map +1 -0
- package/dist/src/algorithms/centrality/index.d.ts +2 -0
- package/dist/src/algorithms/centrality/index.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/index.js +1 -0
- package/dist/src/algorithms/centrality/index.js.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.d.ts +17 -0
- package/dist/src/algorithms/centrality/pagerank.d.ts.map +1 -1
- package/dist/src/algorithms/centrality/pagerank.js +45 -0
- package/dist/src/algorithms/centrality/pagerank.js.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.d.ts.map +1 -1
- package/dist/src/algorithms/community/girvan-newman.js +47 -54
- package/dist/src/algorithms/community/girvan-newman.js.map +1 -1
- package/dist/src/algorithms/community/index.d.ts +2 -0
- package/dist/src/algorithms/community/index.d.ts.map +1 -1
- package/dist/src/algorithms/community/index.js.map +1 -1
- package/dist/src/algorithms/community/label-propagation.d.ts +13 -3
- package/dist/src/algorithms/community/label-propagation.d.ts.map +1 -1
- package/dist/src/algorithms/community/label-propagation.js +50 -35
- package/dist/src/algorithms/community/label-propagation.js.map +1 -1
- package/dist/src/algorithms/community/leiden.d.ts +3 -2
- package/dist/src/algorithms/community/leiden.d.ts.map +1 -1
- package/dist/src/algorithms/community/leiden.js +34 -39
- package/dist/src/algorithms/community/leiden.js.map +1 -1
- package/dist/src/algorithms/community/louvain-optimized.d.ts +112 -0
- package/dist/src/algorithms/community/louvain-optimized.d.ts.map +1 -0
- package/dist/src/algorithms/community/louvain-optimized.js +303 -0
- package/dist/src/algorithms/community/louvain-optimized.js.map +1 -0
- package/dist/src/algorithms/community/louvain.d.ts.map +1 -1
- package/dist/src/algorithms/community/louvain.js +17 -76
- package/dist/src/algorithms/community/louvain.js.map +1 -1
- package/dist/src/algorithms/community/modularity-utils.d.ts +59 -0
- package/dist/src/algorithms/community/modularity-utils.d.ts.map +1 -0
- package/dist/src/algorithms/community/modularity-utils.js +112 -0
- package/dist/src/algorithms/community/modularity-utils.js.map +1 -0
- package/dist/src/algorithms/matching/bipartite.d.ts.map +1 -1
- package/dist/src/algorithms/matching/bipartite.js +8 -40
- package/dist/src/algorithms/matching/bipartite.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/bellman-ford.js +1 -12
- package/dist/src/algorithms/shortest-path/bellman-ford.js.map +1 -1
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts +37 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.d.ts.map +1 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js +193 -0
- package/dist/src/algorithms/shortest-path/bidirectional-dijkstra.js.map +1 -0
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts +5 -2
- package/dist/src/algorithms/shortest-path/dijkstra.d.ts.map +1 -1
- package/dist/src/algorithms/shortest-path/dijkstra.js +15 -14
- package/dist/src/algorithms/shortest-path/dijkstra.js.map +1 -1
- package/dist/src/algorithms/traversal/bfs-unified.d.ts +29 -0
- package/dist/src/algorithms/traversal/bfs-unified.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-unified.js +333 -0
- package/dist/src/algorithms/traversal/bfs-unified.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts +54 -0
- package/dist/src/algorithms/traversal/bfs-variants.d.ts.map +1 -0
- package/dist/src/algorithms/traversal/bfs-variants.js +368 -0
- package/dist/src/algorithms/traversal/bfs-variants.js.map +1 -0
- package/dist/src/algorithms/traversal/bfs.d.ts +4 -18
- package/dist/src/algorithms/traversal/bfs.d.ts.map +1 -1
- package/dist/src/algorithms/traversal/bfs.js +5 -182
- package/dist/src/algorithms/traversal/bfs.js.map +1 -1
- package/dist/src/benchmark-all-algorithms.d.ts +23 -0
- package/dist/src/benchmark-all-algorithms.d.ts.map +1 -0
- package/dist/src/benchmark-all-algorithms.js +462 -0
- package/dist/src/benchmark-all-algorithms.js.map +1 -0
- package/dist/src/clustering/hierarchical.d.ts +16 -15
- package/dist/src/clustering/hierarchical.d.ts.map +1 -1
- package/dist/src/clustering/hierarchical.js +209 -223
- package/dist/src/clustering/hierarchical.js.map +1 -1
- package/dist/src/clustering/index.d.ts +4 -2
- package/dist/src/clustering/index.d.ts.map +1 -1
- package/dist/src/clustering/index.js +2 -2
- package/dist/src/clustering/index.js.map +1 -1
- package/dist/src/clustering/k-core.d.ts +16 -15
- package/dist/src/clustering/k-core.d.ts.map +1 -1
- package/dist/src/clustering/k-core.js +57 -21
- package/dist/src/clustering/k-core.js.map +1 -1
- package/dist/src/clustering/spectral.d.ts +8 -0
- package/dist/src/clustering/spectral.d.ts.map +1 -1
- package/dist/src/clustering/spectral.js +40 -21
- package/dist/src/clustering/spectral.js.map +1 -1
- package/dist/src/core/graph.js +4 -4
- package/dist/src/core/graph.js.map +1 -1
- package/dist/src/flow/ford-fulkerson.d.ts +17 -11
- package/dist/src/flow/ford-fulkerson.d.ts.map +1 -1
- package/dist/src/flow/ford-fulkerson.js +141 -217
- package/dist/src/flow/ford-fulkerson.js.map +1 -1
- package/dist/src/flow/min-cut.d.ts +6 -5
- package/dist/src/flow/min-cut.d.ts.map +1 -1
- package/dist/src/flow/min-cut.js +17 -8
- package/dist/src/flow/min-cut.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/link-prediction/adamic-adar.d.ts.map +1 -1
- package/dist/src/link-prediction/adamic-adar.js +17 -26
- package/dist/src/link-prediction/adamic-adar.js.map +1 -1
- package/dist/src/link-prediction/common-neighbors.d.ts.map +1 -1
- package/dist/src/link-prediction/common-neighbors.js +7 -11
- package/dist/src/link-prediction/common-neighbors.js.map +1 -1
- package/dist/src/optimized/bit-packed.d.ts +143 -0
- package/dist/src/optimized/bit-packed.d.ts.map +1 -0
- package/dist/src/optimized/bit-packed.js +292 -0
- package/dist/src/optimized/bit-packed.js.map +1 -0
- package/dist/src/optimized/csr-graph.d.ts +86 -0
- package/dist/src/optimized/csr-graph.d.ts.map +1 -0
- package/dist/src/optimized/csr-graph.js +341 -0
- package/dist/src/optimized/csr-graph.js.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts +74 -0
- package/dist/src/optimized/direction-optimized-bfs.d.ts.map +1 -0
- package/dist/src/optimized/direction-optimized-bfs.js +223 -0
- package/dist/src/optimized/direction-optimized-bfs.js.map +1 -0
- package/dist/src/optimized/graph-adapter.d.ts +62 -0
- package/dist/src/optimized/graph-adapter.d.ts.map +1 -0
- package/dist/src/optimized/graph-adapter.js +146 -0
- package/dist/src/optimized/graph-adapter.js.map +1 -0
- package/dist/src/optimized/index.d.ts +14 -0
- package/dist/src/optimized/index.d.ts.map +1 -0
- package/dist/src/optimized/index.js +28 -0
- package/dist/src/optimized/index.js.map +1 -0
- package/dist/src/research/grsbm.d.ts.map +1 -1
- package/dist/src/research/grsbm.js +6 -14
- package/dist/src/research/grsbm.js.map +1 -1
- package/dist/src/research/sync.d.ts.map +1 -1
- package/dist/src/research/sync.js +3 -29
- package/dist/src/research/sync.js.map +1 -1
- package/dist/src/research/terahac.d.ts +2 -0
- package/dist/src/research/terahac.d.ts.map +1 -1
- package/dist/src/research/terahac.js +7 -3
- package/dist/src/research/terahac.js.map +1 -1
- package/dist/src/types/index.d.ts +8 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/utils/algorithm-utilities.d.ts +2 -0
- package/dist/src/utils/algorithm-utilities.d.ts.map +1 -0
- package/dist/src/utils/algorithm-utilities.js +2 -0
- package/dist/src/utils/algorithm-utilities.js.map +1 -0
- package/dist/src/utils/graph-converters.d.ts +79 -0
- package/dist/src/utils/graph-converters.d.ts.map +1 -0
- package/dist/src/utils/graph-converters.js +196 -0
- package/dist/src/utils/graph-converters.js.map +1 -0
- package/dist/src/utils/graph-utilities.d.ts +62 -0
- package/dist/src/utils/graph-utilities.d.ts.map +1 -0
- package/dist/src/utils/graph-utilities.js +148 -0
- package/dist/src/utils/graph-utilities.js.map +1 -0
- package/dist/src/utils/index.d.ts +5 -0
- package/dist/src/utils/index.d.ts.map +1 -0
- package/dist/src/utils/index.js +5 -0
- package/dist/src/utils/index.js.map +1 -0
- package/dist/src/utils/math-utilities.d.ts +52 -0
- package/dist/src/utils/math-utilities.d.ts.map +1 -0
- package/dist/src/utils/math-utilities.js +123 -0
- package/dist/src/utils/math-utilities.js.map +1 -0
- package/dist/src/utils/matrix-utilities.d.ts +2 -0
- package/dist/src/utils/matrix-utilities.d.ts.map +1 -0
- package/dist/src/utils/matrix-utilities.js +2 -0
- package/dist/src/utils/matrix-utilities.js.map +1 -0
- package/dist/src/utils/optimization-helpers.d.ts +33 -0
- package/dist/src/utils/optimization-helpers.d.ts.map +1 -0
- package/dist/src/utils/optimization-helpers.js +47 -0
- package/dist/src/utils/optimization-helpers.js.map +1 -0
- package/package.json +18 -3
- package/src/algorithms/centrality/betweenness.ts +167 -199
- package/src/algorithms/centrality/closeness.ts +73 -191
- package/src/algorithms/centrality/degree.ts +3 -3
- package/src/algorithms/centrality/delta-pagerank-simple.ts +366 -0
- package/src/algorithms/centrality/delta-pagerank.ts +435 -0
- package/src/algorithms/centrality/index.ts +2 -0
- package/src/algorithms/centrality/pagerank.ts +66 -0
- package/src/algorithms/community/girvan-newman.ts +52 -59
- package/src/algorithms/community/index.ts +2 -0
- package/src/algorithms/community/label-propagation.ts +64 -35
- package/src/algorithms/community/leiden.ts +40 -41
- package/src/algorithms/community/louvain-optimized.ts +445 -0
- package/src/algorithms/community/louvain.ts +24 -104
- package/src/algorithms/community/modularity-utils.ts +146 -0
- package/src/algorithms/matching/bipartite.ts +8 -44
- package/src/algorithms/shortest-path/bellman-ford.ts +1 -14
- package/src/algorithms/shortest-path/bidirectional-dijkstra.ts +248 -0
- package/src/algorithms/shortest-path/dijkstra.ts +17 -15
- package/src/algorithms/traversal/bfs-unified.ts +433 -0
- package/src/algorithms/traversal/bfs-variants.ts +496 -0
- package/src/algorithms/traversal/bfs.ts +10 -228
- package/src/benchmark-all-algorithms.ts +548 -0
- package/src/clustering/hierarchical.ts +241 -251
- package/src/clustering/index.ts +4 -2
- package/src/clustering/k-core.ts +87 -39
- package/src/clustering/spectral.ts +44 -20
- package/src/core/graph.ts +4 -4
- package/src/flow/ford-fulkerson.ts +186 -254
- package/src/flow/min-cut.ts +21 -11
- package/src/index.ts +6 -0
- package/src/link-prediction/adamic-adar.ts +17 -30
- package/src/link-prediction/common-neighbors.ts +7 -16
- package/src/optimized/bit-packed.ts +340 -0
- package/src/optimized/csr-graph.ts +455 -0
- package/src/optimized/direction-optimized-bfs.ts +291 -0
- package/src/optimized/graph-adapter.ts +200 -0
- package/src/optimized/index.ts +36 -0
- package/src/research/grsbm.ts +7 -16
- package/src/research/sync.ts +3 -32
- package/src/research/terahac.ts +10 -2
- package/src/types/index.ts +8 -0
- package/src/utils/algorithm-utilities.ts +2 -0
- package/src/utils/graph-converters.ts +241 -0
- package/src/utils/graph-utilities.ts +199 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/math-utilities.ts +140 -0
- package/src/utils/matrix-utilities.ts +2 -0
- package/src/utils/optimization-helpers.ts +81 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared modularity calculation utilities for community detection algorithms
|
|
3
|
+
*
|
|
4
|
+
* Used by: Louvain, Leiden, Girvan-Newman
|
|
5
|
+
*/
|
|
6
|
+
import type {Graph} from "../../core/graph.js";
|
|
7
|
+
import type {NodeId} from "../../types/index.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Calculate total edge weight in the graph
|
|
11
|
+
*
|
|
12
|
+
* For undirected graphs, each edge is counted once.
|
|
13
|
+
* For directed graphs, each directed edge is counted once.
|
|
14
|
+
*
|
|
15
|
+
* @param graph - The input graph
|
|
16
|
+
* @returns Total sum of edge weights (default weight is 1)
|
|
17
|
+
*/
|
|
18
|
+
export function getTotalEdgeWeight(graph: Graph): number {
|
|
19
|
+
let totalWeight = 0;
|
|
20
|
+
|
|
21
|
+
for (const edge of graph.edges()) {
|
|
22
|
+
totalWeight += edge.weight ?? 1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return totalWeight;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the total degree (sum of edge weights) for a node
|
|
30
|
+
*
|
|
31
|
+
* Computes the weighted degree by summing all edge weights incident to the node.
|
|
32
|
+
*
|
|
33
|
+
* @param graph - The input graph
|
|
34
|
+
* @param nodeId - The node ID to compute degree for
|
|
35
|
+
* @returns The weighted degree of the node
|
|
36
|
+
*/
|
|
37
|
+
export function getNodeDegree(graph: Graph, nodeId: NodeId): number {
|
|
38
|
+
let degree = 0;
|
|
39
|
+
|
|
40
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
41
|
+
const edge = graph.getEdge(nodeId, neighbor);
|
|
42
|
+
degree += edge?.weight ?? 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return degree;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get communities of neighboring nodes
|
|
50
|
+
*
|
|
51
|
+
* Returns the set of community IDs that neighbors of the given node belong to.
|
|
52
|
+
*
|
|
53
|
+
* @param graph - The input graph
|
|
54
|
+
* @param nodeId - The node to get neighbor communities for
|
|
55
|
+
* @param communities - Map from node IDs to community IDs
|
|
56
|
+
* @returns Set of community IDs that neighbors belong to
|
|
57
|
+
*/
|
|
58
|
+
export function getNeighborCommunities(
|
|
59
|
+
graph: Graph,
|
|
60
|
+
nodeId: NodeId,
|
|
61
|
+
communities: Map<NodeId, number>,
|
|
62
|
+
): Set<number> {
|
|
63
|
+
const neighborCommunities = new Set<number>();
|
|
64
|
+
|
|
65
|
+
for (const neighbor of graph.neighbors(nodeId)) {
|
|
66
|
+
const community = communities.get(neighbor);
|
|
67
|
+
if (community !== undefined) {
|
|
68
|
+
neighborCommunities.add(community);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return neighborCommunities;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Calculate modularity of a partition
|
|
77
|
+
*
|
|
78
|
+
* Modularity measures the quality of a community partition. It compares the
|
|
79
|
+
* number of edges within communities to what would be expected in a random graph.
|
|
80
|
+
*
|
|
81
|
+
* Formula: Q = (1/2m) * Σ[A_ij - γ(k_i * k_j)/(2m)] * δ(c_i, c_j)
|
|
82
|
+
* where:
|
|
83
|
+
* - m = total edge weight
|
|
84
|
+
* - A_ij = adjacency matrix entry (edge weight between i and j)
|
|
85
|
+
* - k_i, k_j = degrees of nodes i and j
|
|
86
|
+
* - γ = resolution parameter (higher values favor smaller communities)
|
|
87
|
+
* - δ = Kronecker delta (1 if same community, 0 otherwise)
|
|
88
|
+
*
|
|
89
|
+
* @param graph - The input graph
|
|
90
|
+
* @param communities - Map from node IDs to community IDs
|
|
91
|
+
* @param resolution - Resolution parameter (default: 1.0)
|
|
92
|
+
* @returns Modularity score (range typically -0.5 to 1.0)
|
|
93
|
+
*/
|
|
94
|
+
export function calculateModularity(
|
|
95
|
+
graph: Graph,
|
|
96
|
+
communities: Map<NodeId, number>,
|
|
97
|
+
resolution = 1.0,
|
|
98
|
+
): number {
|
|
99
|
+
const totalEdgeWeight = getTotalEdgeWeight(graph);
|
|
100
|
+
if (totalEdgeWeight === 0) {
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let modularity = 0;
|
|
105
|
+
|
|
106
|
+
// For undirected graphs, we need to be careful not to double-count edges
|
|
107
|
+
const countedEdges = new Set<string>();
|
|
108
|
+
|
|
109
|
+
// Calculate modularity: Q = (1/2m) * Σ[A_ij - γ(k_i * k_j)/(2m)] * δ(c_i, c_j)
|
|
110
|
+
for (const nodeI of graph.nodes()) {
|
|
111
|
+
for (const nodeJ of graph.nodes()) {
|
|
112
|
+
// Skip if already counted this pair in undirected graph
|
|
113
|
+
if (!graph.isDirected) {
|
|
114
|
+
const nodeIStr = String(nodeI.id);
|
|
115
|
+
const nodeJStr = String(nodeJ.id);
|
|
116
|
+
const edgeKey = nodeIStr <= nodeJStr ? `${nodeIStr}-${nodeJStr}` : `${nodeJStr}-${nodeIStr}`;
|
|
117
|
+
if (countedEdges.has(edgeKey)) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
countedEdges.add(edgeKey);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (communities.get(nodeI.id) === communities.get(nodeJ.id)) {
|
|
125
|
+
const edge = graph.getEdge(nodeI.id, nodeJ.id);
|
|
126
|
+
const reverseEdge = !graph.isDirected ? graph.getEdge(nodeJ.id, nodeI.id) : null;
|
|
127
|
+
|
|
128
|
+
let edgeWeight = 0;
|
|
129
|
+
if (edge) {
|
|
130
|
+
edgeWeight += edge.weight ?? 1;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (reverseEdge && nodeI.id !== nodeJ.id) {
|
|
134
|
+
edgeWeight += reverseEdge.weight ?? 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const degreeI = getNodeDegree(graph, nodeI.id);
|
|
138
|
+
const degreeJ = getNodeDegree(graph, nodeJ.id);
|
|
139
|
+
|
|
140
|
+
modularity += edgeWeight - ((resolution * degreeI * degreeJ) / (2 * totalEdgeWeight));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return modularity / (2 * totalEdgeWeight);
|
|
146
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {Graph} from "../../core/graph.js";
|
|
2
2
|
import type {NodeId} from "../../types/index.js";
|
|
3
|
+
import {bfsColoringWithPartitions} from "../traversal/bfs-variants.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Maximum Bipartite Matching implementation using Hopcroft-Karp algorithm
|
|
@@ -88,53 +89,16 @@ export function maximumBipartiteMatching(
|
|
|
88
89
|
* Check if graph is bipartite and return the partition
|
|
89
90
|
*/
|
|
90
91
|
export function bipartitePartition(graph: Graph): {left: Set<NodeId>, right: Set<NodeId>} | null {
|
|
91
|
-
const
|
|
92
|
-
const left = new Set<NodeId>();
|
|
93
|
-
const right = new Set<NodeId>();
|
|
92
|
+
const result = bfsColoringWithPartitions(graph);
|
|
94
93
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
for (const startNode of nodes) {
|
|
98
|
-
if (color.has(startNode.id)) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// BFS to color the component
|
|
103
|
-
const queue = [startNode.id];
|
|
104
|
-
color.set(startNode.id, true);
|
|
105
|
-
left.add(startNode.id);
|
|
106
|
-
|
|
107
|
-
while (queue.length > 0) {
|
|
108
|
-
const u = queue.shift();
|
|
109
|
-
if (u === undefined) {
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const uColor = color.get(u);
|
|
114
|
-
if (uColor === undefined) {
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const neighbors = Array.from(graph.neighbors(u));
|
|
119
|
-
for (const v of neighbors) {
|
|
120
|
-
if (!color.has(v)) {
|
|
121
|
-
color.set(v, !uColor);
|
|
122
|
-
if (!uColor) {
|
|
123
|
-
left.add(v);
|
|
124
|
-
} else {
|
|
125
|
-
right.add(v);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
queue.push(v);
|
|
129
|
-
} else if (color.get(v) === uColor) {
|
|
130
|
-
// Same color as neighbor - not bipartite
|
|
131
|
-
return null;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
94
|
+
if (!result.isBipartite || !result.partitions) {
|
|
95
|
+
return null;
|
|
135
96
|
}
|
|
136
97
|
|
|
137
|
-
return {
|
|
98
|
+
return {
|
|
99
|
+
left: result.partitions[0],
|
|
100
|
+
right: result.partitions[1],
|
|
101
|
+
};
|
|
138
102
|
}
|
|
139
103
|
|
|
140
104
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {Graph} from "../../core/graph.js";
|
|
2
2
|
import type {NodeId, ShortestPathResult} from "../../types/index.js";
|
|
3
|
+
import {reconstructPath} from "../../utils/graph-utilities.js";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Bellman-Ford algorithm implementation for single-source shortest paths
|
|
@@ -198,17 +199,3 @@ export function hasNegativeCycle(graph: Graph): boolean {
|
|
|
198
199
|
return false;
|
|
199
200
|
}
|
|
200
201
|
|
|
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
|
-
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type {Graph} from "../../core/graph.js";
|
|
2
|
+
import {PriorityQueue} from "../../data-structures/priority-queue.js";
|
|
3
|
+
import type {NodeId, ShortestPathResult} from "../../types/index.js";
|
|
4
|
+
|
|
5
|
+
interface SearchState {
|
|
6
|
+
distances: Map<NodeId, number>;
|
|
7
|
+
previous: Map<NodeId, NodeId | null>;
|
|
8
|
+
visited: Set<NodeId>;
|
|
9
|
+
frontier: PriorityQueue<NodeId>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Bidirectional Dijkstra's algorithm implementation
|
|
14
|
+
*
|
|
15
|
+
* Performs simultaneous forward and backward searches to find shortest paths
|
|
16
|
+
* more efficiently than standard Dijkstra for point-to-point queries.
|
|
17
|
+
*
|
|
18
|
+
* Time Complexity: O(2 * sqrt(V + E) log V) expected for random graphs
|
|
19
|
+
* Space Complexity: O(V)
|
|
20
|
+
*
|
|
21
|
+
* Key optimizations:
|
|
22
|
+
* - Alternating expansion based on frontier size
|
|
23
|
+
* - Early termination when searches meet
|
|
24
|
+
* - Efficient meeting point detection
|
|
25
|
+
*/
|
|
26
|
+
export class BidirectionalDijkstra {
|
|
27
|
+
private graph: Graph;
|
|
28
|
+
private forwardSearch: SearchState;
|
|
29
|
+
private backwardSearch: SearchState;
|
|
30
|
+
private meetingNode: NodeId | null = null;
|
|
31
|
+
private shortestDistance = Infinity;
|
|
32
|
+
|
|
33
|
+
constructor(graph: Graph) {
|
|
34
|
+
this.graph = graph;
|
|
35
|
+
this.forwardSearch = this.initSearchState();
|
|
36
|
+
this.backwardSearch = this.initSearchState();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private initSearchState(): SearchState {
|
|
40
|
+
return {
|
|
41
|
+
distances: new Map(),
|
|
42
|
+
previous: new Map(),
|
|
43
|
+
visited: new Set(),
|
|
44
|
+
frontier: new PriorityQueue<NodeId>(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Find shortest path between source and target nodes
|
|
50
|
+
*/
|
|
51
|
+
public findShortestPath(source: NodeId, target: NodeId): ShortestPathResult | null {
|
|
52
|
+
// Reset state for new search
|
|
53
|
+
this.reset();
|
|
54
|
+
if (!this.graph.hasNode(source)) {
|
|
55
|
+
throw new Error(`Source node ${String(source)} not found in graph`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!this.graph.hasNode(target)) {
|
|
59
|
+
throw new Error(`Target node ${String(target)} not found in graph`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Special case: source equals target
|
|
63
|
+
if (source === target) {
|
|
64
|
+
return {
|
|
65
|
+
distance: 0,
|
|
66
|
+
path: [source],
|
|
67
|
+
predecessor: new Map([[source, null]]),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Initialize forward search from source
|
|
72
|
+
this.forwardSearch.distances.set(source, 0);
|
|
73
|
+
this.forwardSearch.previous.set(source, null);
|
|
74
|
+
this.forwardSearch.frontier.enqueue(source, 0);
|
|
75
|
+
|
|
76
|
+
// Initialize backward search from target
|
|
77
|
+
this.backwardSearch.distances.set(target, 0);
|
|
78
|
+
this.backwardSearch.previous.set(target, null);
|
|
79
|
+
this.backwardSearch.frontier.enqueue(target, 0);
|
|
80
|
+
|
|
81
|
+
// Main search loop
|
|
82
|
+
while (!this.forwardSearch.frontier.isEmpty() ||
|
|
83
|
+
!this.backwardSearch.frontier.isEmpty()) {
|
|
84
|
+
// Alternate between forward and backward search
|
|
85
|
+
// Choose the search with smaller frontier for better performance
|
|
86
|
+
let searchExpanded = false;
|
|
87
|
+
|
|
88
|
+
if (!this.forwardSearch.frontier.isEmpty() &&
|
|
89
|
+
(this.backwardSearch.frontier.isEmpty() ||
|
|
90
|
+
this.forwardSearch.frontier.size() <= this.backwardSearch.frontier.size())) {
|
|
91
|
+
if (this.expandSearch(this.forwardSearch, this.backwardSearch, true)) {
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
searchExpanded = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!this.backwardSearch.frontier.isEmpty() && !searchExpanded) {
|
|
99
|
+
if (this.expandSearch(this.backwardSearch, this.forwardSearch, false)) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Continue until both frontiers are empty or we've found an optimal path
|
|
105
|
+
if (this.forwardSearch.frontier.isEmpty() &&
|
|
106
|
+
this.backwardSearch.frontier.isEmpty()) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (this.meetingNode === null) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
distance: this.shortestDistance,
|
|
117
|
+
path: this.reconstructPath(),
|
|
118
|
+
predecessor: new Map(), // Combined from both searches
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private expandSearch(
|
|
123
|
+
search: SearchState,
|
|
124
|
+
oppositeSearch: SearchState,
|
|
125
|
+
isForward: boolean,
|
|
126
|
+
): boolean {
|
|
127
|
+
const current = search.frontier.dequeue();
|
|
128
|
+
if (current === undefined) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const distance = search.distances.get(current);
|
|
133
|
+
if (distance === undefined) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (search.visited.has(current)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
search.visited.add(current);
|
|
142
|
+
|
|
143
|
+
// Check if we've met the opposite search
|
|
144
|
+
if (oppositeSearch.distances.has(current)) {
|
|
145
|
+
const totalDistance = distance + (oppositeSearch.distances.get(current) ?? 0);
|
|
146
|
+
if (totalDistance < this.shortestDistance) {
|
|
147
|
+
this.shortestDistance = totalDistance;
|
|
148
|
+
this.meetingNode = current;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Explore neighbors
|
|
153
|
+
const neighbors = isForward ?
|
|
154
|
+
Array.from(this.graph.neighbors(current)) :
|
|
155
|
+
Array.from(this.graph.inNeighbors(current));
|
|
156
|
+
|
|
157
|
+
for (const neighbor of neighbors) {
|
|
158
|
+
if (search.visited.has(neighbor)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const edge = isForward ?
|
|
163
|
+
this.graph.getEdge(current, neighbor) :
|
|
164
|
+
this.graph.getEdge(neighbor, current);
|
|
165
|
+
|
|
166
|
+
if (!edge) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const edgeWeight = edge.weight ?? 1;
|
|
171
|
+
if (edgeWeight < 0) {
|
|
172
|
+
throw new Error("Bidirectional Dijkstra does not support negative edge weights");
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const newDistance = distance + edgeWeight;
|
|
176
|
+
const currentNeighborDistance = search.distances.get(neighbor);
|
|
177
|
+
|
|
178
|
+
if (!search.distances.has(neighbor) ||
|
|
179
|
+
newDistance < (currentNeighborDistance ?? Infinity)) {
|
|
180
|
+
search.distances.set(neighbor, newDistance);
|
|
181
|
+
search.previous.set(neighbor, current);
|
|
182
|
+
search.frontier.enqueue(neighbor, newDistance);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private getMinDistance(frontier: PriorityQueue<NodeId>): number {
|
|
190
|
+
if (frontier.isEmpty()) {
|
|
191
|
+
return Infinity;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Since we can't peek at priority directly, we estimate based on the smallest known distance
|
|
195
|
+
const minDist = Infinity;
|
|
196
|
+
|
|
197
|
+
// For a more accurate implementation, we'd need to track the minimum distance
|
|
198
|
+
// For now, we'll use a simple heuristic
|
|
199
|
+
return minDist;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private reconstructPath(): NodeId[] {
|
|
203
|
+
if (!this.meetingNode) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const path: NodeId[] = [];
|
|
208
|
+
|
|
209
|
+
// Reconstruct forward path
|
|
210
|
+
let current: NodeId | null = this.meetingNode;
|
|
211
|
+
const forwardPath: NodeId[] = [];
|
|
212
|
+
|
|
213
|
+
while (current !== null) {
|
|
214
|
+
forwardPath.push(current);
|
|
215
|
+
current = this.forwardSearch.previous.get(current) ?? null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Add forward path in reverse order (except meeting node)
|
|
219
|
+
for (let i = forwardPath.length - 1; i > 0; i--) {
|
|
220
|
+
const node = forwardPath[i];
|
|
221
|
+
if (node !== undefined) {
|
|
222
|
+
path.push(node);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Add meeting node
|
|
227
|
+
path.push(this.meetingNode);
|
|
228
|
+
|
|
229
|
+
// Reconstruct and add backward path
|
|
230
|
+
current = this.backwardSearch.previous.get(this.meetingNode) ?? null;
|
|
231
|
+
while (current !== null) {
|
|
232
|
+
path.push(current);
|
|
233
|
+
current = this.backwardSearch.previous.get(current) ?? null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return path;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Reset the search state for reuse
|
|
241
|
+
*/
|
|
242
|
+
public reset(): void {
|
|
243
|
+
this.forwardSearch = this.initSearchState();
|
|
244
|
+
this.backwardSearch = this.initSearchState();
|
|
245
|
+
this.meetingNode = null;
|
|
246
|
+
this.shortestDistance = Infinity;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type {Graph} from "../../core/graph.js";
|
|
2
2
|
import {PriorityQueue} from "../../data-structures/priority-queue.js";
|
|
3
3
|
import type {DijkstraOptions, NodeId, ShortestPathResult} from "../../types/index.js";
|
|
4
|
+
import {reconstructPath} from "../../utils/graph-utilities.js";
|
|
5
|
+
import {BidirectionalDijkstra} from "./bidirectional-dijkstra.js";
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Dijkstra's algorithm implementation for single-source shortest paths
|
|
@@ -113,12 +115,16 @@ export function dijkstra(
|
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/**
|
|
116
|
-
* Find shortest path between two specific nodes using Dijkstra's algorithm
|
|
118
|
+
* Find shortest path between two specific nodes using optimized Dijkstra's algorithm
|
|
119
|
+
*
|
|
120
|
+
* Uses bidirectional search by default for improved performance on point-to-point queries.
|
|
121
|
+
* Automatically falls back to standard Dijkstra for very small graphs or when explicitly disabled.
|
|
117
122
|
*/
|
|
118
123
|
export function dijkstraPath(
|
|
119
124
|
graph: Graph,
|
|
120
125
|
source: NodeId,
|
|
121
126
|
target: NodeId,
|
|
127
|
+
options: DijkstraOptions = {},
|
|
122
128
|
): ShortestPathResult | null {
|
|
123
129
|
if (!graph.hasNode(source)) {
|
|
124
130
|
throw new Error(`Source node ${String(source)} not found in graph`);
|
|
@@ -137,6 +143,16 @@ export function dijkstraPath(
|
|
|
137
143
|
};
|
|
138
144
|
}
|
|
139
145
|
|
|
146
|
+
// Use bidirectional search by default for point-to-point queries
|
|
147
|
+
// Only disable if explicitly requested or graph is very small (overhead dominates)
|
|
148
|
+
const useBidirectional = options.bidirectional !== false && graph.nodeCount > 10;
|
|
149
|
+
|
|
150
|
+
if (useBidirectional) {
|
|
151
|
+
const biDijkstra = new BidirectionalDijkstra(graph);
|
|
152
|
+
return biDijkstra.findShortestPath(source, target);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Fallback to standard dijkstra
|
|
140
156
|
const results = dijkstra(graph, source, {target});
|
|
141
157
|
return results.get(target) ?? null;
|
|
142
158
|
}
|
|
@@ -240,17 +256,3 @@ export function allPairsShortestPath(graph: Graph): Map<NodeId, Map<NodeId, numb
|
|
|
240
256
|
return results;
|
|
241
257
|
}
|
|
242
258
|
|
|
243
|
-
/**
|
|
244
|
-
* Reconstruct path from predecessor map
|
|
245
|
-
*/
|
|
246
|
-
function reconstructPath(target: NodeId, previous: Map<NodeId, NodeId | null>): NodeId[] {
|
|
247
|
-
const path: NodeId[] = [];
|
|
248
|
-
let current: NodeId | null = target;
|
|
249
|
-
|
|
250
|
-
while (current !== null) {
|
|
251
|
-
path.unshift(current);
|
|
252
|
-
current = previous.get(current) ?? null;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return path;
|
|
256
|
-
}
|