Documentation
¶
Overview ¶
Package search provides graph traversal and path-finding algorithms over the immutable github.com/FlavioCFOliveira/GoGraph/graph/csr.CSR read-only view.
All algorithms operate in graph.NodeID space; callers wishing to work with the user-facing N values resolve them via the originating graph.Mapper. This keeps the hot path allocation-free and free of interface dispatch in the inner loops.
Concurrency ¶
The algorithms are read-only over their CSR input and are safe to invoke concurrently with each other on the same CSR. They are also safe to invoke concurrently with each other across different CSRs. They do not, on their own, hold any mutable state outside the local stack frame.
Allocation and pooling ¶
Working buffers (frontier queue, recursion stack, visited bitset) are pooled across calls via sync.Pool, so a steady-state workload that traverses graphs of similar size reaches zero heap allocations per call after warm-up.
Index ¶
- Variables
- func AStar[W Weight](c *csr.CSR[W], src, dst graph.NodeID, h func(graph.NodeID) W) ([]graph.NodeID, W, error)
- func AStarCtx[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, ...) ([]graph.NodeID, W, error)
- func AStarInto[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, ...) (W, error)
- func BFS[W any](c *csr.CSR[W], src graph.NodeID, visit func(node graph.NodeID, depth int) bool)
- func BFSCtx[W any](ctx context.Context, c *csr.CSR[W], src graph.NodeID, ...) error
- func BFSDirectionOpt[W any](c *csr.CSR[W], src graph.NodeID, visit func(node graph.NodeID, depth int) bool)
- func BellmanFordInto[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID, dist []W, ...) error
- func BiBFS[W any](c *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, error)
- func BiBFSCtx[W any](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, error)
- func BiBFSOn[W any](c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, error)
- func BiBFSOnCtx[W any](ctx context.Context, c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, error)
- func BidirectionalDijkstra[W Weight](c *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, W, error)
- func BidirectionalDijkstraOn[W Weight](c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, W, error)
- func BidirectionalDijkstraOnCtx[W Weight](ctx context.Context, c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, W, error)
- func CountTriangles[W any](c *csr.CSR[W]) (total int64, perNode []int64)
- func CountTrianglesCtx[W any](ctx context.Context, c *csr.CSR[W]) (total int64, perNode []int64, err error)
- func DFS[W any](c *csr.CSR[W], src graph.NodeID, visit func(node graph.NodeID, depth int) bool)
- func DFSCtx[W any](ctx context.Context, c *csr.CSR[W], src graph.NodeID, ...) error
- func Diameter[W any](c *csr.CSR[W]) (lo, hi int, exact bool)
- func DiameterCtx[W any](ctx context.Context, c *csr.CSR[W]) (lo, hi int, exact bool, err error)
- func DijkstraInto[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID, dist []W, ...) error
- func Hierholzer[W any](c *csr.CSR[W]) ([]graph.NodeID, error)
- func HierholzerCtx[W any](ctx context.Context, c *csr.CSR[W]) ([]graph.NodeID, error)
- func HierholzerUndirected[W any](c *csr.CSR[W]) ([]graph.NodeID, error)
- func HierholzerUndirectedCtx[W any](ctx context.Context, c *csr.CSR[W]) ([]graph.NodeID, error)
- func KCore[W any](c *csr.CSR[W]) []int
- func KCoreCtx[W any](ctx context.Context, c *csr.CSR[W]) ([]int, error)
- func PrimMST[W Weight](c *csr.CSR[W], src graph.NodeID) (parent []graph.NodeID, found []bool, totalWeight W, err error)
- func PrimMSTCtx[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID) (parent []graph.NodeID, found []bool, totalWeight W, err error)
- func TarjanSCC[W any](c *csr.CSR[W]) [][]graph.NodeID
- func TarjanSCCCtx[W any](ctx context.Context, c *csr.CSR[W]) ([][]graph.NodeID, error)
- func TopologicalSort[W any](c *csr.CSR[W]) ([]graph.NodeID, error)
- func TopologicalSortCtx[W any](ctx context.Context, c *csr.CSR[W]) ([]graph.NodeID, error)
- func WCC[W any](c *csr.CSR[W]) (component []int, k int, err error)
- func WCCCtx[W any](ctx context.Context, c *csr.CSR[W]) (component []int, k int, err error)
- type APSP
- func DijkstraAPSP[W Weight](c *csr.CSR[W]) (*APSP[W], error)
- func DijkstraAPSPCtx[W Weight](ctx context.Context, c *csr.CSR[W]) (*APSP[W], error)
- func FloydWarshall[W Weight](c *csr.CSR[W]) *APSP[W]
- func FloydWarshallCtx[W Weight](ctx context.Context, c *csr.CSR[W]) (*APSP[W], error)
- func JohnsonAPSP[W Weight](c *csr.CSR[W]) (*APSP[W], error)
- func JohnsonAPSPCtx[W Weight](ctx context.Context, c *csr.CSR[W]) (*APSP[W], error)
- type Assignment
- type BCCResult
- type Distances
- func BellmanFord[W Weight](c *csr.CSR[W], src graph.NodeID) (*Distances[W], error)
- func BellmanFordCtx[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID) (*Distances[W], error)
- func Dijkstra[W Weight](c *csr.CSR[W], src graph.NodeID) (*Distances[W], error)
- func DijkstraCtx[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID) (*Distances[W], error)
- type MSTEdge
- type Matching
- type TC
- type Weight
- type YenPath
- func EppsteinKShortest[W Weight](c *csr.CSR[W], src, dst graph.NodeID, k int) []YenPath[W]deprecated
- func EppsteinKShortestCtx[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, k int) ([]YenPath[W], error)deprecated
- func KShortestPathsLoopless[W Weight](c *csr.CSR[W], src, dst graph.NodeID, k int) []YenPath[W]
- func KShortestPathsLooplessCtx[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, k int) ([]YenPath[W], error)
- func YenKShortest[W Weight](c *csr.CSR[W], src, dst graph.NodeID, k int) []YenPath[W]
- func YenKShortestCtx[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, k int) ([]YenPath[W], error)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrBufferTooSmall = errors.New("search: caller-provided buffer is too small")
ErrBufferTooSmall is returned by the *Into variants when any of the caller-provided scratch slices is shorter than c.MaxNodeID().
var ErrCycle = errors.New("search: cycle detected in directed graph")
ErrCycle is returned by algorithms that require a directed acyclic graph when the input contains a cycle.
var ErrInvalidInput = errors.New("search: input contains NaN or Inf")
ErrInvalidInput is returned by algorithms that detect NaN or +/-Inf in float-valued input arrays. The sentinel is shared across the search and centrality packages so callers can errors.Is against it uniformly.
var ErrNegativeCycle = errors.New("search: negative cycle reachable from source")
ErrNegativeCycle is returned by BellmanFord when the input graph contains a negative cycle reachable from the source. In that case shortest-path distances are not defined.
var ErrNegativeEdgeAPSP = errors.New("search: DijkstraAPSP requires non-negative edge weights")
ErrNegativeEdgeAPSP is returned by DijkstraAPSP when the input CSR contains a strictly-negative edge weight. DijkstraAPSP does not reweight negative edges; callers with mixed-sign weights and no negative cycles should use JohnsonAPSP (which reweights via Bellman-Ford) or FloydWarshall.
var ErrNegativeWeight = errors.New("search: negative edge weight")
ErrNegativeWeight is returned by algorithms that require non-negative edge weights (such as Dijkstra) when the input graph contains an edge with weight strictly less than the zero value of W.
var ErrNoEulerian = errors.New("search: graph has no Eulerian circuit or path")
ErrNoEulerian is returned by Hierholzer when c does not admit an Eulerian circuit or path.
var ErrNoPath = errors.New("search: no path between endpoints")
ErrNoPath is returned by point-to-point search algorithms when no path exists between the requested endpoints.
var ErrNotUndirected = errors.New("search: BiBFS requires an undirected (symmetric) CSR")
ErrNotUndirected was returned by older versions of BiBFS when the supplied CSR was not symmetric. As of Sprint 12 BiBFS now auto-builds the reverse CSR for directed inputs and the error is never produced; the sentinel is kept for backwards compatibility with callers using errors.Is to detect the legacy condition.
Deprecated: BiBFS no longer requires a symmetric CSR.
Functions ¶
func AStar ¶
func AStar[W Weight]( c *csr.CSR[W], src, dst graph.NodeID, h func(graph.NodeID) W, ) ([]graph.NodeID, W, error)
AStar computes a shortest path from src to dst on c, guided by the caller-supplied heuristic h. h must be admissible (h(n) is a lower bound on the true cost from n to dst) for optimality; if h is also consistent (h(u) <= w(u,v) + h(v) for every edge u->v) the search never re-expands a node. Admissibility and consistency are caller contracts; AStar does not validate them.
Returns the path from src to dst (inclusive) with the corresponding total cost, or ErrNoPath when dst is unreachable. Returns ErrNegativeWeight if any edge weight in c is strictly negative. For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass.
For hot loops, prefer the zero-allocation primitive AStarInto.
Example ¶
ExampleAStar finds an optimal path on a weighted graph. With a zero heuristic (trivially admissible) A* degenerates to Dijkstra and still returns the least-cost path and its total cost.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildWeighted assembles a CSR over int64-weighted edges and returns
// it together with the mapper. directed selects a directed graph; when
// false the AdjList mirrors every edge, yielding the symmetric CSR the
// undirected algorithms (Kruskal, Prim) expect.
func buildWeighted(directed bool, edges [][3]int) (*csr.CSR[int64], *graph.Mapper[int]) {
a := adjlist.New[int, int64](adjlist.Config{Directed: directed})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], int64(e[2]))
}
return csr.BuildFromAdjList(a), a.Mapper()
}
// resolvePath maps a path expressed in NodeID space back to the
// user-facing int values, so output is stable regardless of the
// opaque NodeIDs the mapper assigns.
func resolvePath(m *graph.Mapper[int], path []graph.NodeID) []int {
out := make([]int, len(path))
for i, id := range path {
out[i], _ = m.Resolve(id)
}
return out
}
func main() {
c, m := buildWeighted(true, [][3]int{
{0, 1, 10}, {0, 2, 3},
{1, 3, 1},
{2, 1, 4}, {2, 3, 8},
})
src, _ := m.Lookup(0)
dst, _ := m.Lookup(3)
// Zero heuristic: never overestimates the remaining cost.
path, cost, err := search.AStar(c, src, dst, func(graph.NodeID) int64 { return 0 })
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("path %v cost %d\n", resolvePath(m, path), cost)
}
Output: path [0 2 1 3] cost 8
func AStarCtx ¶
func AStarCtx[W Weight]( ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, h func(graph.NodeID) W, ) ([]graph.NodeID, W, error)
AStarCtx is the context-aware variant of AStar. ctx.Err() is checked every 4096 heap pops; on cancellation returns (nil, zero, wrapped ctx.Err()).
func AStarInto ¶
func AStarInto[W Weight]( ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, h func(graph.NodeID) W, dist []W, parent []graph.NodeID, found []bool, path *[]graph.NodeID, ) (W, error)
AStarInto is the zero-allocation primitive behind AStar. The caller provides the dist, parent, and found scratch slices (each at least c.MaxNodeID()) and a path destination slice that is truncated and appended to. The returned cost matches AStar.
If any scratch slice is too small, ErrBufferTooSmall is returned.
Concurrency: the caller's buffers are written in-place; concurrent callers must supply separate buffers.
func BFS ¶
BFS performs breadth-first traversal of c starting at src. The visit callback is invoked for every reached node in non-decreasing depth order, with depth starting at 0 for src; returning false from visit terminates the traversal early. The traversal visits each node at most once.
BFS is allocation-free on the hot path after the first call (working buffers are reused via sync.Pool). Memory consumption per call is bounded by O(maxFrontierWidth) instead of O(reachableSize) thanks to the wavefront design: the current level's nodes are consumed and the next level is built in a separate buffer; the two buffers swap per level.
Example ¶
ExampleBFS traverses an unweighted chain breadth-first and reports the depth at which each node is discovered.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildDirected assembles a directed, unweighted CSR from (src, dst)
// integer pairs and returns it together with the mapper that resolves
// NodeIDs back to the user-facing int values.
func buildDirected(edges [][2]int) (*csr.CSR[struct{}], *graph.Mapper[int]) {
a := adjlist.New[int, struct{}](adjlist.Config{Directed: true})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], struct{}{})
}
return csr.BuildFromAdjList(a), a.Mapper()
}
func main() {
// Chain: 0 -> 1 -> 2 -> 3.
c, m := buildDirected([][2]int{{0, 1}, {1, 2}, {2, 3}})
src, _ := m.Lookup(0)
depth := map[int]int{}
search.BFS(c, src, func(node graph.NodeID, d int) bool {
v, _ := m.Resolve(node)
depth[v] = d
return true // keep traversing
})
for v := 0; v < 4; v++ {
fmt.Printf("node %d at depth %d\n", v, depth[v])
}
}
Output: node 0 at depth 0 node 1 at depth 1 node 2 at depth 2 node 3 at depth 3
func BFSCtx ¶
func BFSCtx[W any](ctx context.Context, c *csr.CSR[W], src graph.NodeID, visit func(node graph.NodeID, depth int) bool) error
BFSCtx is the context-aware variant of BFS. ctx.Err() is checked once per BFS level; on cancellation the traversal returns the wrapped ctx.Err() without invoking visit on the partially-explored frontier.
func BFSDirectionOpt ¶
func BFSDirectionOpt[W any](c *csr.CSR[W], src graph.NodeID, visit func(node graph.NodeID, depth int) bool)
BFSDirectionOpt performs direction-optimising breadth-first search (Beamer, Asanovic, Patterson — SC 2012) over the symmetric adjacency captured by c. The algorithm dynamically switches between top-down (push, expand the current frontier forward) and bottom-up (pull, scan unvisited nodes and check whether any of their neighbours are in the frontier) phases based on the alpha threshold from the paper, giving 3-7x speedups on power-law graphs at the cost of one extra full scan per direction switch.
The implementation expects c to be symmetric (typical for undirected graphs built with [adjlist.Config.Directed]=false). For a directed graph callers should pre-build a symmetric CSR containing both edges and their reverses; the v1 algorithm does not maintain a separate in-edge CSR.
Memory: visited and frontier bitmaps and the cur/next list slices are acquired from a pool; in the steady state BFSDirectionOpt is zero-allocation per call.
func BellmanFordInto ¶
func BellmanFordInto[W Weight]( ctx context.Context, c *csr.CSR[W], src graph.NodeID, dist []W, parent []graph.NodeID, found []bool, ) error
BellmanFordInto is the zero-allocation primitive behind BellmanFord. It writes single-source shortest-path results directly into the caller-provided slices, each of which must have length at least c.MaxNodeID(); otherwise it returns ErrBufferTooSmall. The slices are reset in-place before the traversal.
Concurrency: the caller's slices are written in-place; concurrent callers must supply separate buffers.
func BiBFS ¶
BiBFS performs bidirectional breadth-first search from src to dst on the unweighted graph captured by c. For undirected graphs (a symmetric directed CSR) the same CSR feeds both the forward and reverse expansion; for directed graphs c must be paired with the reverse CSR via BiBFSOn.
Returns the shortest path from src to dst inclusive, or ErrNoPath when the two endpoints are not connected. The returned path length always equals the true unweighted shortest distance: BiBFS completes the current level expansion on the chosen side and picks the global minimum-total collision before terminating, so it never returns a strictly longer path because of neighbour iteration order.
The two frontiers expand alternately; the iteration always grows the smaller frontier next so the search space approximates O(b^(d/2)) instead of O(b^d) for forward-only BFS, where b is the branching factor and d is the path length.
func BiBFSCtx ¶
func BiBFSCtx[W any](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, error)
BiBFSCtx is the context-aware variant of BiBFS. ctx.Err() is checked at every alternation between the forward and backward frontier expansion; on cancellation returns (nil, wrapped ctx.Err()).
On undirected (symmetric) input it walks c in both directions; on directed input it builds the reverse CSR once and delegates to BiBFSOnCtx. The internal reverse build is O(V + E); callers running BiBFS many times on the same graph should hoist the build out via BiBFSOnCtx. The level-complete intersection rule documented on BiBFS is preserved.
func BiBFSOn ¶
BiBFSOn is BiBFS with a caller-provided reverse CSR. Required for directed graphs (where the symmetric closure differs from c) and recommended for any high-frequency caller — the O(V+E) reverse-CSR construction is hoisted out of the inner loop. The level-complete intersection rule documented on BiBFS is preserved.
func BiBFSOnCtx ¶
func BiBFSOnCtx[W any](ctx context.Context, c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, error)
BiBFSOnCtx is the context-aware variant of BiBFSOn. The level-complete intersection rule documented on BiBFS is preserved: after the first cross-frontier collision the loop performs at most one additional expansion on the opposite side and then commits to the global-minimum (forwardDepth + backwardDepth) meet recorded across both expansions.
func BidirectionalDijkstra ¶
func BidirectionalDijkstra[W Weight](c *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, W, error)
BidirectionalDijkstra computes a shortest path from src to dst in c using bidirectional Dijkstra: a forward search from src on the original CSR and a simultaneous reverse search from dst on the transposed CSR (csr.CSR.BuildReverse). Both searches relax non- negative edges with the same heap invariant as Dijkstra; the algorithm terminates when the minimum-key sum across both heaps reaches or exceeds the best meeting-edge cost seen so far.
For random-access graphs the asymptotic gain over one-way Dijkstra is roughly sqrt of the unidirectional ball, with concrete >2x speedups on road-network workloads (deep, low-degree shortest paths).
The reverse CSR is rebuilt internally; callers that issue many point-to-point queries on the same graph should hoist the build out of the hot loop with BidirectionalDijkstraOn.
Returns the path (inclusive of src and dst) and its total cost, ErrNoPath when no s-t path exists, or ErrNegativeWeight if c contains any negative-weight edge. For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass.
func BidirectionalDijkstraOn ¶
func BidirectionalDijkstraOn[W Weight](c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, W, error)
BidirectionalDijkstraOn is BidirectionalDijkstra with a pre- built reverse CSR. Use this when running many point-to-point queries on the same graph; the reverse-CSR construction is O(V+E) and is the only allocation outside the algorithm's working state.
func BidirectionalDijkstraOnCtx ¶
func BidirectionalDijkstraOnCtx[W Weight](ctx context.Context, c, rev *csr.CSR[W], src, dst graph.NodeID) ([]graph.NodeID, W, error)
BidirectionalDijkstraOnCtx is the context-aware variant of BidirectionalDijkstraOn. ctx.Err() is checked every 4096 heap pops; on cancellation returns (nil, zero, wrapped ctx.Err()).
func CountTriangles ¶
CountTriangles returns the total number of triangles in the undirected graph c, plus a per-NodeID count of how many triangles each vertex participates in. c is expected to be a symmetric directed CSR (each undirected edge appears as both (u, v) and (v, u)) — typical of [adjlist.AdjList] with Directed=false.
Algorithm ¶
The implementation is the node-iterator algorithm with degree ordering: at every vertex v we examine pairs of neighbours (u, w) only when both have a strictly higher canonical rank than v. Canonical rank breaks ties on raw NodeID, so each triangle is counted exactly once.
Complexity ¶
Time: O(sum over v of deg(v)^2 in the degree-ordered subgraph),
bounded by O(E * sqrt(E)) per Chiba & Nishizeki 1985. For uniform-degree graphs this collapses to O(E * d_avg); for power-law graphs the sqrt(E) bound is tight.
Space: O(V) for the per-node count array plus O(d_max) scratch
for the per-vertex neighbour scan (reused across vertices).
Streaming variant ¶
CountTriangles requires the full CSR in memory: the inner loop random-accesses neighbour lists of arbitrary vertices. A streaming variant for graphs that do not fit in RAM is not provided — the implementation cost is non-trivial (reservoir sampling à la TRIEST, or a wedge-based estimator) and no production caller has asked for it. If such a caller surfaces, the natural seam is in search/extern/triangles.go alongside the existing semi-external BFS and PageRank.
Concurrency: CountTriangles is safe to invoke concurrently on a shared CSR.
func CountTrianglesCtx ¶
func CountTrianglesCtx[W any](ctx context.Context, c *csr.CSR[W]) (total int64, perNode []int64, err error)
CountTrianglesCtx is the context-aware variant of CountTriangles. ctx.Err() is checked every 4096 outer-loop iterations; on cancellation returns (0, nil, wrapped ctx.Err()).
func DFS ¶
DFS performs iterative depth-first traversal of c starting at src. The visit callback receives every reached node and its depth from src (depth=0 for src); returning false terminates the traversal early. The traversal is iterative (it uses an explicit stack) so graphs with arbitrarily long paths do not risk a Go goroutine stack overflow. Each node is visited at most once. Visitation order is deterministic for any given CSR (it follows the order in which out-neighbours appear in the underlying edges array).
DFS is allocation-free on the hot path after the first call.
Example ¶
ExampleDFS traverses an unweighted chain depth-first; on a chain the visit order and per-node depth coincide with the natural sequence.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildDirected assembles a directed, unweighted CSR from (src, dst)
// integer pairs and returns it together with the mapper that resolves
// NodeIDs back to the user-facing int values.
func buildDirected(edges [][2]int) (*csr.CSR[struct{}], *graph.Mapper[int]) {
a := adjlist.New[int, struct{}](adjlist.Config{Directed: true})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], struct{}{})
}
return csr.BuildFromAdjList(a), a.Mapper()
}
func main() {
// Chain: 0 -> 1 -> 2 -> 3.
c, m := buildDirected([][2]int{{0, 1}, {1, 2}, {2, 3}})
src, _ := m.Lookup(0)
search.DFS(c, src, func(node graph.NodeID, d int) bool {
v, _ := m.Resolve(node)
fmt.Printf("visit %d (depth %d)\n", v, d)
return true // keep traversing
})
}
Output: visit 0 (depth 0) visit 1 (depth 1) visit 2 (depth 2) visit 3 (depth 3)
func DFSCtx ¶
func DFSCtx[W any](ctx context.Context, c *csr.CSR[W], src graph.NodeID, visit func(node graph.NodeID, depth int) bool) error
DFSCtx is the context-aware variant of DFS. ctx.Err() is checked every 4096 frame pops; on cancellation the traversal returns the wrapped ctx.Err() without further visit invocations.
func Diameter ¶
Diameter estimates the diameter of c — the longest shortest-path distance between any two vertices — using a 2-sweep BFS lower bound combined with an iFUB-style fringe upper-bound refinement. Returns (lo, hi, exact) where lo <= true diameter <= hi; exact is true when the bounds have converged and the value is exact.
2-sweep alone is the standard heuristic for road-network-shaped graphs and consistently delivers lo == true diameter in practice; the iFUB refinement tightens hi to lo on most real-world inputs. On adversarial inputs (e.g. expanders) hi may remain strictly above lo, in which case the caller can choose to accept the lower bound or to brute-force the true diameter (V * BFS).
Concurrency: Diameter is safe to invoke concurrently on a shared CSR. The implementation expects c to encode an undirected graph (symmetric directed CSR).
func DiameterCtx ¶
DiameterCtx is the context-aware variant of Diameter. ctx.Err() is checked once per BFS sweep; on cancellation returns (0, 0, false, wrapped ctx.Err()).
func DijkstraInto ¶
func DijkstraInto[W Weight]( ctx context.Context, c *csr.CSR[W], src graph.NodeID, dist []W, parent []graph.NodeID, found []bool, ) error
DijkstraInto is the zero-allocation primitive behind Dijkstra. It writes single-source shortest-path results directly into the caller-provided dist, parent, and found slices, each of which must have length at least c.MaxNodeID(); otherwise it returns ErrBufferTooSmall. The slices are reset in-place before the traversal, so any previous contents are overwritten.
On return, dist[i] holds the cost from src to node i when found[i] is true and parent[i] is the predecessor on the shortest path (with parent[src] = src by convention). When found[i] is false, dist[i] is the zero value of W and parent[i] is zero.
The only heap allocation performed is the priority queue itself, which is obtained from a per-W sync.Pool and is therefore zero in the steady state.
Concurrency: the caller's slices are written in-place; concurrent callers must supply separate buffers. The internal heap pool is safe for concurrent acquisition.
func Hierholzer ¶
Hierholzer computes an Eulerian circuit (or path) over a directed graph captured by c. The algorithm is the classical Hierholzer (1873) iterative form running in O(E).
Returns the trail as a slice of NodeIDs (length = E + 1) or ErrNoEulerian when the necessary conditions (every vertex has in-degree == out-degree for a circuit, or at most one vertex with out-in==1 and one with in-out==1 for a path) are not met, or the graph is not connected through its non-zero degree vertices.
func HierholzerCtx ¶
HierholzerCtx is the context-aware variant of Hierholzer. ctx.Err() is checked every 4096 trail steps; on cancellation returns (nil, wrapped ctx.Err()).
func HierholzerUndirected ¶
HierholzerUndirected computes an Eulerian circuit (or path) over the undirected graph captured by c. c is expected to be a symmetric directed CSR (every {u, v} edge appears as both (u, v) and (v, u)) — typical of [adjlist.AdjList] with Directed=false.
Returns the trail as a slice of NodeIDs (length = E + 1 where E is the number of distinct undirected edges) or ErrNoEulerian when the preconditions are not met. For an Eulerian circuit every non-isolated vertex must have even degree; for an Eulerian path, exactly two vertices may have odd degree (these become the path's endpoints).
func HierholzerUndirectedCtx ¶
HierholzerUndirectedCtx is the context-aware variant of HierholzerUndirected. ctx.Err() is checked every 4096 trail steps; on cancellation returns (nil, wrapped ctx.Err()).
func KCore ¶
KCore computes the coreness number of every vertex in c via the Batagelj-Zaversnik 2003 linear-time peeling algorithm. The result is a NodeID-indexed slice; ghost slots (no incident edge) report 0 — the same coreness as a live but degree-0 vertex.
The coreness of v is the largest k such that v belongs to a connected k-core of c. The algorithm proceeds by repeatedly peeling the lowest-degree vertex, recording its current degree as its coreness, and decrementing the remaining degree of each of its neighbours. Total time is O(V + E) via bucket-list peeling.
Concurrency: KCore is safe to invoke concurrently on a shared CSR.
func KCoreCtx ¶
KCoreCtx is the context-aware variant of KCore. ctx.Err() is checked every 4096 peeled vertices; on cancellation returns (nil, wrapped ctx.Err()).
func PrimMST ¶
func PrimMST[W Weight](c *csr.CSR[W], src graph.NodeID) (parent []graph.NodeID, found []bool, totalWeight W, err error)
PrimMST computes a minimum spanning tree of c rooted at src using Prim's algorithm with a binary-heap priority queue. The returned parent slice maps each NodeID to its predecessor in the tree (parent[src] == src; nodes unreachable from src have parent[i] == 0 but found[i] == false — distinguishable by a parallel found bitmap, returned as the third value).
For a connected undirected graph PrimMST and KruskalMST return trees of identical total weight (the MST weight is unique for distinct-weighted inputs, and tied inputs may yield different but equally-weighted trees).
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass.
Concurrency: PrimMST is safe to invoke concurrently on a shared CSR — it allocates its own working storage.
func PrimMSTCtx ¶
func PrimMSTCtx[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID) (parent []graph.NodeID, found []bool, totalWeight W, err error)
PrimMSTCtx is the context-aware variant of PrimMST. ctx.Err() is checked every 4096 heap pops; on cancellation returns (nil, nil, zero, wrapped ctx.Err()).
func TarjanSCC ¶
TarjanSCC returns the strongly connected components of the directed graph captured by c using Tarjan's algorithm (1972). Each component is returned as a slice of NodeIDs. The implementation is iterative (using an explicit work stack) so it does not risk Go goroutine stack overflow on graphs with deep DFS chains.
Complexity is O(V + E). The implementation is single-threaded.
Only NodeIDs that participate in at least one edge (as source or destination) are considered; NodeIDs that were assigned by the Mapper but never gained an edge are not emitted as singleton SCCs.
Example ¶
ExampleTarjanSCC finds the strongly connected components of a directed graph. Component membership is stable but the order in which components and their nodes are emitted is not, so the result is normalised (each component sorted, then components sorted) before printing.
package main
import (
"fmt"
"sort"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildDirected assembles a directed, unweighted CSR from (src, dst)
// integer pairs and returns it together with the mapper that resolves
// NodeIDs back to the user-facing int values.
func buildDirected(edges [][2]int) (*csr.CSR[struct{}], *graph.Mapper[int]) {
a := adjlist.New[int, struct{}](adjlist.Config{Directed: true})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], struct{}{})
}
return csr.BuildFromAdjList(a), a.Mapper()
}
// resolvePath maps a path expressed in NodeID space back to the
// user-facing int values, so output is stable regardless of the
// opaque NodeIDs the mapper assigns.
func resolvePath(m *graph.Mapper[int], path []graph.NodeID) []int {
out := make([]int, len(path))
for i, id := range path {
out[i], _ = m.Resolve(id)
}
return out
}
func main() {
// {0,1,2} form a cycle; 2->3 and 3 is its own component.
c, m := buildDirected([][2]int{{0, 1}, {1, 2}, {2, 0}, {2, 3}})
sccs := search.TarjanSCC(c)
normalised := make([][]int, 0, len(sccs))
for _, comp := range sccs {
vs := resolvePath(m, comp)
sort.Ints(vs)
normalised = append(normalised, vs)
}
sort.Slice(normalised, func(i, j int) bool { return normalised[i][0] < normalised[j][0] })
fmt.Printf("%d components: %v\n", len(normalised), normalised)
}
Output: 2 components: [[0 1 2] [3]]
func TarjanSCCCtx ¶
TarjanSCCCtx is the context-aware variant of TarjanSCC. ctx.Err() is checked at every restart of the outer DFS loop (i.e. before each new root); on cancellation returns (nil, wrapped ctx.Err()).
func TopologicalSort ¶
TopologicalSort returns a topological ordering of the directed acyclic graph captured by c using Kahn's algorithm. Vertices with no incoming edges are repeatedly emitted; removing them exposes new sources. If any vertex remains unemitted after the algorithm completes, the input has a cycle and ErrCycle is returned.
The returned ordering covers every NodeID for which the CSR has a non-empty out-edge range or at least one incoming edge. Sparse gaps in the NodeID space (NodeIDs that were never assigned by the Mapper) are omitted from the output.
Example ¶
ExampleTopologicalSort orders the vertices of a DAG so that every edge points forwards. The chain below has a single valid ordering, so the resolved output is deterministic.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildDirected assembles a directed, unweighted CSR from (src, dst)
// integer pairs and returns it together with the mapper that resolves
// NodeIDs back to the user-facing int values.
func buildDirected(edges [][2]int) (*csr.CSR[struct{}], *graph.Mapper[int]) {
a := adjlist.New[int, struct{}](adjlist.Config{Directed: true})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], struct{}{})
}
return csr.BuildFromAdjList(a), a.Mapper()
}
// resolvePath maps a path expressed in NodeID space back to the
// user-facing int values, so output is stable regardless of the
// opaque NodeIDs the mapper assigns.
func resolvePath(m *graph.Mapper[int], path []graph.NodeID) []int {
out := make([]int, len(path))
for i, id := range path {
out[i], _ = m.Resolve(id)
}
return out
}
func main() {
// DAG chain: 0 -> 1 -> 2 -> 3 -> 4.
c, m := buildDirected([][2]int{{0, 1}, {1, 2}, {2, 3}, {3, 4}})
order, err := search.TopologicalSort(c)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(resolvePath(m, order))
}
Output: [0 1 2 3 4]
func TopologicalSortCtx ¶
TopologicalSortCtx is the context-aware variant of TopologicalSort. ctx.Err() is checked every 4096 emits; on cancellation returns (nil, wrapped ctx.Err()).
func WCC ¶
WCC computes the weakly-connected components of c — equivalence classes induced by the symmetric closure of c's edge relation. Returns one int per live NodeID labelling its component in [0, K), where K is the number of components, plus K itself. Ghost slots (no incident edge) report -1.
Internally WCC walks every edge once and unions the endpoints in a slice-backed Union-Find; final component IDs are compacted into [0, K) preserving order of first occurrence so the output is deterministic across runs of the same graph.
Concurrency: WCC is safe to invoke concurrently on a shared CSR.
Example ¶
ExampleWCC labels the weakly connected components of a directed graph (edges treated as undirected). The component count is stable; raw labels are compared for equality rather than printed.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildDirected assembles a directed, unweighted CSR from (src, dst)
// integer pairs and returns it together with the mapper that resolves
// NodeIDs back to the user-facing int values.
func buildDirected(edges [][2]int) (*csr.CSR[struct{}], *graph.Mapper[int]) {
a := adjlist.New[int, struct{}](adjlist.Config{Directed: true})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], struct{}{})
}
return csr.BuildFromAdjList(a), a.Mapper()
}
func main() {
// Two components: {0,1,2} and {3,4}.
c, m := buildDirected([][2]int{{0, 1}, {1, 2}, {3, 4}})
comp, k, err := search.WCC(c)
if err != nil {
fmt.Println("error:", err)
return
}
id0, _ := m.Lookup(0)
id2, _ := m.Lookup(2)
id3, _ := m.Lookup(3)
fmt.Printf("components: %d\n", k)
fmt.Printf("0 and 2 together: %v\n", comp[id0] == comp[id2])
fmt.Printf("0 and 3 together: %v\n", comp[id0] == comp[id3])
}
Output: components: 2 0 and 2 together: true 0 and 3 together: false
Types ¶
type APSP ¶
type APSP[W Weight] struct { // contains filtered or unexported fields }
APSP is an all-pairs shortest-paths distance matrix.
APSP is allocated and indexed over the set of live NodeIDs (those with at least one incident edge in the source CSR). The public At method accepts arbitrary NodeIDs and reports the pair as unreachable when either endpoint lies in a ghost slot.
APSP is safe for concurrent reads.
func DijkstraAPSP ¶
DijkstraAPSP computes APSP on c by running Dijkstra from every live vertex. It accepts only non-negative edge weights.
For graphs with negative edges (but no negative cycle), use JohnsonAPSP which prefixes a Bellman-Ford reweighting pass and runs in O(V * (V + E) * log V), or FloydWarshall which tolerates them at the cost of O(V^3) work.
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass.
Complexity: O(V * (V + E) * log V).
func DijkstraAPSPCtx ¶
DijkstraAPSPCtx is the context-aware variant of DijkstraAPSP. ctx.Err() is checked once per source vertex; on cancellation returns (nil, wrapped ctx.Err()).
func FloydWarshall ¶
FloydWarshall computes APSP via the textbook O(V^3) DP, where V is the count of live NodeIDs (not the sparse MaxNodeID). It tolerates negative edge weights and detects negative cycles via a post-DP diagonal scan; in the no-negative-cycle case the diagonal stays at 0.
Reachability is tracked via a parallel found[] bitmap, not an in-band +Inf sentinel — an earlier sentinel constructed by 60 iterations of v++/v+=v wrapped on integer types and silently corrupted distances. The bitmap is correct for every Weight type.
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf; the simple entry returns nil and the Ctx variant returns ErrInvalidInput otherwise. Integer Weight types skip that pass.
When the graph contains a negative-weight cycle, the simple entry returns nil; the Ctx variant returns ErrNegativeCycle (the same sentinel used by BellmanFord). The check is the canonical CLRS §25.2 post-pass: any live vertex whose self-distance has been relaxed below zero lies on a negative cycle.
Concurrency: safe to invoke from any number of goroutines on a shared CSR; allocates its own working buffers per call.
func FloydWarshallCtx ¶
FloydWarshallCtx is the context-aware variant of FloydWarshall. ctx.Err() is checked at every k-pivot iteration (the outermost loop); on cancellation returns (nil, wrapped ctx.Err()).
Errors surfaced: ErrInvalidInput (NaN/Inf float weight), ErrNegativeCycle (negative-weight cycle detected post-DP), or the underlying ctx.Err() on cancellation.
func JohnsonAPSP ¶
JohnsonAPSP computes APSP on c using Johnson's algorithm: a Bellman-Ford reweighting pass from a virtual zero-weight source computes a potential h(v); the original edge weights are reweighted to w'(u,v) = w(u,v) + h(u) - h(v); a Dijkstra from every live vertex on the reweighted (non-negative) graph yields d'(u,v); the original distances are recovered via d(u,v) = d'(u,v) - h[u] + h[v].
Compared to DijkstraAPSP which rejects negative edges, JohnsonAPSP accepts arbitrary signed edge weights and reports a negative cycle reachable from any source via ErrNegativeCycle. Compared to FloydWarshall which is O(V^3), Johnson is O(V * (V + E) * log V) — strictly better on sparse graphs (E = O(V)).
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass. The gate sits at Johnson's public boundary so callers get a single point of failure (the inner Bellman-Ford reweighting would otherwise re-detect it at higher cost).
Floating-point caveat: when W is a floating-point type, the reweight/recover arithmetic w(u,v) + h(u) - h(v) followed by d'(u,v) - h[u] + h[v] can accumulate ULP-level rounding error, so the recovered d(u,v) may differ from FloydWarshall's output by a small tolerance. Integer Weight types reproduce FloydWarshall exactly.
Concurrency: JohnsonAPSP is safe for any number of concurrent invocations on a shared, immutable CSR.
Complexity: O(V * (V + E) * log V) for the Dijkstra pass plus O(V * E) for the Bellman-Ford pass (SPFA worst-case bound).
func JohnsonAPSPCtx ¶
JohnsonAPSPCtx is the context-aware variant of JohnsonAPSP. ctx.Err() is checked once per source vertex during the Dijkstra pass and at every relaxation-round boundary during the Bellman-Ford pass; on cancellation returns (nil, wrapped ctx.Err()).
type Assignment ¶
Assignment is the result of Hungarian: the minimum total cost and the column assigned to each row.
Concurrency: Assignment is a value type returned freshly per call and is safe for concurrent reads.
func Hungarian ¶
func Hungarian(cost []float64, n, m int) (Assignment, error)
Hungarian solves the rectangular assignment problem on the n*m cost matrix using the Jonker-Volgenant / Kuhn-Munkres algorithm. cost is given in row-major order (length n*m). When n <= m every row is matched; rows beyond min(n, m) are unassigned. The algorithm minimises the total cost.
Complexity ¶
Let V = max(n, m). The algorithm runs in O(V^3) time and O(V^2) space (the cost matrix dominates). Each outer iteration performs O(V^2) work to extend the shortest-augmenting-path tree; there are V iterations. The constant factor is small (the inner loop is two pointer-chasing reductions over a row), so 1024x1024 instances complete in well under a second on the headline hardware in docs/profiling.md.
Memory growth is linear in n*m on the call (the cost slice the caller owns) plus O(V) scratch on the heap for the potentials and the row queue. The scratch slices are freshly allocated per call; pool them externally if invoking Hungarian on a tight loop.
Adapted from the standard "potential" formulation. Pass costs directly; do not negate for maximisation.
Returns an empty Assignment paired with ErrInvalidInput when cost contains any NaN or +/-Inf entry — the dual potentials accumulate across iterations and a single non-finite value silently corrupts the entire run, so validation is mandatory.
v1 limitation. Hungarian is float64-only; the Weight constraint supports both integer and float types, but Hungarian's dual-update step requires a representable "infinity" sentinel that Go's generics cannot cleanly produce for arbitrary named numeric types (math.MaxFloat64 is not assignable to a ~int8 or ~uint64 W). Integer-weighted assignment is therefore deferred; callers with integer cost matrices should currently convert to float64.
func HungarianCtx ¶
HungarianCtx is the context-aware variant of Hungarian. ctx.Err() is checked at every row-augmenting iteration; on cancellation returns (zero Assignment, wrapped ctx.Err()).
type BCCResult ¶
type BCCResult struct {
Components [][]graph.NodeID
Bridges [][2]graph.NodeID
Articulation []graph.NodeID
}
BCCResult bundles every output of HopcroftTarjanBCC: the biconnected components (each component a slice of NodeIDs), the bridges (each as an (a, b) pair), and the articulation points.
Concurrency: BCCResult is a value type returned freshly by every call; safe to share across goroutines for read.
func HopcroftTarjanBCC ¶
HopcroftTarjanBCC runs Hopcroft-Tarjan's combined biconnected- components / bridges / articulation-points algorithm on an undirected graph captured by c. Complexity O(V + E).
Multigraph correctness: when an undirected graph has multiple parallel edges between the same vertex pair, every such edge participates in a 2-cycle biconnected component. The algorithm tracks the specific CSR edge index used to enter each vertex (parentEdgeIdx) so that only that one edge is skipped — every other parallel edge to the parent is correctly treated as a real back-edge.
The implementation uses an explicit DFS stack (no recursion) so it survives deep graphs.
func HopcroftTarjanBCCCtx ¶
HopcroftTarjanBCCCtx is the context-aware variant of HopcroftTarjanBCC. ctx.Err() is checked at every outer-DFS root; on cancellation returns (zero BCCResult, wrapped ctx.Err()).
type Distances ¶
type Distances[W Weight] struct { // contains filtered or unexported fields }
Distances is the result of a single-source shortest-path query. It exposes constant-time distance lookup and parent-chain path reconstruction.
Distances is safe for concurrent reads.
func BellmanFord ¶
BellmanFord computes single-source shortest paths in c starting at src. Unlike Dijkstra, it accepts edges with negative weights and detects negative cycles reachable from the source, returning ErrNegativeCycle in that case.
The algorithm is the textbook O(V*E) variant: for each of V-1 rounds it relaxes every edge once; an additional round detects negative cycles by reporting any further successful relaxation.
The implementation reuses the Distances result type and the per-W pooled state of Dijkstra; the inner relaxation loop is zero-alloc.
Input contract. For floating-point Weight types the routine validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip the validation pass entirely.
For hot loops where the caller can amortise buffer allocation, prefer the zero-allocation primitive BellmanFordInto.
Example ¶
ExampleBellmanFord computes shortest paths on a graph that contains a negative-weight edge but no negative cycle — the case Dijkstra cannot handle.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildWeighted assembles a CSR over int64-weighted edges and returns
// it together with the mapper. directed selects a directed graph; when
// false the AdjList mirrors every edge, yielding the symmetric CSR the
// undirected algorithms (Kruskal, Prim) expect.
func buildWeighted(directed bool, edges [][3]int) (*csr.CSR[int64], *graph.Mapper[int]) {
a := adjlist.New[int, int64](adjlist.Config{Directed: directed})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], int64(e[2]))
}
return csr.BuildFromAdjList(a), a.Mapper()
}
func main() {
// 0->1 (4), 0->2 (5), 1->3 (5), 2->1 (-3), 2->3 (6).
// Cheapest 0->1 is via 2: 5 + (-3) = 2, so 0->3 is 2 + 5 = 7.
c, m := buildWeighted(true, [][3]int{
{0, 1, 4}, {0, 2, 5},
{1, 3, 5},
{2, 1, -3}, {2, 3, 6},
})
src, _ := m.Lookup(0)
d, err := search.BellmanFord(c, src)
if err != nil {
fmt.Println("error:", err)
return
}
for v := 0; v < 4; v++ {
id, _ := m.Lookup(v)
dist, _ := d.Distance(id)
fmt.Printf("dist to %d = %d\n", v, dist)
}
}
Output: dist to 0 = 0 dist to 1 = 2 dist to 2 = 5 dist to 3 = 7
func BellmanFordCtx ¶
func BellmanFordCtx[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID) (*Distances[W], error)
BellmanFordCtx is the context-aware variant of BellmanFord. ctx.Err() is checked at every relaxation round boundary; on cancellation returns (nil, wrapped ctx.Err()).
func Dijkstra ¶
Dijkstra computes single-source shortest paths in c starting at src over non-negative edge weights. It returns ErrNegativeWeight (without performing any traversal) when any weight in c is strictly less than the zero value of W. For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass.
The implementation uses a classic binary-heap priority queue. Working storage is pooled across calls so steady-state workloads reach zero allocations per inner-loop iteration.
For hot loops where the caller can amortise buffer allocation (e.g. Yen's k-shortest-paths), prefer the zero-allocation primitive DijkstraInto.
Example ¶
ExampleDijkstra computes single-source shortest-path distances on a positively weighted directed graph and queries the cost to each node.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildWeighted assembles a CSR over int64-weighted edges and returns
// it together with the mapper. directed selects a directed graph; when
// false the AdjList mirrors every edge, yielding the symmetric CSR the
// undirected algorithms (Kruskal, Prim) expect.
func buildWeighted(directed bool, edges [][3]int) (*csr.CSR[int64], *graph.Mapper[int]) {
a := adjlist.New[int, int64](adjlist.Config{Directed: directed})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], int64(e[2]))
}
return csr.BuildFromAdjList(a), a.Mapper()
}
func main() {
// CLRS-style graph; shortest distances from 0 are 0,7,3,8,5.
c, m := buildWeighted(true, [][3]int{
{0, 1, 10}, {0, 2, 3},
{1, 3, 1},
{2, 1, 4}, {2, 3, 8}, {2, 4, 2},
{3, 4, 7},
})
src, _ := m.Lookup(0)
d, err := search.Dijkstra(c, src)
if err != nil {
fmt.Println("error:", err)
return
}
for v := 0; v < 5; v++ {
id, _ := m.Lookup(v)
dist, _ := d.Distance(id)
fmt.Printf("dist to %d = %d\n", v, dist)
}
}
Output: dist to 0 = 0 dist to 1 = 7 dist to 2 = 3 dist to 3 = 8 dist to 4 = 5
func DijkstraCtx ¶
func DijkstraCtx[W Weight](ctx context.Context, c *csr.CSR[W], src graph.NodeID) (*Distances[W], error)
DijkstraCtx is the context-aware variant of Dijkstra. ctx.Err() is checked every 4096 heap pops; on cancellation it returns (nil, wrapped ctx.Err()).
func (*Distances[W]) Distance ¶
Distance returns the cost of the shortest path from the source to node and true if node is reachable, or the zero value of W and false otherwise.
type MSTEdge ¶
MSTEdge identifies one edge in a minimum-spanning-tree result. From and To are the endpoints in CSR NodeID space; Weight is the edge's weight.
func KruskalMST ¶
KruskalMST computes a minimum spanning forest of c using Kruskal's algorithm (1956): sort all edges by weight ascending, then iterate adding each edge that connects two distinct components (tracked via a slice-backed Union-Find). For a connected graph the result has exactly maxID-1 edges; for a disconnected graph it has maxID - numComponents edges.
c is expected to be an undirected graph encoded as a symmetric directed CSR (every {u, v} edge appears as both (u, v) and (v, u)); only one direction of each pair is added to the result, chosen canonically as u <= v.
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns ErrInvalidInput otherwise; integer Weight types skip that pass.
Concurrency: KruskalMST is safe to invoke from any number of goroutines on a shared CSR — it allocates its own working storage.
Example ¶
ExampleKruskalMST computes a minimum spanning tree of an undirected weighted graph. The graph is built undirected so the CSR is symmetric, as Kruskal requires; the total weight is deterministic.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildWeighted assembles a CSR over int64-weighted edges and returns
// it together with the mapper. directed selects a directed graph; when
// false the AdjList mirrors every edge, yielding the symmetric CSR the
// undirected algorithms (Kruskal, Prim) expect.
func buildWeighted(directed bool, edges [][3]int) (*csr.CSR[int64], *graph.Mapper[int]) {
a := adjlist.New[int, int64](adjlist.Config{Directed: directed})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], int64(e[2]))
}
return csr.BuildFromAdjList(a), a.Mapper()
}
func main() {
// Undirected weighted graph on {0,1,2,3}.
c, _ := buildWeighted(false, [][3]int{
{0, 1, 1}, {1, 2, 2}, {2, 3, 3}, {0, 3, 4}, {0, 2, 5},
})
edges, total, err := search.KruskalMST(c)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%d edges, total weight %d\n", len(edges), total)
}
Output: 3 edges, total weight 6
func KruskalMSTCtx ¶
func KruskalMSTCtx[W Weight](ctx context.Context, c *csr.CSR[W]) (edges []MSTEdge[W], totalWeight W, err error)
KruskalMSTCtx is the context-aware variant of KruskalMST. ctx.Err() is checked once before the sort and every 4096 edges during the scan; on cancellation returns (nil, zero, wrapped ctx.Err()).
type Matching ¶
type Matching struct {
// MatchL maps each left vertex to its matched right vertex or
// -1 if unmatched.
MatchL []graph.NodeID
// MatchR is the symmetric map from right to left.
MatchR []graph.NodeID
// Size is the number of matched edges.
Size int
}
Matching is the result of HopcroftKarp.
Matching is safe for concurrent reads.
func HopcroftKarp ¶
HopcroftKarp computes a maximum-cardinality matching on the bipartite graph captured by c. The first nLeft NodeIDs are assumed to form the left partition; the rest form the right partition. Edges should point from left to right (the algorithm follows c's adjacency for the left vertices).
Complexity O(E * sqrt(V)).
func HopcroftKarpCtx ¶
HopcroftKarpCtx is the context-aware variant of HopcroftKarp. ctx.Err() is checked at every phase boundary (BFS-layer + DFS-augment pair); on cancellation returns (zero Matching, wrapped ctx.Err()).
type TC ¶
type TC struct {
// contains filtered or unexported fields
}
TC is a transitive-closure reachability oracle: a bitset matrix over the live NodeID space of the source CSR. The matrix is dense in memory (V * V bits, padded to whole uint64 words) but supports O(1) point-to-point reachability queries via TC.Reachable.
TC is safe for concurrent reads.
func TransitiveClosure ¶
TransitiveClosure builds the reachability oracle of c using Warshall's O(V^3 / 64) bitset variant: a square V*V bit-matrix is seeded with the direct adjacency (and the diagonal), and then for every k-pivot the algorithm OR's row k into every other row whose k-th bit is set. The bit-parallel inner step exploits the fact that the per-word AND/OR runs over 64 destinations at a time.
For sparse graphs prefer BFS-per-source; this implementation is only competitive when V is small enough that the V*V/8 bytes of state fit in RAM (V=10k -> 12.5 MB; V=100k -> 1.25 GB).
Concurrency: TransitiveClosure is safe to invoke concurrently on a shared CSR; the returned TC is safe for concurrent reads.
func TransitiveClosureCtx ¶
TransitiveClosureCtx is the context-aware variant of TransitiveClosure. ctx.Err() is checked at every k-pivot; on cancellation returns (nil, wrapped ctx.Err()).
type Weight ¶
type Weight interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
~float32 | ~float64
}
Weight is the constraint accepted by weighted algorithms such as Dijkstra. It permits all built-in numeric types and their named derivatives. Comparisons rely on the standard ordering.
type YenPath ¶
YenPath is one shortest path produced by YenKShortest.
Concurrency: YenPath values are freshly allocated per call and safe for concurrent reads.
func EppsteinKShortest
deprecated
EppsteinKShortest is the deprecated former name of KShortestPathsLoopless. The implementation shipped under this symbol is a best-first enumeration over the loopless-path tree, not the heap-of-heaps construction of Eppstein 1998; the audit on 2026-05-20 renamed it to reflect what the code actually does. The alias is kept for one major version so existing callers do not break; new code should use KShortestPathsLoopless.
Deprecated: use KShortestPathsLoopless.
func EppsteinKShortestCtx
deprecated
func KShortestPathsLoopless ¶
KShortestPathsLoopless computes up to k loopless shortest paths from src to dst in c using a best-first expansion over the implicit loopless-path tree. Each priority-queue entry is a (cost, path) pair; popping the cheapest entry that ends at dst yields the next k-shortest path. The loopless guard excludes any expansion whose neighbour is already present in the popped path.
K bounds and memory growth ¶
Each pop expands one path of length up to V, so a worst-case run does O(k * V * Δ) work and stores O(k * V) words in the priority queue. Practical guidance:
- k <= 100: queue stays comfortably below a megabyte even on million-edge graphs.
- k <= 10 000: queue grows to roughly k * diameter * word_size bytes; budget accordingly and pre-allocate.
- k unbounded (`enumerate all simple paths until exhaustion`): this implementation will exhaust because the path count can be exponential in V (think dense graphs); cap k explicitly to keep the queue bounded.
For graphs where k is comparable to the number of simple paths the routine matches Yen's asymptotic behaviour without paying the k spur-Dijkstra rounds; on sparse graphs with few alternative routes YenKShortest is typically faster in practice.
This is NOT the heap-of-heaps construction of Eppstein 1998 ("Finding the k Shortest Paths", SIAM J. Comput.). That algorithm builds an implicit graph D(G) over sidetrack edges of the shortest-path tree and achieves O(m + n log n + k); the implementation here is the simpler best-first enumeration that GoGraph has historically shipped under the EppsteinKShortest name. See EppsteinKShortest for the deprecated alias preserved for backwards compatibility.
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns nil through the simple entry (and ErrInvalidInput through the Ctx variant) otherwise; integer Weight types skip that pass.
Safe for concurrent use against an immutable CSR; the call holds no shared state across invocations.
func KShortestPathsLooplessCtx ¶
func KShortestPathsLooplessCtx[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, k int) ([]YenPath[W], error)
KShortestPathsLooplessCtx is the context-aware variant of KShortestPathsLoopless. ctx.Err() is checked every 4096 heap pops; on cancellation returns (nil, wrapped ctx.Err()).
func YenKShortest ¶
YenKShortest computes up to k loopless shortest paths from src to dst on c using Yen's algorithm (1971). Returns paths sorted by total cost ascending; an empty slice when src cannot reach dst.
For floating-point Weight types it validates that no edge weight is NaN or +/-Inf and returns nil through the simple entry (and ErrInvalidInput through the Ctx variant) otherwise; integer Weight types skip that pass.
K bounds and memory growth ¶
The implementation runs at most k * (V + E) Dijkstra calls and is suitable for small-to-medium k. Practical guidance:
- k <= 16: free. Comfortable on any graph the rest of the library handles.
- k <= 100: comfortable on graphs up to a few million edges.
- k <= 1000: feasible but candidate-heap memory scales with the cumulative number of candidate paths considered. The paths themselves are O(diameter) each, so the working set grows as O(k * diameter) for the candidate heap plus the reusable O(V) Dijkstra scratch.
- k >> 1000: prefer Eppstein's algorithm (O(E + k log k) with implicit-path representation), scheduled for a later task. Yen's spur-and-reroot pattern compounds the per-iteration cost too quickly past this point.
Returned []YenPath retains pointers into the CSR vertex/edge arrays — do NOT free the underlying CSR while iterating.
Example ¶
ExampleYenKShortest enumerates the k loopless shortest paths between two nodes, returned in non-decreasing total cost.
package main
import (
"fmt"
"github.com/FlavioCFOliveira/GoGraph/graph"
"github.com/FlavioCFOliveira/GoGraph/graph/adjlist"
"github.com/FlavioCFOliveira/GoGraph/graph/csr"
"github.com/FlavioCFOliveira/GoGraph/search"
)
// buildWeighted assembles a CSR over int64-weighted edges and returns
// it together with the mapper. directed selects a directed graph; when
// false the AdjList mirrors every edge, yielding the symmetric CSR the
// undirected algorithms (Kruskal, Prim) expect.
func buildWeighted(directed bool, edges [][3]int) (*csr.CSR[int64], *graph.Mapper[int]) {
a := adjlist.New[int, int64](adjlist.Config{Directed: directed})
for _, e := range edges {
_ = a.AddEdge(e[0], e[1], int64(e[2]))
}
return csr.BuildFromAdjList(a), a.Mapper()
}
// resolvePath maps a path expressed in NodeID space back to the
// user-facing int values, so output is stable regardless of the
// opaque NodeIDs the mapper assigns.
func resolvePath(m *graph.Mapper[int], path []graph.NodeID) []int {
out := make([]int, len(path))
for i, id := range path {
out[i], _ = m.Resolve(id)
}
return out
}
func main() {
// Two parallel routes 0 -> 3: cost 2 (via 1) and cost 4 (via 2).
c, m := buildWeighted(true, [][3]int{
{0, 1, 1}, {1, 3, 1},
{0, 2, 2}, {2, 3, 2},
})
src, _ := m.Lookup(0)
dst, _ := m.Lookup(3)
paths := search.YenKShortest(c, src, dst, 2)
for i, p := range paths {
fmt.Printf("path %d: %v cost %d\n", i, resolvePath(m, p.Nodes), p.Cost)
}
}
Output: path 0: [0 1 3] cost 2 path 1: [0 2 3] cost 4
func YenKShortestCtx ¶
func YenKShortestCtx[W Weight](ctx context.Context, c *csr.CSR[W], src, dst graph.NodeID, k int) ([]YenPath[W], error)
YenKShortestCtx is the context-aware variant of YenKShortest. ctx.Err() is checked at every spur iteration; on cancellation returns (nil, wrapped ctx.Err()).
Memory: the implementation allocates one O(V) scratch set (dist/parent/found/visited/excluded) and one O(E) edge-index map at entry, then reuses them across all internal Dijkstra calls. The v1.0 implementation reallocated all of these per spur step.
Source Files
¶
- astar.go
- bcc.go
- bellman_ford.go
- bfs_do.go
- bibfs.go
- bidijkstra.go
- diameter.go
- dijkstra.go
- eppstein.go
- floyd_warshall.go
- hierholzer.go
- hierholzer_undirected.go
- hopcroft_karp.go
- hungarian.go
- johnson.go
- kcore.go
- kruskal.go
- kshortest_loopless.go
- prim.go
- search.go
- tarjan.go
- topo.go
- transitive_closure.go
- triangles.go
- wcc.go
- weight_validation.go
- yen.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package centrality implements vertex importance metrics.
|
Package centrality implements vertex importance metrics. |
|
Package community implements community detection algorithms for undirected graphs.
|
Package community implements community detection algorithms for undirected graphs. |
|
Package extern provides graph algorithms that operate directly on a Tier 2 (mmap-backed) csrfile.Reader without first materialising the CSR in memory.
|
Package extern provides graph algorithms that operate directly on a Tier 2 (mmap-backed) csrfile.Reader without first materialising the CSR in memory. |
|
Package flow implements network-flow algorithms over directed capacitated graphs.
|
Package flow implements network-flow algorithms over directed capacitated graphs. |