rubylabs 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/tsplab.rb CHANGED
@@ -6,24 +6,15 @@
6
6
 
7
7
  =begin
8
8
  todo unit tests
9
- todo set up options, defaults as in mars, spheres (@@options...)
10
- todo :trace as one of the options, array of things to print
11
- TODO test predigree counts
12
- TODO add "phylogeny" links -- keep refs to parent(s)
13
- TODO test: selection only, no mutations
14
- TODO test: random draws only, no selection
15
- TODO plot space: get all 180K costs, order lexicographically, plot (smoothed)
16
-
17
- TODO hide mutate, cross methods -- but accept optional params in call to reproduce constructor so students can control effects to see how method works
18
- TODO (idea is that genes are fixed when critter made, don't change after that)
19
- =end
20
-
21
- =begin
22
- TODO clean up interface
23
- todo two constructors (mutate, cross) instead of one
24
- todo distances are floats, not ints
25
- todo wider histogram, up to 100 bins 3 pixels wide
26
- todo line color = blue if bin width 4 or less
9
+ todo keep track of how tour made, color those from crosses a different color
10
+ todo ? later -- hightlight changes when drawing tour
11
+ todo possible error in call to m.make_tour(:mutate,t) when t is not full size tour of m
12
+ todo kinda brittle with popsize param -- interaction between histogram and popsize needs
13
+ to be stabilized (see esp need to pass popsize to rebuild_population)
14
+ todo fix labels -- top level methods need to erase labels from rsearch or other experiments
15
+ todo to_s for maps should print : in front of symbol labels
16
+ todo clean up rsearch, add :begin/:end wrapper
17
+ todo catch ^C interrupt in esearch? each_permutation?
27
18
  =end
28
19
 
29
20
  module RubyLabs
@@ -31,170 +22,276 @@ module RubyLabs
31
22
  module TSPLab
32
23
 
33
24
  require 'set'
25
+ require "permute.rb"
26
+
27
+ MapView = Struct.new(:cities, :nodes, :links, :labels, :histogram, :options)
28
+ ItemWithDirection = Struct.new(:value, :direction)
29
+
30
+ class ItemWithDirection
31
+ def inspect
32
+ (self.direction ? "<" : ">") + self.value.inspect
33
+ end
34
+ end
34
35
 
35
- MapView = Struct.new(:cities, :nodes, :links, :histogram, :options)
36
-
37
36
  =begin rdoc
38
-
39
- Objects of the CityList class store city names and distances between pairs of
40
- cities. The instance variables are a hash associating a key with a
41
- city name and a matrix of distances. The matrix is a hash of
42
- hashes, indexed by city, where distances are driving times in
43
- minutes.
44
-
37
+ A Map is a 2D array of distances between pairs of cities. A new Map object
38
+ will initially be empty. Call m[i,j] = x to assign the distance between i and
39
+ j. Maps are symmetric (i.e. m[i,j] == m[j,i] for all i and j) so assigning a
40
+ value to m[i,j] automatically assigns the same value to m[j,i]. Indices can be
41
+ strings, symbols, or integers.
45
42
  =end
43
+
44
+ class Map
46
45
 
47
- # Public methods:
48
- # new(f) initializes an object using names and distances in file f
49
- # size() returns number of cities
50
- # keys() returns an array of keys
51
- # values() returns an array of city names
52
- # [x] returns the name of the city with key x
53
- # distance(x,y) returns the distance between cities with keys x and y
54
- # coords(x) returns x, y coordinates of city
46
+ # Make a new distance matrix.
55
47
 
56
- class CityList
57
-
58
- attr_reader :cities, :matrix
59
-
60
48
  def initialize(arg)
49
+ @labels = Array.new
50
+ @dist = Array.new
51
+ @coords = Array.new
61
52
  if arg.class == String || arg.class == Symbol
62
53
  read_file(arg)
63
- elsif arg.class == Array
64
- make_map(arg)
65
54
  elsif arg.class == Fixnum
66
55
  make_random_map(arg)
67
- else
68
- raise "CityList: parameter must be a file name, array of points, or an integer"
56
+ elsif arg.class == Array
57
+ make_map(arg)
58
+ elsif arc.class != NilClass
59
+ raise "Map.new: parameter must be a file name, array of points, or an integer"
69
60
  end
70
61
  end
71
62
 
72
- def inspect()
73
- n = @matrix.length
74
- return sprintf "TSPLab::CityList (%d x %d)", n, n-1
63
+ def inspect
64
+ sprintf "#<TSPLab::Map [%s]>", @labels.join(",")
75
65
  end
76
-
77
- def dump
78
- (?A..@key-1).each do |i|
79
- (i+1..@key-1).each do |j|
80
- printf "%s %s %d\n", @cities[i], @cities[j], @matrix[i][j]
66
+
67
+ alias to_s inspect
68
+
69
+ # print the current state of the metric; the parameter is the field width (number
70
+ # of chars in each matrix entry)
71
+
72
+ def display(fw = nil)
73
+ if fw.nil?
74
+ lw = labels.inject(0) { |max, x| n = x.to_s.length; (n > max) ? n : max }
75
+ dw = (log10(@maxdist).ceil+4)
76
+ fw = max(lw+1,dw)
77
+ end
78
+ res = " " * fw
79
+ @labels.each { |name| res << sprintf("%#{fw-1}s ", name.to_s[0..fw-1]) }
80
+ res += "\n"
81
+ @dist.each_with_index do |a,i|
82
+ res += sprintf("%#{fw-1}s ", @labels[i].to_s[0..fw-1])
83
+ a.each { |x| res += (x.nil? ? " -- " : sprintf("%#{fw}.2f", x)) }
84
+ res += "\n"
85
+ end
86
+ puts res
87
+ end
88
+
89
+ # method to make tours
90
+
91
+ def make_tour(*args)
92
+ begin
93
+ raise "usage" if args.length == 0
94
+ case args[0]
95
+ when :random
96
+ tour = Tour.new(self, permute!(labels)) # note labels returns clone of @labels array...
97
+ when :mutate
98
+ raise "usage" unless args.length >= 2 && args[1].class == Tour && (args[2].nil? || args[2].class == Fixnum)
99
+ child = args[1].clone
100
+ dmax = args[2] ? args[2] : 1
101
+ i = rand(size)
102
+ d = 1 + rand(dmax)
103
+ # puts "mutate #{i} #{d}"
104
+ tour = child.mutate!(i,d)
105
+ child.parent = args[1]
106
+ child.op = [:mutate, i, d]
107
+ when :cross
108
+ raise "usage" unless args.length == 3 && args[1].class == Tour && args[2].class == Tour
109
+ child = args[1].clone
110
+ i = rand(size)
111
+ n = 1 + rand(size-1)
112
+ # puts "cross #{i} #{n}"
113
+ tour = child.cross!(args[2], i, n)
114
+ child.parent = args[1]
115
+ child.op = [:cross, args[2], i, n]
116
+ else
117
+ raise "usage" unless args[0].class == Array
118
+ a = args[0]
119
+ errs = 0
120
+ a.each do |x|
121
+ if ! @labels.include?(x)
122
+ puts "unknown city: #{x}"
123
+ errs += 1
124
+ end
125
+ end
126
+ raise "errors in list of cities" if errs > 0
127
+ tour = Tour.new(self, a)
128
+ end
129
+ rescue Exception => e
130
+ if e.message == "usage"
131
+ puts "Usage:"
132
+ puts " make_tour( [x,y,..] )"
133
+ puts " make_tour( :random )"
134
+ puts " make_tour( :mutate, t [,n] )"
135
+ puts " make_tour( :cross, t1, t2 )"
136
+ else
137
+ puts "make_tour: #{e.message}"
138
+ end
139
+ return nil
140
+ end
141
+
142
+ return tour
143
+
144
+ end
145
+
146
+ # Generate all possible tours using the Johnson-Trotter algorithm. The method
147
+ # works by generating all permutations of array indices, then for each permutation
148
+ # generating an array of labels.
149
+
150
+ def each_tour
151
+ a = []
152
+ n = @labels.length
153
+ for i in 1...n do
154
+ a << ItemWithDirection.new(i, true)
155
+ end
156
+ loop do
157
+ # yield [0] + a
158
+ yield Tour.new(self, [@labels[0]] + a.map { |x| @labels[x.value] })
159
+ mover = nil
160
+ for i in 0...a.length
161
+ mover = i if movable(a,i) && (mover.nil? || a[i].value > a[mover].value)
81
162
  end
82
- end
83
- return true
84
- end
85
-
86
- def print_matrix
87
- # colarray =
88
- puts " " + (?B..(@key-1)).map { |x| x.chr }.join(" ")
89
- (?A..(@key-2)).each_with_index do |i, n|
90
- s = sprintf "%2s: ", i.chr
91
- s += " " * n
92
- (i+1..@key-1).each do |j|
93
- s += sprintf " %3d", @matrix[i][j]
163
+ break if mover.nil?
164
+ k = a[mover].value
165
+ # puts "mover = #{mover} k = #{k}"
166
+ break if k == 2
167
+ adj = a[mover].direction ? mover-1 : mover+1
168
+ a[adj], a[mover] = a[mover], a[adj]
169
+ for i in 0...a.length
170
+ if a[i].value > k
171
+ a[i].direction ^= true
172
+ end
94
173
  end
95
- puts s
96
- end
97
- return true
98
- end
99
-
100
- def size()
101
- return @key - ?A
174
+ end
175
+ end
176
+
177
+ # return the number of rows in the matrix
178
+
179
+ def size
180
+ return @dist.length
181
+ end
182
+
183
+ # return the first pair of city labels
184
+
185
+ def first
186
+ return @labels[0..1]
102
187
  end
103
188
 
104
- def keys()
105
- return @cities.keys.sort.map{|x| x.chr}
189
+ # this iterator will generate the remaining pairs of labels
190
+
191
+ def rest
192
+ n = @labels.length
193
+ @labels.each_with_index do |x,i|
194
+ @labels.last(n-i-1).each_with_index do |y,j|
195
+ next if i == 0 && j == 0
196
+ yield(x,y)
197
+ end
198
+ end
199
+ end
200
+
201
+ # get/set the distance between labels i and j (which can be)
202
+ # given in either order, i.e. d[i,j] is the same as d[j,i]
203
+
204
+ def [](i,j)
205
+ ix = @labels.index(i) or return nil
206
+ jx = @labels.index(j) or return nil
207
+ ix, jx = jx, ix if ix < jx
208
+ @dist[ix][jx]
106
209
  end
107
210
 
108
- def values()
109
- return @cities.keys.sort.collect{|x| @cities[x]}
211
+ def []=(i,j,val)
212
+ raise "Map: can't assign to diagonal" if i == j
213
+ ix = index_of(i)
214
+ jx = index_of(j)
215
+ ix, jx = jx, ix if ix < jx
216
+ @dist[ix][jx] = val.to_f
110
217
  end
111
218
 
112
- def [](x)
113
- x = x[0] if x.class == String
114
- return nil unless x >= ?A && x < @key
115
- return @cities[x]
219
+ def labels
220
+ return @labels.clone
116
221
  end
117
222
 
118
- def distance(x,y)
119
- x = x[0] if x.class == String
120
- y = y[0] if y.class == String
121
- return nil unless x >= ?A && x < @key
122
- return nil unless y >= ?A && y < @key
123
- return @matrix[x][y]
223
+ def dist
224
+ d = Array.new
225
+ @dist.each { |row| d << row.clone }
226
+ return d
124
227
  end
125
228
 
126
- def coords(name)
127
- return @coords[name]
229
+ def coords(x)
230
+ return @coords[index_of(x)]
231
+ end
232
+
233
+ # method left over from Metric class, probably not useful for TSP, but...
234
+
235
+ def delete(i)
236
+ ix = @labels.index(i) or return nil
237
+ (ix...@labels.length).each { |x| @dist[x].slice!(ix) }
238
+ @dist.slice!(ix)
239
+ @labels.slice!(ix)
128
240
  end
129
-
130
- # private methods -- return the key for a city (make a new one if necessary),
131
- # get a time value from an input line, save a distance
132
241
 
133
242
  private
134
243
 
244
+ def index_of(i)
245
+ if (ix = @labels.index(i)) == nil
246
+ ix = @labels.length
247
+ @labels << i
248
+ @dist[ix] = Array.new
249
+ @dist[ix][ix] = 0.0
250
+ end
251
+ return ix
252
+ end
253
+
135
254
  def read_file(fn)
136
255
  matrixfilename = fn.to_s + ".txt"
137
- matrixfilename = File.join(@@dataDirectory, matrixfilename)
256
+ matrixfilename = File.join(@@tspDirectory, matrixfilename)
138
257
  raise "Matrix not found: #{matrixfilename}" unless File.exists?(matrixfilename)
139
258
 
140
- @key = ?A
141
- @cities = Hash.new # initially a map from name string to one-letter key
142
- @matrix = Hash.new
143
- @coords = Hash.new
144
259
  readingLocations = true
260
+ @maxdist = 0.0
145
261
 
146
262
  File.open(matrixfilename).each do |line|
147
263
  line.chomp!
148
264
  next if line.length == 0
265
+ next if line[0] == ?#
149
266
  rec = line.split
150
267
  if rec[0][0] == ?:
151
268
  readingLocations = false if rec[0] == ":matrix"
152
269
  # tbd -- deal with other directives
153
270
  elsif readingLocations
154
- @coords[getKey(rec[2]).chr] = [rec[0].to_i, rec[1].to_i]
271
+ x = rec[2].to_sym
272
+ # printf "loc #{x} at #{rec[0]}, #{rec[1]}\n"
273
+ @coords[index_of(x)] = [rec[0].to_i, rec[1].to_i]
274
+ # p index_of(x)
275
+ # p @coords[index_of(x)]
155
276
  else
156
- i = getKey(rec[0])
157
- j = getKey(rec[1])
158
- dist = rec[2].to_i
159
- assign(i,j,dist)
160
- assign(j,i,dist)
277
+ i = rec[0].to_sym
278
+ j = rec[1].to_sym
279
+ d = rec[2].to_f
280
+ # printf "dist #{rec[0]} to #{rec[1]} = #{rec[2]}\n"
281
+ self[i,j] = d
282
+ @maxdist = d if d > @maxdist
161
283
  end
162
284
  end
163
285
 
164
- @cities = @cities.invert # from now on @cities is a map from key to name string
165
-
166
- errs = 0
167
- (?A...@key).each do |i|
168
- (i+1...@key).each do |j|
169
- unless @matrix[i][j] && @matrix[j][i]
170
- errs += 1
171
- puts "CityList.new: missing distance between #{@cities[i]} and #{@cities[j]}"
172
- end
173
- end
286
+ errs = []
287
+ i, j = first
288
+ errs << [i,j] if j.nil?
289
+ self.rest do |i,j|
290
+ errs << [i,j] unless self[i,j] != nil
174
291
  end
175
- raise "CityList.new: errors in input file" if errs > 0
292
+ raise "Map.new: missing distances #{errs.inspect}" if errs.length > 0
176
293
  end
177
-
178
- def make_map(a)
179
- @key = ?A + a.length
180
- @cities = Hash.new
181
- @matrix = Hash.new
182
- @coords = Hash.new
183
- for i in 0...a.length
184
- chi = ?A + i
185
- @cities[chi] = chi.chr
186
- for j in (i+1)...a.length
187
- x = a[i][0] - a[j][0]
188
- y = a[i][1] - a[j][1]
189
- d = Math.sqrt(x**2 + y**2).round
190
- chj = ?A + j
191
- assign(chi,chj,d)
192
- assign(chj,chi,d)
193
- end
194
- @coords[chi.chr] = a[i]
195
- end
196
- end
197
-
294
+
198
295
  def make_random_map(n)
199
296
  h = Hash.new
200
297
  a = Array.new
@@ -209,326 +306,413 @@ minutes.
209
306
  make_map(a)
210
307
  end
211
308
 
212
- def getKey(s)
213
- if @cities[s] == nil
214
- @cities[s] = @key
215
- @key += 1
309
+ def make_map(a)
310
+ @maxdist = 0.0
311
+ for i in 0...a.length
312
+ for j in (i+1)...a.length
313
+ x = a[i][0] - a[j][0]
314
+ y = a[i][1] - a[j][1]
315
+ d = sqrt(x**2 + y**2)
316
+ self[i,j] = d
317
+ @maxdist = d if d > @maxdist
318
+ end
319
+ @coords[i] = a[i]
216
320
  end
217
- return @cities[s]
218
321
  end
219
-
220
- def getValue(s)
221
- a = s.scan(/(\d+)/)
222
- if a.length == 1
223
- return a[0][0].to_i
322
+
323
+ # helper method for each_tour iterator, used by Johnson-Trotter algorithm
324
+
325
+ def movable(a, i)
326
+ if a[i].direction
327
+ return i > 0 && a[i].value > a[i-1].value
224
328
  else
225
- return 0
329
+ return i < a.length-1 && a[i].value > a[i+1].value
226
330
  end
227
331
  end
228
332
 
229
- def assign(i,j,v)
230
- if @matrix[i] == nil
231
- @matrix[i] = Hash.new
232
- end
233
- @matrix[i][j] = v
234
- end
235
-
236
- end # class CityList
333
+ end # class Map
334
+
237
335
 
238
336
  =begin rdoc
239
-
240
- Each Tour object is a string where each letter represents a city, along with a cost
241
- defined by a CityList matrix that must be specified when the tour is created. If
242
- an initial string is given verify it contains only valid keys. If no initial
243
- string is given make a tour with a random permutation of all city keys.
244
-
245
- The +nm+ and +nx+ attributes keep track of a tour's "pedigree." The counters are
246
- inherited from a parent, and incremented whenever a mutation or crossover is applied.
247
-
337
+ A Tour object is an array of city names, corresponding to the cities visited, in order,
338
+ by the salesman. Attributes are the path, its cost, a unique tour ID, and a reference to
339
+ the matrix used to define the distance between pairs of cities.
340
+
341
+ Class methods access the number of tours created, or reset the tour counter to 0.
248
342
  =end
249
343
 
250
- =begin
251
- TODO don't give access to path (unless there's a way to make it read-only -- freeze it?)
252
- =end
344
+ # TODO don't give access to path (unless there's a way to make it read-only -- freeze it?)
253
345
 
