From: Philipp Hofmann Date: 2008-02-19T08:27:07+09:00 Subject: Re: [QUIZ] The Smallest Circle (#157) --4Ckj6UjgE2iN1+kY Content-Type: text/plain; charset=us-ascii Content-Disposition: inline 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 --4Ckj6UjgE2iN1+kY Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="smallest_enclosing_circle.rb" class Point < Struct.new(:x, :y) def self.random Point.new(rand, rand) end def to_s "(#{x}, #{y})" end def distance(p) Math.sqrt((x-p.x)**2+(y-p.y)**2) end def middle(p) Point.new((self.x+p.x)/2, (self.y+p.y)/2) end end class Circle < Struct.new(:center, :radius) def to_s "{center:#{center}, radius:#{radius}}" end end def generate_samples(n) (1..n).map { Point.random } end def encircle(points) # only one point? then we're done return Circle.new(points.first, 0) if points.size==1 # firstly, we calculate the convex hull # this will reduce points to calculate # especially for larger sets hull = ConvexHull::convex_hull(points) draw_hull(hull.clone, :lightgray) if VISUALIZE # find the two spots on the hull with the larges distance # these will make a good starting point distances0 = {} list_of_points = hull.clone until list_of_points.empty? p1 = list_of_points.shift list_of_points.each do |p2| distances0[[p1, p2]] = p1.distance(p2) end end max0 = distances0.values.max p0, p1 = distances0.invert[max0] circle0 = Circle.new(p0.middle(p1), max0/2) hull.delete(p0) hull.delete(p1) # we're done if the hull only had two points return circle0 if hull.empty? # find the point that has the largest # distance to the center of circle0 distances1 = {} hull.each do |point| distances1[point] = circle0.center.distance(point) end max1 = distances1.values.max # if the farest point from center is # closer then the radius we're done draw_circle(circle0.center, circle0.radius, :green) if VISUALIZE return circle0 if max1 <= circle0.radius p2 = distances1.invert[max1] circle1 = SOLE::circle_of(p0, p1, p2) hull.delete(p2) # find the point that has the largest # distance to the center of circle1 distances2 = {} hull.each do |point| distances2[point] = circle1.center.distance(point) end max2 = distances2.values.max # if the farest point from center is in the circle we're done draw_circle(circle1.center, circle1.radius, :blue) if VISUALIZE return circle1 if max2 <= circle1.radius # ok, now we fall back to brute force on the convex hull # we can do this because the random distribution given # leads to an approximate square and therefore to convex hulls # with very few points, if this weren't the case we should # uses clarksons probabilistic approach here, especcially # for larger sets of points. hull.concat([p0, p1, p2]) sets_of_three = SubSet::subsets(hull, 3) solutions = [] sets_of_three.each do |subset| circle2 = SOLE::circle_of(*subset) distances3 = {} hull.each do |p3| distances3[p3] = circle2.center.distance(p3) unless subset.include?(p3) end max3 = distances3.values.max solutions << circle2 if max3 <= circle2.radius end circle3 = solutions.sort_by { |circle| circle.radius }.first draw_circle(circle3.center, circle3.radius, :red) if VISUALIZE return circle3 end # --- calculate subsets module SubSet # i admit this could definitly be improved def self.subsets(set, ranks=nil) ranks ||= 0..set.size ranks = ranks..ranks unless ranks.is_a?(Range) sets = all_subsets(set) sets.select { |s| ranks.include?(s.size) } end def self.all_subsets(set) return [[]] if set.empty? set = set.clone first = set.shift sets = all_subsets(set) sets.concat(sets.collect { |s| [first] + s }) return sets end end # --- calculate the convex hull module ConvexHull # after graham & andrew def self.convex_hull(points) lop = points.sort_by { |p| p.x } left = lop.shift right = lop.pop lower, upper = [left], [left] lower_hull, upper_hull = [], [] det_func = determinant_function(left, right) until lop.empty? p = lop.shift ( det_func.call(p) < 0 ? lower : upper ) << p end lower << right until lower.empty? lower_hull << lower.shift while (lower_hull.size >= 3) && !convex?(lower_hull.last(3), true) last = lower_hull.pop lower_hull.pop lower_hull << last end end upper << right until upper.empty? upper_hull << upper.shift while (upper_hull.size >= 3) && !convex?(upper_hull.last(3), false) last = upper_hull.pop upper_hull.pop upper_hull << last end end upper_hull.shift upper_hull.pop lower_hull + upper_hull.reverse end private def self.determinant_function(p0, p1) proc { |p| ((p0.x-p1.x)*(p.y-p1.y))-((p.x-p1.x)*(p0.y-p1.y)) } end def self.convex?(list_of_three, lower) p0, p1, p2 = list_of_three (determinant_function(p0, p2).call(p1) > 0) ^ lower end end # --- calculate a circle out of three given points module SOLE def self.circle_of(*three_points) m = sole_matrix_for_circle(three_points) solve_sole!(m) x = m[1].last y = m[2].last r = Math.sqrt(x*x + y*y - m[0].last) Circle.new(Point.new(x, y), r) end private def self.sole_matrix_for_circle(points) matrix = [] c = 0 points.each do |p| matrix[c] = [1] matrix[c] << -2 * p.x matrix[c] << -2 * p.y matrix[c] << -p.x**2 - p.y**2 c += 1 end matrix end # solve system of linear equations def self.solve_sole!(matrix) num_equations = matrix.size num_variables = matrix.first.size (num_variables-1).times do |j| q = matrix[j][j] if q == 0 for i in j+1..num_equations-1 do if matrix[i][j] != 0 for k in 0..num_variables-1 do matrix[j][k] += matrix[i][k] end q = matrix[j][j] break end end end if q != 0 for k in 0..num_variables-1 matrix[j][k] = matrix[j][k] / q end end for i in 0..num_equations-1 if i != j q = matrix[i][j] for k in 0..num_variables-1 matrix[i][k] -= matrix[j][k] * q end end end end end end # ----- visualize def scale(value) $size / 6 + value * $size / 1.5 end def draw_circle(center, radius, color=false) $gc.stroke_color(color.to_s) if color $gc.circle(scale(center.x), scale(center.y), scale(center.x), scale(center.y+radius)) end def draw_point(center, color=false) $gc.stroke_color(color.to_s) if color $gc.circle(scale(center.x), scale(center.y), scale(center.x), scale(center.y)+2) end def draw_line(p0, p1, color=false) $gc.stroke_color(color.to_s) if color $gc.line(scale(p0.x), scale(p0.y), scale(p1.x), scale(p1.y)) end def draw_hull(points, color=false) first = points.shift prev = first until points.empty? p = points.shift draw_line(prev, p, color) prev = p end draw_line(prev, first, color) end # ----- if executed directly if __FILE__ == $0 VISUALIZE = true if VISUALIZE require 'RMagick' $size = 600 $gc = Magick::Draw.new $gc.fill_opacity(0) $gc.stroke_width(2) $gc.stroke_opacity(1) end samples = generate_samples(ARGV[0].to_i) circle = encircle(samples) puts circle if VISUALIZE samples.each { |p| draw_point(p, :black) } canvas = Magick::ImageList.new canvas.new_image($size, $size) $gc.draw(canvas) canvas.display end end --4Ckj6UjgE2iN1+kY--