凸包总结

凸包的相关“test”测试

一些定义:

  • 凸包[Convex Hull]:简单理解为将很多钉子围住的“皮筋”。

  • 极点(Extreme Point):有一个点集S。如果存在通过点P的直线L,使得点集S中除了点P之外的其他的点都在这个直线的同一侧,则这个点P就是极点(Extreme Points)

  • 极边(Extreme Edge):两个极点连成的边,剩余的所有点均会在该边的一侧。

To-Left Test

To-Left测试是相对于另外这两个点所确定的那条有向直线而言的,任何一条有向直线不仅会把平面分成两部分,我们还可以知道哪一边是左边,哪一边是右边。To-Left,其实就是说这个点相对于刚才的那条有向线而言到底是位于它的左侧(就是true),还是右侧(就是false)。

这里用到了行列式来求三角形面积(请看下面手写证明)。下图中的这个行列式实际上算的是它的”面积(指:有向面积)”的两倍

1
2
3
4
5
6
7
8
9
10
bool ToLeft (Point p, Point q, Point s) //判断点s对于线段pq的位置
{
    return Area2(p,q,s);
}

int Area2(Point p, Point q, Point s)
{
        return p.x*q.y - p.y*q.x + q.x*s.y - q.y*s.x
            +s.x * p.y - s.y *p.x;
}

行列式来求三角形面积的证明:

img

图片来源于:https://zhuanlan.zhihu.com/p/35543479

In-Trangle Test

其实这个算法就是判断点是否在三角形内部。

这个测试最直接的理解为:使用了三次To-Left Test,三角形有三条边,所以每一条边都测试一次:

判断极边

1
2
3
4
5
6
7
8
9
10
11
12
 //判断极边的核心
void checkEdge(Point S[], int n, int p, int q)
{
    bool lEmpty =TRUE, REmpty = TRUE;
    for( int k=0; k<n&&(LEmpty||REmpty); k++)
    {
        if (k!=p && k!=q)
            ToLeft(S[p], S[q], S[k]) ? LEmpty=FALSE: REmpty=FALSE;
    }
    if(LEmpty || REmpty)
        S[p].extreme =S[q].extreme= TRUE;
}

in-convex-polygon test

判定待定点是否位于某多边形内部(in-convex-polygon test)

实现的方法就是:按一定方向(约定为逆时针)凸包的每条边和待定点做ToLeft test,一旦有一次test为false就说明改点在凸包外面。

构造凸包的方法

1 利用极点法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void extremePoint (point S[], int n)
{
    for(int s=0; s<n; s++)
        S[s].extreme = TRUE;
    for(int p=0; p<n; p++)
		for(int q=p+1; q<n; q++)
			for(int r=q+1; r<n; r++)
                for(int s=0; s<n; s++)
                {
					if(s==p || s==q || s==r ||!S[s].extreme)
                        continue;
                    if(Intriangle(S[p],S[q],S[r],S[s]))
                        S[s].extreme = FALSE;
                }
}

时间复杂度:O(n^4)

2 利用极边法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void markEE(point S[], int n)
{
    for (int k=0;k<n;k++) //将所有点的初始状态都设置成:非极点
        S[K].extreme = FALSE; 
    for (int p=0; p<n; p++) //遍历每条边,看他是否是极边
        for(q=p+1; q<n; q++)
            checkEdge(S,n,p,q) //判断极边的核心
}
 //判断极边的核心
void checkEdge(Point S[], int n, int p, int q)
{
    bool lEmpty =TRUE, REmpty = TRUE;
    for( int k=0; k<n&&(LEmpty||REmpty); k++)
    {
        if (k!=p && k!=q)
            ToLeft(S[p], S[q], S[k]) ? LEmpty=FALSE: REmpty=FALSE;
    }
    if(LEmpty || REmpty)
        S[p].extreme =S[q].extreme= TRUE;
}

时间复杂度:O(n^3)

3 incremental construction(增量构造)

详细讲解:https://www.longlongqin.top/archives/7c53.html

该算法的核心步骤就是:复杂度 O(n^2)

  1. 判定新加入点与凸包的位置关系:用in-convex-polygon test

    in-convex-polygon test在上面有讲

  2. 向凸包插入新点:support-line

    新点准备插入现有凸包

    如上面的点x,如何插入现有凸包当中呢?

    插入过程:插入过程其实就是寻找两个连接点s和t,然后将新点x与t、s分别连接得到新的凸包。t、s两个点将原凸包的边界分成两部分:st和ts两个邮箱线段。构造新凸包就要保留远端st,舍弃近端ts。取代ts的是x和s、t的连接线xt和xs。其中xt、xs被称为切线(tangent)或者support line(支撑线)。(配合下图理解)

    img

    • 现在就是如何找到t、s这两个点?

      在凸包上任取一点v,按时针方向v点会有一个直接前驱点和直接后继点。考察有向直线xv与点v直接前驱和直接后继的位置关系(两次to left test),记为一个pattern表

      【结果无非是四种情况:v的直接前驱和直接后继相对于有向直线xv的位置是RL,LR,LL,RR。例如上图黄色点v,是R和L;蓝色点v分别是L和R。实际上凸包边界st上所有点的pattern都为RL,ts上所有点的pattern都为LR。关键点在于:点S的pattern是LL,点t的pattern为RR。】

