习题课5-2(最近点对、线段相交)

习题课5-2

最近点对

  • 给定n个二维平面的点,求距离最近的点对,并输出它们的距离
  • 计算几何与分治法的结合题
  • 另外一种解法,kd-tree,在空间里面找点的树形结构

分治

  • 子序列那道题,把序列分成两半,分别求解左右两边
  • 4-2,递归求子序列的题的思路

解法1

  • 分治算两边

  • 令solve(l,r)表示第l个点到第r个点的最近点对的距离

  • s o l v e ( l , r ) = m i n { s o l v e ( l , m i d ) , s o l v e ( m i d + 1 , r ) , c a l ( l , r , m i d ) } solve(l,r) = min\{solve(l,mid),solve(mid+1,r),cal(l,r,mid)\} solve(l,r)=min{ solve(l,mid),solve(mid+1,r),cal(l,r,mid)}

  • mid = (l+r)>>1 并取下界; call(l,r,mid)表示第一个点在(l,mid),第二个点在(mid+1,r)所得到的最近点对的距离

  • d = min{solve(l,mid),solve(mid+1,r)}

  • 怎么计算cal(l,r,mid)函数?

  • 设a(mid)点的横坐标为x_1,则我们只需要考虑(l,r)中所有横坐标在(x_1-d,x_1+d)的点就好了

  • 每一个点,只需要考虑它相邻的5个点

  • 如果超过6个点,就违背了d这个属性的假设

  • 在中间的条带里面,对y轴排序时,对于左边的点,找出右半边y值比它大的六个点,更新一下d值,用归并排序

代码解析

  • int md = a[mid].x;
    
                handle(l,mid);
                handle(mid+1,r);
    
  • 注意md这个式子要写在递归之前

  • 每一次的中间值是x轴排序后的平均值

  • Arrays.sort(a, 1, n + 1, new Comparator<ip>() {
          
          
                    @Override
                    public int compare(ip a, ip b) {
          
          
                        if (a.x<b.x){
          
          
                            return -1;
                        }else if (a.x > b.x){
          
          
                            return 1;
                        }else {
          
          
                            if (a.y < b.y) {
          
          
                                return -1;
                            }else if (a.y > b.y) {
          
          
                                return 1;
                            }else {
          
          
                                return 0;
                            }
                        }
                    }
                });
    
  • if (r-l<=1){
          
          
                    if (a[l].y > a[r].y) {
          
          
                        ip tmp = a[l];
                        a[l] = a[r];
                        a[r] = tmp;
                    }
                    if (l!=r){
          
          
                        ans = min(ans,dis(a[l],a[r]));
                    }
                    return;
                }
    
  • 当只有两个点时,对y轴进行排序,同时更新ans

  • for (int i = l,j=mid+1; i <=mid || j<=r;) {
          
          
                    for (;i<=mid && md-a[i].x>=ans;++i);
                    for (;j<=r && a[j].x - md>=ans;++j);
                    if (i<=mid && (j>r||a[i].y<a[j].y)){
          
          
                        b[cnt++] = a[i++];
                    }else if (j <= r){
          
          
                        b[cnt++] = a[j++];
                    }
                }
    
  • for (;i<=mid && md-a[i].x>=ans;++i);
    
  • 去除左半部分大于等于md-a(x)的点

  • for (;j<=r && a[j].x - md>=ans;++j);
    
  • 去除右半部分大于等于md-a(x)的点

  • if (i<=mid && (j>r||a[i].y<a[j].y)){
          
          
                        b[cnt++] = a[i++];
                    }else if (j <= r){
          
          
                        b[cnt++] = a[j++];
                    }
    
  • 枚举的条件是i <=mid || j<=r,

  • 当左半部分有点,并且1.右半部分没点,则将左半部分的点加到临时数组中,2.右半部分优点,但是左边点的y小于右边点的y,则仍然将左半部分的点加到临时数组中,否则将点加到右半部分

  • b数组中求出的点就是框住的点

  • for (int i = 0; i < cnt; i++) {
          
          
                    for (int j = i+1; j < cnt && b[j].y-b[i].y<ans; j++) {
          
          
                        ans = min(ans,dis(b[i],b[j]));
                    }
                }
    
  • 由于for (int j = i+1; j < cnt && b[j].y-b[i].y<ans; j++)的循环是常数级别的,二重循环整体仍然是O(n)的,所以框住部分的点进行二重循环是可以的,通过框住部分的点对ans进行更新

  • 因为对y轴是排过序的,所以当b[j].y-b[i].y>=ans后,就可以终止这一步的循环

  • cnt = 0;
                for (int i = l,j=mid+1; i <=mid || j<=r;) {
          
          
                    if (i<=mid && (j>r||a[i].y<a[j].y)){
          
          
                        b[cnt++] = a[i++];
                    }else if (j<=r){
          
          
                        b[cnt++] = a[j++];
                    }
                }
    
  • 把所有的点对y轴进行归并排序

  • 最后将排序的结果复制到a数组中

