Skip to content

Commit

Permalink
path: allow Shortest to traverse negative cycles
Browse files Browse the repository at this point in the history
Also add checks for cases where negative cycles exist but are not marked
to protect against future shortest path function additions.
  • Loading branch information
kortschak committed Aug 30, 2017
1 parent d7342e6 commit da91c24
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 7 deletions.
1 change: 1 addition & 0 deletions graph/path/bellman_ford_moore.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func BellmanFordFrom(u graph.Node, g graph.Graph) (path Shortest, ok bool) {
panic("bellman-ford: unexpected invalid weight")
}
if path.dist[j]+w < path.dist[k] {
path.hasNegativeCycle = true
return path, false
}
}
Expand Down
59 changes: 59 additions & 0 deletions graph/path/negative_cycles_example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright ©2017 The gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package path_test

import (
"fmt"
"math"

"gonum.org/v1/gonum/graph/path"
"gonum.org/v1/gonum/graph/simple"
)

func ExampleBellmanFordFrom_negativecycles() {
// BellmanFordFrom can be used to find a non-exhaustive
// set of negative cycles in a graph. Enumerating the
// exhaustive list requires iterations of the procedure
// here successively omitting links from the new node
// to already found negative cycles.

// Construct a graph with a negative cycle.
edges := []simple.WeightedEdge{
{F: simple.Node('a'), T: simple.Node('b'), W: -2},
{F: simple.Node('b'), T: simple.Node('c'), W: 6},
{F: simple.Node('c'), T: simple.Node('a'), W: -5},
{F: simple.Node('d'), T: simple.Node('c'), W: -3},
{F: simple.Node('d'), T: simple.Node('e'), W: 8},
{F: simple.Node('e'), T: simple.Node('b'), W: 9},
{F: simple.Node('e'), T: simple.Node('c'), W: 2},
}
g := simple.NewWeightedDirectedGraph(0, math.Inf(1))
for _, e := range edges {
g.SetWeightedEdge(e)
}

// Add a zero-cost path to all nodes from a new node Q.
for _, n := range g.Nodes() {
g.SetWeightedEdge(simple.WeightedEdge{F: simple.Node('Q'), T: n})
}

// Find the shortest path to each node from Q.
pt, ok := path.BellmanFordFrom(simple.Node('Q'), g)
if ok {
fmt.Println("no negative cycle present")
return
}
for _, n := range []simple.Node{'a', 'b', 'c', 'd', 'e'} {
p, w := pt.To(n)
if math.IsNaN(w) {
fmt.Printf("negative cycle in path to %c path:%c\n", n, p)
}
}

// Output:
// negative cycle in path to a path:[a b c a]
// negative cycle in path to b path:[b c a b]
// negative cycle in path to c path:[c a b c]
}
46 changes: 39 additions & 7 deletions graph/path/shortest.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import (
"math/rand"

"gonum.org/v1/gonum/graph"
"gonum.org/v1/gonum/graph/internal/set"
"gonum.org/v1/gonum/mat"
)

// Shortest is a shortest-path tree created by the BellmanFordFrom or DijkstraFrom
// single-source shortest path functions.
type Shortest struct {
// from holds the source node given to
// DijkstraFrom.
// the function that returned the
// Shortest value.
from graph.Node

// nodes hold the nodes of the analysed
Expand All @@ -42,6 +44,13 @@ type Shortest struct {
// tree of the graph. The index is a
// linear mapping of to-dense-id.
next []int

// hasNegativeCycle indicates
// whether the Shortest includes
// a negative cycle. This should
// be set by the function that
// returned the Shortest value.
hasNegativeCycle bool
}

func newShortestFrom(u graph.Node, nodes []graph.Node) Shortest {
Expand Down Expand Up @@ -80,7 +89,8 @@ func (p Shortest) set(to int, weight float64, mid int) {
// From returns the starting node of the paths held by the Shortest.
func (p Shortest) From() graph.Node { return p.from }

// WeightTo returns the weight of the minimum path to v.
// WeightTo returns the weight of the minimum path to v. If the path to v includes
// a negative cycle, the returned weight will not reflect the true path weight.
func (p Shortest) WeightTo(v graph.Node) float64 {
to, toOK := p.indexOf[v.ID()]
if !toOK {
Expand All @@ -89,20 +99,42 @@ func (p Shortest) WeightTo(v graph.Node) float64 {
return p.dist[to]
}

// To returns a shortest path to v and the weight of the path.
// To returns a shortest path to v and the weight of the path. If the path
// to v includes a negative cycle, one pass through the cycle will be included
// in path and weight will be returned as NaN.
func (p Shortest) To(v graph.Node) (path []graph.Node, weight float64) {
to, toOK := p.indexOf[v.ID()]
if !toOK || math.IsInf(p.dist[to], 1) {
return nil, math.Inf(1)
}
from := p.indexOf[p.from.ID()]
path = []graph.Node{p.nodes[to]}
for to != from {
path = append(path, p.nodes[p.next[to]])
to = p.next[to]
weight = math.Inf(1)
if p.hasNegativeCycle {
seen := make(set.Ints)
seen.Add(from)
for to != from {
if seen.Has(to) {
weight = math.NaN()
break
}
seen.Add(to)
path = append(path, p.nodes[p.next[to]])
to = p.next[to]
}
} else {
n := len(p.nodes)
for to != from {
path = append(path, p.nodes[p.next[to]])
to = p.next[to]
if n < 0 {
panic("path: unexpected negative cycle")
}
n--
}
}
reverse(path)
return path, p.dist[p.indexOf[v.ID()]]
return path, math.Min(weight, p.dist[p.indexOf[v.ID()]])
}

// AllShortest is a shortest-path tree created by the DijkstraAllPaths, FloydWarshall
Expand Down

0 comments on commit da91c24

Please sign in to comment.