分治法-最近对问题和凸包问题
前面博客中有用蛮力法解决过最近对问题和凸包问题。
4.6.1 最近对问题
设P1,P2,P3,…,Pn是平面上n个点构成的集合S,解决问题之前,假定这些点都是按照它们的x轴坐标升序排列的。我们可以画一条垂直线x=c,将这些点分为两个包含n/2个点的子集S1、S2,分别位于直线x=c的两侧。遵循分治的思想,分别递归的求出S1、S2的最近对,比如d1、d2,并设d=min{d1,d2}。此时d并不是所有点对的最小距离,离分界线x=c为d的两侧仍可能存在更小距离的点对,因此我们还需在以x=c对称、宽度为2d的垂直带中检查是否存在这样的点对。设C1、C2分别是该垂直带位于直线x=c两侧的点集,对于C1中的每个点P(x0,y0),我们都需要检查C2中的点是否小于d。显然,这种点坐标y应在区间(y-d,y+d)内。
代码实现:
/** * 分治法解决最近对问题 * @author xiaofeig * @since 2015.9.19 * @param 目标点集(要求是按x坐标排好序的) * @return 返回最近距离的两点下标以及最近距离的平方 * */ public static int[] closestPoints(Point[] points){ if(points==null||points.length==0||points.length==1){ return null; }else if(points.length==2){ int d=(points[0].x-points[1].x)*(points[0].x-points[1].x)+(points[0].y-points[1].y)*(points[0].y-points[1].y); return new int[]{0,1,d}; } int c=points.length/2; Point[] leftPart=Arrays.copyOfRange(points, 0, c); Point[] rightPart=Arrays.copyOfRange(points, c, points.length); int[] leftResult=closestPoints(leftPart); int[] rightResult=closestPoints(rightPart); int[] result;//最终结果 if(leftResult==null){ result=rightResult; //子集下标恢复成母集下标 result[0]=result[0]+c; result[1]=result[1]+c; }else if(rightResult==null){ result=leftResult; }else{ if(leftResult[2]<rightResult[2]){ result=leftResult; }else{ result=rightResult; //子集下标恢复成母集下标 result[0]=result[0]+c; result[1]=result[1]+c; } } //比较位于x=c两侧的垂直带中的点距 int leftIndex; for(leftIndex=c-1;leftIndex>=0;leftIndex--){ if(points[leftIndex].x<=points[c].x-result[2]){ break; } } leftIndex++; int rightIndex; for(rightIndex=c;rightIndex<points.length;rightIndex++){ if(points[rightIndex].x>=points[c].x+result[2]){ break; } } rightIndex--; while(leftIndex<c){ int index=c; while(index<=rightIndex){ int d=(points[leftIndex].x-points[index].x)*(points[leftIndex].x-points[index].x)+(points[leftIndex].y-points[index].y)*(points[leftIndex].y-points[index].y); if(d<result[2]){ result[0]=leftIndex; result[1]=index; result[2]=d; } index++; } leftIndex++; } return result; }
由于点集需要按照x坐标排序,这里顺便也给出合并排序的代码:
/** * 合并排序,按照x坐标 * @author xiaofeig * @since 2015.9.17 * @param array 要排序的点集 * */ public static void quickSort(Point[] array){ if(array.length>1){ int s=partition(array); Point[] leftPart=Arrays.copyOfRange(array, 0, s); Point[] rightPart=Arrays.copyOfRange(array, s+1, array.length); quickSort(leftPart); quickSort(rightPart); //合并中轴元素的左右两部分,包括中轴元素 for(int i=0;i<leftPart.length;i++){ array[i]=leftPart[i]; } for(int i=0;i<rightPart.length;i++){ array[i+leftPart.length+1]=rightPart[i]; } } } /** * 合并排序划分区 * @author xiaofeig * @since 2015.9.17 * @param array 要分区的数组 * @return 返回中轴元素的最终下标 * */ public static int partition(Point[] array){ int i=0,j=array.length; do{ do{ i++; }while(i<array.length-1&&array[i].x<array[0].x); do{ j--; }while(j>1&&array[j].x>array[0].x); Point temp=array[i]; array[i]=array[j]; array[j]=temp; }while(i<j); Point temp=array[i]; array[i]=array[j]; array[j]=temp; if(array[j].x<=array[0].x){//中轴右侧元素均大于中轴元素时无需交换 temp=array[0]; array[0]=array[j]; array[j]=temp; }else{ j--;//中轴右侧元素均大于中轴元素时须将j值降至0 } return j; }
算法分析:
关于该算法对n个预排序点的运行时间,与如下递推式:
T(n)=2T(n/2)+M(n),M(n)是合并较小子问题所用的时间
可以得出T(n)属于O(nlogn)。
4.6.2 凸包问题
这次我们讨论的是用分治算法解决凸包问题,这个算法也称为快包,因为它的操作和快速排序的操作十分类似。假设P1,P2,…,Pn是平面上n>1个点构成的集合S,且这些点都是按照x轴坐标升序排列的,则可以证明位于最左边和最右边的P2,Pn一定是该集合的凸包顶点。向量P1Pn把点分为两个集合:向量P1Pn左侧和右侧的点分别构成的集合S1,S2,我们将凸包位于向量P1Pn左侧的部分成为上包,位于右侧的部分成为下包。
先来说说如何构建上包,如果S1为空,则线段P1P2就是上包;如果不为空,我们可以在S1中找到距离直线P1Pn最远的点Pmax,也就是在S1中找到一个点Pmax使三角形PmaxP1Pn的面积最大。可以证明如下几点:
当找到Pmax之后,我们可以令Pn=Pmax,继续以递归的方式寻找位于P1Pn上侧的凸包顶点。
至于如何判断三角形P1P2P3的面积是否是最大的,可以通过如下行列式来判断,它等于行列式绝对值的1/2:
当P3(x3,y3)位于向量P1P2左侧时,表达式符号为正;位于右侧时,符号为负(正负可以判断S1,S2)。
代码实现:
/** * 分治法解决凸包问题 * @author xiaofeig * @since 2015.9.20 * @param points 目标点集(要求是按x坐标排好序的) * @return 返回有序的极点集合 * */ public static List<Integer> convexHull(Point[] points){ List<Integer> indexs=new LinkedList<Integer>(); for(int i=0;i<points.length;i++){ indexs.add(i); } List<Integer> upperHull=upperHull(points, indexs);//得到上包结果 List<Integer> lowerHull=lowerHull(points, indexs);//得到下包结果 //将上包结果和下包结果合并,并返回 List<Integer> result=new LinkedList<Integer>(); result.add(indexs.get(0)); for(int i=0;i<lowerHull.size();i++){ result.add(lowerHull.get(i)); } result.add(indexs.get(indexs.size()-1)); for(int i=0;i<upperHull.size();i++){ result.add(upperHull.get(i)); } return result; } /** * 分治法解决上包问题 * @author xiaofeig * @since 2015.9.20 * @param points 目标点集(要求是按x坐标排好序的) * @param indexs 目标点集序列 * @return 返回有序的点集序列 * */ public static List<Integer> upperHull(Point[] points,List<Integer> indexs){ int dmax=0;//记录最大距离 Integer pmax=0;//记录最大距离那点的下标 Point p1=points[indexs.get(0)];//分界向量起点 Point p2=points[indexs.get(indexs.size()-1)];//分界向量终点 List<Integer> newIndexs=new LinkedList<Integer>();//位于分界向量左侧的点集下标 for(int i=1;i<indexs.size()-1;i++){ int d=p1.x*p2.y+points[indexs.get(i)].x*p1.y+p2.x*points[indexs.get(i)].y-points[indexs.get(i)].x*p2.y-p2.x*p1.y-p1.x*points[indexs.get(i)].y; if(d>0){ newIndexs.add(indexs.get(i)); if(d>dmax){ dmax=d; pmax=indexs.get(i); } } } if(pmax==0){ return new LinkedList<Integer>(); } //构建新目标点集序列 List<Integer> newIndexs1=new LinkedList<Integer>(); List<Integer> newIndexs2=new LinkedList<Integer>(); newIndexs1.add(pmax); newIndexs2.add(indexs.get(0)); for(Integer i:newIndexs){ newIndexs1.add(i); newIndexs2.add(i); } newIndexs1.add(indexs.get(indexs.size()-1)); newIndexs2.add(pmax); //处理结果点集序列 List<Integer> result1=upperHull(points, newIndexs1); List<Integer> result2=upperHull(points, newIndexs2); result1.add(pmax); for(Integer i:result2){ result1.add(i); } return result1; } /** * 分治法解决下包问题 * @author xiaofeig * @since 2015.9.20 * @param points 目标点集(要求是按x坐标排好序的) * @param indexs 目标点集序列 * @return 返回有序的点集序列 * */ public static List<Integer> lowerHull(Point[] points,List<Integer> indexs){ int dmin=0;//记录最大距离 Integer pmin=0;//记录最大距离那点的下标 Point p1=points[indexs.get(0)];//分界向量起点 Point p2=points[indexs.get(indexs.size()-1)];//分界向量终点 List<Integer> newIndexs=new LinkedList<Integer>();//位于分界向量左侧的点集下标 for(int i=1;i<indexs.size()-1;i++){ int d=p1.x*p2.y+points[indexs.get(i)].x*p1.y+p2.x*points[indexs.get(i)].y-points[indexs.get(i)].x*p2.y-p2.x*p1.y-p1.x*points[indexs.get(i)].y; if(d<0){ newIndexs.add(indexs.get(i)); if(d<dmin){ dmin=d; pmin=indexs.get(i); } } } if(pmin==0){ return new LinkedList<Integer>(); } //构建新目标点集序列 List<Integer> newIndexs1=new LinkedList<Integer>(); List<Integer> newIndexs2=new LinkedList<Integer>(); newIndexs1.add(indexs.get(0)); newIndexs2.add(pmin); for(Integer i:newIndexs){ newIndexs1.add(i); newIndexs2.add(i); } newIndexs1.add(pmin); newIndexs2.add(indexs.get(indexs.size()-1)); //处理结果点集序列 List<Integer> result1=lowerHull(points, newIndexs1); List<Integer> result2=lowerHull(points, newIndexs2); result1.add(pmin); for(Integer i:result2){ result1.add(i); } return result1; }
算法分析:
快报有着和快速排序相同的最差效率θ(n2)。解决上包问题和解决下包问题十分类似,代码也只有极少的改动,代码写得不太好,有很多重复的部分,其实这些应该都可以合并的。