标程

  • static class Task {
          
          
    
    
            final int N = 300005;
    
            // 用于存储一个二维平面上的点
            class ip {
          
          
                int x, y;
    
                // 构造函数
                ip(int x, int y) {
          
          
                    this.x = x;
                    this.y = y;
                }
    
            }
            ip[] a = new ip[N];
            ip[] b = new ip[N];
    
            // 计算x的平方
            long sqr(long x) {
          
          
                return x * x;
            }
    
            // 计算点a和点b的距离
            double dis(ip a, ip b) {
          
          
                return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
            }
    
            // 最终答案
            double ans;
    		
        	// 分治法求最近点对
        	// l,r:表示闭区间[l,r]
            void handle(int l,int r){
          
          
                // 边界情况
                // 两个点
                if (r-l<=1){
          
          
                    // 两个点时需要对y轴排序
                    if (a[l].y > a[r].y) {
          
          
                        ip tmp = a[l];
                        a[l] = a[r];
                        a[r] = tmp;
                    }
                    // 两个点时求解距离,更新ans
                    if (l!=r){
          
          
                        ans = min(ans,dis(a[l],a[r]));
                    }
                    return;
                }
    			// 按x轴方向将数据集分成两半
                // 分治计算两遍
                int mid = (l+r)>>1;
                // 中间值
                int md = a[mid].x;
    			
                // 递归求解,分治
                handle(l,mid);
                handle(mid+1,r);
    
                int cnt = 0;
                // (x_1-d,x_1+d)需要把框住的点拿出来
                // 对y轴进行归并排序,并且去掉,与中间点mid的距离(x轴),比答案要大的点
                // i <=mid || j<=r   也就是说当右半部分的点完了之后,左半部分的点仍然是可以执行的
                for (int i = l,j=mid+1; i <=mid || j<=r;) {
          
          
                    // 去除左半部分不在里面的点
                    for (;i<=mid && md-a[i].x>=ans;++i);
                    // 去除右半部分不在里面的点
                    for (;j<=r && a[j].x - md>=ans;++j);
                    // 对y轴进行归并排序
                    // 比前几周讲的归并排序要简单
                    // i<=mid  左半部分还有点
                    // (j>r||a[i].y<a[j].y)  右半部分没有点  或者右半部分有点,但是比左半部分小
                    if (i<=mid && (j>r||a[i].y<a[j].y)){
          
          
                        b[cnt++] = a[i++];
                    }else if (j <= r){
          
          
                        b[cnt++] = a[j++];
                    }
                }
                
                // 现在b数组里的点是按y轴升序的,根据结论,一个点周围不会超过6个点,因此j不会循环很多次,复杂度得到保证,所以大胆写一个二重循环,实际复杂度是O(kn),也就是O(n)的
                for (int i = 0; i < cnt; i++) {
          
          
                    //  b[j].y-b[i].y<ans,因为是排过序的,4号点的距离都大于ans,5号点的ans也肯定大于ans
                    for (int j = i+1; j < cnt && b[j].y-b[i].y<ans; j++) {
          
          
                        ans = min(ans,dis(b[i],b[j]));
                    }
                }
    			
                // 对y轴进行归并排序,不去掉任何点
                // 对所有的点再进行归并排序
                cnt = 0;
                for (int i = l,j=mid+1; i <=mid || j<=r;) {
          
          
                    if (i<=mid && (j>r||a[i].y<a[j].y)){
          
          
                        b[cnt++] = a[i++];
                    }else if (j<=r){
          
          
                        b[cnt++] = a[j++];
                    }
                }
    			
                // 将b数组排好的值重新赋值给原数组a
                for (int i = 0; i < cnt; i++) {
          
          
                    a[l+i] = b[i];
                }
            }
    
            // 计算最近点对的距离
            // n:n个点
            // X, Y:分别表示x轴坐标和y轴坐标,下标从0开始
            // 返回值:最近的距离
            double getAnswer(int n, int[] X, int[] Y) {
          
          
                // 把点初始化到a数组中
                for (int i = 0; i < n; i++) {
          
          
                    a[i+1] = new ip(X[i],Y[i]);
                }
                // 初始化答案
                ans = 1e100;
                // 比较函数,先比较x轴,再比较y轴
                Arrays.sort(a, 1, n + 1, new Comparator<ip>() {
          
          
                    @Override
                    public int compare(ip a, ip b) {
          
          
                        if (a.x<b.x){
          
          
                            return -1;
                        }else if (a.x > b.x){
          
          
                            return 1;
                        }else {
          
          
                            if (a.y < b.y) {
          
          
                                return -1;
                            }else if (a.y > b.y) {
          
          
                                return 1;
                            }else {
          
          
                                return 0;
                            }
                        }
                    }
                });
                handle(1,n);
                return ans;
            }
    
            void solve(InputReader in, PrintWriter out) {
          
          
                int n = in.nextInt();
                int[] X = new int[n];
                int[] Y = new int[n];
                for (int i = 0; i < n; ++i) {
          
          
                    X[i] = in.nextInt();
                    Y[i] = in.nextInt();
                }
                out.printf("%.2f\n", getAnswer(n, X, Y));
            }
    
        }
    

