// Weighted shortest-path search: single-source Dijkstra with path// reconstruction, and the bidirectional point-to-point variant that meets// in the middle.package chickpeasimport ()// WeightFn returns the non-negative cost of traversing a rel (Dijkstra's// assumption). It receives the step's source node and the RelRef, so it can// read a stored weight via RelProp(rel.Pos, ...) or compute a derived cost;// +Inf prunes the rel.typeWeightFnfunc(from NodeID, rel RelRef) float64// ShortestPaths is the result of a Dijkstra search: the shortest distance// to every reached node, with predecessors for path reconstruction. Small// searches keep map state; a search that settles a large set switches to// dense id-space arrays mid-run (see densify), so point queries stay// cheap while component-scale searches avoid per-edge map probes.typeShortestPathsstruct { source NodeID// Sparse state (dense == false). dist map[NodeID]float64 prev map[NodeID]NodeID// Dense state: distances indexed by id (+Inf = unreached), prev // indexed by id (prevNone = none), reached in settle-set order. dense bool distA []float64 prevA []NodeID reached []NodeID distMap map[NodeID]float64// Distances() materialization, lazy}// prevNone marks a dense-mode node with no predecessor.const prevNone = NodeID(^uint32(0))// Distance is the shortest distance from the source to node; ok is false// when unreached.func ( *ShortestPaths) ( NodeID) (float64, bool) {if .dense {ifint() >= len(.distA) || math.IsInf(.distA[], 1) {return0, false }return .distA[], true } , := .dist[]return , }// Reached reports whether node was reached from the source.func ( *ShortestPaths) ( NodeID) bool { , := .Distance()return}// Distances is every reached node with its shortest distance (the source// itself at 0). The returned map is the result's own -- callers must not// mutate it while still using PathTo.func ( *ShortestPaths) () map[NodeID]float64 {if !.dense {return .dist }if .distMap == nil { .distMap = make(map[NodeID]float64, len(.reached))for , := range .reached { .distMap[] = .distA[] } }return .distMap}// PathTo is the shortest path from the source to target as a node sequence// (source first); ok is false when target was unreached.func ( *ShortestPaths) ( NodeID) ([]NodeID, bool) {if !.Reached() {returnnil, false } := []NodeID{}for := ; != .source; {varNodeIDif .dense { = .prevA[]if == prevNone {returnnil, false } } else {varbool , = .prev[]if ! {returnnil, false } } = append(, ) = }for , := 0, len()-1; < ; , = +1, -1 { [], [] = [], [] }return , true}// lookup is the mode-dispatched dist read used by the search loop.func ( *ShortestPaths) ( NodeID) (float64, bool) {if .dense { := .distA[]return , !math.IsInf(, 1) } , := .dist[]return , }// set is the mode-dispatched relax write.func ( *ShortestPaths) ( NodeID, float64, NodeID) {if .dense {ifmath.IsInf(.distA[], 1) { .reached = append(.reached, ) } .distA[] = .prevA[] = return } .dist[] = .prev[] = }// densify moves a search that has settled a large set onto dense// id-space arrays: one O(idspace) initialization buys per-edge relaxes// with no hashing. Runs at most once per search.func ( *ShortestPaths) ( int) { .distA = make([]float64, ) .prevA = make([]NodeID, )for := range .distA { .distA[] = math.Inf(1) .prevA[] = prevNone } .reached = make([]NodeID, 0, 2*len(.dist))for , := range .dist { .distA[] = .reached = append(.reached, ) }for , := range .prev { .prevA[] = } .dense = true .dist, .prev = nil, nil}// dijkstraHeap is a min-heap ordered by cost, ties broken by node id (a// total order, so runs are deterministic).type dijkstraState struct { cost float64 node NodeID}type dijkstraHeap []dijkstraState// less orders by cost, ties by node id -- a total order for// deterministic runs.func ( dijkstraHeap) (, int) bool {if [].cost != [].cost {return [].cost < [].cost }return [].node < [].node}// push and pop are hand-rolled sift operations: the container/heap form// boxes every state into an interface value, allocating once per push in// the search's hottest loop.func ( *dijkstraHeap) ( dijkstraState) { * = append(*, ) := *for := len() - 1; > 0; { := ( - 1) / 2if !.less(, ) {break } [], [] = [], [] = }}func ( *dijkstraHeap) () dijkstraState { := * := [0] := len() - 1 [0] = [] = [:] * = for := 0; ; { := 2* + 1if >= {break } := if := + 1; < && .less(, ) { = }if !.less(, ) {break } [], [] = [], [] = }return}// Dijkstra runs weighted single-source shortest paths from source over the// m-matched rels in dir, reaching the whole component.func ( *Snapshot) ( NodeID, Direction, RelMatch, WeightFn) *ShortestPaths {return .dijkstra(, , , , false, 0)}// DijkstraTo is Dijkstra stopping as soon as target's shortest distance is// known -- the single-pair form; the result still answers every node// settled before the stop.func ( *Snapshot) (, NodeID, Direction, RelMatch, WeightFn) *ShortestPaths {return .dijkstra(, , , , true, )}// dijkstraDenseAt is the settled-set size at which a search abandons its// maps for dense id-space arrays -- big enough that point queries never// pay the O(idspace) initialization, small enough that component-scale// searches amortize it away.const dijkstraDenseAt = 4096func ( *Snapshot) ( NodeID, Direction, RelMatch, WeightFn,bool, NodeID) *ShortestPaths { := &ShortestPaths{source: ,dist: map[NodeID]float64{: 0},prev: map[NodeID]NodeID{}, } := dijkstraHeap{{cost: 0, node: }}forlen() > 0 { := .pop()if , := .lookup(.node); && .cost > {continue// stale heap entry: a shorter path was found after pushing }if && .node == {break }for := range .RelsMatch(.node, , ) { := .cost + (.node, )ifmath.IsInf(, 1) {continue// +Inf prunes the rel, per the WeightFn contract }if , := .lookup(.Neighbor); ! || < { .set(.Neighbor, , .node) .push(dijkstraState{cost: , node: .Neighbor}) } }if !.dense && len(.dist) >= dijkstraDenseAt { .densify(int(.CSRIDSpace())) } }return}// wspScratch is one bidirectional search's reusable state, pooled so a// stage running many point-to-point searches (one per candidate pair)// pays map and heap construction once per pool entry instead of per// search -- clear keeps the maps' buckets, the heaps keep their backing.type wspScratch struct { distF, distB map[NodeID]float64 heapF, heapB dijkstraHeap}var wspPool = sync.Pool{New: func() any {return &wspScratch{distF: map[NodeID]float64{}, distB: map[NodeID]float64{}}}}// WeightedShortestPath is the shortest-path cost from source to target via// bidirectional Dijkstra: it searches from both ends and meets in the// middle, exploring far fewer nodes than a one-directional search for a// point-to-point query. ok is false when target is unreachable. The// backward search follows the reverse of dir, so weight must be symmetric// (the usual case for an undirected Both traversal). For distances to many// targets, or the path itself, use Dijkstra.func ( *Snapshot) (, NodeID, Direction, RelMatch, WeightFn) (float64, bool) {if == {return0, true } := .Reverse() := wspPool.Get().(*wspScratch)deferwspPool.Put()clear(.distF)clear(.distB) , := .distF, .distB [] = 0 [] = 0 := append(.heapF[:0], dijkstraState{cost: 0, node: }) := append(.heapB[:0], dijkstraState{cost: 0, node: })deferfunc() { .heapF, .heapB = [:0], [:0] }()// Best meeting cost so far; the frontiers cannot beat topF + topB. := math.Inf(1)forlen() > 0 || len() > 0 { , := math.Inf(1), math.Inf(1)iflen() > 0 { = [0].cost }iflen() > 0 { = [0].cost }if + >= {break }// Expand whichever frontier is currently cheaper. , , , := &, , , if > { , , , = &, , , }iflen(*) == 0 {continue } := .pop()if , := [.node]; && .cost > {continue// stale }// s.node is settled on this side; if the other side reached it too, // the two halves form a candidate path.if , := [.node]; && .cost+ < { = .cost + }for := range .RelsMatch(.node, , ) { := .cost + (.node, )ifmath.IsInf(, 1) {continue// +Inf prunes the rel, per the WeightFn contract }if , := [.Neighbor]; ! || < { [.Neighbor] = .push(dijkstraState{cost: , node: .Neighbor}) } } }if !math.IsInf(, 1) {return , true }return0, false}
The pages are generated with Goldsv0.8.7. (GOOS=linux GOARCH=amd64)
Golds is a Go 101 project developed by Tapir Liu.
PR and bug reports are welcome and can be submitted to the issue list.
Please follow @zigo_101 (reachable from the left QR code) to get the latest news of Golds.