Skip to content

Commit

Permalink
dag: TransitiveReduction
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchellh committed Feb 28, 2015
1 parent abd68c2 commit ed2075e
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 0 deletions.
74 changes: 74 additions & 0 deletions dag/dag.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,46 @@ func (g *AcyclicGraph) Root() (Vertex, error) {
return roots[0], nil
}

// TransitiveReduction performs the transitive reduction of graph g in place.
// The transitive reduction of a graph is a graph with as few edges as
// possible with the same reachability as the original graph. This means
// that if there are three nodes A => B => C, and A connects to both
// B and C, and B connects to C, then the transitive reduction is the
// same graph with only a single edge between A and B, and a single edge
// between B and C.
//
// The graph must be valid for this operation to behave properly. If
// Validate() returns an error, the behavior is undefined and the results
// will likely be unexpected.
//
// Complexity: O(V(V+E)), or asymptotically O(VE)
func (g *AcyclicGraph) TransitiveReduction() {
// Find the root-like objects to start a depthFirstWalk from.
frontier := make([]Vertex, 0, 5)
for _, v := range g.Vertices() {
if g.UpEdges(v).Len() == 0 {
frontier = append(frontier, v)
}
}

// Do a DFS
g.depthFirstWalk(frontier, func(v Vertex) error {
parents := g.UpEdges(v).List()
targets := g.DownEdges(v)

for _, rawParent := range parents {
parent := rawParent.(Vertex)
shared := g.DownEdges(parent).Intersection(targets)
for _, rawTarget := range shared.List() {
target := rawTarget.(Vertex)
g.RemoveEdge(BasicEdge(parent, target))
}
}

return nil
})
}

// Validate validates the DAG. A DAG is valid if it has a single root
// with no cycles.
func (g *AcyclicGraph) Validate() error {
Expand Down Expand Up @@ -161,3 +201,37 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
<-doneCh
return errs
}

// depthFirstWalk does a depth-first walk of the graph starting from
// the vertices in start. This is not exported now but it would make sense
// to export this publicly at some point.
func (g *AcyclicGraph) depthFirstWalk(start []Vertex, cb WalkFunc) error {
seen := make(map[Vertex]struct{})
frontier := make([]Vertex, len(start))
copy(frontier, start)
for len(frontier) > 0 {
// Pop the current vertex
n := len(frontier)
current := frontier[n-1]
frontier = frontier[:n-1]

// Check if we've seen this already and return...
if _, ok := seen[current]; ok {
continue
}
seen[current] = struct{}{}

// Visit the current node
if err := cb(current); err != nil {
return err
}

// Visit targets of this in reverse order.
targets := g.DownEdges(current).List()
for i := len(targets) - 1; i >= 0; i-- {
frontier = append(frontier, targets[i].(Vertex))
}
}

return nil
}
57 changes: 57 additions & 0 deletions dag/dag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dag
import (
"fmt"
"reflect"
"strings"
"sync"
"testing"
)
Expand Down Expand Up @@ -48,6 +49,44 @@ func TestAcyclicGraphRoot_multiple(t *testing.T) {
}
}

func TestAyclicGraphTransReduction(t *testing.T) {
var g AcyclicGraph
g.Add(1)
g.Add(2)
g.Add(3)
g.Connect(BasicEdge(1, 2))
g.Connect(BasicEdge(1, 3))
g.Connect(BasicEdge(2, 3))
g.TransitiveReduction()

actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphTransReductionStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}

func TestAyclicGraphTransReduction_more(t *testing.T) {
var g AcyclicGraph
g.Add(1)
g.Add(2)
g.Add(3)
g.Add(4)
g.Connect(BasicEdge(1, 2))
g.Connect(BasicEdge(1, 3))
g.Connect(BasicEdge(1, 4))
g.Connect(BasicEdge(2, 3))
g.Connect(BasicEdge(2, 4))
g.Connect(BasicEdge(3, 4))
g.TransitiveReduction()

actual := strings.TrimSpace(g.String())
expected := strings.TrimSpace(testGraphTransReductionMoreStr)
if actual != expected {
t.Fatalf("bad: %s", actual)
}
}

func TestAcyclicGraphValidate(t *testing.T) {
var g AcyclicGraph
g.Add(1)
Expand Down Expand Up @@ -156,3 +195,21 @@ func TestAcyclicGraphWalk_error(t *testing.T) {

t.Fatalf("bad: %#v", visits)
}

const testGraphTransReductionStr = `
1
2
2
3
3
`

const testGraphTransReductionMoreStr = `
1
2
2
3
3
4
4
`
14 changes: 14 additions & 0 deletions dag/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ func (s *Set) Include(v interface{}) bool {
return ok
}

// Intersection computes the set intersection with other.
func (s *Set) Intersection(other *Set) *Set {
result := new(Set)
if other != nil {
for _, v := range s.m {
if other.Include(v) {
result.Add(v)
}
}
}

return result
}

// Len is the number of items in the set.
func (s *Set) Len() int {
if s == nil {
Expand Down

0 comments on commit ed2075e

Please sign in to comment.