线段相交

  • 给定n条二维平面上的线段,求这些线段的交点个数
  • 标程是Bo algorithm,时间复杂度是O(nlogn),下方给出的答案是暴力解法

解法1

  • Bo算法

  • 邓老师的mooc学习

蛮力算法

  • 二维点集

  • typedef double lf;
    const lf eps = 1e-7;
    
    struct ip{
          
          
    	lf x,y;
    	ip(const lf&x =0,const lf &y=0) : x(x),y(y) {
          
          }
    	void scan(){
          
          
    		scanf("%lf%lf",&x,&y);
    	}
    	void printf() const{
          
          
    		printf("(%2.f,%2.f)",x,y);
    	}
    	void println() const{
          
          
    		printf("(%2.f,%2.f)\n",x,y);
    	}
    }None(1e100,1e100);
    
    typedef ip iv;
    
    struct seg{
          
          
    	ip a,b;
    	seg(const ip &a = None, const ip &b = None) : a(a),b(b) {
          
          }
    	void println() const {
          
          
        	a.print();putchar(' ');b.println();
        }
    };
    
    // 比较x的差值是否超过精度,再比较y的差值是否超过精度
    bool operator < (const ip &a, const ip &b) {
          
          
        return abs(a.x-b.x)<=eps?(abs(a.y-b.y)<=eps?0:a.y<b.y):a.x<b.x;
    }
    
    // x和y的差值的绝对值小于精度
    bool operator == (const ip &a, const ip &b) {
          
          
        return abs(a.x-b.x)<=eps && abs(a.y-b.y)<=eps;
    }
    
    bool operator != (const ip &a, const ip &b) {
          
          
        return !(a==b);
    }
    
    
    ip operator + (const ip &a, const iv &b) {
          
          
        return ip(a.x+b.x,a.y+b.y);
    }
    
    iv operator - (const ip &a, const iv &b) {
          
          
        return iv(a.x-b.x,a.y-b.y);
    }
    
    // 点乘,也就是内积
    lf operator * (const iv &a, const iv &b) {
          
          
        return a.x*b.x+a.y*b.y;
    }
    
    // 叉积
    lf operator ^ (const iv &a, const iv &b) {
          
          
        return a.x*b.y-a.y*b.x;
    }
    
    // 实数乘向量,即向量放大了多少倍
    iv operator * (const iv &a, const lf &b) {
          
          
        return iv(a.x*b,a.y*b);
    }
    // 叉积>=-eps,相当于>=0
    bool toLeft(const iv &a,const iv &b){
          
          
    	return (a^b)>=-eps;
    }
    
    // 三个点,相减再toLeft
    bool toLeft(const ip &a, const ip &b, const ip &c){
          
          
    // c test ab left
        return toLeft(b-a,c-a);
    }
    
    // b点是否在线段a上
    // 先判断矩形,再判断两个点是否在线段左右两边
    bool onSeg(const seg &a, const ip &b){
          
          
        // 是否在矩形里面
    	if(min(a.a.x,a.b.x)<=b.x && b.x<= max(a.a.x,a.b.x)&&min(a.a.y,a.b.y)<=b.y&&b.y<=max(a.a.y,a.b.y)){
          
          
            // 是否在线段左右两边
        	return abs(b-a.a)^(b-a.b)<=eps;
        }
        return 0;
    }
    
    // 线段相交,需要考虑点相交于哪个地方?有很多情况
    ip intersection(const seg &a,const seg &b){
          
          
    	if(onSeg(b,a.a))
            return a.a;
        else if(onSeg(b,a.b))
            return a.b;
        else if(onSeg(a,b.a))
            return b.a;
        else if(onSeg(a,b.b))
            return b.b;
        iv va = a.b-a.a,vb= b.b-b.a;
        if((toLeft(va,b.a-a.a)^toLeft(va,b.b-a.a))&&
           (toLeft(vb,a.a-b.a)^toLeft(vb,a.b-b.a)){
          
          
        	iv u = a.a-b.a;
            // 两条直线,已知一个点和一个向量,怎么求两条直线的交点
            lf t = (vb^u)/(va^vb);
            return a.a+va*t;
        }
        return None;
    }
    
    // 测试函数
    bool gg(const seg &a, const seg &b){
          
          
    	if(onSeg(b,a.a)||onSeg(b,a.b)||onSeg(a,b.a)||onSeg(a,b.b)){
          
          
        	if(((a.b-a.a)^(b.b-b.a))<=eps)
                return 1;
        }
        return 0;
    }
           
    const int N = 100005;
          
    seg a[N];
    set<ip> s;
    
    int main(){
          
          
    	int n;
        scanf("%d",&n);
        for(int i=1;i<=n;i++)
            a[i].a.scan(),a[i].b.scan();
        // 枚举所有的线段
        for(int i=1;i<=n;i++)
            for(int j=i+1;j<=n;j++){
          
          
            	if(gg(a[i],a[j])){
          
          
                	puts("gg");
                    return 0;
                }
                // 两条线段求交点
                ip t = intersection(a[i],a[j]);
                // 如果交点不等于None,就将答案插入s里面
                if(t!=None)
                    s.insert(t);
            }
        printf("%d\n",int(s.size()));
        return 0;
    }
    
    
    
    
    
  • eps是误差精度,让0.999等于1

  • 从1到n枚举n条线段,求它们的交点,如果交点不等于None,就插入这个交点。

多边形的kernel

  • 一个多边形的kernel,为能看到所有边界的内部点集合
  • 如果一个多边形存在费控的kernel,那么我们称它是个star
  • 给定一个简单多边形,判断是否是star,并求出其kernel

解法

  • 半平面交
  • 学习邓老师慕课里面的讲解

猜你喜欢

转载自blog.csdn.net/Markland_l/article/details/109441637
5-2
今日推荐