-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathGraph+TransitiveReduction.swift
More file actions
99 lines (76 loc) · 3.59 KB
/
Graph+TransitiveReduction.swift
File metadata and controls
99 lines (76 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import SwiftyToolz
public extension Graph
{
/**
The [minumum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction) of an **acyclic** `Graph`
🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones!
*/
func filteredTransitiveReduction() -> Self
{
var minimumEquivalentGraph = self
minimumEquivalentGraph.filterTransitiveReduction()
return minimumEquivalentGraph
}
/**
Filter an **acyclic** `Graph` down to its [minumum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction)
🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones!
*/
mutating func filterTransitiveReduction()
{
filterEdges(findTransitiveReductionEdges())
}
/**
Edges that are *not* in the [minumum equivalent graph](https://en.wikipedia.org/wiki/Transitive_reduction) of an **acyclic** `Graph`
🛑 This only works on acyclic graphs and might even hang or crash on cyclic ones!
*/
func findTransitiveReductionEdges() -> EdgeIDs
{
var idsOfTransitiveEdges = EdgeIDs() // or "shortcuts" of longer paths; or "implied" edges
var consideredAncestorsHash = [NodeID: NodeIDs]()
for sourceNode in sources
{
idsOfTransitiveEdges += findTransitiveEdges(around: sourceNode,
reachedAncestors: [],
consideredAncestorsHash: &consideredAncestorsHash)
}
let idsOfAllEdges = EdgeIDs(edgeIDs)
return idsOfAllEdges - idsOfTransitiveEdges
}
private func findTransitiveEdges(around node: Node,
reachedAncestors: NodeIDs,
consideredAncestorsHash: inout [NodeID: NodeIDs]) -> EdgeIDs
{
// to make this not hang in cycles it might be enough to just ensure that node is not in reachedAncestors ...
let consideredAncestors = consideredAncestorsHash[node.id, default: NodeIDs()]
let ancestorsToConsider = reachedAncestors - consideredAncestors
if !reachedAncestors.isEmpty && ancestorsToConsider.isEmpty
{
// found shortcut edge on a path we've already traversed, so we reached no new ancestors
return []
}
consideredAncestorsHash[node.id, default: NodeIDs()] += ancestorsToConsider
var idsOfTransitiveEdges = EdgeIDs()
// base case: add edges from all reached ancestors to all reachable neighbours of node
let descendants = node.descendantIDs
for descendant in descendants
{
for ancestor in ancestorsToConsider
{
let edgeID = Edge.ID(ancestor, descendant)
if contains(edgeID)
{
idsOfTransitiveEdges += edgeID
}
}
}
// recursive calls on descendants
for descendantID in descendants
{
guard let descendant = self.node(with: descendantID) else { continue }
idsOfTransitiveEdges += findTransitiveEdges(around: descendant,
reachedAncestors: ancestorsToConsider + node.id,
consideredAncestorsHash: &consideredAncestorsHash)
}
return idsOfTransitiveEdges
}
}