@@ -843,6 +843,29 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
843843 }
844844}
845845
846+ /** @internal */
847+ const rebuildAdjacency = < N , E , T extends Kind = "directed" > (
848+ mutable : MutableGraph < N , E , T >
849+ ) : void => {
850+ mutable . adjacency . clear ( )
851+ mutable . reverseAdjacency . clear ( )
852+
853+ for ( const nodeIndex of mutable . nodes . keys ( ) ) {
854+ mutable . adjacency . set ( nodeIndex , [ ] )
855+ mutable . reverseAdjacency . set ( nodeIndex , [ ] )
856+ }
857+
858+ for ( const [ edgeIndex , edgeData ] of mutable . edges ) {
859+ mutable . adjacency . get ( edgeData . source ) ! . push ( edgeIndex )
860+ mutable . reverseAdjacency . get ( edgeData . target ) ! . push ( edgeIndex )
861+
862+ if ( mutable . type === "undirected" ) {
863+ mutable . adjacency . get ( edgeData . target ) ! . push ( edgeIndex )
864+ mutable . reverseAdjacency . get ( edgeData . source ) ! . push ( edgeIndex )
865+ }
866+ }
867+ }
868+
846869/**
847870 * Reverses all edge directions in a mutable graph by swapping source and target nodes.
848871 *
@@ -869,6 +892,10 @@ export const mapEdges = <N, E, T extends Kind = "directed">(
869892export const reverse = < N , E , T extends Kind = "directed" > (
870893 mutable : MutableGraph < N , E , T >
871894) : void => {
895+ if ( mutable . type === "undirected" ) {
896+ return
897+ }
898+
872899 // Reverse all edges by swapping source and target
873900 for ( const [ index , edgeData ] of mutable . edges ) {
874901 mutable . edges . set ( index , {
@@ -878,22 +905,7 @@ export const reverse = <N, E, T extends Kind = "directed">(
878905 } )
879906 }
880907
881- // Clear and rebuild adjacency lists with reversed directions
882- mutable . adjacency . clear ( )
883- mutable . reverseAdjacency . clear ( )
884-
885- // Rebuild adjacency lists with reversed directions
886- for ( const [ edgeIndex , edgeData ] of mutable . edges ) {
887- // Add to forward adjacency (source -> target)
888- const sourceEdges = mutable . adjacency . get ( edgeData . source ) || [ ]
889- sourceEdges . push ( edgeIndex )
890- mutable . adjacency . set ( edgeData . source , sourceEdges )
891-
892- // Add to reverse adjacency (target <- source)
893- const targetEdges = mutable . reverseAdjacency . get ( edgeData . target ) || [ ]
894- targetEdges . push ( edgeIndex )
895- mutable . reverseAdjacency . set ( edgeData . target , targetEdges )
896- }
908+ rebuildAdjacency ( mutable )
897909
898910 // Invalidate cycle flag since edge directions changed
899911 mutable . isAcyclic = Option . none ( )
@@ -1423,8 +1435,11 @@ export const hasEdge = <N, E, T extends Kind = "directed">(
14231435 // Check if any edge in the adjacency list connects to the target
14241436 for ( const edgeIndex of adjacencyList ) {
14251437 const edge = graph . edges . get ( edgeIndex )
1426- if ( edge !== undefined && edge . target === target ) {
1427- return true
1438+ if ( edge !== undefined ) {
1439+ const neighbor = graph . type === "undirected" && edge . target === source ? edge . source : edge . target
1440+ if ( neighbor === target ) {
1441+ return true
1442+ }
14281443 }
14291444 }
14301445
@@ -1460,6 +1475,31 @@ export const edgeCount = <N, E, T extends Kind = "directed">(
14601475 graph : Graph < N , E , T > | MutableGraph < N , E , T >
14611476) : number => graph . edges . size
14621477
1478+ const getDirectedNeighbors = < N , E > (
1479+ graph : Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > ,
1480+ nodeIndex : NodeIndex ,
1481+ direction : Direction
1482+ ) : Array < NodeIndex > => {
1483+ const adjacencyMap = direction === "incoming"
1484+ ? graph . reverseAdjacency
1485+ : graph . adjacency
1486+
1487+ const adjacencyList = adjacencyMap . get ( nodeIndex )
1488+ if ( adjacencyList === undefined ) {
1489+ return [ ]
1490+ }
1491+
1492+ const result : Array < NodeIndex > = [ ]
1493+ for ( const edgeIndex of adjacencyList ) {
1494+ const edge = graph . edges . get ( edgeIndex )
1495+ if ( edge !== undefined ) {
1496+ result . push ( direction === "incoming" ? edge . source : edge . target )
1497+ }
1498+ }
1499+
1500+ return result
1501+ }
1502+
14631503/**
14641504 * Returns the neighboring nodes (targets of outgoing edges) for a given node.
14651505 *
@@ -1498,24 +1538,47 @@ export const neighbors = <N, E, T extends Kind = "directed">(
14981538 return getUndirectedNeighbors ( graph as any , nodeIndex )
14991539 }
15001540
1501- const adjacencyList = graph . adjacency . get ( nodeIndex )
1502- if ( adjacencyList === undefined ) {
1503- return [ ]
1504- }
1541+ return getDirectedNeighbors ( graph as Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > , nodeIndex , "outgoing" )
1542+ }
15051543
1506- const result : Array < NodeIndex > = [ ]
1507- for ( const edgeIndex of adjacencyList ) {
1508- const edge = graph . edges . get ( edgeIndex )
1509- if ( edge !== undefined ) {
1510- result . push ( edge . target )
1511- }
1544+ /**
1545+ * Returns the outgoing neighbor node indices for a node in a directed graph.
1546+ *
1547+ * Throws a `GraphError` when used with an undirected graph.
1548+ *
1549+ * @since 3.22.0
1550+ * @category queries
1551+ */
1552+ export const successors = < N , E > (
1553+ graph : Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > ,
1554+ nodeIndex : NodeIndex
1555+ ) : Array < NodeIndex > => {
1556+ if ( ( graph as Graph < N , E , Kind > | MutableGraph < N , E , Kind > ) . type === "undirected" ) {
1557+ throw new GraphError ( { message : "Cannot get successors of undirected graph" } )
15121558 }
1559+ return getDirectedNeighbors ( graph , nodeIndex , "outgoing" )
1560+ }
15131561
1514- return result
1562+ /**
1563+ * Returns the incoming neighbor node indices for a node in a directed graph.
1564+ *
1565+ * Throws a `GraphError` when used with an undirected graph.
1566+ *
1567+ * @since 3.22.0
1568+ * @category queries
1569+ */
1570+ export const predecessors = < N , E > (
1571+ graph : Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > ,
1572+ nodeIndex : NodeIndex
1573+ ) : Array < NodeIndex > => {
1574+ if ( ( graph as Graph < N , E , Kind > | MutableGraph < N , E , Kind > ) . type === "undirected" ) {
1575+ throw new GraphError ( { message : "Cannot get predecessors of undirected graph" } )
1576+ }
1577+ return getDirectedNeighbors ( graph , nodeIndex , "incoming" )
15151578}
15161579
15171580/**
1518- * Get neighbors of a node in a specific direction for bidirectional traversal .
1581+ * Get directed neighbors of a node in a specific direction.
15191582 *
15201583 * @example
15211584 * ```ts
@@ -1537,36 +1600,19 @@ export const neighbors = <N, E, T extends Kind = "directed">(
15371600 * const incoming = Graph.neighborsDirected(graph, nodeB, "incoming")
15381601 * ```
15391602 *
1603+ * @deprecated Use {@link successors} for outgoing neighbors or {@link predecessors} for incoming neighbors.
15401604 * @since 3.18.0
15411605 * @category queries
15421606 */
1543- export const neighborsDirected = < N , E , T extends Kind = "directed" > (
1544- graph : Graph < N , E , T > | MutableGraph < N , E , T > ,
1607+ export const neighborsDirected = < N , E > (
1608+ graph : Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > ,
15451609 nodeIndex : NodeIndex ,
15461610 direction : Direction
15471611) : Array < NodeIndex > => {
1548- const adjacencyMap = direction === "incoming"
1549- ? graph . reverseAdjacency
1550- : graph . adjacency
1551-
1552- const adjacencyList = adjacencyMap . get ( nodeIndex )
1553- if ( adjacencyList === undefined ) {
1554- return [ ]
1555- }
1556-
1557- const result : Array < NodeIndex > = [ ]
1558- for ( const edgeIndex of adjacencyList ) {
1559- const edge = graph . edges . get ( edgeIndex )
1560- if ( edge !== undefined ) {
1561- // For incoming direction, we want the source node instead of target
1562- const neighborNode = direction === "incoming"
1563- ? edge . source
1564- : edge . target
1565- result . push ( neighborNode )
1566- }
1612+ if ( ( graph as Graph < N , E , Kind > | MutableGraph < N , E , Kind > ) . type === "undirected" ) {
1613+ throw new GraphError ( { message : "Cannot get directed neighbors of undirected graph" } )
15671614 }
1568-
1569- return result
1615+ return getDirectedNeighbors ( graph , nodeIndex , direction )
15701616}
15711617
15721618// =============================================================================
@@ -1961,7 +2007,11 @@ export const isAcyclic = <N, E, T extends Kind = "directed">(
19612007 recursionStack . add ( node )
19622008
19632009 // Get neighbors for this node
1964- const nodeNeighbors = Array . from ( neighborsDirected ( graph , node , "outgoing" ) )
2010+ const nodeNeighbors = getDirectedNeighbors (
2011+ graph as Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > ,
2012+ node ,
2013+ "outgoing"
2014+ )
19652015 stack [ stack . length - 1 ] = [ node , nodeNeighbors , 0 , false ]
19662016 continue
19672017 }
@@ -2112,7 +2162,7 @@ const getTraversalNeighbors = <N, E, T extends Kind>(
21122162) : Array < NodeIndex > =>
21132163 graph . type === "undirected"
21142164 ? getUndirectedNeighbors ( graph as any , nodeIndex )
2115- : neighborsDirected ( graph , nodeIndex , direction )
2165+ : getDirectedNeighbors ( graph as Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > , nodeIndex , direction )
21162166
21172167const getTraversableNeighbor = < N , E , T extends Kind > (
21182168 graph : Graph < N , E , T > | MutableGraph < N , E , T > ,
@@ -2202,9 +2252,13 @@ export const connectedComponents = <N, E>(
22022252 * @since 3.18.0
22032253 * @category algorithms
22042254 */
2205- export const stronglyConnectedComponents = < N , E , T extends Kind = "directed" > (
2206- graph : Graph < N , E , T > | MutableGraph < N , E , T >
2255+ export const stronglyConnectedComponents = < N , E > (
2256+ graph : Graph < N , E , "directed" > | MutableGraph < N , E , "directed" >
22072257) : Array < Array < NodeIndex > > => {
2258+ if ( ( graph as Graph < N , E , Kind > | MutableGraph < N , E , Kind > ) . type === "undirected" ) {
2259+ throw new GraphError ( { message : "Cannot find strongly connected components of undirected graph" } )
2260+ }
2261+
22082262 const visited = new Set < NodeIndex > ( )
22092263 const finishOrder : Array < NodeIndex > = [ ]
22102264 // Iterate directly over node keys
@@ -2230,7 +2284,7 @@ export const stronglyConnectedComponents = <N, E, T extends Kind = "directed">(
22302284 }
22312285
22322286 visited . add ( node )
2233- const nodeNeighborsList = neighbors ( graph , node )
2287+ const nodeNeighborsList = getDirectedNeighbors ( graph , node , "outgoing" )
22342288 stack [ stack . length - 1 ] = [ node , nodeNeighborsList , 0 , false ]
22352289 continue
22362290 }
@@ -2348,6 +2402,22 @@ export interface BellmanFordConfig<E> {
23482402 cost : ( edgeData : E ) => number
23492403}
23502404
2405+ const validateNonNegativeEdgeWeights = < N , E , T extends Kind = "directed" > (
2406+ graph : Graph < N , E , T > | MutableGraph < N , E , T > ,
2407+ cost : ( edgeData : E ) => number ,
2408+ algorithm : string
2409+ ) : Map < EdgeIndex , number > => {
2410+ const edgeWeights = new Map < EdgeIndex , number > ( )
2411+ for ( const [ edgeIndex , edgeData ] of graph . edges ) {
2412+ const weight = cost ( edgeData . data )
2413+ if ( weight < 0 || Number . isNaN ( weight ) ) {
2414+ throw new GraphError ( { message : `${ algorithm } requires non-negative edge weights` } )
2415+ }
2416+ edgeWeights . set ( edgeIndex , weight )
2417+ }
2418+ return edgeWeights
2419+ }
2420+
23512421/**
23522422 * Find the shortest path between two nodes using Dijkstra's algorithm.
23532423 *
@@ -2390,6 +2460,8 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
23902460 throw missingNode ( target )
23912461 }
23922462
2463+ const edgeWeights = validateNonNegativeEdgeWeights ( graph , cost , "Dijkstra's algorithm" )
2464+
23932465 // Early return if source equals target
23942466 if ( source === target ) {
23952467 return Option . some ( {
@@ -2450,12 +2522,7 @@ export const dijkstra = <N, E, T extends Kind = "directed">(
24502522 const edge = graph . edges . get ( edgeIndex )
24512523 if ( edge !== undefined ) {
24522524 const neighbor = getTraversableNeighbor ( graph , currentNode , edge )
2453- const weight = cost ( edge . data )
2454-
2455- // Validate non-negative weights
2456- if ( weight < 0 ) {
2457- throw new Error ( `Dijkstra's algorithm requires non-negative edge weights, found ${ weight } ` )
2458- }
2525+ const weight = edgeWeights . get ( edgeIndex ) !
24592526
24602527 const newDistance = currentDistance + weight
24612528 const neighborDistance = distances . get ( neighbor ) !
@@ -2709,6 +2776,8 @@ export const astar = <N, E, T extends Kind = "directed">(
27092776 throw missingNode ( target )
27102777 }
27112778
2779+ const edgeWeights = validateNonNegativeEdgeWeights ( graph , cost , "A* algorithm" )
2780+
27122781 // Early return if source equals target
27132782 if ( source === target ) {
27142783 return Option . some ( {
@@ -2784,12 +2853,7 @@ export const astar = <N, E, T extends Kind = "directed">(
27842853 const edge = graph . edges . get ( edgeIndex )
27852854 if ( edge !== undefined ) {
27862855 const neighbor = getTraversableNeighbor ( graph , currentNode , edge )
2787- const weight = cost ( edge . data )
2788-
2789- // Validate non-negative weights
2790- if ( weight < 0 ) {
2791- throw new Error ( `A* algorithm requires non-negative edge weights, found ${ weight } ` )
2792- }
2856+ const weight = edgeWeights . get ( edgeIndex ) !
27932857
27942858 const tentativeGScore = currentGScore + weight
27952859 const neighborGScore = gScore . get ( neighbor ) !
@@ -3468,6 +3532,7 @@ export const topo = <N, E, T extends Kind = "directed">(
34683532 [ Symbol . iterator ] : ( ) => {
34693533 const inDegree = new Map < NodeIndex , number > ( )
34703534 const remaining = new Set < NodeIndex > ( )
3535+ const initialSet = new Set ( initials )
34713536 const queue = [ ...initials ]
34723537
34733538 // Initialize in-degree counts
@@ -3482,12 +3547,15 @@ export const topo = <N, E, T extends Kind = "directed">(
34823547 inDegree . set ( edgeData . target , currentInDegree + 1 )
34833548 }
34843549
3485- // Add nodes with zero in-degree to queue if no initials provided
3486- if ( initials . length === 0 ) {
3487- for ( const [ nodeIndex , degree ] of inDegree ) {
3488- if ( degree === 0 ) {
3489- queue . push ( nodeIndex )
3490- }
3550+ for ( const nodeIndex of initials ) {
3551+ if ( inDegree . get ( nodeIndex ) ! !== 0 ) {
3552+ throw new GraphError ( { message : `Initial node ${ nodeIndex } has incoming edges` } )
3553+ }
3554+ }
3555+
3556+ for ( const [ nodeIndex , degree ] of inDegree ) {
3557+ if ( degree === 0 && ! initialSet . has ( nodeIndex ) ) {
3558+ queue . push ( nodeIndex )
34913559 }
34923560 }
34933561
@@ -3499,7 +3567,11 @@ export const topo = <N, E, T extends Kind = "directed">(
34993567 remaining . delete ( current )
35003568
35013569 // Process outgoing edges, reducing in-degree of targets
3502- const neighbors = neighborsDirected ( graph , current , "outgoing" )
3570+ const neighbors = getDirectedNeighbors (
3571+ graph as Graph < N , E , "directed" > | MutableGraph < N , E , "directed" > ,
3572+ current ,
3573+ "outgoing"
3574+ )
35033575 for ( const neighbor of neighbors ) {
35043576 if ( remaining . has ( neighbor ) ) {
35053577 const currentInDegree = inDegree . get ( neighbor ) || 0
0 commit comments