8 votes

Programming Challenge: Polygon analysis.

It's time for another programming challenge!

Given a list of coordinate pairs on a 2D plane that describe the vertices of a polygon, determine whether the polygon is concave or convex.

Since a polygon could potentially be any shape if we don't specify which vertices connect to which, we'll assume that the coordinates are given in strict order such that adjacent coordinates in the list are connected. Specifically, if we call the list V[1, n] and say that V[i] <-> V[j] means "vertex i and vertex j are connected", then for each arbitrary V[i] we have V[i-1] <-> V[i] <-> V[i+1]. Moreover, since V[1] and V[n] are at the ends of the list, V[1] <-> V[n] holds (i.e. the list "wraps around").

Finally, for simplicity we can assume that all coordinates are unique, that all polygon descriptions generate valid polygons with 3 or more non-overlapping sides, and that, yes, we're working with coordinates that exist in the set of real numbers only. Don't over-complicate it :)

For those who want an even greater challenge, extend this out to work with 3D space!

20 comments

  1. [4]
    onyxleopard
    (edited )
    Link
    #!/usr/bin/env python3 """This program models 2D polygons and provides some basic geometric analysis of polygons to test if they are concave or convex.""" from math import atan2, degrees from...
    #!/usr/bin/env python3
    
    """This program models 2D polygons and provides some basic geometric analysis 
    of polygons to test if they are concave or convex."""
    
    from math import atan2, degrees
    from collections import namedtuple
    
    def ngrams(iterable, n=1):
        """Generate ngrams from an iterable
        
        l = range(5)
        list(l) -> [0, 1, 2, 3, 4, 5]
        list(ngrams(l, n=1)) -> [(0,), (1,), (2,), (3,), (4,)]
        list(ngrams(l, n=2)) -> [(0, 1), (1, 2), (2, 3), (3, 4)]
        list(ngrams(l, n=3)) -> [(0, 1, 2), (1, 2, 3), (2, 3, 4)]
        
        """
        return zip(*(iterable[i:] for i in range(n)))
    
    def angle(v1, v2, v3):
        """Compute the angle of 3 vertices in degrees"""
        return degrees(
            atan2(v1.x - v2.x, v2.y - v1.y) - atan2(v2.x - v3.x, v3.y - v2.y)
        )
    
    def z(v1, v2, v3):
        """Compute the z component of the cross product of 3 vertices"""
        return (v1.x - v2.x) * (v2.y - v3.y) - (v1.y - v2.y) * (v2.x - v3.x)
    
    Vertex = namedtuple('Vertex', ['x', 'y'])
    
    class Polygon():
        """A class modeling a 2D polygon as a list of Vertices (where a Vertex
        is a namedtuple with 2 attributes: x and y)"""
        def __init__(self, *vertices):
            self.vertices = list(vertices)
            if not all((isinstance(v, Vertex) for v in self.vertices)):
                raise ValueError('expected iterable of Vertices')
            if not len(self.vertices) >= 3:
                raise ValueError(
                    f'expected at least 3 Vertices (got {len(vertices)})'
                )
        
        def __str__(self):
            return f'{self.__class__.__name__}{self.vertices}'
        
        def __repr__(self):
            return str(self)
        
        def __len__(self):
            return len(self.vertices)
        
        def __iter__(self):
            return iter(self.vertices)
        
        def trigrams(self):
            """Generate all trigrams of Vertices in the Polygon"""
            first, second, *rest = self.vertices
            *rest, penultimate, ultimate = self.vertices
            yield from ngrams(self.vertices, n=3)
            yield penultimate, ultimate, first
            yield ultimate, first, second
        
        def angles(self):
            """Generate all the angles of the Vertices in the Polygon"""
            yield from (angle(*trigram) for trigram in self.trigrams())
        
        def convex(self):
            """Return True if the Polygon is convex and False otherwise"""
            zs = [z(*trigram) > 0 for trigram in self.trigrams()]
            return all(zs) or not any(zs)
        
        def concave(self):
            """Return True if the Polygon is concave and False otherwise"""
            return not self.convex()
    
    if __name__ == '__main__':
        polygons = [
            Polygon(
                Vertex(x=0, y=0),
                Vertex(x=0, y=1),
                Vertex(x=1, y=0)
            ),
            Polygon(
                Vertex(x=0, y=0),
                Vertex(x=0, y=1),
                Vertex(x=1, y=1),
                Vertex(x=1, y=0)
            ),
            Polygon(
                Vertex(x=0, y=1),
                Vertex(x=0, y=0),
                Vertex(x=1, y=0),
                Vertex(x=-1, y=-1)
            )
        ]
        for polygon in polygons:
            print(f'{polygon}.convex(): {polygon.convex()}')
    

    Run it:

    $ ./polygons.py
    Polygon(Vertex(x=0, y=0), Vertex(x=0, y=1), Vertex(x=1, y=0)).convex(): True
    Polygon(Vertex(x=0, y=0), Vertex(x=0, y=1), Vertex(x=1, y=1), Vertex(x=1, y=0)).convex(): True
    Polygon(Vertex(x=0, y=1), Vertex(x=0, y=0), Vertex(x=1, y=0), Vertex(x=-1, y=-1)).convex(): False
    
    5 votes
    1. [2]
      cfabbro
      Link Parent
      Just FYI, you can use syntax-highlighting for programming languages on Tildes if you specify the language of the code after the initial triple-backtick:...

      Just FYI, you can use syntax-highlighting for programming languages on Tildes if you specify the language of the code after the initial triple-backtick:
      https://docs.tildes.net/text-formatting#preformatted-text-and-code

      3 votes
      1. onyxleopard
        Link Parent
        Thanks! I didn’t use a triple-backtick originally, I just used a code block by indenting lines.

        Thanks! I didn’t use a triple-backtick originally, I just used a code block by indenting lines.

        1 vote
    2. jonluca
      Link Parent
      This is great! Very pythonic answer.

      This is great! Very pythonic answer.

      3 votes
  2. [4]
    Flashynuff
    Link
    I originally tried to go through and check if there were any interior angles greater than 180 degrees. Unfortunately, math is definitely not my strong suit and I wasn't able to figure out how to...

    I originally tried to go through and check if there were any interior angles greater than 180 degrees. Unfortunately, math is definitely not my strong suit and I wasn't able to figure out how to calculate the correct angle from vectors.

    There's good news though, I was able to figure out a much shorter approach that checks directionality instead. I think it's close to what @onyxleopard did from this link regarding cross products.

    Here's my solution, in Ruby.

    require 'matrix'
    
    polygon_1 = [
      [1,1],
      [5,1],
      [5,5],
      [3,3],
      [1,5]
    ]
    
    polygon_2 = [
      [1,1],
      [5,1],
      [5,5],
      [3,7],
      [1,5]
    ]
    
    def convert_two_points_to_vector(point_1, point_2)
      result = Vector[point_2[0] - point_1[0], point_2[1]- point_1[1]]
    end
    
    def cross_product(v1, v2)
      v1[0] * v2[1] - v1[1] * v2[0]
    end
    
    def analyze_polygon(polygon)
      # go through the array backwards, since Ruby will wrap around backwards (but not forwards)
      concave = false;
      for i in (polygon.length - 1).downto(0)
        a = convert_two_points_to_vector(polygon[i], polygon[i-1])
        b = convert_two_points_to_vector(polygon[i-1], polygon[i-2])
        cross = cross_product(a, b)
        if (i == polygon.length - 1)
          dir = cross > 0 # set dir on the first time through
        elsif (dir == cross < 0) # check if the direction has changed
          concave = true
          break
        end
      end
      puts "Polygon #{polygon.to_s} is #{concave ? 'concave' : 'convex'}"
    end
    
    analyze_polygon(polygon_1)
    analyze_polygon(polygon_2)
    

    output:

    $ ruby analysis.rb
    Polygon [[1, 1], [5, 1], [5, 5], [3, 3], [1, 5]] is concave
    Polygon [[1, 1], [5, 1], [5, 5], [3, 7], [1, 5]] is convex
    
    3 votes
    1. bltzkrg22
      Link Parent
      While you say that math is not your strong suit, you have unconsciously done exactly what is required to check if the angle is greater than 180 degrees. If a and b are vectors which lie on the xy...

      While you say that math is not your strong suit, you have unconsciously done exactly what is required to check if the angle is greater than 180 degrees. If a and b are vectors which lie on the xy plane, then their cross product is equal to this:

      figure
      equation

      The sign of the right side depends only on the sign of sin, and:

      equation

      (this is a simplification, but you get the idea). And since phi is counted counter-clockwise from a to b, either every cross you calculate must be positive (when the vertices are oriented counter-clockwise) , or negative (when the vertices are clockwise).

      2 votes
    2. [2]
      onyxleopard
      Link Parent
      I also started down the angles route (and I even left in some of the fruits of my labor down that line in my solution in the Polygon.angles method). And I gave up on it when I couldn’t figure out...

      I also started down the angles route (and I even left in some of the fruits of my labor down that line in my solution in the Polygon.angles method). And I gave up on it when I couldn’t figure out how to orient the angles. That’s when I remembered the ‘direction’ trick.

      1 vote
      1. Flashynuff
        Link Parent
        Yeah, that's exactly where I gave up on that approach too. Thanks for blazing the trail :)

        Yeah, that's exactly where I gave up on that approach too. Thanks for blazing the trail :)

        2 votes
  3. Flashynuff
    Link
    I wanted to make sure I actually knew what was happening in my earlier Ruby solution and that I didn't just luck into some Ruby magic, so I reimplemented that same solution in Java. import...

    I wanted to make sure I actually knew what was happening in my earlier Ruby solution and that I didn't just luck into some Ruby magic, so I reimplemented that same solution in Java.

    import java.util.Arrays;
    
    public class PolygonAnalysis {
      public static void main(String[] args) {
        double[][] polygon1 = {
          {1,1},
          {5,1},
          {5,5},
          {3,3},
          {1,5}
        };
        
        double[][] polygon2 = {
          {1,1},
          {5,1},
          {5,5},
          {3,7},
          {1,5}
        };
    
        double[][][] polygons = {
          polygon1,
          polygon2
        };
    
        for (double[][] polygon : polygons) {
          String msg = isConcave(polygon) ? "concave" : "convex";
          System.out.println("Polygon " + Arrays.deepToString(polygon) + " is " + msg);
        }
      }
    
      public static double crossProduct(double[] v1, double[] v2) {
        double c = (v1[0] * v2[1]) - (v1[1] * v2[0]);
        return c;
      }
    
      public static double[] createVector(double[] p1, double[] p2) {
        double[] newVector = {
          p2[0] - p1[0],
          p2[1] - p1[1]
        };
        return newVector;
      }
    
      public static boolean isConcave(double[][] polygon) {
        boolean dir = false;
        double cross = 0;
    
        if (polygon.length <= 3) 
          return false;
    
        for (int i = 0; i < polygon.length - 1 ; i++ ) {
          if (i == polygon.length - 2) {
            double[] v1 = createVector(polygon[i], polygon[i+1]);
            double[] v2 = createVector(polygon[i+1], polygon[0]);
            cross = crossProduct(v1, v2);
          }
          else if (i == polygon.length - 1) {
            double[] v1 = createVector(polygon[i], polygon[0]);
            double[] v2 = createVector(polygon[0], polygon[1]);
            cross = crossProduct(v1, v2);
          }
          else {
            double[] v1 = createVector(polygon[i], polygon[i+1]);
            double[] v2 = createVector(polygon[i+1], polygon[i+2]);
            cross = crossProduct(v1, v2);
          }
    
          if(i == 0)
            dir = cross > 0;
          else if (dir == cross < 0)
            return true;
        }
    
        return false;
      }
    }
    

    output:

    $ java PolygonAnalysis
    Polygon [[1.0, 1.0], [5.0, 1.0], [5.0, 5.0], [3.0, 3.0], [1.0, 5.0]] is concave
    Polygon [[1.0, 1.0], [5.0, 1.0], [5.0, 5.0], [3.0, 7.0], [1.0, 5.0]] is convex
    
    2 votes
  4. [11]
    bltzkrg22
    Link
    That’s too easy. Given a set of points on a plane, there is no more than one convex polygon with all vertices in those points. If you have an ordered list, you can just start with the first vertex...

    That’s too easy. Given a set of points on a plane, there is no more than one convex polygon with all vertices in those points.

    If you have an ordered list, you can just start with the first vertex and check if the next angle is always less than 180 degrees (considering two cases, since the list can be oriented either clockwise or counter-clockwise).

    1. [10]
      onyxleopard
      Link Parent
      If it’s so easy, why don’t you write a program to do it ;) The point of the challenges is to practice programming, not solve a hard problem.

      If it’s so easy, why don’t you write a program to do it ;)

      The point of the challenges is to practice programming, not solve a hard problem.

      9 votes
      1. [9]
        Emerald_Knight
        Link Parent
        Additionally, the fun of a programming challenge is figuring out how to solve it in the first place. There are quite a few programmers who are either math-averse or don't have a mathematics...

        Additionally, the fun of a programming challenge is figuring out how to solve it in the first place. There are quite a few programmers who are either math-averse or don't have a mathematics background in general, so the "angle greater than 180 degrees" solution may not be obvious to many. There are also other ways to solve the problem without that concept in mind (e.g. determining via a count of shifts in directionality), so part of the fun is also seeing how other people approach the problem. You could also discuss submitted solutions to determine the pros and cons of using one over the other in terms of anything from efficiency to generic extensions to long-term maintainability.

        6 votes
        1. onyxleopard
          Link Parent
          Yes, that’s a good point. I am fairly math averse myself (and the math I have studied a bit is discrete math). I just so happened to have remembered hearing of using the z component of the cross...

          Yes, that’s a good point. I am fairly math averse myself (and the math I have studied a bit is discrete math). I just so happened to have remembered hearing of using the z component of the cross product of vectors to compute the 'direction' (there’s probably a term of art for this?) of 2d vectors here. So I sort of cheated here because I didn’t know the math, I just implemented it.

          4 votes
        2. [7]
          bltzkrg22
          Link Parent
          By too easy, I mean that the assumption that the points are given in a strict order is unnecessary, and makes the problem trivial. There’s little fun in solving trivial puzzles. Just modify the...

          By too easy, I mean that the assumption that the points are given in a strict order is unnecessary, and makes the problem trivial. There’s little fun in solving trivial puzzles. Just modify the assumptions, and it gets a little more interesting (though still not very hard):

          Given a list of coordinate pairs on a 2D plane, determine whether you can draw a convex polygon with vertices in each of the points.

          1. [6]
            Emerald_Knight
            Link Parent
            Not all problems are intended to be difficult. I like to give a mix of easy, intermediate, and difficult problems so that less experienced programmers aren't alienated. Everyone should be able to...

            Not all problems are intended to be difficult. I like to give a mix of easy, intermediate, and difficult problems so that less experienced programmers aren't alienated. Everyone should be able to participate.

            6 votes
            1. [5]
              Flashynuff
              Link Parent
              Non-mathematics person chiming in here. I have no idea what I'm doing with vectors, so this problem is on the harder side of the challenges that you've posted that I've completed. Seeing someone...

              Non-mathematics person chiming in here. I have no idea what I'm doing with vectors, so this problem is on the harder side of the challenges that you've posted that I've completed. Seeing someone say "this is so easy it's trivial" is more than a little discouraging.

              8 votes
              1. onyxleopard
                Link Parent
                Don’t get discouraged by mathy people saying something is trivial. There’s an old joke that I heard from one of my undergrad professors that ‘trivial’ in math is code for ‘I’m too lazy to work...

                Don’t get discouraged by mathy people saying something is trivial. There’s an old joke that I heard from one of my undergrad professors that ‘trivial’ in math is code for ‘I’m too lazy to work this out fully’.

                6 votes
              2. [3]
                Emerald_Knight
                Link Parent
                This is why I was certain to mention that there are programmers like yourself who may not have a mathematics background (or don't have a background in that particular area of mathematics). Certain...

                This is why I was certain to mention that there are programmers like yourself who may not have a mathematics background (or don't have a background in that particular area of mathematics). Certain problems are going to feel trivial to certain people because those people have a particular background in some field, and for others the problems can be fairly difficult because of that lack of background. This is a reality that professional programmers experience every day.

                It's for this reason that I haven't adopted a "difficulty" scale of any kind for these programming challenges. It would be so arbitrary as to be completely useless, and unlucky beginner programmers in particular who see "easy" and just happen to not do well with the particular problem may end up throwing in the proverbial towel altogether.

                Please try not to feel discouraged. I've seen some of your past contributions and you're doing perfectly fine.

                5 votes
                1. [2]
                  Flashynuff
                  Link Parent
                  Thank you, I appreciate it! Thank you for posting these as well.

                  Thank you, I appreciate it! Thank you for posting these as well.

                  5 votes
                  1. Emerald_Knight
                    Link Parent
                    No problem! Also, for the record, I just spent the last hour taking a crack at this problem and ended up rage-quitting because I found that my solution required a lot of reworking to get things to...

                    No problem!

                    Also, for the record, I just spent the last hour taking a crack at this problem and ended up rage-quitting because I found that my solution required a lot of reworking to get things to function correctly. You have me beat by default. So there's that ;)

                    3 votes