[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

[QUIZ] Itinerary for a Traveling Salesman (#142

James Gray

10/5/2007 1:28:00 PM

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rub...

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Morton Goldberg

A salesman wants to call on his customers, each of which is located in a
different city. He asks you to prepare an itinerary for him that will minimize
his driving miles. The itinerary must take him to each city exactly once and
return him to his starting point. Can you write a Ruby program to generate such
an itinerary?

This problem is famous and known to be NP-complete. So how can you be expected
to solve it as a weekend Ruby Quiz project? It's unreasonable isn't it? Yes,
it is, unless the conditions are relaxed somewhat.

First, let's restrict the problem to a space for which the solution is known: a
grid of points as defined by the following Ruby code:

# Square grid (order n**2, where n is an integer > 1). Grid points are
# spaced on the unit lattice with (0, 0) at the lower left corner and
# (n-1, n-1) at the upper right.

class Grid
attr_reader :n, :pts, :min
def initialize(n)
raise ArgumentError unless Integer === n && n > 1
@n = n
@pts = []
n.times do |i|
x = i.to_f
n.times { |j| @pts << [x, j.to_f] }
end
# @min is length of any shortest tour traversing the grid.
@min = n * n
@min += Math::sqrt(2.0) - 1 if @n & 1 == 1
end
end

Second, let's relax the requirement that the itinerary be truly minimal. Let's
only require that it be nearly minimal, say, within 5%. Now you can tackle the
problem with one of several heuristic optimization algorithms which run in
polynomial time. In particular, you could use a genetic algorithm (GA).

Genetic Algorithm (GA)

From one point of view a GA is a stochastic technique for solving an
optimization problem--for finding the extremum of a function. From another
point of view it is applied Darwinian evolution.

To see how a GA works, let's look at some pseudocode.

Step 1. Generate a random initial population of itineraries.
Step 2. Replicate each itinerary with some variation.
Step 3. Rank the population according to a fitness function.
Step 4. Reduce the population to a prescribed size,
keeping only the best ranking itineraries.
Step 5. Go to step 2 unless best itinerary meets an exit criterion.

Simple, isn't it? But perhaps some discussion will be useful.

Step 1. You can get the points you need to generate a new random itinerary by
calling *pts* on an instance *grid* of the Grid class shown above.

Step 2. GAs apply what are called *genetic operators* to replicas of the
population to produce variation. Genetic operators are usually referred to by
biological sounding names such *mutation*, *recombination*, or *crossover*.
Recombination means some kind of permutation. In my GA solution of the problem
proposed here, I used two recombination operators, *exchange* and *reverse*.
Exchange means cutting an itinerary at three points (yielding four fragments)
and swapping the middle two fragments. Reverse means cutting an itinerary at
two points (yielding three fragments) and reversing the order of the middle
fragment.

Step 3. The fitness function for the traveling salesman problem can be the
total distance traversed when following an itinerary (including the return to
the starting point), or it can be a related function that reaches its minimum
for exactly the same itineraries as does the total distance function.

A GA can be rather slow, but has the advantage of being easy to implement. My
experience with problem I have proposed is that a GA can produce a solution
meeting the 5% criterion reasonably quickly and that it can find an exact
solution if given enough time.

An Exact Solution

To give you an idea of how a solution looks when plotted, here is a picture of a
minimal tour on a 7 X 7 grid.

*--*--*--* *--*--*
| | | |
* *--* *--* *--*
| | | |
* * *--*--*--* *
| | / |
* *--*--*--*--* *
| |
*--* *--*--*--* *
| | | |
*--* * *--*--* *
| | | |
*--*--* *--*--*--*

This exact solution was found by my Ruby GA solver in about two minutes.

Wikipedia Links

Two Wikipedia pages have a great deal of relevant information on the topic of
this quiz.

http://en.wikipedia.org/wiki/Traveling_salesm...

http://en.wikipedia.org/wiki/Genetic...

17 Answers

Simon Kröger

10/6/2007 3:54:00 PM

0


> A salesman wants to call on his customers, each of which is located in a
> different city. He asks you to prepare an itinerary for him that will minimize
> his driving miles. The itinerary must take him to each city exactly once and
> return him to his starting point. Can you write a Ruby program to generate such
> an itinerary?
> [...]

Sorry if i'm just stating the obvious - this quiz isn't about finding a
solution (fast and correct) but to implement the genetic algorithm, right?

I'm just asking myself if i missed a (or maybe the) point...

cheers

Simon

James Gray

10/6/2007 5:08:00 PM

0

On Oct 6, 2007, at 10:55 AM, Simon Kröger wrote:

>
>> A salesman wants to call on his customers, each of which is
>> located in a
>> different city. He asks you to prepare an itinerary for him that
>> will minimize
>> his driving miles. The itinerary must take him to each city
>> exactly once and
>> return him to his starting point. Can you write a Ruby program to
>> generate such
>> an itinerary?
>> [...]
>
> Sorry if i'm just stating the obvious - this quiz isn't about
> finding a
> solution (fast and correct) but to implement the genetic algorithm,
> right?
>
> I'm just asking myself if i missed a (or maybe the) point...

The goal of this quiz was to come up with a good problem to
experiment with genetic algorithms on, yes. Morton and I discussed
that quite a bit.

However, as you all know by now, I'm not a big restrictions kind of
guy. So I suggested Morton lay out the problem and describe the way
we think would be fun to solve it.

That leaves the choice of strategy to the solver and we won't take
away your keyboard if you don't use a genetic algorithm. ;)

James Edward Gray II

P.S. Of course, if you've been waiting to play with genetic
algorithms, this is your chance! I was itching for a good excuse to
try one, so that's how I built my solution. This is a good problem
for the experiment, I think.


Morton Goldberg

10/6/2007 5:20:00 PM

0

On Oct 6, 2007, at 11:55 AM, Simon Kröger wrote:

>> A salesman wants to call on his customers, each of which is
>> located in a
>> different city. He asks you to prepare an itinerary for him that
>> will minimize
>> his driving miles. The itinerary must take him to each city
>> exactly once and
>> return him to his starting point. Can you write a Ruby program to
>> generate such
>> an itinerary?
>> [...]
>
> Sorry if i'm just stating the obvious - this quiz isn't about
> finding a
> solution (fast and correct) but to implement the genetic algorithm,
> right?
>
> I'm just asking myself if i missed a (or maybe the) point...

It's true that this quiz resulted from James asking for a quiz
featuring genetic algorithms, and it's also true that I wish to
encourage participants to implement a genetic algorithm. On the other
hand, any solution to the problem is certainly welcome. Also, given
the restriction to an n x n grid, it _is_ possible to write a fast,
exact solution.

Regards, Morton

Eric Mahurin

10/6/2007 8:06:00 PM

0

On 10/5/07, Ruby Quiz <james@grayproductions.net> wrote:
> Second, let's relax the requirement that the itinerary be truly minimal. Let's
> only require that it be nearly minimal, say, within 5%. Now you can tackle the
> problem with one of several heuristic optimization algorithms which run in
> polynomial time. In particular, you could use a genetic algorithm (GA).

As far as I know, a genetic algorithm isn't going to guarantee any
approximation factor (within 1.05 of optimal as suggested above),
right? To get that kind of guaranteed accuracy level, I think you'd
need to go with Arora's quite complex polynomial-time approximation
scheme, which I have yet to see an implementation of. Personally, I'd
like to see it implemented for the (rectilinear) steiner tree problem
(his techniques work on a whole slew of geometric graph problems).

James Gray

10/6/2007 8:58:00 PM

0

On Oct 6, 2007, at 3:05 PM, Eric Mahurin wrote:

> On 10/5/07, Ruby Quiz <james@grayproductions.net> wrote:
>> Second, let's relax the requirement that the itinerary be truly
>> minimal. Let's
>> only require that it be nearly minimal, say, within 5%. Now you
>> can tackle the
>> problem with one of several heuristic optimization algorithms
>> which run in
>> polynomial time. In particular, you could use a genetic algorithm
>> (GA).
>
> As far as I know, a genetic algorithm isn't going to guarantee any
> approximation factor (within 1.05 of optimal as suggested above),
> right?

Genetic algorithms don't guarantee the solution, that's correct. But
it's pretty easy to get one working reasonably well for this.

James Edward Gray II


Morton Goldberg

10/7/2007 1:31:00 AM

0

On Oct 6, 2007, at 4:05 PM, Eric Mahurin wrote:

> On 10/5/07, Ruby Quiz <james@grayproductions.net> wrote:
>> Second, let's relax the requirement that the itinerary be truly
>> minimal. Let's
>> only require that it be nearly minimal, say, within 5%. Now you
>> can tackle the
>> problem with one of several heuristic optimization algorithms
>> which run in
>> polynomial time. In particular, you could use a genetic algorithm
>> (GA).
>
> As far as I know, a genetic algorithm isn't going to guarantee any
> approximation factor (within 1.05 of optimal as suggested above),
> right? To get that kind of guaranteed accuracy level, I think you'd
> need to go with Arora's quite complex polynomial-time approximation
> scheme, which I have yet to see an implementation of. Personally, I'd
> like to see it implemented for the (rectilinear) steiner tree problem
> (his techniques work on a whole slew of geometric graph problems).

You are correct. For the general shortest tour problem, a genetic
algorithm can't guarantee a solution within a specific error bound. I
should have been clearer about that.

However, for the grid version of the problem with the grid not too
big (say, n x n <= 10 x 10), it is not at all hard to come up with a
genetic algorithm that meets the 5% goal. And how many salesmen make
trips with more than 100 cities on their itinerary? :)

I really don't want this quiz to be too hard. I don't expect anyone
participating in this quiz to work with really large grids. A
solution that can get to within 5% on a 10 x 10 grid is a good one as
far as I'm concerned.

Regards, Morton

James Gray

10/8/2007 12:33:00 AM

0

On Oct 7, 2007, at 4:30 PM, Dave Pederson wrote:

> Hi all-

Hi Dave.

> This is my first quiz.

I did forward your first message to the list, but I'm glad to see you
decided to join us.

> Since I am very new to ruby (been at it in my spare time now for
> about a
> month), I would appreciate any comments/suggestions anyone may have.

Some thoughts:

def copy
Gene.new(@city, @lat, @lon)
end

All objects include a dup() method that would be equivalent to this.

For:

def eql?(gene)
self == gene
end

Or just:

alias_method :eql?, :==

With:

def ==(chrom)
false unless chrom.class == self.class && size == chrom.size
0.upto(size-1) do |i|
return false unless self[i] == chrom[i]
end
true
end

This is a light suggestion, but we tend to do as few type checks as
possible in Ruby. You may have a reason for doing it here, but most
of the time we find that leaving them out gives us more options.

Those are just some general points I saw. Overall, it's a nice
solution. Thanks for sharing.

James Edward Gray II

Martin DeMello

10/8/2007 2:56:00 AM

0

On 10/7/07, Dave Pederson <dave.pederson.ruby@gmail.com> wrote:

> def ==(gene)
> gene.class == self.class &&
> @city == gene.city &&
> @lat == gene.lat &&
> @lon == gene.lon
> end

The 'pretty' way to do this: [gene.class, gene.city, gene.lat,
gene.long] == [self.class, @city, @lat, @long]

Introduces a bit of overhead due to the temp array creation, so you
probably want to benchmark when doing this in an inner loop.

martin

Eugene Kalenkovich

10/8/2007 4:31:00 AM

0

One more variation of GA theme. For mutationsd I use reversals only, with
variable lengths up to 1/2 of trip (+2). Apparently random shift also helps
a lot.
Additional twist to the algorithm, that works only for a limited problem
space (but works well) - population and productivity increase in time
exponentially. I've tried different rates of increase, **1 gives a realy
good speed in lucky cases, but miserable on bad ones. **2 is good, but
decreases speed of lucky ones too much. **1.5 seems to be a good compromise
(I am starting to think that "good enough", being a pragmatic one, should be
a Ruby slogan). I have no mathematical explanation/proof, but can expect
that magic number is either 1.5 or 1.(3) :)

