前置知识
凸多边形
凸多边形是指所有内角大小都在 [0, Π]
范围内的 简单多边形。
凸包
在平面上能包含所有给定点的最小凸多边形叫做凸包。
其定义为:对于给定集合X
,所有包含 X
的凸集的交集 S
被称为 X
的 凸包。
实际上可以理解为用一个橡皮筋包含住所有给定点的形态。
凸包用最小的周长围住了给定的所有点。如果一个凹多边形围住了所有的点,它的周长一定不是最小,如下图。根据三角不等式,凸多边形在周长上一定是最优的。
向量点乘:a · b = |a||b|cosθ = x1x2 + y1y2
,表示数量积,内积
向量叉乘:a × b = |a||b|sinθ = x1y2 - x2y1
,表示向量积,外积
Andrew算法
步骤:
- 双关键字排序:第一关键字
x
从小到大,第二关键字y
从小到大
显然排序后最小的元素和最大的元素一定在凸包上。 - 用栈维护凸包,先扫上半边,再扫下半边
- 若栈中只有一个元素,直接入栈即可
- 正着扫描,若栈中元素数大于等于两个,假设栈顶元素为
b
,栈顶第二个元素为a
,待考虑点为c
,若ab × ac < 0
,说明是顺时针方向,将c
入栈,若ab × ac > 0
,说明是逆时针方放,将b
出栈,迭代考虑栈顶元素,最后将c
入栈
同时记得记录栈中元素st
,避免反着扫描时再次用到 - 栈底元素记为没有被使用过,保证最后闭合
- 反着扫描,同步骤4
时间复杂度: O(nlogn)
Graham算法
类似Anbdrew算法
找到最低点,以该点为基准对所有点按极角进行排序,极角相同的按极值从小到大排序。
一次遍历就可以求出凸包上的点
Jarvis 算法
相较于Graham算法,没有前置的排序,每次暴力寻找当前最右侧点,使得其余点均在其左侧
时间复杂度:O(n2)
例题
587. 安装栅栏
难度困难126收藏分享切换为英文接收动态反馈
在一个二维的花园中,有一些用 (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 之间。
- 花园至少有一棵树。
- 所有树的坐标都是不同的。
- 输入的点没有顺序。输出顺序也没有要求。
思路:
Andrew算法求凸包
class Solution {
public int[][] outerTrees(int[][] trees) {
int n = trees.length;
Arrays.sort(trees, (o1, o2) -> (o1[0] == o2[0] ? o1[1] - o2[1] : o1[0] - o2[0]));
int[] stk = new int[n + 1];
boolean[] st = new boolean[n];
int p = -1;
for (int i = 0; i < n; i++) {
while (p >= 1 && cross(trees, stk[p - 1], stk[p], i) > 0) {
st[stk[p]] = false;
p--;
}
stk[++p] = i;
st[i] = true;
}
st[0] = false;
for (int i = n - 1; i >= 0; i--) {
if (st[i]) continue;
while (p >= 1 && cross(trees, stk[p - 1], stk[p], i) > 0) {
p--;
}
stk[++p] = i;
}
int[][] res = new int[p][];
for (int i = 0; i < p; i++) {
res[i] = trees[stk[i]];
}
return res;
}
int cross(int[][] t, int a, int b, int c) {
int x1 = t[b][0] - t[a][0], y1 = t[b][1] - t[a][1];
int x2 = t[c][0] - t[a][0], y2 = t[c][1] - t[a][1];
return x1 * y2 - x2 * y1;
}
}
方法2:Graham算法 :::danger 注意本题需要求出凸包上的所有点,纯粹Graham算法最后存在共线情况时只能求出两端的点,所以需要稍加改进,对最后共线的点按照距离从大到小排序 :::
class Solution {
public int[][] outerTrees(int[][] trees) {
int n = trees.length;
if (n < 4) {
return trees;
}
int bottom = 0;
/* 找到 y 最小的点 bottom*/
for (int i = 0; i < n; i++) {
if (trees[i][1] < trees[bottom][1]) {
bottom = i;
}
}
swap(trees, bottom, 0);
/* 以 bottom 原点,按照极坐标的角度大小进行排序 */
Arrays.sort(trees, 1, n, (a, b) -> {
int diff = cross(trees[0], a, b) - cross(trees[0], b, a);
if (diff == 0) {
return distance(trees[0], a) - distance(trees[0], b);
} else {
return -diff;
}
});
/* 对于凸包最后且在同一条直线的元素按照距离从大到小进行排序 */
int r = n - 1;
while (r >= 0 && cross(trees[0], trees[n - 1], trees[r]) == 0) {
r--;
}
System.out.println(trees[0][0] + " " + trees[0][1]);
for (int i = r + 1; i < n; i++) {
System.out.println(trees[i][0] + " " + trees[i][1]);
}
for (int l = r + 1, h = n - 1; l < h; l++, h--) {
swap(trees, l, h);
}
Deque<Integer> stack = new ArrayDeque<Integer>();
stack.push(0);
stack.push(1);
for (int i = 2; i < n; i++) {
int top = stack.pop();
/* 如果当前元素与栈顶的两个元素构成的向量顺时针旋转,则弹出栈顶元素 */
while (!stack.isEmpty() && cross(trees[stack.peek()], trees[top], trees[i]) < 0) {
top = stack.pop();
}
stack.push(top);
stack.push(i);
}
int size = stack.size();
int[][] res = new int[size][2];
for (int i = 0; i < size; i++) {
res[i] = trees[stack.pop()];
}
return res;
}
public int cross(int[] p, int[] q, int[] r) {
return (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0]);
}
public int distance(int[] p, int[] q) {
return (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1]);
}
public void swap(int[][] trees, int i, int j) {
int temp0 = trees[i][0], temp1 = trees[i][1];
trees[i][0] = trees[j][0];
trees[i][1] = trees[j][1];
trees[j][0] = temp0;
trees[j][1] = temp1;
}
}