圖表畫法中的凸包問題:給菜園圍一圈柵欄需要花多少錢?

前言

最近日子苦還房貸 ,聽人說大頭菜現(xiàn)在商機很足,于是我決定自己去種大頭菜,來實現(xiàn)快速的發(fā)家致富

image.png

好景不長,種的大肉菜才剛長出來一點點,昨天夜里就冒出來了一頭野豬拱了我好幾顆菜,看來必須要上柵欄了。但是怎么圍柵欄這事難倒我了。

早先種大頭菜的時候偷懶一把種子直接甩了出去,現(xiàn)如今長的七零八落。也不好把大頭菜都拔起來再擺整齊種下去,這大大增加了圍柵欄的難度。

但是沒有關(guān)系!隔壁的猴子同學(xué)家里的菜園圍的嚴(yán)嚴(yán)實實,一次也沒被野豬偷過。找他來幫幫忙不就可以完美解決了嗎?

image.png

想到這里我就以一顆長的最好看的大頭菜為報酬,請了隔壁的猴子同學(xué)來授之以漁。

猴子同學(xué)的思路講解

這個問題可以抽象為 凸包問題

在給定的點中,找出有限點構(gòu)造一個最小凸邊形,使其可以將給定的所有點都包含在內(nèi)

最關(guān)鍵的一點是把所有的菜【一下簡化為點】,都放在柵欄的內(nèi)側(cè)。所以可以從 判斷點是否在線段的某一邊 來下手。

image.png

判斷點是否在線段的某一邊

那么如何判斷呢?這里要引入一個數(shù)學(xué)概念,叉乘

由叉乘的定義可知,在三維坐標(biāo)系中。向量 a 叉乘 向量 b 可以得到一個垂直于 a b 向量組成平面的新向量 c,且它的模長等于向量 a b 形成的平行四邊形面積。且 c 根據(jù) a b 的方向不同,也會有自己的方向。這里可以根據(jù)右手定則來得知向量 c 的方向。

那么在這里,我們不需要用到全部 叉乘 的特點。由于我們凸包構(gòu)造的是二維坐標(biāo)系,那么擴展到三維的話 z 向量系數(shù)一定為 0。所以簡單來說二維平面上三點 A, B, C 構(gòu)造出的向量 ab 叉乘 ac 得出的向量必定是在 z 軸上的向量(0x, 0y, kz)。那么通過計算出單位向量 z 的系數(shù) k。 如果 k > 0 ,那么點 C 在向量 ab 的左邊。 k = 0, 點 C 在向量 ab 上。 k < 0,點 C 在向量 ab 右邊。

叉乘的計算可以寫為


image.png

所以如果在二維坐標(biāo)系中,就可以簡化為 U ?? V = (u1v2 - u2v1)k

這里把 u v 更換為 x y, 那么我們可以根據(jù) (x1y2 - x2y1) 的值是否大于等于 0 來進行判斷

然后以它為核心思想,從最基礎(chǔ)的窮舉方法來開始優(yōu)化

給定 n 個點,那么會有 n * (n - 1) / 2 條線,對每一條線進行計算,剩余的 (n - 2) 個點是否都位于這點線段的一側(cè)。如果是,那么這兩點為 凸點。

窮舉法復(fù)雜度為 O(n^3)

這里可以繼續(xù)對窮舉法進行優(yōu)化,首先取縱坐標(biāo)最小的一個點,如果有兩個就取其中一個,它肯定在凸包上。

然后以它為極坐標(biāo)中心,對剩下的所有點進行一個角度排序,排序結(jié)果如下圖。

image.png

然后就按順序依次對每一個點 P_k 開始依次嘗試剩下的 P_{n-k} 個點,找到第一個滿足條件的點【其他點都在P_kP_{n-k}線段左側(cè)】。

這樣可以降低時間復(fù)雜度到 O(n^2)