I believe that result has a good balance between code simplicity and
performance (any volunteers to test performance of different solutions on
cases >7 ?)

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
require 'enumerator'
require 'grid'

def distance(p1,p2,sqrt=true)
dist=((p1[0]-p2[0])**2 +(p1[1]-p2[1])**2)
sqrt ? Math::sqrt(dist) : dist
end

def length(path, sqrt=true)
len=distance(path[0],path[-1],sqrt)
path.each_cons(2) {|p1,p2| len+=distance(p1,p2,sqrt)}
len
end

def mutation(path,i)
len=path.length
rev=i%(len/2)+2
shift=rand(len-1)
pos=rand(len-rev)
newpts=path[shift..-1]+path[0...shift]
newpts[pos,rev]=newpts[pos,rev].reverse
newpts
end

num,pct=ARGV
num||=5
pct||=0
num=num.to_i
pct=pct.to_i

grid=Grid.new(num)
pass=grid.min+grid.min*pct/100.0+0.1
pathset=[grid.pts]
count=0
while (length(pathset[0])>pass) do
count+=1
newpaths=[]
sample=(count**1.5).round
pathset.each { |path| sample.times {|i| newpaths << mutation(path,i)} }
pathset=(pathset+newpaths).sort_by { |path|
length(path,false) }.first(sample)
puts "#{count}. #{length(pathset[0])}"
end
p pathset[0]



