dag: TransitiveReduction
This commit is contained in:
parent
abd68c2c87
commit
ed2075e384
74
dag/dag.go
74
dag/dag.go
|
@ -40,6 +40,46 @@ func (g *AcyclicGraph) Root() (Vertex, error) {
|
||||||
return roots[0], nil
|
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
|
// Validate validates the DAG. A DAG is valid if it has a single root
|
||||||
// with no cycles.
|
// with no cycles.
|
||||||
func (g *AcyclicGraph) Validate() error {
|
func (g *AcyclicGraph) Validate() error {
|
||||||
|
@ -161,3 +201,37 @@ func (g *AcyclicGraph) Walk(cb WalkFunc) error {
|
||||||
<-doneCh
|
<-doneCh
|
||||||
return errs
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package dag
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
@ -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) {
|
func TestAcyclicGraphValidate(t *testing.T) {
|
||||||
var g AcyclicGraph
|
var g AcyclicGraph
|
||||||
g.Add(1)
|
g.Add(1)
|
||||||
|
@ -156,3 +195,21 @@ func TestAcyclicGraphWalk_error(t *testing.T) {
|
||||||
|
|
||||||
t.Fatalf("bad: %#v", visits)
|
t.Fatalf("bad: %#v", visits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const testGraphTransReductionStr = `
|
||||||
|
1
|
||||||
|
2
|
||||||
|
2
|
||||||
|
3
|
||||||
|
3
|
||||||
|
`
|
||||||
|
|
||||||
|
const testGraphTransReductionMoreStr = `
|
||||||
|
1
|
||||||
|
2
|
||||||
|
2
|
||||||
|
3
|
||||||
|
3
|
||||||
|
4
|
||||||
|
4
|
||||||
|
`
|
||||||
|
|
14
dag/set.go
14
dag/set.go
|
@ -36,6 +36,20 @@ func (s *Set) Include(v interface{}) bool {
|
||||||
return ok
|
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.
|
// Len is the number of items in the set.
|
||||||
func (s *Set) Len() int {
|
func (s *Set) Len() int {
|
||||||
if s == nil {
|
if s == nil {
|
||||||
|
|
Loading…
Reference in New Issue