254
346
  class Tour
255
- attr_reader :id, :path, :cost, :matrix, :nm, :nx
347
+ attr_reader :id, :path, :cost, :matrix
348
+ attr_accessor :parent, :op
256
349
 
257
350
  @@id = 0
258
-
259
- def initialize(m, s = nil, nm = 0, nx = 0)
260
- if s == nil
261
- @path = m.keys.to_s.permute!
262
- else
263
- skeys = Set.new(m.keys)
264
- used = Set.new
265
- s.each_byte do |x|
266
- raise "Tour.new: invalid character in tour: #{x}" unless skeys.member?(x.chr)
267
- raise "Tour.new: duplicate character in tour: #{x.chr}" if used.member?(x)
268
- used.add(x)
269
- end
270
- @path = s.clone
271
- end
272
- @cost = pathcost(m,@path)
273
- @matrix = m # need matrix to update cost after mutation...
274
- @id = @@id
275
- @nm = nm
276
- @nx = nx
277
- @@id += 1
351
+
352
+ def Tour.reset
353
+ @@id = 0
278
354
  end
279
-
280
- # An alterntaive constructor -- either copy a single parent and add
281
- # a point mutation, or cross two parents
282
-
283
- def Tour.reproduce(x, y = nil, trace = nil)
284
- if y.nil? || y == :trace
285
- i = rand(x.path.length)
286
- j = (i+1) % x.path.length
287
- # j = rand(x.path.length)
288
- trace = :trace if y == :trace # handle calls like reproduce(x, :trace)
289
- x.clone.mutate(i, j, trace)
290
- else
291
- i = rand(x.path.length)
292
- loop { j = rand(x.path.length); break if j != i }
293
- i, j = j, i if i > j
294
- x.clone.cross(y, i, j, trace)
295
- end
296
- end
297
-
298
- # Another constructor -- return the next tour lexicographically
299
- # following tour t
300
355
 
