分治法-最近对问题和凸包问题

前面博客中有用蛮力法解决过最近对问题和凸包问题。

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是上包的顶点
  • 包含在三角形PmaxP1Pn之中的点都不可能是上包的顶点

    clip_image001

当找到Pmax之后,我们可以令Pn=Pmax,继续以递归的方式寻找位于P1Pn上侧的凸包顶点。

至于如何判断三角形P1P2P3的面积是否是最大的,可以通过如下行列式来判断,它等于行列式绝对值的1/2:

image

当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)。解决上包问题和解决下包问题十分类似,代码也只有极少的改动,代码写得不太好,有很多重复的部分,其实这些应该都可以合并的。

posted @ 2015-09-21 20:02  wokelon  阅读(4792)  评论(0编辑  收藏  举报