Contact
Département d'informatique
Université du Québec à Montréal
CP 8888, Succ. Centre-ville
Montréal (Québec) H3C 3P8
Phone: 514-987-3000, #5516
Office: PK-4525
Email: blondin_masse[dot]alexandre
[at]uqam[dot]ca
About
I have completed my Ph.D. in mathematics and computer science under the supervision of Professors Srecko Brlek, from Université du Québec à Montréal, in Canada, and Laurent Vuillon, from Université de Savoie, in France.Since August 1st, 2014, I'm a regular assistant-professor at Université du Québec à Montréal, in Canada.
Links
- My profile on Google Scholar
- My GitLab repositories
- My Bitbucket repositories
- My Github repositories
Graham's scan
[français]On 02/06/2015 by Alexandre Blondin Massé
I just realized today that I have not written on that blog for two years. It is definitely time that I take it more seriously.
For the last months, I have started using the functional programming language Haskell. Since I have never been an expert and since it has been a while, I would like to share with you the steps I have been following in order to implement Graham's scan which computes the convex hull of a set of points in the plane.
First, since we handle 2D points, it is useful to define de corresponding data type. I assume that the coordinates are doubles (Double in Haskell):
-- 2D points data Point2D = Point2D Double Double deriving (Show, Eq)
I recall that, thanks to the expression deriving (Show, Eq), it is possible to display (Show) a point when we use the ghci interpreter and also to test for point equality (Eq), which is done coordinate-wise.
A useful function is the square of the distance between two points:
-- Square of distance between points squareDistance :: Point2D -> Point2D -> Double squareDistance (Point2D a b) (Point2D c d) = (c - a) * (c - a) + (d - b) * (d - b)
We will also need a sample to test our algorithm:
-- Points points = [Point2D 0 0, Point2D 2 9, Point2D 3 4, Point2D 0 5, Point2D 6 2, Point2D 7 3, Point2D 4 1, Point2D 5 7]
A graphical representation can be obtained with the Sage Cell Server :
Now, let me list below the 3 main steps of Graham's algorithm:
- One identifies the lowest point. If two points share the same y-coordinate, then one chooses the leftmost one. The minimal point is called the origin.
- One sorts the points in increasing order of angle with respect to the origin. Note that, by choice of the origin, the angle is between 0∘ and 180∘. If two points have the same angle, the the comparison is done on the distance with respect to the origin.
- For each successive triple of points, one verifies if they form a left turn,
a right turn or a straight line.
- If it is a left turn, we move on to the next triple;
- Otherwise, we eliminate the central point from the convex hull. Then, we keep the two remaining points in the next triple.
Step 1 is easy. It suffices to define a lexicographic order on the points with respect to the y-coordinate first, then the x coordinate.
-- Lexicographic order on points (with respect to y and then to x) comparePoints :: Point2D -> Point2D -> Ordering comparePoints (Point2D a b) (Point2D c d) | b < d = LT | b > d = GT | b == d = compare a c
This function returns the value LT, EQ or GT (of Ordering type) accordingly. The origin is then retrieved with the help of the expression l'expression
List.minimumBy comparePoints points
To complete step 2, it suffices to introduce an order relation based on the angle of each point in the polar coordinates system, where the origin is the lowest-leftmost point computed before. Notice that when the angle is equal, then the distance with respect to the origin is used. This ensures that the origin is always the first point in the resulting list:
-- Angle comparator between two points with respect to some origin compareAngle :: Point2D -> Point2D -> Point2D -> Ordering compareAngle origin@(Point2D a b) p@(Point2D c d) q@(Point2D e f) | cross > 0 = LT | cross == 0 = Ord.comparing (squareDistance origin) p q | cross < 0 = GT where cross = (c - a) * (f - b) - (d - b) * (e - a)
The first parameter is the origin. The next two parameters are the two points we wish to compare. This test is very standard in algorithmic geometry: it consists in verifying the sign of the cross product of two vectors induced by the three points. According to whether the sign is positive, negative or null, one deduces that the angle of the first point is smaller, greater or equal to the second one. The reader less familiar with linear algebra might be interested in toying with the following interact to verify the property:
Hence, in the main function, we first sort the points, then we call grahamScan to complete the processing:
-- Convex hull computation convexHull :: [Point2D] -> [Point2D] convexHull points = grahamScan (List.sortBy (compareAngle origin) points) where origin = List.minimumBy comparePoints points
Notice how short is the scan:
-- Graham scan grahamScan [] = [] grahamScan [p] = [p] grahamScan [p,q] = [p,q] grahamScan (p:q:r:ps) | compareAngle q p r == GT = p:(grahamScan (q:r:ps)) | otherwise = grahamScan (p:r:ps)
We may verify that the function returns what is expected when applied to the set of points mentioned above:
Main> convexHull points [Point2D 0.0 0.0,Point2D 4.0 1.0,Point2D 6.0 2.0,Point2D 7.0 3.0, Point2D 5.0 7.0,Point2D 2.0 9.0,Point2D 0.0 5.0]
Here is the complete code:
-- Imports import qualified Data.List as List import qualified Data.Ord as Ord -- 2D points data Point2D = Point2D Double Double deriving (Show, Eq) -- Square of distance between points squareDistance :: Point2D -> Point2D -> Double squareDistance (Point2D a b) (Point2D c d) = (c - a) * (c - a) + (d - b) * (d - b) -- Points points = [Point2D 0 0, Point2D 2 9, Point2D 3 4, Point2D 0 5, Point2D 6 2, Point2D 7 3, Point2D 4 1, Point2D 5 7] -- Angle comparator between two points with respect to some origin compareAngle :: Point2D -> Point2D -> Point2D -> Ordering compareAngle origin@(Point2D a b) p@(Point2D c d) q@(Point2D e f) | cross > 0 = LT | cross == 0 = Ord.comparing (squareDistance origin) p q | cross < 0 = GT where cross = (c - a) * (f - b) - (d - b) * (e - a) -- Lexicographic order on points (with respect to y and then to x) comparePoints :: Point2D -> Point2D -> Ordering comparePoints (Point2D a b) (Point2D c d) | b < d = LT | b > d = GT | b == d = compare a c -- Convex hull computation convexHull :: [Point2D] -> [Point2D] convexHull points = grahamScan (List.sortBy (compareAngle origin) points) where origin = List.minimumBy comparePoints points -- Graham scan grahamScan [] = [] grahamScan [p] = [p] grahamScan [p,q] = [p,q] grahamScan (p:q:r:ps) | compareAngle q p r == GT = p:(grahamScan (q:r:ps)) | otherwise = grahamScan (p:r:ps)
It is interesting to see that the relations using more space are those about order relations. Once they are done, the core of the algorithm does not need more than 10 lines, including commentaries.