301
- def Tour.next(t)
302
- p = t.path.clone
303
- n = p.length
304
- # find largest j s.t. path[j] < path[j+1]
305
- j = n-2
306
- while j >= 0
307
- break if p[j] < p[j+1]
308
- j -= 1
309
- end
310
- return nil if j < 0
311
- # find largest i s.t. path[j] < path[i]
312
- i = n-1
313
- loop do
314
- break if p[j] < p[i]
315
- i -= 1
316
- end
317
- # exchange path[j], path[i]
318
- p[j], p[i] = p[i], p[j]
319
- # reverse path from j+1 to end
320
- tmp = p.slice!(j+1, n-1)
321
- p += tmp.reverse
322
- return Tour.new(t.matrix, p)
356
+ def Tour.count
357
+ return @@id
323
358
  end
324
359
 
325
- def to_s()
326
- return "\##{@id}: #{@path} / #{@cost}"
360
+ def initialize(m, s)
361
+ @matrix = m
362
+ @path = s.clone
363
+ @cost = pathcost
364
+ @id = @@id
365
+ @@id += 1
327
366
  end
328
367
 
329
368
  def inspect
330
- return to_s
369
+ sprintf "#<TSPLab::Tour %s (%.2f)>", @path.inspect, @cost
331
370
  end
