lib/rgl/transitivity.rb
require 'rgl/base' require 'rgl/adjacency' require 'rgl/condensation' module RGL module Graph # Returns an {DirectedAdjacencyGraph} which is the transitive closure of # this graph. Meaning, for each path u -> ... -> v in this graph, the path # is copied and the edge u -> v is added. This method supports working with # cyclic graphs by ensuring that edges are created between every pair of # vertices in the cycle, including self-referencing edges. # # This method should run in O(|V||E|) time, where |V| and |E| are the number # of vertices and edges respectively. # # Raises {NotDirectedError} if run on an undirected graph. # @return DirectedAdjacencyGraph def transitive_closure raise NotDirectedError, "transitive_closure only supported for directed graphs" unless directed? # Compute a condensation graph in order to hide cycles. cg = condensation_graph # Use a depth first search to calculate the transitive closure over the # condensation graph. This ensures that as we traverse up the graph we # know the transitive closure of each subgraph rooted at each node # starting at the leaves. Subsequent root nodes which consume these # subgraphs by way of the nodes' immediate successors can then immediately # add edges to the roots of the subgraphs and to every successor of those # roots. tc_cg = DirectedAdjacencyGraph.new cg.depth_first_search do |v| # For each vertex v, w, and x where the edges v -> w and w -> x exist in # the source graph, add edges v -> w and v -> x to the target graph. cg.each_adjacent(v) do |w| tc_cg.add_edge(v, w) tc_cg.each_adjacent(w) do |x| tc_cg.add_edge(v, x) end end # Ensure that a vertex with no in or out edges is added to the graph. tc_cg.add_vertex(v) end # Expand the condensed transitive closure. # # For each trivial strongly connected component in the condensed graph, # add the single node it contains to the new graph and add edges for each # edge the node begins in the original graph. # For each NON-trivial strongly connected component in the condensed # graph, add each node it contains to the new graph and add edges to # every node in the strongly connected component, including self # referential edges. Then for each edge of the original graph from any # of the contained nodes, add edges from each of the contained nodes to # all the edge targets. g = DirectedAdjacencyGraph.new tc_cg.each_vertex do |scc| scc.each do |v| # Add edges between all members of non-trivial strongly connected # components (size > 1) and ensure that self referential edges are # added when necessary for trivial strongly connected components. if scc.size > 1 || has_edge?(v, v) scc.each do |w| g.add_edge(v, w) end end # Ensure that a vertex with no in or out edges is added to the graph. g.add_vertex(v) end # Add an edge from every member of a strongly connected component to # every member of each strongly connected component to which the former # points. tc_cg.each_adjacent(scc) do |scc2| scc.each do |v| scc2.each do |w| g.add_edge(v, w) end end end end # Finally, the transitive closure... g end # Returns an {DirectedAdjacencyGraph} which is the transitive reduction # of this graph. Meaning, that each edge u -> v is omitted if path # u -> ... -> v exists. This method supports working with cyclic graphs; # however, cycles are arbitrarily simplified which may lead to variant, # although equally valid, results on equivalent graphs. # # This method should run in O(|V||E|) time, where |V| and |E| are the number # of vertices and edges respectively. # # Raises {NotDirectedError} if run on an undirected graph. # @return DirectedAdjacencyGraph def transitive_reduction raise NotDirectedError, "transitive_reduction only supported for directed graphs" unless directed? # Compute a condensation graph in order to hide cycles. cg = condensation_graph # Use a depth first search to compute the transitive reduction over the # condensed graph. This is similar to the computation of the transitive # closure over the graph in that for any node of the graph all nodes # reachable from the node are tracked. Using a depth first search ensures # that all nodes reachable from a target node are known when considering # whether or not to add an edge pointing to that target. tr_cg = DirectedAdjacencyGraph.new paths_from = {} cg.depth_first_search do |v| paths_from[v] = Set.new cg.each_adjacent(v) do |w| # Only add the edge v -> w if there is no other edge v -> x such that # w is reachable from x. Make sure to completely skip the case where # x == w. unless cg.to_enum(:each_adjacent, v).any? { |x| x != w && paths_from[x].include?(w) } tr_cg.add_edge(v, w) # For each vertex v, track all nodes reachable from v by adding node # w to the list as well as all the nodes readable from w. paths_from[v] << w paths_from[v].merge(paths_from[w]) end end # Ensure that a vertex with no in or out edges is added to the graph. tr_cg.add_vertex(v) end # Expand the condensed transitive reduction. # # For each trivial strongly connected component in the condensed graph, # add the single node it contains to the new graph and add edges for each # edge the node begins in the original graph. # For each NON-trivial strongly connected component in the condensed # graph, add each node it contains to the new graph and add arbitrary # edges between the nodes to form a simple cycle. Then for each strongly # connected component adjacent to the current one, find and add the first # edge which exists in the original graph, starts in the first strongly # connected component, and ends in the second strongly connected # component. g = DirectedAdjacencyGraph.new tr_cg.each_vertex do |scc| # Make a cycle of the contents of non-trivial strongly connected # components. scc_arr = scc.to_a if scc.size > 1 || has_edge?(scc_arr.first, scc_arr.first) 0.upto(scc_arr.size - 2) do |idx| g.add_edge(scc_arr[idx], scc_arr[idx + 1]) end g.add_edge(scc_arr.last, scc_arr.first) end # Choose a single edge between the members of two different strongly # connected component to add to the graph. edges = self.to_enum(:each_edge) tr_cg.each_adjacent(scc) do |scc2| g.add_edge( *edges.find do |v, w| scc.member?(v) && scc2.member?(w) end ) end # Ensure that a vertex with no in or out edges is added to the graph. scc.each do |v| g.add_vertex(v) end end # Finally, the transitive reduction... g end end # module Graph end # module RGL