习题课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
解法
- 半平面交
- 学习邓老师慕课里面的讲解