到了這一步,想繼續(xù)優(yōu)化就得換一個思路,考慮是否可以不再對每一個點都進行嘗試。Graham 介紹

這里通過從最低點開始,然后分析凸包的特點找規(guī)律,距離它角度最小的第一個點肯定是凸包上的點【當(dāng)然角度最大的也是】,那么把 p0p1 入棧,接著考慮在線段 p1p2 是否是左轉(zhuǎn)【可以想象一個人先沿著 p0p1 走,到了 p1 的時候是需要向左轉(zhuǎn)還是向右轉(zhuǎn)】。如果是向左,那么這個點入棧,繼續(xù)下一個點。如果這個點向右,那么說明當(dāng)前棧頂不是凸包上的點,出棧。然后再次拿棧頂兩點來和當(dāng)前拿的點比較。這樣如此反復(fù)。

復(fù)雜度 O(n * log n)

最后附上 Graham 的簡單實現(xiàn)。

// 叉乘
function crossProduct(p0, p1, p2) {
  const vectorA = {
    x: p1.x - p0.x,
    y: p1.y - p0.y,
  }
  const vectorB = {
    x: p2.x - p0.x,
    y: p2.y - p0.y,
  }
  // 向量叉乘,這里簡化了 z 軸
  return vectorA.x * vectorB.y - vectorA.y * vectorB.x
}

function getConvexHull(pointData) {
  const result = []
  const arr = pointData.sort((a, b) => a.y - b.y)
  const p0 = arr.shift()

  // 最低點一定在凸包上,有多個最低點可以隨便選一個
  result.push(p0)

  // 按角度排序
  const sortedPoint = arr
    .map((p) => {
      const cos = (p.x - p0.x) / Math.sqrt(Math.pow(p.y - p0.y, 2) + Math.pow(p.x - p0.x, 2))
      return {
        ...p,
        cos,
      }
    })
    .sort((a, b) => b.cos - a.cos)
    .map((p) => {
      return { x: p.x, y: p.y }
    })

  // 按照凸包的性質(zhì),第一個點必定在凸包上
  result.push(sortedPoint.shift())

  sortedPoint.forEach((p, index) => {
    while (crossProduct(result[result.length - 2], result[result.length - 1], p) < 0) {
      // 在右方,說明棧頂不是凸包上的點
      result.pop()
    }
    // 在一條線上的情況,多去一個點,屬于優(yōu)化操作
    if (crossProduct(result[result.length - 2], result[result.length - 1], p) === 0) result.pop()

    // 在左邊及線上
    result.push(p)
  })

  return result
}

const pointData = [
  { x: 1, y: 28 },
  { x: 2, y: 1 },
  { x: 2, y: 12 },
  { x: 3, y: 46 },
  { x: 4, y: 1 },
  { x: 5, y: 11 },
  { x: 6, y: 21 },
  { x: 5, y: 51 },
  { x: 6, y: 1 },
  { x: 7, y: 43 },
  { x: 10, y: 45 },
]
const areaData = getConvexHull(JSON.parse(JSON.stringify(pointData)))

/*
 * console areaData:
 *
 * {x: 2, y: 1}
 * {x: 6, y: 1}
 * {x: 10, y: 45}
 * {x: 5, y: 51}
 * {x: 3, y: 46}
 * {x: 1, y: 28}
 */

后記

通過猴子同學(xué)的一番講解操作,我成功的學(xué)會了如何圍柵欄,雖然在最后大頭菜經(jīng)濟泡沫了,但是萬萬沒想到,我在這次經(jīng)濟泡沫中大肆兜售的【我不可能能用這么少的錢圍起最好的柵欄】一書同樣讓我實現(xiàn)了財富自由。

-------------------

原文鏈接:[https://mp.weixin.qq.com/s/7Mek0vdv7r04zkj7oyISuw] (https://mp.weixin.qq.com/s/7Mek0vdv7r04zkj7oyISuw)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容