332
371
 
333
- # Reset the id to 0 (e.g. to count number of objects)
334
-
335
- def Tour.reset
336
- @@id = 0
337
- end
338
-
339
- def Tour.count
340
- @@id
341
- end
372
+ alias to_s inspect
342
373
 
343
374
  # A copy of a tour needs its own new id and copy of the path and pedigree
344
375
 
345
376
  def clone
346
- return Tour.new(@matrix, @path, @nm, @nx)
377
+ # return Tour.new(@matrix, @path, @nm, @nx)
378
+ return Tour.new(@matrix, @path)
347
379
  end
348
380
 
349
- # Exchange mutation (called EM by Larranaga et al). Simply swaps two
350
- # cities at the specified locations.
381
+ # Exchange mutation (called 'EM' by Larranaga et al). Swaps node i with one
382
+ # d links away (d = 1 means neighbor). An optimization (has a big impact when
383
+ # tours are 20+ cities) computes new cost by subtracting and adding single
384
+ # link costs instead of recomputing full path length. Notation: path
385
+ # through node i goes xi - i - yi, and path through j is xj - j - yj.
351
386
 
352
- def mutate(i, j, trace = nil)
353
- puts "mutate: #{i} <-> #{j}" if ! trace.nil?
387
+ def mutate!(i, d = 1)
388
+ raise "mutate!: index #{i} out of range 0..#{@path.length}" unless (i >=0 && i < @path.length)
389
+ return if d == 0 # these two special cases won't occur when
390
+ if d == @path.length-1 # mutate! called from evolve, but....
391
+ i = (i-1) % @path.length
392
+ d = 1
393
+ end
394
+
395
+ j = (i+d) % @path.length # location of swap
396
+
397
+ xi = (i-1) % @path.length # locations before, after i
398
+ yi = (i+1) % @path.length
399
+ xj = (j-1) % @path.length # locations before, after j
400
+ yj = (j+1) % @path.length
401
+
402
+ if d == 1
403
+ @cost -= @matrix[ @path[xi], @path[i] ]
404
+ @cost -= @matrix[ @path[j], @path[yj] ]
405
+ @cost += @matrix[ @path[xi], @path[j] ]
406
+ @cost += @matrix[ @path[i], @path[yj] ]
407
+ else
408
+ @cost -= @matrix[ @path[xi], @path[i] ]
409
+ @cost -= @matrix[ @path[i], @path[yi] ]
410
+ @cost -= @matrix[ @path[xj], @path[j] ]
411
+ @cost -= @matrix[ @path[j], @path[yj] ]
412
+ @cost += @matrix[ @path[xi], @path[j] ]
413
+ @cost += @matrix[ @path[j], @path[yi] ]
414
+ @cost += @matrix[ @path[xj], @path[i] ]
415
+ @cost += @matrix[ @path[i], @path[yj] ]
416
+ end
417
+
354
418
  @path[i], @path[j] = @path[j], @path[i]
355
- @cost = pathcost(@matrix,@path)
356
- @nm += 1
419
+
420
+ # uncomment to verify path cost logic
421
+ # if (@cost - pathcost).abs > 0.001
422
+ # puts "#{i} #{j}" + self.to_s + " / " + @cost.to_s
423
+ # end
424
+
357
425
  self
358
426
  end
359
427
 
360
- # Order cross-over (called OX1 by Larranaga et al). Save a chunk of the
428
+ # Order cross-over (called 'OX1' by Larranaga et al). Save a chunk of the
361
429
  # current tour, then copy the remaining cities in the order they occur in
362
- # tour t.
430
+ # tour t. i is the index of the place to start copying, n is the number to
431
+ # copy.
363
432
 
