问题背景
二维平面上有n个点(x,y),现在我们要找一个多边形(也可能是一条直线,比如只有两个点或多点贡献),使得这个多边形完全包住这些点(在边界内含边界),返回在多边形上的点的坐标。(可以想象成围栅栏——安装栅栏)
一、初步思考
这个多边形该如何寻找?或者说如何围栅栏?直观地来看,我们要选找最"外围"的点,这些点之间的连线就是我们要寻找的图形。可是如何定义这个外围?如何比较两个点哪个更靠外呢?
我们可以看到初始最左边的点(横坐标最小)一定是在边界上的,我们从他出发,开始寻找。
二、向量的叉乘
叉乘区别于数量积的点乘,得到的结果依旧是一个向量,且结果向量与原来的两个向量相互垂直。新向量的方向可以用右手定则来判定
我们看到,此时 n=a * b,n指向上方, b 在 a 的左侧。当我们把 b 放在 a 的右边的时候,n 指向下。由此,我们可以通过 a * b 的指向来判断 b 在 a 的哪一侧。可是我们不是二维平面吗?这个不用担心,只要把 a 和 b 的z轴坐标设成0就可以了。所以 n 的z轴坐标计算公式为
z
=
a
∗
b
=
(
x
y
′
−
x
′
y
)
z=a*b=(xy'-x'y)
z=a∗b=(xy′−x′y)
结合题目来看,我们已经找到了最左边的点 p,如何继续寻找呢(横坐标最小不适用了),我们要找到下一个最左边的q点呢。我们就要保证其余所有点r 满足 r 在向量pq左边。我们要计算pq*qr的值。
代码实现:
public static int cross(int[]p,int[]q,int[] r){
return (q[0]-p[0])*(r[1]-q[1])-(r[0]-q[0])*(q[1]-p[1]);
}
三、思路
1.Jarvis算法
代码如下:
public static int[][] Jarvis(int[][] nodes){ //Jarvis算法
int n=nodes.length;
if(n<4)
return nodes; //n-1? n=2? n=3? 想象此时为什么图形
int left=0;
for(int i=1;i<n;i++){
if(nodes[i][0]<nodes[left][0]){
left=i;
} //寻找最左边的点(坐标寻找)
}
List<int[]>ans=new ArrayList<>(); //存放结果
int[] visit=new int[n]; //访问标记,Java里int型数组初始所有元素0
int p=left;
do{
int q=(p+1)%n;
for(int r=0;r<n;r++) //找到最左边的数字
if((cross(nodes[p],nodes[q],nodes[r])<0))
q=r;
for(int i=0;i<n;i++){
if(visit[i]==1||i==p||i==q)
continue; //已经放进去了或者是它自己,不参与比较
if(cross(nodes[p],nodes[q],nodes[i])==0){
ans.add(nodes[i]); //p,q,r,三点共线,放进去
visit[i]=1; //在候选集中去掉
}
}
if(visit[q]==0){ //没访问过
ans.add(nodes[q]);
visit[q]=1; //在候选集中去掉
}
p=q; //更换顶点继续寻找
}while(p!=left);
return ans.toArray(new int[][]{}); //要求返回数组
}
2.其他算法
凸包问题还有许多其他的做法,比如Graham算法和Andrew算法等等。
Graham算法是一种基于极坐标从下而上的做法,它的时间复杂度要优于Jarvis算法,f(n)=O(nlogn)。缺点是对数组的预处理(极坐标排序)要相对繁琐,而且最后一条边上点的顺序是反过来的,因此处理上来比较麻烦,个人觉得Jarvis算法更为简单直观,感兴趣的同学可以去了解一下。
测试
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
int[][] nodes=new int[n][2];
for(int i=0;i<n;i++){
nodes[i][0]=sc.nextInt();
nodes[i][1]=sc.nextInt();
}
int[][] ans=Jarvis(nodes);
for(int[]an:ans){
System.out.println();
for(int a:an){
System.out.print(a+" ");
}
}
sc.close();
}
凸包问题的解决方法比较多,但是几乎每种方法涉及到了一些数学方法,这也启示我们在平时刷题时也要保持数学的一些思考。