说了这么多,其实我们可以将上面的两步合为一步:对于每个待定点x,不必特意去考虑它与凸包的位置关系,而是遍历凸包上每一个点。

对于凸包边界上的每一个点,我们都能通过两次to left test迅速判断出pattern。

对于x位于凸包外部的情况,经过遍历凸包的点,我们很容易就能得到s和t的位置,得到两条support line,从而构造出新的凸包;

而对于x位于凸包内部的情况,凸包边界每个点都不可能出现RR或LL的情况,直接舍弃x即可。

4 Jarvis March

详细讲解:https://longlongqin.top/archives/9c7f.html

又称:Gift Wrapping

算法步骤:

  1. 初始化所有点,设置点集的初态为 非极点
  2. 找到开始的第一个极点:用LTL方法
  3. 寻找下一个极点:用ToLeft test寻找下一个极点
  4. 循环步骤3,直到找到所有极点

也就是:首先从任何一个极点(用LTL确定)开始,然后找到一条以这个极点为端点的极边。然后沿着这个极边的另一个端点(endpoint)出发,再找出下一条极边。如此反复操作,最终会找到一条以最初极点为endpoint的极边,得到一个封闭的环,凸包也构造完成。

image-20200312175230158

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int LTL(Point S[], int n) //寻找最下and最左的点作为第一个极点
{
    int ltl = 0; 
    for (int k=1; k<n; k++)
    {
        if(S[k].y < S[ltl].y || (S[k].y==S[ltl].y && S[k].x<S[ltl].x) )
            ltl = k;
    }
    return ltl;
}

void Javis (Point S[], int n)
{
    for (int k=0; k<n; k++)
    	S[k].extreme=FALSE; //1.将所有点标记为非极点
    
    int ltl = LTL(S, n); //2.找到ltl
    int k = ltl; 
    
    do
    {
        S[k].extreme = true;
        int s = -1; //要找的下一个极点用s表示
        
        for (int t=0; t<n; t++)
        {
            if (t!=k && t!=s && ( s==-1 || !ToLeft(S[K],S[s],S[t]) ) )
                s = t; 
        }  
        S[k].succ = s; //新的极边确定
        k = s; //更新k的值,变为下一次查找的边的起点
    } while(k != ltl) //如果循环回到了原来的点,则结束
}

复杂度:Jarvis March算法算法的复杂度更准确的表示为O(nh)。h(凸包边界的点的个数)又由最终输出结果,即凸包本身来决定,输出结果决定了构造过程的复杂度,这就是所谓的“输出敏感性”。这种类型的算法又被称为output sensitive algorithm。这种特性在其它凸包算法中也会体现。

5 Graham Scan

详细讲解;https://longlongqin.top/archives/3478.html

算法流程:

  1. 预排序(presorting):即找到某个基准点,然后将其余所有的点按照相对于基准点的极坐标排序。它主要做了三个事情:

    1、找出基准点:用lowest-then-leftmost point(LTL),然后对其他点按照极坐标排序:根据极角排序的方法,在:(https://longlongqin.top/archives/510d.html#补:根据极角排序)

    2、找出起始边:从排好序的点集,选取前两个点,就是起始边。

    3、将起始边 与 剩下的n-2条边分别存储在不同的栈(stack)中:如下图

  2. Scan扫描

    这一步是算法的核心。

    scan的过程主要关注三个点:栈S的栈顶(S[0])、次栈顶(S[1])和栈T的栈顶(T[0])。也就下图红色标注的三个点:

    img

    • 可以观察到,每次待处理的S[0]和S[1]构成的边一定是一条极边(如上图点1和点2),算法关键步骤就是对边这条极边和T[0]做to left test,判断T[0]位于边S[0]S[1]的左边还是右边。若在左边则继续拓展,若在右边则否定掉此前认定的极边。无论结果如何,每次判定都会将问题规模缩小一个单元,算法结束时T最终肯定为空。T空后,S中存留下的点正是凸包的极点,这些点自底而上正是凸包边界点的逆时针遍历,也得到了整个凸包构造问题的解。

复杂度:

Graham Scan算法复杂度由三部分决定:

  • persorting,采用一般排序算法,复杂度是O(nlogn)
  • 逐步迭代,O(n):算法一步步纳入新点,会迭代n步。
  • scan,O(?)

所以算法的总体复杂度:O(nlogn + n * ?) ,可见scan的复杂度决定了算法总体的复杂度。

  • 算法一步步纳入新点,会迭代n步。但是在每个点上都有可能做回溯操作,所以scan的复杂度是不确定的。我们来以上图最坏情况为例,到第8个点时判定为false,舍弃点7,回溯。下一步判断也为false,舍弃点6,回溯。如此回溯直到算法开始的点2。这次scan倒退了高达O(n)个点,如果每次scan都是如此那么算法整体复杂度就为:O(nlogn + n * n) = O(n^2)了,那这种算法的意义也就不大了。

其实上述分析并非错误,只是不够精确。O(n^2)确实是Graham Scan算法的一个上界,但是这个上界并不是紧的。

问题就出在分析假定了每次都会出现回退高达O(n)个点。

代码部分:https://longlongqin.top/archives/d4fa.html

6 [Divide And Conquer]

详细讲解:https://longlongqin.top/archives/f32f.html


发布了33 篇原创文章 · 获赞 10 · 访问量 1万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 游动-白 设计师: 上身试试

分享到微信朋友圈

×

扫一扫,手机浏览