364
- def cross(t, i, j, trace = nil)
365
- puts "cross: copy #{i}..#{j}" if ! trace.nil?
366
- @path = @path[i..j]
367
- t.path.each_byte do |c|
433
+ def cross!(t, i, n)
434
+ j = (i + n - 1) % @path.length
435
+ if i <= j
436
+ p = @path[i..j]
437
+ else
438
+ p = @path[i..-1]
439
+ p += @path[0..j]
440
+ end
441
+ @path = p
442
+ t.path.each do |c|
368
443
  @path << c unless @path.include?(c)
369
444
  end
370
- @cost = pathcost(@matrix,@path)
371
- @nx += 1 + t.nx
372
- @nm += t.nm
445
+ @cost = pathcost
373
446
  self
374
447
  end
375
448
 
376
- # private methods -- compute the cost of a path, given a matrix; apply
377
- # mutations (point mutations, cross-overs)
378
-
379
- private
380
-
381
- def pathcost(m,s)
382
- sum = m.distance(s[0],s[-1]) # link from end to start
383
- for i in 0..s.length-2
384
- sum += m.distance(s[i],s[i+1])
449
+ # Not used normally, but is use in unit tests to make sure mutate! is computing
450
+ # the right costs with its optimized strategy
451
+
452
+ def pathcost
453
+ sum = @matrix[ @path[0], @path[-1] ]
454
+ for i in 0..@path.length-2
455
+ sum += @matrix[ @path[i], @path[i+1] ]
385
456
  end
386
457
  return sum
387
458
  end
388
459
 
389
460
  end # class Tour
461
+
462
+ # Combinatorics... tempting to write factorial with inject, but this is for the
463
+ # students to look at....
390
464
 
391
- # Make a population of size n -- n instances of tours made from a set
392
- # of cities
465
+ def factorial(n)
466
+ f = 1
467
+ for i in 2..n
468
+ f *= i
469
+ end
470
+ return f
471
+ end
472
+
473
+ def ntours(n)
474
+ return factorial(n-1) / 2
475
+ end
476
+
477
+ # Random search
478
+
479
+ def rsearch( matrix, nsamples )
480
+ Tour.reset
481
+ best = matrix.make_tour(:random)
482
+ if @@drawing
483
+ make_histogram([]) # clear histogram display
484
+ view_tour(best)
485
+ init_labels(["tours:", "cost:"])
486
+ end
487
+ (nsamples-1).times do |i|
488
+ t = matrix.make_tour(:random)
489
+ update = t.cost < best.cost
490
+ best = t if update
491
+ if @@drawing
492
+ view_tour(t) if update
493
+ labels = []
494
+ labels << sprintf( "#tours: %d", Tour.count )
495
+ labels << sprintf( "cost: %.2f", best.cost )
496
+ update_labels( labels )
497
+ end
498
+ end
499
+ Canvas.sync
500
+ return best
501
+ end
502
+
503
+ =begin rdoc
504
+ Make a set of +n+ random tours from map +m+
505
+ =end
393
506
 
394
- def initPopulation(n,m)
507
+ # :begin :init_population
508
+ def init_population( m, n )
395
509
  a = Array.new
396
510
 
397
511
  n.times do
398
- a.push(Tour.new(m))
512
+ a << m.make_tour(:random)
399
513
  end
400
514
 
401
515
  a.sort! { |x,y| x.cost <=> y.cost }
402
-
516
+
517
+ if @@drawing
518
+ make_histogram(a)
519
+ Canvas.sync
520
+ end
521
+
403
522
  return a
404
523
  end
524
+ # :end :init_population
525
+
526
+
527
+ =begin rdoc
528
+ +select_survivors(p, options)+ Apply "natural selection" to population +p+. Sort by
529
+ fitness, then remove individual +i+ with probability +i/n+ where +n+ is the population
530
+ size. Note the first item in the array is always kept since +0/n = 0+.
531
+ =end
405
532
 
406
- # Evolve a population. Sort by fitness, then remove individuals with
407
- # a probability based on their ranking (the ith individual survives
408
- # with p = (n-i)/n). Then Build back up to the original population
409
- # size via copies with mutations or crossovers. An optional second
410
- # argument is a collection of parameters that can override various
411
- # options (e.g. the probability of survival).
412
-
413
- def evolve(p, param={})
414
- debug = param[:trace] ? true : false
415
- pcross = param[:pcross] ? param[:pcross] : 0.25
416
- plotci = param[:plotci]
417
-
418
- p.sort! { |x,y| x.cost <=> y.cost }
419
- i = 0
420
- n = p.length
421
-
422
- if plotci
423
- m = 0.0
424
- p.each { |t| m += t.cost }
425
- m = m / p.length
426
- printf "%.2f %.2f %.2f\n", m, p[0].cost, p[-1].cost
427
- end
428
-
429
- # phase 1 -- delete random tours
533
+ # p.sort! { |x,y| (x.cost == y.cost) ? (x.id <=> y.id) : (x.cost <=> y.cost) }
430
534
 
431
- while i < p.length
432
- pk = (n-i).to_f / n
433
- if rand < pk
434
- puts "keep #{p[i]}" if debug
435
- i += 1 # keep this tour, skip to next one
436
- else
437
- puts "zap #{p[i]}" if debug
438
- p.delete_at(i) # zap this one; keep i at same value
535
+ # :begin :select_survivors
536
+ def select_survivors( population, toplevel = true )
537
+ population.sort! { |x,y| x.cost <=> y.cost }
538
+ n = population.size
539
+
540
+ (n-1).downto(1) do |i|
541
+ if ( rand < (i.to_f / n) )
542
+ # population.delete_at(i)
543
+ population[i].op = :zap
439
544
  end
440
545
  end
546
+
547
+ if @@drawing && toplevel
548
+ show_survivors( population )
549
+ Canvas.sync
550
+ end
551
+
552
+ return population
553
+ end
554
+ # :end :select_survivors
441
555
 
442
- # phase 2 -- build back up to n tours; prev is the index of the last
443
- # tour from the previous generation (candidates for cloning/crossing)
556
+ =begin rdoc
557
+ +rebuild_population(p, n, options)+ Add new Tour objects to population +p+ until the size
558
+ of the population reaches +n+. Each new Tour is a mutation of one or two Tours currently
559
+ in +p+. The hash +options+ has mutation parameters, e.g. the probability of doing a crossover
560
+ vs. point mutation.
561
+ =end
444
562
 
