凸包を求めるアルゴリズム（ギフト包装法）

> 数学における凸包または凸包絡は、与えられた集合を含む最小の凸集合である。 [Wikipedia: 凸包](https://ja.wikipedia.org/wiki/%E5%87%B8%E5%8C%85)

![gift-wrapping.png](https://qiita-image-store.s3.amazonaws.com/0/17134/b5405c4c-0db4-79d7-829b-f8c76276986e.png)

上図のように点の集合が与えられた時にその凸包を求めるアルゴリズムを実装してみたのでメモとして残しておきます。

## :new_moon: Linear Algebra
まずは計算に必要になる点やベクトルと言った概念を定義しておきます。

```hs
-- | ２次元の点
type Point = (Double, Double)

-- | ２次元ベクトル
type Vector = (Double, Double)

-- | ２点からベクトルを作成
to :: Point -> Point -> Vector
to (x0, y0) (x1, y1) = (x1 - x0, y1 - y0)

-- | ノルムを計算する
norm :: Vector -> Double
norm (x, y) = sqrt $ x ^ 2 + y ^ 2

-- | 単位ベクトルに変換する
normalize :: Vector -> Vector
normalize v@(x, y) = let r = norm v in (x / r, y / r)

-- | 直交ベクトルに変換する
orthogonalize :: Vector -> Vector
orthogonalize (x, y) = (-y, x)

-- | 内積を取る
dot :: Vector -> Vector -> Double
dot (x0, y0) (x1, y1) = x0 * x1 + y0 * y1
```

## :first_quarter_moon: ギフト包装法

> ギフト包装法やJarvisの行進法とは、計算幾何学における点の集合の凸包を求めるアルゴリズム。 [Wikipedia: ギフト包装法](https://ja.wikipedia.org/wiki/%E3%82%AE%E3%83%95%E3%83%88%E5%8C%85%E8%A3%85%E6%B3%95)

ギフト包装法は凸包を求める最良のアルゴリズムというわけではないようですが、仕組みがとてもわかり易かったので実装してみました。

簡単に説明すると

1. まず凸包の境界に含まれる１点を見つける
2. 選んだ1点から別の点に直線を結び他のすべての点がその直線の片側に来るような点を見つける
3. 2を1で選んだ点が再び選ばれるまで繰り返す

という感じです

```hs
import Data.List (sort, sortOn, (\\))

-- | 与えられたベクトルとその始点からの偏角を計算する
angle :: Point -> Vector -> Point -> Double
angle p0 v p1 =
  let nv = normalize v
      ov = orthogonalize nv
      v' = p0 `to` p1
   in atan2 (v' `dot` ov) (v' `dot` nv)

-- | ２次元の線
type Line = [Point]

-- | ギフト包装法で与えられた点群の凸法を求める
giftWrapping :: [Point] -> Line
giftWrapping ps | length ps < 3 = ps
giftWrapping ps =
  let p0 = head $ sort ps
      vx = (1.0, 0.0)
      p1 = head . sortOn (angle p0 vx) $ ps \\ [p0]
   in go [p1, p0] ps
  where
    go :: [Point] -> [Point] -> [Point]
    go history ps =
      let [p2, p1] = take 2 history
          v = p1 `to` p2
          pn' = head . sortOn (angle p2 v) $ ps \\ [p2, p1]
       in if pn' == last history
             then pn' : history
             else go (pn' : history) ps
```

`angle` は与えられた点`p0`とベクトル`v`からそれを基準とした別の点`p1`の偏角を計算する関数です。図にすると以下のような `θ` を求めています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/17134/c096d860-2cb2-5693-b87c-d9d3684341f7.png)

## :waxing_gibbous_moon: 動作確認
作ったギフト包装法のアルゴリズムを検証してみましょう。今回は [`mwc-random`](https://hackage.haskell.org/package/mwc-random) を使ってランダムな点を生成し、[`Chart`](https://hackage.haskell.org/package/Chart) と [`Chart-cairo`](https://hackage.haskell.org/package/Chart-cairo) を使って描画して画像に書き出しました。

```hs
import Graphics.Rendering.Chart.Easy (plot, line, points, def)
import Graphics.Rendering.Chart.Backend.Cairo (toFile)
import System.Random.MWC (withSystemRandom, asGenIO)
import System.Random.MWC.Distributions (normal)

-- | ランダムな２次元の点群を生成する
generatePoints :: Int -> IO [Point]
generatePoints n = do
  withSystemRandom . asGenIO $ \gen -> do
    xs <- sequence . replicate n $ normal 0 1 gen
    ys <- sequence . replicate n $ normal 0 1 gen
    pure $ zip xs ys

-- | 結果をプロットして画像に書き出す
saveImage :: FilePath -> [Point] -> Line -> IO ()
saveImage path ps ln = toFile def path $ do
  plot (line "convex hull" [ln])
  plot (points "points" ps)

main :: IO ()
main = do
  ps <- generatePoints 100
  let ln = giftWrapping ps
  saveImage "gift-wrapping.png" ps ln
```

結果を見てみましょう

![gift-wrapping.png](https://qiita-image-store.s3.amazonaws.com/0/17134/192751dc-803e-f4ac-4ade-4b90b6cf86e0.png)

うまくいってそうですね :tada: 

## :full_moon: 完全なプログラム

```hs
import Data.List (sort, sortOn, (\\))

import Graphics.Rendering.Chart.Easy (plot, line, points, def)
import Graphics.Rendering.Chart.Backend.Cairo (toFile)
import System.Random.MWC (withSystemRandom, asGenIO)
import System.Random.MWC.Distributions (normal)

--------------------
-- Linear Algebra
--------------------

-- | ２次元の点
type Point = (Double, Double)

-- | ２点からベクトルを作成する
to :: Point -> Point -> Vector
to (x0, y0) (x1, y1) = (x1 - x0, y1 - y0)

-- | ２次元ベクトル
type Vector = (Double, Double)

-- | ノルムを計算する
norm :: Vector -> Double
norm (x, y) = sqrt $ x ^ 2 + y ^ 2

-- | 単位ベクトルに変換する
normalize :: Vector -> Vector
normalize v@(x, y) = let r = norm v in (x / r, y / r)

-- | 直交ベクトルに変換する
orthogonalize :: Vector -> Vector
orthogonalize (x, y) = (-y, x)

-- | 内積を取る
dot :: Vector -> Vector -> Double
dot (x0, y0) (x1, y1) = x0 * x1 + y0 * y1

-----------------------------
-- Gift Wrapping Algorithm
-----------------------------

-- | 与えられたベクトルとその始点からの偏角を計算する
angle :: Point -> Vector -> Point -> Double
angle p0 v p1 =
  let nv = normalize v
      ov = orthogonalize nv
      v' = p0 `to` p1
   in atan2 (v' `dot` ov) (v' `dot` nv)

-- | ２次元の線
type Line = [Point]

-- | ギフト包装法で与えられた点群の凸法を求める
giftWrapping :: [Point] -> Line
giftWrapping ps | length ps < 3 = ps
giftWrapping ps =
  let p0 = head $ sort ps
      vx = (1.0, 0.0)
      p1 = head . sortOn (angle p0 vx) $ ps \\ [p0]
   in go [p1, p0] ps
  where
    go :: [Point] -> [Point] -> [Point]
    go history ps =
      let [p2, p1] = take 2 history
          v = p1 `to` p2
          pn' = head . sortOn (angle p2 v) $ ps \\ [p2, p1]
       in if pn' == last history
             then pn' : history
             else go (pn' : history) ps

-----------
-- Misc.
-----------

-- | ランダムな２次元の点群を生成する
generatePoints :: Int -> IO [Point]
generatePoints n = do
  withSystemRandom . asGenIO $ \gen -> do
    xs <- sequence . replicate n $ normal 0 1 gen
    ys <- sequence . replicate n $ normal 0 1 gen
    pure $ zip xs ys

-- | 結果をプロットして画像に書き出す
saveImage :: FilePath -> [Point] -> Line -> IO ()
saveImage path ps ln = toFile def path $ do
  plot (line "convex hull" [ln])
  plot (points "points" ps)

----------
-- Main
----------

main :: IO ()
main = do
  ps <- generatePoints 100
  let ln = giftWrapping ps
  saveImage "gift-wrapping.png" ps ln
```