【问题标题】:Cycle detection within a Hash in RubyRuby中哈希内的循环检测
【发布时间】:2019-10-29 00:48:12
【问题描述】:

通过哈希,我有一个“工作”列表,每个工作都有一个 ID 和一个父级。具有父级的作业在其父级存在之前无法执行。如何检测依赖循环?

数据集如下图:

jobs = [
  {:id => 1,  :title => "a",  :pid => nil},
  {:id => 2,  :title => "b",  :pid => 3},
  {:id => 3,  :title => "c",  :pid => 6},
  {:id => 4,  :title => "d",  :pid => 1},
  {:id => 5,  :title => "e",  :pid => nil},
  {:id => 6,  :title => "f",  :pid => 2},
]

'id' 的序列因此是: 1 > 2 > 3 > 6 > 2 > 3 > 6.... 等

【问题讨论】:

    标签: floyd-cycle-finding


    【解决方案1】:

    这称为“拓扑排序”,Ruby 有它built in。当父母了解他们的孩子而不是孩子了解他们的父母时,它会更有效地工作。这是低效的版本;你可以通过重写你的数据结构来加速它(变成一个有:children而不是:pid的哈希,这样tsort_each_child就可以去node[:children].each而不是过滤整个数组)。

    由于TSort 被设计为混合使用,我们需要为数据创建一个新类(或者交替优化或污染Array)。 #tsort 将产生一个从孩子到父母排序的列表;既然你想要父母在孩子之前,我们可以#reverse结果。

    require 'tsort'
    
    class TSArray < Array
      include TSort
      alias tsort_each_node each
      def tsort_each_child(node)
        each { |child| yield child if child[:pid] == node[:id] }
      end
    end
    
    begin
      p TSArray.new(jobs).tsort.reverse
    rescue TSort::Cyclic
      puts "Nope."
    end
    

    【讨论】:

    • 嗨,谢谢 - 在实验中我可以移除反向并仍然获得相同的结果,我不确定这会产生不利影响吗?
    • 如果您只想检测周期性,则不需要reverse。如果您想在没有周期的情况下进行双亲排序(如您在问题中所述),那么您需要reverse,因为tsort 对双亲前排序。
    【解决方案2】:

    用于检测有向图中的环的各种算法是为任意有向图设计的。这里描绘的图表要简单得多,因为每个孩子最多有父母。这样可以很容易地确定是否存在循环,并且可以非常快速地完成。

    我将问题解释为,如果存在一个循环,您希望返回一个,而不仅仅是确定是否存在一个。

    代码

    require 'set'
    
    def cycle_present?(arr)
      kids_to_parent = arr.each_with_object({}) { |g,h| h[g[:id]] = g[:pid] }
      kids = kids_to_parent.keys
      while kids.any?
        kid = kids.first
        visited = [kid].to_set
        loop do
          parent = kids_to_parent[kid]
          break if parent.nil? || !kids.include?(parent)
          return construct_cycle(parent, kids_to_parent) unless visited.add?(parent)
          kid = parent 
        end
        kids -= visited.to_a
      end
      false
    end
    
    def construct_cycle(parent, kids_to_parent)
      arr = [parent]
      loop do
        parent = kids_to_parent[parent]
        arr << parent
        break arr if arr.first == parent
      end
    end
    

    示例

    cycle_present?(jobs)
      #=> [2, 3, 6, 2]
    
    arr = [{:id=>1, :title=>"a", :pid=>nil},
           {:id=>2, :title=>"b", :pid=>1},
           {:id=>3, :title=>"c", :pid=>1},
           {:id=>4, :title=>"d", :pid=>2},
           {:id=>5, :title=>"e", :pid=>2},
           {:id=>6, :title=>"f", :pid=>3}] 
    cycle_present?(arr)
      #=> false
    

    说明

    这里是带有 cmets 和 puts 语句的方法。

    def cycle_present?(arr)
      kids_to_parent = arr.each_with_object({}) { |g,h| h[g[:id]] = g[:pid] }
      puts "kids_to_parent = #{kids_to_parent}"                                #!!
      # kids are nodes that may be on a cycle
      kids = kids_to_parent.keys
      puts "kids = #{kids}"                                                    #!!
      while kids.any?
        # select a kid
        kid = kids.first
        puts "\nkid = #{kid}"                                                  #!!
        # construct a set initially containing kid
        visited = [kid].to_set
        puts "visited = #{visited}"                                            #!!
        puts "enter loop do"                                                   #!!
    

        loop do
          # determine kid's parent, if has one
          parent = kids_to_parent[kid]
          puts "  parent = #{parent}"                                          #!!
          if parent.nil?                                                       #!!
            puts "  parent.nil? = true, so break"                              #!!
          elsif !kids.include?(parent)
            puts "  kids.include?(parent) #=> false, parent has been excluded" #!!
          end                                                                  #!!
          # if the kid has no parent or the parent has already been removed
          # from kids we can break and eliminate all kids in visited
          break if parent.nil? || !kids.include?(parent)
          # try to add parent to set of visited nodes; if can't we have
          # discovered a cycle and are finished
          puts "  visited.add?(parent) = #{!visited.include?(parent)}"         #!! 
          puts "  return construct_cycle(parent, kids_to_parent)" if
            visited.include?(parent)                                           #!!
          return construct_cycle(parent, kids_to_parent) unless visited.add?(parent)
          puts "  now visited = #{visited}"                                    #!!
          # the new kid is the parent of the former kid
          puts "  set kid = #{parent}"                                         #!!
          kid = parent 
        end
    

        # we found a kid with no parent, or a parent who has already
        # been removed from kids, so remove all visited nodes
        puts "after loop, set kids = #{kids - visited.to_a}"                   #!!
        kids -= visited.to_a
      end
      puts "after while loop, return false"                                    #!!
      false
    end
    

    def construct_cycle(parent, kids_to_parent)
      puts
      arr = [parent]
      loop do
        parent = kids_to_parent[parent] 
        puts "arr = #{arr}, parent = #{parent}                                 #!!
        arr << parent
        break arr if arr.first == parent
      end
    end
    

    cycle_present?(jobs)
    

    显示以下内容:

    kid = 1
    visited = #<Set: {1}>
    enter loop do
      parent = 
      parent.nil? = true, so break
    after loop, set kids = [2, 3, 4, 5, 6]
    
    kid = 2
    visited = #<Set: {2}>
    enter loop do
      parent = 3
      visited.add?(parent) = true
      now visited = #<Set: {2, 3}>
      set kid = 3
      parent = 6
      visited.add?(parent) = true
      now visited = #<Set: {2, 3, 6}>
      set kid = 6
      parent = 2
      visited.add?(parent) = false
      return construct_cycle(parent, kids_to_parent)
    
    arr=[2], parent = 3
    arr=[2, 3], parent = 6
    arr=[2, 3, 6], parent = 2
      #=> [2, 3, 6, 2] 
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2013-02-07
      • 2015-08-25
      • 1970-01-01
      • 2012-03-05
      • 1970-01-01
      • 1970-01-01
      • 2012-01-11
      相关资源
      最近更新 更多