Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

graph/path: Improve performance of YenKShortestPaths #2004

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
graph/path: add single-pair shortest path variant
This variant can terminate early when the target is reached. This can
make a large difference in running time for Yen's k-shortest paths
algorithm, as it makes many calls to the shortest path subroutine.
  • Loading branch information
BarrensZeppelin committed Nov 27, 2024
commit 764985128fd61e997e2acfd4e4fde72835da1d24
27 changes: 26 additions & 1 deletion graph/path/dijkstra.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,30 @@ import (
//
// The time complexity of DijkstraFrom is O(|E|.log|V|).
func DijkstraFrom(u graph.Node, g traverse.Graph) Shortest {
return dijkstraFrom(u, nil, g)
}

// DijkstraFromTo returns a shortest path from u to v in the graph g. The
// result is equivalent to DijkstraFrom(u, g).To(v), but DijkstraFromTo can be
// more efficient, as it can terminate early if v is reached. If the graph does
// not implement Weighted, UniformCost is used. DijkstraFromTo will panic if g
// has a u-reachable negative edge weight that is discovered before reaching v.
//
// The time complexity of DijkstraFromTo is O(|E|.log|V|).
func DijkstraFromTo(u graph.Node, v graph.Node, g traverse.Graph) (path []graph.Node, weight float64) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func DijkstraFromTo(u graph.Node, v graph.Node, g traverse.Graph) (path []graph.Node, weight float64) {
func DijkstraFromTo(u, t graph.Node, g traverse.Graph) (path []graph.Node, weight float64) {

The second param is not a v.

// An implementation of Bidirectional Dijkstra's algorithm could be even
// more efficient, but it requires a transposed (or undirected) graph.
if v == nil {
panic("dijkstra: nil target node")
}
return dijkstraFrom(u, v, g).To(v.ID())
}

// Shared implementation of DijkstraFrom and DijkstraFromTo.
func dijkstraFrom(u, target graph.Node, g traverse.Graph) Shortest {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Shared implementation of DijkstraFrom and DijkstraFromTo.
func dijkstraFrom(u, target graph.Node, g traverse.Graph) Shortest {
func dijkstraFrom(u, t graph.Node, g traverse.Graph) Shortest {

var path Shortest
if h, ok := g.(graph.Graph); ok {
// Use the incremental version when a target is provided
if h, ok := g.(graph.Graph); target == nil && ok {
if h.Node(u.ID()) == nil {
return Shortest{from: u}
}
Expand Down Expand Up @@ -58,6 +80,9 @@ func DijkstraFrom(u graph.Node, g traverse.Graph) Shortest {
continue
}
mnid := mid.node.ID()
if target != nil && mnid == target.ID() {
break
}
to := g.From(mnid)
for to.Next() {
v := to.Node()
Expand Down
66 changes: 46 additions & 20 deletions graph/path/dijkstra_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,29 @@ func TestDijkstraFrom(t *testing.T) {
g.SetWeightedEdge(e)
}

checkPath := func(typ string, p []graph.Node, weight float64) {
if weight != test.Weight {
t.Errorf("%q %s: unexpected weight from To: got:%f want:%f",
test.Name, typ, weight, test.Weight)
}

var got []int64
for _, n := range p {
got = append(got, n.ID())
}
ok := len(got) == 0 && len(test.WantPaths) == 0
for _, sp := range test.WantPaths {
if reflect.DeepEqual(got, sp) {
ok = true
break
}
}
if !ok {
t.Errorf("%q %s: unexpected shortest path:\ngot: %v\nwant from:%v",
test.Name, typ, p, test.WantPaths)
}
}

for _, tg := range []struct {
typ string
g traverse.Graph
Expand Down Expand Up @@ -56,38 +79,41 @@ func TestDijkstraFrom(t *testing.T) {
t.Fatalf("%q %s: unexpected from node ID: got:%d want:%d", test.Name, tg.typ, pt.From().ID(), test.Query.From().ID())
}

p, weight := pt.To(test.Query.To().ID())
if weight != test.Weight {
t.Errorf("%q %s: unexpected weight from To: got:%f want:%f",
test.Name, tg.typ, weight, test.Weight)
}
if weight := pt.WeightTo(test.Query.To().ID()); weight != test.Weight {
t.Errorf("%q %s: unexpected weight from Weight: got:%f want:%f",
test.Name, tg.typ, weight, test.Weight)
}

var got []int64
for _, n := range p {
got = append(got, n.ID())
}
ok := len(got) == 0 && len(test.WantPaths) == 0
for _, sp := range test.WantPaths {
if reflect.DeepEqual(got, sp) {
ok = true
break
}
}
if !ok {
t.Errorf("%q %s: unexpected shortest path:\ngot: %v\nwant from:%v",
test.Name, tg.typ, p, test.WantPaths)
}
p, weight := pt.To(test.Query.To().ID())
checkPath(tg.typ, p, weight)

np, weight := pt.To(test.NoPathFor.To().ID())
if pt.From().ID() == test.NoPathFor.From().ID() && (np != nil || !math.IsInf(weight, 1)) {
t.Errorf("%q %s: unexpected path:\ngot: path=%v weight=%f\nwant:path=<nil> weight=+Inf",
test.Name, tg.typ, np, weight)
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be in the loop, with helper sharing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Test the single-destination variant.
var (
p []graph.Node
weight float64
panicked bool
)
func() {
defer func() {
panicked = recover() != nil
}()
p, weight = DijkstraFromTo(test.Query.From(), test.Query.To(), g.(graph.Graph))
}()
if panicked || test.HasNegativeWeight {
if !test.HasNegativeWeight {
t.Errorf("%q: unexpected panic", test.Name)
}
continue
}

checkPath("from-to", p, weight)
}
}

Expand Down
4 changes: 2 additions & 2 deletions graph/path/yen_ksp.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func YenKShortestPaths(g graph.Graph, k int, cost float64, s, t graph.Node) [][]
yk.weight = UniformCost(g)
}

shortest, weight := DijkstraFrom(s, yk).To(t.ID())
shortest, weight := DijkstraFromTo(s, t, yk)
cost += weight // Set cost to absolute cost limit.
switch len(shortest) {
case 0:
Expand Down Expand Up @@ -73,7 +73,7 @@ func YenKShortestPaths(g graph.Graph, k int, cost float64, s, t graph.Node) [][]
yk.removeNode(u.ID())
}

spath, weight := DijkstraFrom(spur, yk).To(t.ID())
spath, weight := DijkstraFromTo(spur, t, yk)
if weight > cost || math.IsInf(weight, 1) {
continue
}
Expand Down