[QUIZ] The Smallest Circle (#157)

The three rules of Ruby Quiz 2:

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 2 by submitting ideas as often as you can! (A permanent, new website is in the works for Ruby Quiz 2. Until then, please visit the temporary website at <http://matthew.moss.googlepages.com/home>.

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.

···

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

The Smallest Circle
by Matthew Moss

Your task this week sounds simple enough, but may be more difficult than it first appears. Given a set of points on a plane, your goal is to find the smallest circle that encloses those points.

You are to provide a function, *encircle*, that takes an array of points and returns the smallest circle surrounding those points. Start with the following base code and extend as needed to solve the problem:

     class Point < Struct.new(:x, :y)
         def self.random
             Point.new(rand, rand)
         end

         def to_s
             "(#{x}, #{y})"
         end
     end

     class Circle < Struct.new(:center, :radius)
         def to_s
             "{center:#{center}, radius:#{radius}}"
         end
     end

     def encircle(points) # takes array of Point objects
         # returns a Circle object
     end

I will be running several tests on the submitted solutions, with various point sets, to see how well they perform at this task. I recommend you you test your algorithm with a variety of sample sets, from small sets consisting of just 1-5 points, up to medium and larger sets, containing a few thousand points.

To generate an array of random points, start with the above code and add:

     def generate_samples(n)
         (1..n).map { Point.random }
     end

And then you may test your implementation like this:

     # encircle 10 random points
     puts encircle( generate_samples(10) )

As mentioned, this one may be more difficult than it seems. Feel free to estimate the smallest circle, if you get stuck on getting the exact solution.

If one of the points is on the enclosing circle, is that a valid solution ?

May I propose a bit refined random method?

     class Point < Struct.new(:x, :y)
         def self.random
             Point.new(0.5 - rand, 0.5 - rand)
         end

This should give us negative coordinates as well. :slight_smile:

···

On Feb 15, 3:19 pm, Matthew D Moss <matthew.m...@gmail.com> wrote:

The Smallest Circle
by Matthew Moss

Your task this week sounds simple enough, but may be more difficult
than it first appears. Given a set of points on a plane, your goal is
to find the smallest circle that encloses those points.

You are to provide a function, *encircle*, that takes an array of
points and returns the smallest circle surrounding those points.
Start with the following base code and extend as needed to solve the
problem:

     class Point < Struct.new(:x, :y)
         def self.random
             Point.new(rand, rand)
         end

--
Cheers and welcome new quizmasters!
Alex

Hi,

here's my solution of this nice quiz. It's heavily inspired by the last
week's quiz, i.e. solving a non-linear equation. This time the program
solves the non-smooth convex optimization problem

    min{max{|p_i-x|^2: p_i the given points}: x in R^k},

where k=1,2,3,... (dimension), using a simple subgradient algorithm.
Therefore, it works for an arbitrary number of points
(up to 10000 in about 40s on my 5 year old notebook) and an
arbitrary dimension (number of coordinates) of the points. The solutions
are usually a good approximation of the optimal ones.

···

================================================================
# extends point-class with an arbitrary dimension
# and simple vector-operations
class Point
    def initialize( *coords )
        @coords = coords.map{|x| x.to_f}
    end

    def size
        @coords.size
    end

    def []( index )
        @coords[index]
    end

    def []= ( index, x )
        @coords[index] = x
    end
    
    def self.random( n = 2 )
        Point.new( *(1..n).map{ 0.5 - rand } )
    end

    def +(p)
        return Point.new( *(0...size).map{|i| @coords[i] + p[i]} )
    end

    def -(p)
        return Point.new( *(0...size).map{|i| @coords[i] - p[i]} )
    end

    def *(a)
        return Point.new( *@coords.map{|x| x * a} )
    end

    def /(a)
        return Point.new( *@coords.map{|x| x / a} )
    end

    # calculate the inner product if this point with p
    def ip(p)
        (0...size).inject(0) { |sum, i| sum + @coords[i] * p[i] }
    end

    def to_s
        "(#{@coords.join(', ')})"
    end
end

class Circle < Struct.new(:center, :radius)
    def to_s
        "{center:#{center}, radius:#{radius}}"
    end
end

def generate_samples(n, dim = 2)
    (1..n).map { Point.random(dim) }
end

# calculate value and subgradient of
# f(x,y) = max{(x-x_i)^2 + (y-y_i)^2: (x_i,y_i) in points}
def evaluate( m, points )
    y_big = nil
    grad = nil
    points.each do |p|
        d = (m - p)
        y = d.ip( d ) # y = sum (m_i-p_i)^2
        if y_big.nil? or y > y_big
            y_big = y
            grad = d*2
        end
    end

    return [y_big, grad]
end

# perform simple subgradient algorithm
# with given starting-point and number of iterations
def encircle( points,
              x_start = Point.new( *([0]*points[0].size) ),
              max_iter = 100 )
    x = x_start
    y, g = nil, nil

    for k in 1..max_iter do
        y, g = evaluate( x, points )
        x = x - g/k
    end

    return Circle.new(x, Math.sqrt(y))
end

puts encircle( generate_samples(10000, 3) )

Bye,
Frank

Mine is here:

http://www.pastie.org/153343

Lifted mostly from http://www.cs.mcgill.ca/~cs507/projects/1998/jacob/. Classic Convex Hull based solution that seems to scale O(n).

-Doug Seifert

Hello,

Had a lot of fun with this one :slight_smile: My solution works by taking the average of
all points as the center, and the farthest outlying point as the radius. An
optimal solution is then found by moving the circle center towards the
outlier as the radius is reduced, while all points still are within the
radius. The same principles could be applied for higher dimensions as well.

class Point < Struct.new(:x, :y)
    def self.random
        Point.new(rand, rand)
    end

    def to_s
        "(#{x}, #{y})"
    end
end

class Circle < Struct.new(:center, :radius)
    def to_s
        "{center:#{center}, radius:#{radius}}"
    end
end

# Calculate distance between points
def distance(pt_a, pt_b)
  Math.sqrt((pt_a.x - pt_b.x) * (pt_a.x - pt_b.x) +
            (pt_a.y - pt_b.y) * (pt_a.y - pt_b.y))
end

# Determine if given points are all within a circle
def inside_circle?(points, circle)
  for point in points
    dist = distance(point, circle.center)
    return false if dist > circle.radius
  end
  true
end

def encircle(points) # takes array of Point objects
  # find the average midpoint of all points
  mid = Point.new(
    points.inject(0){|sum, pt| sum += pt.x} / (points.size * 1.0),
    points.inject(0){|sum, pt| sum += pt.y} / (points.size * 1.0))

  # sort points by longest distance from midpoint
  # longest point (index 0) is the initial radius
  points.sort!{|a,b| distance(mid, a) <=> distance(mid, b) }.reverse!

  # Taking the average midpoint does not necessarily work because the points
may
  # be weighted more heavily to one side. We correct for this by sliding the
circle
  # along the line from its original center to the outmost point, decreasing
the
  # radius as we go. Keep doing this until the circle can be made no
smaller.
  point = points[0]
  slope = (mid.y - point.y) / (mid.x - point.x)
  new_pt, delta, sign = mid, 0.0, 1.0
  sign = -1.0 if mid.x > point.x
  while inside_circle?(points, Circle.new(new_pt, distance(new_pt, point)))
    mid = new_pt
    delta += 0.000001 * sign
    new_pt = Point.new(mid.x + delta, mid.y + (slope * (delta)))
  end

  return Circle.new(mid, distance(mid, point))
end

def generate_samples(n)
    (1..n).map { Point.random }
end

Here are Pasties of the program and a RMagick script to generate images:

Algorithm: http://pastie.caboo.se/153387
Graphics code: http://pastie.caboo.se/153388
Images:
http://justin.ethier.googlepages.com/157_jae_25_2.jpg
http://justin.ethier.googlepages.com/157_jae_100_0.jpg
http://justin.ethier.googlepages.com/157_jae_1000_3.jpg

Thanks,

Justin

···

On Feb 15, 2008 8:19 AM, Matthew D Moss <matthew.moss@gmail.com> wrote:

The three rules of Ruby Quiz 2:

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 2 by submitting ideas as often as you can! (A
permanent, new website is in the works for Ruby Quiz 2. Until then,
please visit the temporary website at <http://
matthew.moss.googlepages.com/home>.

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.

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

The Smallest Circle
by Matthew Moss

Your task this week sounds simple enough, but may be more difficult
than it first appears. Given a set of points on a plane, your goal is
to find the smallest circle that encloses those points.

You are to provide a function, *encircle*, that takes an array of
points and returns the smallest circle surrounding those points.
Start with the following base code and extend as needed to solve the
problem:

    class Point < Struct.new(:x, :y)
        def self.random
            Point.new(rand, rand)
        end

        def to_s
            "(#{x}, #{y})"
        end
    end

    class Circle < Struct.new(:center, :radius)
        def to_s
            "{center:#{center}, radius:#{radius}}"
        end
    end

    def encircle(points) # takes array of Point objects
        # returns a Circle object
    end

I will be running several tests on the submitted solutions, with
various point sets, to see how well they perform at this task. I
recommend you you test your algorithm with a variety of sample sets,
from small sets consisting of just 1-5 points, up to medium and
larger sets, containing a few thousand points.

To generate an array of random points, start with the above code and
add:

    def generate_samples(n)
        (1..n).map { Point.random }
    end

And then you may test your implementation like this:

    # encircle 10 random points
    puts encircle( generate_samples(10) )

As mentioned, this one may be more difficult than it seems. Feel free
to estimate the smallest circle, if you get stuck on getting the
exact solution.

Greetings,

Not sure if my solution is worth posting.... I took a naive
axis-aligned bounding box approach.

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_naive_aabb.rb

I knew it would be inaccurate, but I was curious how bad it
would be, so I wrote an OpenGL/GLUT based visualizer:

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_smallest_circle_visualization.rb

(The visualizer will work with any quiz solution that
provides the encircle() and generate_samples(n) methods,
and Point#x and Point#y.)

Occasionally the naive approach lucks out with a dataset
that allows it to look good:

But more often it looks something like this:

I had planned to improve my solution by taking an approach
similar to Justin Ethier's. . . . By iterating and "moving the
circle center towards the outlier as the radius is reduced,
while all points still are within the radius."

I may still code that if I have time... but then again, it's
already been done. :slight_smile:

Regards,

Bill

Hi,

here's my solution. I initially thought that the best circle would be either defined by a diameter (2 points) or one combinations of 3 points through which it passes. The idea was that given three points defining a circle a fourth point is either in the circle or defines a circle with 2 from the 3 originals that includes the remaining point.

This would be neat as I wasn't so happy about a gradient walking solution. In this case it can be proven that you can deterministically get the best solution by gradient walking (I don't think there are local maximums) but it's not the general case. Unfortunately it can be proven that such a circle can encircle all points, but not that it's the smallest (and my experiments proved that it isn't). Finally this is even slower than my proposed solution below, ...

So here's a more brute force method : minimizing the maximum distance with some optimizations (heuristic for initial values and a not so naive 2D-space walker). There's probably some more optimizations for walking the gradient but I'm happy with the current speed (~10s for 10000 points on my 1.06GHz Core Duo).

This seems similar in spirit to Frank's solution, but less clean from the Math PoV and more optimized for speed.

It outputs the solution and creates a gnuplot plotting file for you to verify (you have the path used by the algorithm to find the center). I'm no Gnuplot expert so unfortunately fancy colors are out and crappy legends in...

Good reading,

Lionel

···

=============================================
include Math

# Uses 10 points by default
POINT_COUNT = (ARGV[0].to_i == 0) ? 10 : ARGV[0].to_i

class Point < Struct.new(:x, :y)
  def self.random
    Point.new(rand, rand)
  end

  def self.generate_samples(n)
    (1..n).map { self.random }
  end

  def to_s
    "(#{x}, #{y})"
  end
end

class PotentialCenter < Struct.new(:x, :y, :points)
  # Square distance with a point
  def square_distance(point)
    ((point.x - x) ** 2) + ((point.y - y) ** 2)
  end

  # Maximum square distance with my points
  # It's cached because it's called several times and can be very costly
  def max_square_distance
    @sq_dist ||= (points.map { |point| square_distance(point) }.max)
  end

  # Compute the best move with given distance (ro)
  # and angles (thetas)
  # returns nil if none is better than the current position
  def best_move(ro, thetas)
    potential_moves = thetas.map { |theta|
      new_x = x + (ro * cos(theta))
      new_y = y + (ro * sin(theta))
      PotentialCenter.new(new_x,new_y,points)
    }
    choose_move_1(potential_moves)
  end

  # Dumber, faster
  def choose_move_1(potential_moves)
    potential_moves.detect { |move|
      max_square_distance > move.max_square_distance
    }
  end

  # Look for the shortest path, slower (~1.4x on average with many points)
  def choose_move_2(potential_moves)
    potential_moves.reject! { |move|
      max_square_distance <= move.max_square_distance
    }
    potential_moves.min { |c1,c2|
      c1.max_square_distance <=> c2.max_square_distance
    }
  end

  # Check that the given offset is big enough to move
  # the current position
  # (floating point rounding errors prevent infinite precision...)
  def can_be_moved_by(offset)
    ((x + offset) != x) || ((y + offset) != y)
  end

  def to_s
    "(#{x}, #{y})"
  end
end

class Circle < Struct.new(:center, :radius)
  def to_s
    "{center:#{center}, radius:#{radius}}"
  end
end

def chose_original_center_and_move_amount(points)
  # Start with a good potential (around the middle of the point cloud)
  max_x = points.map{|p| p.x}.max
  min_x = points.map{|p| p.x}.min
  x = (max_x + min_x) / 2
  max_y = points.map{|p| p.y}.max
  min_y = points.map{|p| p.y}.min
  y = (max_y + min_y) / 2
  center = PotentialCenter.new(x, y, points)
  # The original displacement value is based on the fact that a truly
  # random distribution gets a mean value with a precision of this order.
  # The actual center is probably reachable with a few of these steps
  move_amount = ((max_y - min_y) + (max_x - min_x)) / points.size
  return [center, move_amount]
end

# Look for the best center by minimizing
# its maximum distance with the given points
# This is the same as minimizing its square
# (the maximum of the square distances)
# This can be seen as a dichotomy on a 2-dimensional space
def encircle(points)
  center, ro = chose_original_center_and_move_amount(points)
  # We'll move with polar coordinates (distance + angle : ro/theta)
  # How many directions do we follow?
  # each new angle creates a new (costly) max_square_distance call
  # so we take the minimum needed to move around and climb the
  # square_distance gradient: 3
  angle_count = 3
  thetas = (1..angle_count).map { |c| (2 * c * Math::PI) / angle_count}
  iter = 0
  # Trace our moves
  center_list = []

  loop do
    center_list << center
    iter += 1
    puts "#{iter}: #{center.max_square_distance}"
    # Is there a best move available ?
    if new_center = center.best_move(ro, thetas)
      center = new_center
    else
      ro /= 2
      # We stop when ro becomes too small to move the center
      break unless center.can_be_moved_by(ro)
    end
  end
  puts "Circle found in #{iter} iterations"
  return [ Circle.new(center, sqrt(center.max_square_distance)),
           center_list ]
end

points = Point.generate_samples(POINT_COUNT)
t = Time.now
circle, center_list = encircle(points)
puts circle
puts "Found in: #{Time.now - t}s"

# Generate a GnuPlot command file to see the result
f = File.open('plotcommand.cmd', 'w')
f.print <<EOF
set parametric
set size square
set xrange [-0.2:1.2]
set yrange [-0.2:1.2]
set multiplot
plot "-"
#{points.map { |p| "#{p.x} #{p.y}" }.join("\n")}
end
plot "-" with lines
#{center_list.map { |p| "#{p.x} #{p.y}" }.join("\n")}
end
plot [0:2*pi] (sin(t)*#{circle.radius})+#{circle.center.x},(cos(t)*#{circle.radius})+#{circle.center.y}
set nomultiplot
EOF
f.close

puts <<EOF
use 'gnuplot -persist plotcommand.cmd' to see
the circle, points and path to the center
EOF

This is really way more difficult than it seemed to me...

I was approaching some geometrical solution just to be shocked that
there are cases there not both ends of the longest chord lay on the
smallest circle!

Check out these (found by pure luck and help of random):

-0.385628 -0.435444
-0.319182 +0.478400
+0.257012 +0.333913
-0.243603 -0.496925

The longest chord is from points[0] to points[2] (of length 1.002445),
but correct solution appears to be the circle around a triangle [[1],
[2], [3]]. It includes second to longest chord [1]->[3] of length
0.978249.

···

On Feb 15, 3:19 pm, Matthew D Moss <matthew.m...@gmail.com> wrote:

As mentioned, this one may be more difficult than it seems. Feel free
to estimate the smallest circle, if you get stuck on getting the
exact solution.

--
Alex

Here is my solution. Although it uses a brute force approach if the
first attempts (driven by obvious constraints) of determining the
circle fail, it scales on larger sets, because the given equal
distibution of random points form an approximate square on these
sets. In that case the convex hull which is used for the brute force
has very few points.

I also did some visualization with RMagick.

g phil

smallest_enclosing_circle.rb (7.23 KB)

Okay, I know that this probably isn't the best first impression I
could make, but is there someone who would mind doing a summary for
this week's quiz?

I developed a fever of about 101F last night and have it today, will
probably carry it into tomorrow. I can still do a summary, though it
will likely show up Friday and be shorter than perhaps is usual,
because I can barely stay focused on something for more than 5 minutes
at the moment.

If someone is up for it, just reply here and have at it. I will
appreciate it enormously.

First, I want to thank everyone participating and watching this week
for being patient with me while I was down with the flu. I am almost
completely recovered at this point, and I hope that doesn't come back
for a good, long time.

Second, thanks to all quizzers who made an attempt at the quiz, even
after the scary warning I made about the difficulty.

Third, I had hoped to provide a set of tests to help quizzers check
their solutions, but the flu hit me pretty hard and I was unable. A
special thanks go to Bill Kelly, Thomas ML and Lionel Bouton; they
took time to benchmark the solutions for both speed and accuracy, when
I didn't have the ability to do it.

Now, about the smallest enclosing circle problem... I warned from the
start that the problem might prove more difficult than it appears.
It's a problem that has been attacked and solved numerous times. [1][2]
[3][4][5]

One easy to understand algorithm was originally described by Welzl[6],
and Rick DeNatale *almost* came up with the same answer:

The basic approach is to start with a circle containing only the first point,
and then add the points one by one changing the circle as necessary. So:

First point

   Set center to the point and radius to zero.

Subsequent points.

   If the point is already in the circle then the point is simply added to
   the collection of points and the circle is unchanged....

   If the point is further from the center than the radius, the we know that
   it must lie on the circumference of the new circle which will contain all
   of the points examined thus far.

The point where Rick's propsed algorithm fails is in determining what
other points will be part of the circle's boundary.

I don't intend to get into the finer details of these algorithms here;
I've provided a number of references for those who are interested in
crafting an exact solution. In particular, Douglas Seifert did provide
such a solution.

The solution I am going to examine here is that of Frank Fischer,
whose solution is actually the slowest according to the benchmarks.
However, his solution was consistently close, simple to understand,
and provides a technique that can be used for many kinds of problems,
perhaps not always for speed but sometimes for sanity.

Frank's Circle class is identical to that provided by the quiz, so
I'll skip that. Let's look at his modified Point class:

    class Point
        def initialize( *coords )
            @coords = coords.map{|x| x.to_f}
        end

The initializer takes an array of numbers, rather than just the two
that my original Point class (derived from Struct) provided. Frank's
goal is to support more than just two dimensions; in fact, his
solution will work with any dimensions. The asterisk placed in front
of the parameter name means that the caller doesn't need to
specifically provide an array... just the content. So the call:

    p = Point.new(1, 2, 3, 4, 5)

Will set coords to the array [1, 2, 3, 4, 5] inside the initializer.
The initializer stores these numbers away after ensuring they all look
like floats.

        def size
            @coords.size
        end

        def ( index )
            @coords[index]
        end

        def = ( index, x )
            @coords[index] = x
        end

        def self.random( n = 2 )
            Point.new( *(1..n).map{ 0.5 - rand } )
        end

        def +(p)
            return Point.new( *(0...size).map{|i| @coords[i] +
p[i]} )
        end

        def -(p)
            return Point.new( *(0...size).map{|i| @coords[i] -
p[i]} )
        end

        def *(a)
            return Point.new( *@coords.map{|x| x * a} )
        end

        def /(a)
            return Point.new( *@coords.map{|x| x / a} )
        end

        # calculate the inner product if this point with p
        def ip(p)
            (0...size).inject(0) { |sum, i| sum + @coords[i] * p[i] }
        end

        def to_s
            "(#{@coords.join(', ')})"
        end
    end

Frank adds a number of arithmetic support functions for his Point
class. Looking at this reminds me of the standard library class
Vector... and it's not surprising, as the intent of the Point class
overlaps significantly with the Vector class. I hoped someone might
make use of Vector; alas, I think the starting code I provided was
probably a bit too suggestive. Lesson learned.

Next we break down Frank's evaluation function.

    def evaluate( m, points )
        y_big = nil
        grad = nil

Here, m is a point to evaluate as the center of a circle, and points
is the list of points that need to be enclosed. This function aims to
calculate two things. First, y_big is the minimum radius of a circle
centered at m that would enclose all the points. (Actually, it's the
square of the radius, which is a simpler calculation that avoids the
costly Math.sqrt function.) Second, grad is the gradient, which I'll
explain later.

y_big and grad both start out invalid, but provided we have at least
one point to enclose, they will both have values at the end. Now we
loop over all the points.

        points.each do |p|
            d = (m - p)

Here, d is the delta between the circle's center and the current
point. It's a vector, which means it has direction (pointing at m from
p) and length (the distance from p to m).

            y = d.ip( d )

Function ip is documented by Frank as the inner product (also known as
the dot product). The inner product has a number of uses. In this
particular case, taking the inner product of d against itself gives us
the squared length of d. If we come back to the idea that we're trying
to find a circle, if m is the center and p is on the circle, then the
length of d would be the radius. So y is the radius squared.

            if y_big.nil? or y > y_big
                y_big = y
                grad = d*2
            end
        end

Now we check to see if y is the largest squared radius we've found so
far; if so, remember it. Otherwise, keep the existing largest squared
radius y_big. Hit the end of the loop and keep checking points.

In order to enclose all the points, we remember the largest squared
radius y_big. One way to shrink that radius in a future iteration is
to move the center towards the point farthest away from our current
center m. Above, we mentioned that d was a vector that pointed away
from p towards m. That's exactly the direction we're looking for...
well, exactly the opposite direction, but that's easily remedied by
using subtraction later rather than addition. So d is scaled (by two,
maintaining direction but with length equal to the diameter) and
assigned to grad.

        return [y_big, grad]
    end

Once all points have been examined, we return the largest squared
radius and the corresponding gradient. Now let's look at Frank's final
function that makes use of this evaluation.

    def encircle( points,
                  x_start = Point.new( *([0]*points[0].size) ),
                  max_iter = 100 )

Frank's encircle function takes extra arguments, but provides sensible
defaults so that it still matches the quiz's requirements. x_start is
a point at the origin (i.e. all components are zero) which Frank uses
as the initial guess for the circle's center. max_iter is the number
of iterations performed in an attempt to narrow down the best answer.

        x = x_start
        y, g = nil, nil

        for k in 1..max_iter do
            y, g = evaluate( x, points ) # step 1
            x = x - g/k # step 2
        end

We run in a loop max_iter times, repeating two simple steps.

First, we evaluate the "fitness" of the current circle center x given
the list of points that we need to enclose. This function we described
above, and gives us back y as the squared radius of the circle and g
as the gradient used for improving upon our initial guess.

Second, we compute a new circle center x by moving it along the
gradient. As mentioned earlier, the gradient points toward the
circle's center and away from the furthest point; by subtracting it
here, that moves the new circle's center towards the furthest point in
the hope that will reduce the largest radius.

If we kept moving the circle's center about by the full length of the
gradient, we'd keep jumping around without ever narrowing in on a
solution. The answer to this problem is to scale down the gradient by
k which increases each iteration. So initial jumps are large, but each
additional jump is less and less.

        return Circle.new(x, Math.sqrt(y))
    end

After all iterations are done, we now have a circle. Since y has
always been the square of the radius, we finally take its square root
to get the true radius.

Note that this is not an exact solution to the quiz problem, but it
has shown to consistently come reasonably close to the exact solution.
And sometimes reasonably close is sufficient. The reason I picked
Frank's solution is not for its exactness nor its speed, but because
it is an elegant demonstration of a general technique. If you can
provide a fitness function (in our case, the circle radius) and some
sort of gradient, you can consider this technique for other problems.

Thanks to everyone who participated in this first quiz of mine.
Tomorrow we'll have something very simple that'll get everyone saying,
"Hello!".

[1] Smallest Enclosing Circle Problem
[2] Min_sphere_of_spheres_d<Traits>
[3] http://www.inf.ethz.ch/personal/gaertner/miniball.html
[4] http://www.cs.mcgill.ca/~cs507/projects/1998/jacob/problem.html
[5]
http://citeseer.ist.psu.edu/cache/papers/cs/30066/http:zSzzSzpage.inf.fu-berlin.dezSz~svenzSzown_workzSzmin-circle_impl_tr-b-98-04.pdf/smallest-enclosing-circles-an.pdf
[6] http://citeseer.ist.psu.edu/235065.html

Horea Raducan wrote:

If one of the points is on the enclosing circle, is that a valid solution ?

I wouldn't care about such details :
the points are defined by floating point coordinates : I'd say that defining a circle with a center and a radius in floating point which passes exactly through such points is most often impossible.
There are obvious exceptions like circles with zero radius and some values where there's no approximation in the computations but these cases aren't likely to matter here.

Lionel

* Horea Raducan, 2008-02-16, 03:06:

If one of the points is on the enclosing circle, is that a valid
solution ?

This is necessarily so if the task is to make sense. Assume you have two
points A and B and construct M such that M is the middle of AB. Then the
smallest circle that encloses A, M, and B should be the one that has M
as its center and has a diameter equal to the length of AB because
otherwise you run into the fundamental issue that the infimum of the
set of the diameters of all circles that enclose A, B, and M is the
length of AB while a circle with precisely this diameter does not meet
the requirements.

In other words: Yes, it would make no sense not to allow this as a
valid solution because it is *so* easy to construct a case in that this
definition runs into fundamental trouble.

Just in case: The fundamental issue is that assuming the solution
'point on circle' is invalid would mean that the very basic case of
three equidistant points on a straight line had no valid solution at
all.

While mathematics is no natural science you will find very few cases in
which the definitions (like that of 'enclosing circle' in this case)
AIM AT being incompatible with common sense.

Just to give an example of alien-looking definitions that make perfect
sense: The field being defined by the set {0,1} with multiplication and
addition defined as

0*0 = 0*1 = 1*0 = 0, 1*1 = 1
0+0 = 1+1 = 0, 0+1 = 1+0 = 0

This can be interpreted as binary digits with the operations AND
(as multiplication) and XOR (as addition). Take a look at this table:

a b | c s
----|-----
0 0 | 0 0
0 1 | 0 1
1 0 | 0 1
1 1 | 1 0

a and b are the inputs, c is the result of AND and s is the result of
XOR. Read 'c' as 'carry' and 's' as 'sum' and you see an addition of
two bits with carry.

Hope that was not to much math hacking :slight_smile:

Josef 'Jupp' Schugt

···

--
Blog available at http://cip.physik.uni-bonn.de/~jupp/
PGP key with id 6CC6574F available at http://wwwkeys.de.pgp.net/
Jabber - http://www.jabber.org/ - contact information on request

Yes, points on the circle are considered enclosed.

Which also means that for an input set of one point, your function
should return a circle with that point as its center and a zero
radius.

···

On Feb 15, 1:06 pm, "Horea Raducan" <hradu...@gmail.com> wrote:

If one of the points is on the enclosing circle, is that a valid solution ?

Interesting, the numbering is continuing from Ruby Quiz ?
Not starting at 1???

SUGGESTION: I don't have time to do this quiz, but it looks like fun. I for one would encourage anybody to do it with some visual representation! (suggested libs: RMagick, Gosu, Rubygame, Ruby/SDL, etc...)
An audio representation could be interesting as well (perhaps a sort of sonar?)

May I propose a bit refined random method?

 class Point &lt; Struct\.new\(:x, :y\)
     def self\.random
         Point\.new\(0\.5 \- rand, 0\.5 \- rand\)
     end

This should give us negative coordinates as well. :slight_smile:

Sure, but your algorithm should work with any set of random points,
whether they fit in the unit square from 0 to 1, -0.5 to 0.5, or
coordinates outside the unit square.

Nice solution, this was also one of my first ideas. The problem with
this approach is if many points are close to each other and only few
points are far away. In this case the center is near that mass of points
and the line-search may fail to find an optimal solution. For example:

puts encircle( ([[0.0,0.0]] * 100 + [[0.0,1.0], [1.0,0.0]]).map{|p|
        Point.new(*p)})

(100 points at (0,0), one at (1,0) and one at (0,1)) gives a solution of
{center:(0.00980392156862745, 0.00980392156862745), radius:0.990244611507173}
but optimal would be (1/2, 1/2) and radius 1/sqrt(2).

Frank

···

On 2008-02-17, Justin Ethier <justin.ethier@gmail.com> wrote:

Hello,

Had a lot of fun with this one :slight_smile: My solution works by taking the average of
all points as the center, and the farthest outlying point as the radius. An
optimal solution is then found by moving the circle center towards the
outlier as the radius is reduced, while all points still are within the
radius. The same principles could be applied for higher dimensions as well.

Had a lot of fun with this one :slight_smile: My solution works by taking the average of
all points as the center, and the farthest outlying point as the radius. An
optimal solution is then found by moving the circle center towards the
outlier as the radius is reduced

Well, I didn't have the time to check out how to do this the right way
(as Douglas did it). I nevertheless tried several approaches. This one
has some imprecision built-in but it turns out (in comparison with
Douglas' solution) that for random point sets, it performs more or
less well (for an incorrect semi-solution).

I also attach some code to run several solution against each other,
which is fun if you exclude the precise solutions. This requires the
script lib from http://redshift.sourceforge.net/script/\.

Cool quiz BTW that made me refresh my knowledge on triangles etc. I
wish a had had more time so that I could also read up on appropriate
algorithms.

Regards,
Thomas.

My semi-solution that uses a similar approach as Justin:

def encircle(points) # takes array of Point objects
    points = points.uniq
    return if points.nil? or points.empty?

    x = points.inject(0.0) {|a, p| a += p.x} / points.size
    y = points.inject(0.0) {|a, p| a += p.y} / points.size
    a = Point.new(x, y)
    return Circle.new(a, 0) unless points.size > 1

    f = points.sort_by {|p| distance(a, p)}[-1]
    df = distance(f, a)
    b = med(f, a)
    e = 1e-12
    1000.times do
        db = distance(f, b)
        if points.all? {|p| distance(b, p) <= db + e}
            da = distance(f, a)
            if (da - db).abs < e
                return Circle.new(b, db)
            else
                a, b = b, med(f, b)
            end
        else
            b = med(a, b)
        end
    end
    raise RuntimeError
end

def med(a, b)
    Point.new((b.x - a.x) / 2 + a.x, (b.y - a.y) / 2 + a.y)
end

def distance(p1, p2)
    Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
end

The code to run the tests:

#!/usr/bin/env ruby19

require 'script'

files = [
    's_tml.rb',
    's_frank.rb',
    's_justin.rb',
    's_douglas.rb',
]

def generate_samples(n)
    (1..n).map { [rand - 0.5, rand - 0.5] }
end

# Numer of samples
# N = 10
N = 50
# N = 100
# N = 200

# Tolerance
E = 1e-10

# Your favourite solution, you might want to change this
F = 's_tml.rb'

sets = Array.new(N) {|i| generate_samples(i + 2)}
solutions = Hash.new {|h, k| h[k] = {}}

# Best choice per sample
winners = Hash.new {|h,k| h[k] = 0}

# Number of invalid solutions
losers = Hash.new {|h,k| h[k] = 0}

puts
files.each do |f|
    print f
    script = Script.load(f)
    sets.each_with_index do |ps, i|
        if i % 10 == 0
            print '.'; STDOUT.flush
        end
        ps = ps.map {|p| script::Point.new(*p)}
        v = script.encircle(ps)
        if ps.any? {|p| distance(v.center, p) > v.radius + E}
            losers[f] += 1
            print 'x'; STDOUT.flush
            v.radius += 1000.0
        end
        solutions[i][f] = v
    end
    puts
end
puts

f_dist = 0

puts " %s" % files.map {|f| '%10s' % f}.join(' ')
solutions.each do |i, s|
    winner = files.sort_by {|f| s[f] ? s[f].radius : 100000000}[0]
    winners[winner] += 1
    f_d = (s[F].radius - s[winner].radius).abs
    f_dist = f_d if f_d > f_dist
    puts '%5d %10s %s (%6.4f)' % [
        i,
        '(%s)' % winner,
        files.map {|f| s[f] && '%10.4f' % s[f].radius}.join(' '),
        f_d,
    ]
end
puts

puts "Winners (best circle):"
winners.each do |f, n|
    puts '%10s %d' % [f, n]
end
puts

puts "Losers (invalid solutions):"
losers.each do |f, n|
    puts '%10s %d' % [f, n]
end
puts

puts "Favourite vs Winner:"
puts 'Max distance: %6.4f' % f_dist
puts

I had planned to improve my solution by taking an approach
similar to Justin Ethier's. . . . By iterating and "moving the
circle center towards the outlier as the radius is reduced,
while all points still are within the radius."

I may still code that if I have time... but then again, it's
already been done. :slight_smile:

Ah, what the heck... :slight_smile: Here 'tis..

http://tastyspleen.net/~billk/ruby/quiz/157-smallest-circle/157_smallest_circle.rb

# Algorithm used: First enclose the points in a simple
# axis-aligned bounding box, and choose the center of the
# box as the initial center of the circle. Then, binary
# search to determine how far we can move the center point
# toward the furthest outlying point, while keeping the
# rest of the points still inside the circle.

Regards,

Bill

···

From: "Bill Kelly" <billk@cts.com>