一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第22天,点击查看活动详情。
在一个二维的花园中,有一些用 (x, y) 坐标表示的树。由于安装费用十分昂贵,你的任务是先用最短的绳子围起所有的树。只有当所有的树都被绳子包围时,花园才能围好栅栏。你需要找到正好位于栅栏边界上的树的坐标。
示例 1:
输入: [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]]
输出: [[1,1],[2,0],[4,2],[3,3],[2,4]]
解释:
示例 2:
输入: [[1,2],[2,2],[4,2]]
输出: [[1,2],[2,2],[4,2]]
解释:
即使树都在一条直线上,你也需要先用绳子包围它们。
意:
- 所有的树应当被围在一起。你不能剪断绳子来包围树或者把树分成一组以上。
- 输入的整数在 0 到 100 之间。
- 花园至少有一棵树。
- 所有树的坐标都是不同的。
- 输入的点没有顺序。输出顺序也没有要求。
Jarvis 算法
此题为经典的求凸包的算法,详细的算法原理可以参考「凸包」。
算法背后的想法非常简单。首先必须要从凸包上的某一点开始,比如从给定点集中最左边的点开始,例如最左的一点 。然后选择 点使得所有点都在向量 的左方或者右方,我们每次选择左方,需要比较所有点以 为原点的极坐标角度。然后以 为原点,重复这个步骤,依次找到 。 给定原点 ,如何找到点 ,满足其余的点 均在向量 的左边,我们使用「向量叉积」来进行判别。我们可以知道两个向量 的叉积大于 时,则两个向量之间的夹角小于 ,两个向量之间构成的旋转方向为逆时针,此时可以知道 一定在 的左边;叉积等于 时,则表示两个向量之间平行, 在同一条直线上;叉积小于 时,则表示两个向量之间的夹角大于 ,两个向量之间构成的旋转方向为顺时针,此时可以知道 一定在 的右边。为了找到点 ,我们使用函数 ,这个函数有 个参数,分别是当前凸包上的点 ,下一个会加到凸包里的点 ,其他点空间内的任何一个点 ,通过计算向量 的叉积来判断旋转方向,如果剩余所有的点 均满足在向量 的左边,则此时我们将 加入凸包中。下图说明了这样的关系,点 rrr 在向量 的左边。
var outerTrees = function(trees) {
const n = trees.length;
if (n < 4) {
return trees;
}
let leftMost = 0;
for (let i = 0; i < n; i++) {
if (trees[i][0] < trees[leftMost][0]) {
leftMost = i;
}
}
const res = [];
const visit = new Array(n).fill(0);
let p = leftMost;
do {
let q = (p + 1) % n;
for (let r = 0; r < n; r++) {
/* 如果 r 在 pq 的右侧,则 q = r */
if (cross(trees[p], trees[q], trees[r]) < 0) {
q = r;
}
}
/* 是否存在点 i, 使得 p 、q 、i 在同一条直线上 */
for (let i = 0; i < n; i++) {
if (visit[i] || i === p || i === q) {
continue;
}
if (cross(trees[p], trees[q], trees[i]) === 0) {
res.push(trees[i]);
visit[i] = true;
}
}
if (!visit[q]) {
res.push(trees[q]);
visit[q] = true;
}
p = q;
} while (p !== leftMost);
return res;
}
const cross = (p, q, r) => {
return (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0]);
};