applicationsonline/librarian

View on GitHub
lib/librarian/algorithms.rb

Summary

Maintainability
A
35 mins
Test Coverage
require "set"
require "tsort"

module Librarian
  module Algorithms

    class GraphHash < Hash
      include TSort
      def tsort_each_node(&block)
        keys.sort.each(&block) # demand determinism
      end
      def tsort_each_child(node, &block)
        children = self[node]
        children && children.sort.each(&block) # demand determinism
      end
      class << self
        def from(hash)
          o = new
          hash.each{|k, v| o[k] = v}
          o
        end
      end
    end

    module AdjacencyListDirectedGraph
      extend self

      def cyclic?(graph)
        each_cyclic_strongly_connected_component_set(graph).any?
      end

      # Topological sort of the graph with an approximately minimal feedback arc
      # set removed.
      def tsort_cyclic(graph)
        fag = feedback_arc_graph(graph)
        reduced_graph = subtract_edges_graph(graph, fag)
        GraphHash.from(reduced_graph).tsort
      end

      # Returns an approximately minimal feedback arc set, lifted into a graph.
      def feedback_arc_graph(graph)
        edges_to_graph(feedback_arc_set(graph))
      end

      # Returns an approximately minimal feedback arc set.
      def feedback_arc_set(graph)
        fas = feedback_arc_set_step0(graph)
        feedback_arc_set_step1(graph, fas)
      end

      private

      def edges_to_graph(edges)
        graph = {}
        edges.each do |(u, v)|
          graph[u] ||= []
          graph[u] << v
          graph[v] ||= nil
        end
        graph
      end

      def subtract_edges_graph(graph, edges_graph)
        xgraph = {}
        graph.each do |n, vs|
          dests = edges_graph[n]
          xgraph[n] = !vs ? vs : !dests ? vs : vs - dests
        end
        xgraph
      end

      def each_cyclic_strongly_connected_component_set(graph)
        return enum_for(__method__, graph) unless block_given?
        GraphHash.from(graph).each_strongly_connected_component do |scc|
          if scc.size == 1
            n = scc.first
            vs = graph[n] or next
            vs.include?(n) or next
          end
          yield scc
        end
      end

      # Partitions the graph into its strongly connected component subgraphs,
      # removes the acyclic single-vertex components (multi-vertex components
      # are guaranteed to be cyclic), and yields each cyclic strongly connected
      # component.
      def each_cyclic_strongly_connected_component_graph(graph)
        return enum_for(__method__, graph) unless block_given?
        each_cyclic_strongly_connected_component_set(graph) do |scc|
          sccs = scc.to_set
          sccg = GraphHash.new
          scc.each do |n|
            vs = graph[n]
            sccg[n] = vs && vs.select{|v| sccs.include?(v)}
          end
          yield sccg
        end
      end

      # The 0th step of computing a feedback arc set: pick out vertices whose
      # removals will make the graph acyclic.
      def feedback_arc_set_step0(graph)
        fas = []
        each_cyclic_strongly_connected_component_graph(graph) do |scc|
          scc.keys.sort.each do |n| # demand determinism
            vs = scc[n] or next
            vs.size > 0 or next
            vs.sort! # demand determinism
            fas << [n, vs.shift]
            break
          end
        end
        fas
      end

      # The 1st step of computing a feedback arc set: pick out vertices from the
      # 0th step whose removals turn out to be unnecessary.
      def feedback_arc_set_step1(graph, fas)
        reduced_graph = subtract_edges_graph(graph, edges_to_graph(fas))
        fas.select do |(u, v)|
          reduced_graph[u] ||= []
          reduced_graph[u] << v
          cyclic = cyclic?(reduced_graph)
          reduced_graph[u].pop if cyclic
          cyclic
        end
      end

    end

  end
end