summaryrefslogtreecommitdiff
path: root/lib/tsort.rb
blob: 2c3c4b2c4e8a5bd575eea2596d92e8b3b5ba30bb (plain)
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
=begin
= tsort.rb

tsort.rb provides a module for topological sorting and
strongly connected components.

== Example

  require 'tsort'

  class Hash
    include TSort
    alias tsort_each_node each_key
    def tsort_each_child(node, &block)
      fetch(node).each(&block)
    end
  end

  {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
  #=> [3, 2, 1, 4]

  {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components
  #=> [[4], [2, 3], [1]]

== TSort module
TSort implements topological sorting using Tarjan's algorithm for
strongly connected components.

TSort is designed to be able to use with any object which can be interpreted
as a directed graph.
TSort requires two methods to interpret a object as a graph:
tsort_each_node and tsort_each_child.

* tsort_each_node is used to iterate for all nodes over a graph.
* tsort_each_child is used to iterate for child nodes of a given node.

The equality of nodes are defined by eql? and hash since
TSort uses Hash internally.

=== methods
--- tsort 
    returns a topologically sorted array of nodes.
    The array is sorted from children to parents:
    I.e. the first element has no child and the last node has no parent.

    If there is a cycle, (({TSort::Cyclic})) is raised.

--- tsort_each {|node| ...}
    is the iterator version of the (({tsort})) method.
    (({((|obj|)).tsort_each})) is similar to (({((|obj|)).tsort.each})) but
    modification of ((|obj|)) during the iteration may cause unexpected result.

    (({tsort_each})) returns (({nil})).
    If there is a cycle, (({TSort::Cyclic})) is raised.

--- strongly_connected_components
    returns strongly connected components as an array of array of nodes.
    The array is sorted from children to parents.
    Each elements of the array represents a strongly connected component.

--- each_strongly_connected_component {|nodes| ...}
    is the iterator version of the (({strongly_connected_components})) method.
    (({((|obj|)).each_strongly_connected_component})) is similar to
    (({((|obj|)).strongly_connected_components.each})) but
    modification of ((|obj|)) during the iteration may cause unexpected result.

    (({each_strongly_connected_component})) returns (({nil})).

--- each_strongly_connected_component_from(node) {|nodes| ...}
    iterates over strongly connected component in the subgraph reachable from 
    ((|node|)).

    Return value is unspecified.

    (({each_strongly_connected_component_from})) doesn't call
    (({tsort_each_node})).

--- tsort_each_node {|node| ...}
    should be implemented by a extended class.

--- tsort_each_child(node) {|child| ...}
    should be implemented by a extended class.

== More Realistic Example
Very simple `make' like tool can be implemented as follows:

  require 'tsort'

  class Make
    def initialize
      @dep = {}
      @dep.default = []
    end

    def rule(outputs, inputs=[], &block)
      triple = [outputs, inputs, block]
      outputs.each {|f| @dep[f] = [triple]}
      @dep[triple] = inputs
    end

    def build(target)
      each_strongly_connected_component_from(target) {|ns|
        if ns.length != 1
          fs = ns.delete_if {|n| Array === n}
          raise TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}")
        end
        n = ns.first
        if Array === n
          outputs, inputs, block = n
          inputs_time = inputs.map {|f| File.mtime f}.max
          begin
            outputs_time = outputs.map {|f| File.mtime f}.min
          rescue Errno::ENOENT
            outputs_time = nil
          end
          if outputs_time == nil ||
             inputs_time != nil && outputs_time <= inputs_time
            sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i
            block.call
          end
        end
      }
    end

    def tsort_each_child(node, &block)
      @dep[node].each(&block)
    end
    include TSort
  end

  def command(arg)
    print arg, "\n"
    system arg
  end

  m = Make.new
  m.rule(%w[t1]) { command 'date > t1' }
  m.rule(%w[t2]) { command 'date > t2' }
  m.rule(%w[t3]) { command 'date > t3' }
  m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' }
  m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' }
  m.build('t5')

== Bugs

* (('tsort.rb')) is wrong name because this library uses
  Tarjan's algorithm for strongly connected components.
  Although (('strongly_connected_components.rb')) is correct but too long,

== References
R. E. Tarjan, 
Depth First Search and Linear Graph Algorithms,
SIAM Journal on Computing, Vol. 1, No. 2, pp. 146-160, June 1972.

#@Article{Tarjan:1972:DFS,
#  author =       "R. E. Tarjan",
#  key =          "Tarjan",
#  title =        "Depth First Search and Linear Graph Algorithms",
#  journal =      j-SIAM-J-COMPUT,
#  volume =       "1",
#  number =       "2",
#  pages =        "146--160",
#  month =        jun,
#  year =         "1972",
#  CODEN =        "SMJCAT",
#  ISSN =         "0097-5397 (print), 1095-7111 (electronic)",
#  bibdate =      "Thu Jan 23 09:56:44 1997",
#  bibsource =    "Parallel/Multi.bib, Misc/Reverse.eng.bib",
#}
=end

module TSort
  class Cyclic < StandardError
  end

  def tsort
    result = []
    tsort_each {|element| result << element}
    result
  end

  def tsort_each
    each_strongly_connected_component {|component|
      if component.size == 1
        yield component.first
      else
        raise Cyclic.new "topological sort failed: #{component.inspect}"
      end
    }
  end

  def strongly_connected_components
    result = []
    each_strongly_connected_component {|component| result << component}
    result
  end

  def each_strongly_connected_component(&block)
    id_map = {}
    stack = []
    tsort_each_node {|node|
      unless id_map.include? node
        each_strongly_connected_component_from(node, id_map, stack, &block)
      end
    }
    nil
  end

  def each_strongly_connected_component_from(node, id_map={}, stack=[], &block)
    minimum_id = node_id = id_map[node] = id_map.size
    stack_length = stack.length
    stack << node

    tsort_each_child(node) {|child|
      if id_map.include? child
        child_id = id_map[child]
        minimum_id = child_id if child_id && child_id < minimum_id
      else
        sub_minimum_id =
          each_strongly_connected_component_from(child, id_map, stack, &block)
        minimum_id = sub_minimum_id if sub_minimum_id < minimum_id
      end
    }

    if node_id == minimum_id
      component = stack.slice!(stack_length .. -1)
      component.each {|n| id_map[n] = nil}
      yield component
    end

    minimum_id
  end

  def tsort_each_node
    raise NotImplementedError.new
  end

  def tsort_each_child(node)
    raise NotImplementedError.new
  end
end

if __FILE__ == $0
  require 'runit/testcase'
  require 'runit/cui/testrunner'

  class Hash
    include TSort
    alias tsort_each_node each_key
    def tsort_each_child(node, &block)
      fetch(node).each(&block)
    end
  end

  class Array
    include TSort
    alias tsort_each_node each_index
    def tsort_each_child(node, &block)
      fetch(node).each(&block)
    end
  end

  class TSortTest < RUNIT::TestCase
    def test_dag
      h = {1=>[2, 3], 2=>[3], 3=>[]}
      assert_equal([3, 2, 1], h.tsort)
      assert_equal([[3], [2], [1]], h.strongly_connected_components)
    end

    def test_cycle
      h = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
      assert_equal([[4], [2, 3], [1]],
        h.strongly_connected_components.map {|nodes| nodes.sort})
      assert_exception(TSort::Cyclic) { h.tsort }
    end

    def test_array
      a = [[1], [0], [0], [2]]
      assert_equal([[0, 1], [2], [3]],
        a.strongly_connected_components.map {|nodes| nodes.sort})

      a = [[], [0]]
      assert_equal([[0], [1]],
        a.strongly_connected_components.map {|nodes| nodes.sort})
    end
  end

  RUNIT::CUI::TestRunner.run(TSortTest.suite)
end