445
- best = p[0]
446
- prev = p.length
563
+ # :begin :rebuild_population
564
+ def rebuild_population( population, popsize, userOptions = {} )
565
+ options = @@tourOptions.merge(userOptions)
566
+ tracing = (options[:trace] == :on)
567
+
568
+ map = population[0].matrix # assume they're all from the same map...
569
+
570
+ dist = options[:distribution]
571
+ psmall, plarge, pcross = options[:profiles][dist]
572
+ sdmax = options[:sdmax] || ((map.size > 10) ? (map.size / 10) : 1)
573
+ ldmax = options[:ldmax] || ((map.size > 10) ? (map.size / 4) : 1)
447
574
 
448
- while p.length < n
449
- mom = p[rand(prev)]
450
- if rand < pcross
451
- dad = p[rand(prev)]
452
- kid = Tour.reproduce(mom,dad)
453
- puts "#{mom} x #{dad} => #{kid}" if debug
575
+ (popsize-1).downto(1) do |i|
576
+ population.delete_at(i) if population[i].op == :zap
577
+ end
578
+
579
+ prev = population.length # items before this index are from previous generation
580
+
581
+ while population.length < popsize
582
+ r = rand
583
+ if r < 1.0 - (psmall + plarge + pcross)
584
+ kid = map.make_tour( :random )
585
+ elsif r < 1.0 - (plarge + pcross)
586
+ mom = population[ rand(prev) ]
587
+ kid = map.make_tour( :mutate, mom, sdmax )
588
+ elsif r < 1.0 - pcross
589
+ mom = population[ rand(prev) ]
590
+ kid = map.make_tour( :mutate, mom, ldmax )
454
591
  else
455
- kid = Tour.reproduce(mom)
456
- puts "#{mom} => #{kid}" if debug
592
+ mom = population[ rand(prev) ]
593
+ dad = population[ rand(prev) ]
594
+ kid = map.make_tour( :cross, mom, dad )
457
595
  end
458
- p.push(kid)
459
- best = kid if kid.cost < best.cost
460
- end
461
-
462
- return best
463
-
464
- end
465
-
466
- # High level interface -- make a population for a set of cities, then
467
- # call evolve() until the best tour doesn't change after some number of
468
- # iterations (also passed as a parameter). Print the tour when done.
469
-
470
- def bestTour(matrix, param={})
471
- popsize = param[:popsize] ? param[:popsize] : 10
472
- maxgen = param[:maxgen] ? param[:maxgen] : 25
473
- maxstatic = param[:maxstatic] ? param[:maxstatic] : maxgen/2
474
-
475
- p = initPopulation(popsize,matrix)
476
- best = p[0]
477
- nstatic = 0
478
- ngen = 0
479
-
480
- if param[:verbose]
481
- puts "popsize: #{popsize}"
482
- puts "maxgen: #{maxgen}"
483
- puts "maxstatic: #{maxstatic}"
484
- puts "pcross: #{param[:pcross]}"
596
+ population << kid
485
597
  end
486
598
 
487
599
  if @@drawing
488
- view_tour(best)
489
- make_histogram(p)
600
+ update_histogram(population)
601
+ Canvas.sync
490
602
  end
491
603
 
492
- while ngen < maxgen && nstatic < maxstatic
493
- puts "best tour: #{p[0]}" if param[:verbose]
494
- evolve(p, param)
495
- if p[0].cost < best.cost
496
- best = p[0]
497
- nstatic = 0
498
- view_tour(best) if @@drawing
604
+ return population
605
+ end
606
+ # :end :rebuild_population
607
+
608
+ # :begin :evolve
609
+ def evolve( population, maxgen, ngen = 0, options = {} )
610
+ popsize = population.length
611
+ best = population[0]
612
+ while ngen < maxgen
613
+ select_survivors( population, false )
614
+ rebuild_population( population, popsize, options)
615
+ if (population[0].cost - best.cost).abs > 1e-10
616
+ best = population[0]
617
+ updated = true
618
+ # @@bestGeneration = ngen
499
619
  else
500
- nstatic += 1
620
+ updated = false
501
621
  end
502
- update_histogram(p) if @@drawing
503
622
  ngen += 1
504
- end
623
+ update_tour_display( population, ngen, updated ) if @@drawing
624
+ end
625
+ Canvas.sync if @@drawing
626
+ return best
627
+ end
628
+ # :end :evolve
505
629
 
506
- # puts "#{ngen} generations (#{nstatic} at best value)"
507
- puts "#{ngen} generations"
508
-
509
- if param[:verbose]
510
- puts "best cost: #{best.cost}"
511
- puts "pedigree: #{best.nx} cross #{best.nm} mutate"
512
- puts "best tour:"
513
- best.path.each_byte do |x|
514
- puts " " + matrix[x]
630
+ =begin rdoc
631
+ +esearch( map, n, options)+ Top level method for the genetic algorithm. +m+ is a Map
632
+ containing pairwise distances between a set of cities. The hash named
633
+ +options+ contains tour parameters, e.g. population size and mutation probabilities.
634
+ Make an initial population of random tours, then iterate +n+ times, calling +select_survivors+
635
+ and +rebuild_population+ to evolve an optimal tour. The return value is the lowest cost
636
+ tour after the last round of evolution.
637
+ =end
638
+
639
+ def esearch( matrix, maxgen, userOptions = {} )
640
+ options = @@tourOptions.merge(userOptions)
641
+
642
+ if options[:continue]
643
+ if @@previousPopulation
644
+ population = @@previousPopulation
645
+ options = @@previousOptions
646
+ ngen = @@previousNGen
647
+ else
648
+ puts "no saved population"
649
+ return nil
650
+ end
651
+ else
652
+ Tour.reset
653
+ population = init_population( matrix, options[:popsize] )
654
+ ngen = 0
655
+ if @@drawing
656
+ init_labels(["generations:", "tours:", "cost:"])
657
+ view_tour( population[0] )
515
658
  end
516
659
  end
660
+
661
+ begin
662
+ check_mutation_parameters(options)
663
+ rescue Exception => e
664
+ puts "esearch: " + e
665
+ return false
666
+ end
667
+
668
+ evolve( population, maxgen, ngen, options )
669
+
670
+ @@previousPopulation = population
671
+ @@previousOptions = options
672
+ @@previousNGen = maxgen
673
+
674
+ Canvas.sync if @@drawing
517
675
 
518
- return best
676
+ return population[0]
519
677
  end
520
678
 