Joseph Seaton

10/8/2007 5:32:00 PM

0

I'm afraid my solution is rather late since I spent so much time trying
to tweak it.

Nothing special (and very inelegant), but it manages to get a 7*7 grid
to within 5% in about 40s (give or take quite a bit):

#!/usr/bin/env ruby
require 'rubygems'
require 'rvg/rvg'
require 'grid'
include Magick

class GeneticGridGuess
def initialize grid
@grid, @min = grid.pts, grid.min*1.05
puts "Minumum time (within 5%): #{@min}"
@len, @seg = @grid.length, (@grid.length*0.3).ceil
@psize = Math.sqrt(@len).ceil*60
@mby = (@psize/20).ceil
@pop = []
@psize.times do
i = @grid.sort_by { rand }
@pop << [dist(i),i]
end
popsort
end
def solve
while iter[0] > @min
puts @pop[0][0]
end
@pop[0]
end
def iter
@pop = (@pop[0..20]*@mby).collect do |e|
n = e[1].dup
case rand(10)
when 0..6 #Guesses concerning these values
seg = rand(@seg)
r = rand(@grid.length-seg+1)
n[r,seg] = n[r,seg].reverse
when 7
n = n.slice!(rand(@grid.length)..-1) + n
when 8..9
r = []
3.times { r << rand(@grid.length)}
r.sort!
n = n[0...r[0]] + n[r[1]...r[2]] + n[r[0]...r[1]] + n[r[2]..-1]
end
[dist(n),n]
end
popsort
@pop[0]
end
def dist i
#Uninteresting but fast as I can make it:
t = 0
g = i+[i[0]]
@len.times do |e|
t += Math.sqrt((g[e][0]-g[e+1][0])**2+(g[e][1]-g[e+1][1])**2)
end
t
end
def popsort
@pop = @pop.sort_by { |e| e[0] }
end
end

gridsize = ARGV[0] ? ARGV[0].to_i : (print "Grid size: "; STDIN.gets.to_i)
grid = GeneticGridGuess.new(Grid.new(gridsize)).solve

puts "In time #{grid[0]}:"
grid[1].each do |e|
print "#{e[0].to_i},#{e[1].to_i} "
end
puts

if !ARGV[1]
RVG.new(gridsize*100,gridsize*100) do |canvas|
canvas.background_fill = 'white'
cgrid = grid[1].collect do |e|
[e[0]*100+10,e[1]*100+10]
end
cgrid.each do |point|
canvas.circle(5,point[0],point[1]).styles(:fill=>'black')
end
canvas.polygon(*cgrid.flatten).styles(:stroke=>'black',
:stroke_width=>2, :fill=>'none')
end.draw.display
end

Suggestions very welcome, particularly concerning speed

Joe