521
- # Make a random set of n cities on a 1000 x 1000 grid
679
+ def update_tour_display(population, ngen, draw_tour)
680
+ view_tour(population[0]) if draw_tour
681
+ update_histogram(population)
682
+ labels = []
683
+ labels << sprintf( "generations: %d", ngen )
684
+ labels << sprintf( "#tours: %d", Tour.count )
685
+ labels << sprintf( "cost: %.2f", population[0].cost )
686
+ update_labels(labels)
687
+ end
522
688
 
523
- def random_cities(n)
524
- a = Array.new
525
- n.times { x = rand(1000); y = rand(1000); a << [x,y] }
526
- return a
689
+ =begin rdoc
690
+ Make sure the mutation parameters are valid
691
+ =end
692
+
693
+ def check_mutation_parameters(options)
694
+ dist = options[:distribution]
695
+ profiles = options[:profiles]
696
+ raise "specify mutation probabilities with :distribution option" unless dist
697
+ floaterr = "distribution must be an array of three numbers between 0.0 and 1.0"
698
+ symbolerr = "distribution must be one of #{profiles.keys.inspect}"
699
+ if dist.class == Symbol
700
+ raise symbolerr unless profiles[dist]
701
+ elsif dist.class == Array
702
+ raise floaterr unless dist.length == 3
703
+ sum = 0.0
704
+ dist.each { |x| raise floaterr if (x < 0.0 || x > 1.0); sum += x}
705
+ raise "sum of probabilities must be 1.0" unless (sum - 1).abs < Float::EPSILON
706
+ profiles[:user] = dist
707
+ options[:distribution] = :user
708
+ else
709
+ raise "#{floaterr} or #{symbolerr}"
710
+ end
527
711
  end
528
712
 
529
713
  def checkout(matrix, filename = nil)
530
714
  matrixfilename = matrix.to_s + ".txt"
531
- matrixfilename = File.join(@@dataDirectory, matrixfilename)
715
+ matrixfilename = File.join(@@tspDirectory, matrixfilename)
532
716
  if !File.exists?(matrixfilename)
533
717
  puts "Matrix not found: #{matrixfilename}"
534
718
  return nil
@@ -548,14 +732,16 @@ inherited from a parent, and incremented whenever a mutation or crossover is app
548
732
 
549
733
  def view_map(m, userOptions = {} )
550
734
  options = @@mapOptions.merge(userOptions)
551
- Canvas.init(700, 500, "TSPLab")
735
+ Canvas.init(800, 500, "TSPLab")
552
736
  links = Array.new
553
737
  nodes = Array.new
554
- m.keys.each do |loc|
738
+ r = options[:dotRadius]
739
+ m.labels.each do |loc|
555
740
  x, y = m.coords(loc)
556
- nodes << Canvas.circle( x, y, options[:dotRadius], :fill => options[:dotColor] )
741
+ nodes << Canvas.circle( x, y, r, :fill => options[:dotColor] )
742
+ Canvas.text(loc.to_s, x+r, y+r)
557
743
  end
558
- @@drawing = MapView.new(m, nodes, links, Array.new, options)
744
+ @@drawing = MapView.new(m, nodes, links, [], [], options)
559
745
  Canvas.sync
560
746
  return true
561
747
  end
@@ -565,105 +751,211 @@ inherited from a parent, and incremented whenever a mutation or crossover is app
565
751
  puts "call view_map to initialize the canvas"
566
752
  return nil
567
753
  end
754
+ options = @@tourOptions.merge(userOptions)
568
755
  map = @@drawing.cities
569
756
  @@drawing.links.each { |x| x.delete }
570
757
  @@drawing.links.clear
571
- x0, y0 = map.coords(t.path[-1].chr)
758
+ x0, y0 = map.coords(t.path[-1])
572
759
  for i in 0...t.path.length
573
- x1, y1 = map.coords(t.path[i].chr)
760
+ x1, y1 = map.coords(t.path[i])
574
761
  @@drawing.links << Canvas.line(x0, y0, x1, y1)
575
762
  x0, y0 = x1, y1
576
763
  end
577
764
  @@drawing.nodes.each { |x| x.raise }
578
765
  Canvas.sync
766
+ return true
767
+ end
768
+
769
+ def init_labels(a)
770
+ labelx = 525
771
+ labely = 350
772
+ dy = 20
773
+ labels = @@drawing.labels
774
+ if labels.length > 0
775
+ labels.each { |x| x.text = "" }
776
+ end
777
+ labels.clear
778
+ for i in 0...a.length
779
+ if i == labels.length
780
+ labels << Canvas.text("", labelx, labely)
781
+ labely += dy
782
+ end
783
+ labels[i].text = a[i]
784
+ end
785
+ end
786
+
787
+ def update_labels(a)
788
+ labels = @@drawing.labels
789
+ for i in 0...labels.length
790
+ labels[i].text = a[i]
791
+ end
579
792
  end
580
793
 
581
- # todo -- get drawing params from options
582
- # todo -- make_histogram and update_histogram are private
583
- # todo -- check to see if new histogram has same number of bins as old one -- throw some out or make some new ones
584
- # todo -- scale w to fit number of tours, min size = 1
585
- # todo -- max bins 100 -- if pop size over that just show first 100 tours
586
- # todo! color bin red if new tour comes from crossover
794
+ # todo! color bin red if new tour comes from crossover? but what if > 1 tour per bin?
795
+
796
+ # note: calling make_histogram with an empty list erases the previous histogram
587
797
 
588
798
  def make_histogram(pop)
589
799
  if @@drawing.histogram.length > 0
590
- update_histogram(pop)
591
- else
592
- x = 520
593
- y = 100
594
- ymax = 200.0
595
- w = 5
596
- scale = ymax / pop[-1].cost
597
- pop[0..29].each do |t|
598
- h = t.cost*scale
599
- @@drawing.histogram << Canvas.rectangle(x, y+ymax-h, x+w, y+ymax, :fill => 'darkblue' )
600
- x += w
601
- end
602
- @@drawing.options[:scale] = scale
603
- end
800
+ @@drawing.histogram.each { |box| box.delete }
801
+ @@drawing.histogram.clear
802
+ end
803
+ return if pop.length == 0
804
+ x = @@histogramOptions[:x]
805
+ y = @@histogramOptions[:y]
806
+ ymax = @@histogramOptions[:ymax]
807
+ w = @@histogramOptions[:binwidth]
808
+ scale = ymax / pop[-1].cost
809
+ npb = (pop.length / @@histogramOptions[:maxbins]).ceil
810
+ nbins = pop.length / npb
811
+ (0..pop.length-1).step(npb) do |i|
812
+ h = pop[i..(i+npb-1)].inject(0.0) { |sum, tour| sum + tour.cost }
813
+ h = (h/npb)*scale
814
+ @@drawing.histogram << Canvas.rectangle(x, y+ymax-h, x+w, y+ymax, :fill => 'darkblue' )
815
+ x += w
816
+ end
817
+ @@drawing.options[:scale] = scale
818
+ @@histogramOptions[:npb] = npb
819
+ @@histogramOptions[:nbins] = nbins
820
+ @@recolor = false
604
821
  end
605
822
 
606
823
  def update_histogram(pop)
607
- y = 100
608
- ymax = 200.0
824
+ y = @@histogramOptions[:y]
825
+ ymax = @@histogramOptions[:ymax]
826
+ npb = @@histogramOptions[:npb]
609
827
  scale = @@drawing.options[:scale]
610
828
  @@drawing.histogram.each_with_index do |box, i|
611
- h = pop[i].cost * scale
829
+ j = i*npb
830
+ if j > pop.length
831
+ h = 0
832
+ else
833
+ h = pop[j..(j+npb-1)].inject(0.0) { |sum, tour| sum + tour.cost }
834
+ h = (h/npb)*scale
835
+ end
612
836
  x0, y0, x1, y1 = box.coords
613
837
  box.coords = x0, y+ymax-h, x1, y1
838
+ box.fill = 'darkblue' if @@recolor
839
+ end
840
+ end
841
+
842
+ def show_survivors(pop)
843
+ hist = @@drawing.histogram
844
+ return unless pop.length == hist.length
845
+ hist.each_with_index do |box, i|
846
+ box.fill = 'gray' if pop[i].op == :zap
614
847
  end
848
+ @@recolor = true
615
849
  end
616
850
 
851
+ def show_lineage(tour, userOptions = {} )
852
+ options = @@lineageOptions.merge(userOptions)
853
+ dt = options[:dt]
854
+ a = []
855
+ while tour.parent
856
+ a << tour
857
+ tour = tour.parent
858
+ end
859
+ init_labels(["id:", "cost:", "op:"]) if @@drawing
860
+ a.reverse.each do |x|
861
+ puts x.inspect + ": " + x.op.inspect
862
+ if @@drawing
863
+ view_tour(x)
864
+ labels = []
865
+ labels << sprintf( "id: %d", x.id )
866
+ labels << sprintf( "cost: %.2f", x.cost )
867
+ labels << sprintf( "op: %s", x.op.inspect )
868
+ update_labels(labels)
869
+ sleep(dt)
870
+ end
871
+ end
872
+ return true
873
+ end
874
+
617
875
  # Values accessible to all the methods in the module
618
876
 
619
- @@dataDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'tsp')
877
+ @@tspDirectory = File.join(File.dirname(__FILE__), '..', 'data', 'tsp')
620
878
 
621
879
  @@mapOptions = {
622
880
  :dotColor => '#cccccc',
623
881
  :dotRadius => 5.0,
624
882
  }
625
883
 
884
+ # @@rsearchOptions = {
885
+ # :pause => 0.0,
886
+ # }
887
+
626
888
  @@tourOptions = {
627
-
889
+ :popsize => 10,
890
+ :maxgen => 25,
891
+ :profiles => {
892
+ :mixed => [0.5, 0.25, 0.25],
893
+ :random => [0.0, 0.0, 0.0],
894
+ :all_small => [1.0, 0.0, 0.0],
895
+ :all_large => [0.0, 1.0, 0.0],
896
+ :all_cross => [0.0, 0.0, 1.0],
897
+ },
898
+ :distribution => :all_small,
899
+ :pause => 0.0,
900
+ }
901
+
902
+ @@histogramOptions = {
903
+ :x => 520.0,
904
+ :y => 100.0,
905
+ :ymax => 200.0,
906
+ :maxbins => 50.0,
907
+ :binwidth => 5,
908
+ }
909
+
910
+ @@lineageOptions = {
911
+ :dt => 2.0,
912
+ :first => 0,
913
+ :last => -1,
628
914
  }
629
915
 
630
916
  @@drawing = nil
631
-
917
+ @@previousPopulation = nil
918
+
632
919
  end # module TSPLab
633
920
 
634
921
  end # module RubyLabs
635
922
 
636
- # Additions to the String class
637
923
 
638
- class String
924
+ module Enumerable
639
925
 
640
926
  =begin rdoc
641
- Call <tt>s.permute</tt> to scramble the characters in string +s+.
927
+ <tt>each_permtation</tt> is an iterator that makes all permutations of
928
+ a container in lexicographic order.
642
929
  =end
643
930
 
644
- def permute!
645
- for i in 0..length-2
646
- r = rand(length-i) + i # i <= r < length
647
- self[i],self[r] = self[r],self[i]
648
- end
649
- self
650
- end
651
-
652
-
653
- =begin rdoc
654
- Call <tt>s.rotate</tt> to "rotate" the characters in string +s+, i.e. detach
655
- the last character in +s+ and reinsert it at the front.
656
- =end
657
-
658
- def rotate!(n)
659
- if n > 0
660
- tail = self[n..-1]
661
- self[n..-1] = ""
662
- self.insert(0,tail)
931
+ def each_permutation
932
+ p = self.clone
933
+ n = p.length
934
+ res = []
935
+ loop do
936
+ yield p if block_given?
937
+ res << p.clone
938
+ # find largest j s.t. path[j] < path[j+1]
939
+ j = n-2
940
+ while j >= 0
941
+ break if p[j] < p[j+1]
942
+ j -= 1
943
+ end
944
+ break if j < 0
945
+ # find largest i s.t. path[j] < path[i]
946
+ i = n-1
947
+ loop do
948
+ break if p[j] < p[i]
949
+ i -= 1
950
+ end
951
+ # exchange path[j], path[i]
952
+ p[j], p[i] = p[i], p[j]
953
+ # reverse path from j+1 to end
954
+ tmp = p.slice!(j+1, n-1)
955
+ p += tmp.reverse
663
956
  end
664
- self
957
+ return block_given? ? nil : res
665
958
  end
666
-
959
+
667
960
  end
668
961
 
669
-