计算几何小结

好像又鸽子了几天博客

poj真是神奇的网站……

基础知识

点积

(a·b=a.x*b.x+a.y*b.y)

$a$在$b$上的投影乘以$b$的模长

叉积

(a×b=a.x*b.y-a.y*b.x)

$a,b$围成的平行四边形的有向面积

判断线段相交(跨立实验)

由一条线段的端点向另一条线段两端点做叉积,判断符号是否一致

向量旋转

逆时针旋转$k$度:

inline vector turn (vector a,double k)
{
	return vector(a.x*cos(k)-a.y*sin(k),a.x*sin(k)+a.y*cos(k));
}

判断点在多边形内部

由一点引一条射线,若与多边形有奇数个交点则在多边形内部,否则在多边形外

二维凸包

求凸包

二维平面上给定$n$个点求凸包

显然$y$最小的点应该在凸包上

我们把$y$最小的点先选出来,将其他点极角排序

按照极角序将点入栈,利用斜率单调性判断是否弹栈

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
namespace red{
#define y1 qwq
#define int long long
#define eps (1e-10)
	inline int read()
	{
		int x=0;char ch,f=1;
		for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
		if(ch=='-') f=0,ch=getchar();
		while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
		return f?x:-x;
	}
	const int N=1e5+10;
	int n;
	int st[N],top;
	double ret;
	struct node
	{
		double x,y;
		node (double tx=0,double ty=0){x=tx,y=ty;}
		inline double operator ^ (const node &t) const
		{
			return x*t.y-y*t.x;
		}
		inline node operator - (const node &t) const
		{
			return node(x-t.x,y-t.y);
		}
	}a[N];
	inline double sqr(double x){return x*x;}
	inline double dis(node a,node b)
	{
		return sqrt(sqr(b.x-a.x)+sqr(b.y-a.y));
	}
	inline bool cmp1(node a,node b)
	{
		return a.x==b.x?a.y<b.y:a.x<b.x;
	}
	inline bool cmp2(node q,node b)
	{
		double tmp=(q-a[1])^(b-a[1]);
		if(!tmp) return q.x==b.x?q.y>b.y:q.x<b.x;
		return tmp>0;
	}
	inline void main()
	{
		n=read();
		for(int i=1;i<=n;++i)
		{
			scanf("%lf%lf",&a[i].x,&a[i].y);
		}
		sort(a+1,a+n+1,cmp1);
		int tot=0;
		a[0].x=-1e9+7;
		for(int i=1;i<=n;++i)
		{
			if(a[i].x!=a[tot].x||a[i].y!=a[tot].y) a[++tot]=a[i];
		}
		sort(a+2,a+tot+1,cmp2);
		st[++top]=1;st[++top]=2;
		for(int i=3;i<=tot;++i)
		{
			while(top>2&&((a[st[top-1]]-a[st[top]])^(a[i]-a[st[top]]))>=0) --top;
			st[++top]=i;
		}
		for(int i=1;i<top;++i)
		{
			ret+=dis(a[st[i]],a[st[i+1]]);
		}
		ret+=dis(a[st[top]],a[st[1]]);
		printf("%.2lf",ret);
	}
}
signed main()
{
	red::main();
	return 0;
}

二维凸包面积

利用叉积

inline double cross(node a,node b,node c)
{
	return (b-a)^(c-a);//叉积
}
inline double are()
{
	double ret=0;
	for(int i=1;i<n;++i) ret+=fabs(cross(a[1],a[i],a[i+1]));
	return ret*2;
}

多次判断点在凸包内部

将凸包分成$n-2$个三角形,每次取中间的三角形,用叉积判断当前点在三角形的内部还是左侧或右侧,二分求解

没写过

动态二维凸包

初始有三个点,每次加入一个点,维护凸包

由于凸包只会扩大,所以以初始的三点围成的三角形的重心为基准点求其他点的极角,将点插入合适的位置,再判断两侧的点是否删除

用平衡树/$set$维护

注意每次插入一个点不一定只弹出相邻的两个点……

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
using namespace std;
namespace red{
#define y1 qwq
#define int long long
#define double long double
#define iter set<node>::iterator
#define eps (1e-8)
	inline int read()
	{
		int x=0;char ch,f=1;
		for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
		if(ch=='-') f=0,ch=getchar();
		while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
		return f?x:-x;
	}
	const int N=1e5+10;
	int n,opt;
	struct node
	{
		double x,y,ang;
		node(double tx=0,double ty=0,double tang=0){x=tx,y=ty,ang=tang;}
		friend bool operator < (node a,node b)
		{
			return (a.ang<b.ang)||(a.ang==b.ang&&a.x<b.x);
		}
		inline node operator + (const node b) const
		{
			return node(x+b.x,y+b.y,0);
		}
		inline double operator ^ (const node &b) const
		{
			return x*b.y-y*b.x;
		}
		inline node operator - (const node &b) const
		{
			return node(x-b.x,y-b.y,0);
		}
	}a[N],zx;
	set<node> q;
	iter it1,it2;
	iter pre(iter x)
	{
		return x==q.begin()?--q.end():--x;
	}
	iter nxt(iter x)
	{
		return ++x==q.end()?q.begin():x;
	}
	inline void main()
	{
		n=read();
		for(int i=1;i<=3;++i)
		{
			opt=read();
			a[i].x=read(),a[i].y=read();
			zx=zx+a[i];
			a[i].x*=3,a[i].y*=3;
		}
		for(int i=1;i<=3;++i)
		{
			a[i].ang=atan2(a[i].y-zx.y,a[i].x-zx.x);
			q.insert(a[i]);
		}
		for(int i=4;i<=n;++i)
		{
			opt=read();
			a[i].x=read()*3,a[i].y=read()*3;
			a[i].ang=atan2(a[i].y-zx.y,a[i].x-zx.x);
			it2=q.lower_bound(a[i]);
			if(it2==q.end()) it2=q.begin();
			it1=pre(it2);
			if(opt==1)
			{
				if(((*it2-a[i])^(*it1-a[i]))<=0) continue;
				q.insert(a[i]);
				iter now=q.find(a[i]);
				iter tx=pre(now),ty=pre(tx);
				
				while(((*ty-*now)^(*tx-*now))<=0) q.erase(tx),tx=ty,ty=pre(tx);
				tx=nxt(now),ty=nxt(tx);
				while(((*ty-*now)^(*tx-*now))>=0) q.erase(tx),tx=ty,ty=nxt(tx);
			}
			else
			{
				if(((*it2-a[i])^(*it1-a[i]))<=0) puts("YES");
				else puts("NO");
			}
		}
	}
}
signed main()
{
	red::main();
	return 0;
}

旋转卡壳

组合数学入门题

众所周知,旋转卡壳有$232*2=24$种读法(雾

其实就是单调性优化的枚举?

给定凸包求最大直径

然后我们发现枚举一个点的话另一个点有单调性,所以这个过程是$O(n)$的……

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;
namespace red{
#define y1 qwq
#define int long long
#define eps (1e-10)
	inline int read()
	{
		int x=0;char ch,f=1;
		for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
		if(ch=='-') f=0,ch=getchar();
		while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
		return f?x:-x;
	}
	const int N=1e5+10;
	int n;
	int st[N],top;
	double ret;
	struct node
	{
		double x,y;
		node (double tx=0,double ty=0){x=tx,y=ty;}
		inline double operator ^ (const node &t) const
		{
			return x*t.y-y*t.x;
		}
		inline node operator - (const node &t) const
		{
			return node(x-t.x,y-t.y);
		}
	}a[N];
	inline double sqr(double x){return x*x;}
	inline double dis(node a,node b)
	{
		return sqr(b.x-a.x)+sqr(b.y-a.y);
	}
	inline bool cmp1(node a,node b)
	{
		return a.x==b.x?a.y<b.y:a.x<b.x;
	}
	inline bool cmp2(node q,node b)
	{
		double tmp=(q-a[1])^(b-a[1]);
		if(!tmp) return q.x==b.x?q.y>b.y:q.x<b.x;
		return tmp>0;
	}
	inline double are(node a,node b,node c)
	{
		return fabs((a.x*b.y+b.x*c.y+c.x*a.y-a.x*c.y-b.x*a.y-c.x*b.y)/2);
	}
	inline void main()
	{
		n=read();
		for(int i=1;i<=n;++i)
		{
			scanf("%lf%lf",&a[i].x,&a[i].y);
		}
		sort(a+1,a+n+1,cmp1);
		int tot=0;
		a[0].x=-1e9+7;
		for(int i=1;i<=n;++i)
		{
			if(a[i].x!=a[tot].x||a[i].y!=a[tot].y) a[++tot]=a[i];
		}
		sort(a+2,a+tot+1,cmp2);
		st[++top]=1;st[++top]=2;
		for(int i=3;i<=tot;++i)
		{
			while(top>2&&((a[st[top-1]]-a[st[top]])^(a[i]-a[st[top]]))>=0) --top;
			st[++top]=i;
		}
		tot=top;
		for(int i=1;i<=tot;++i) st[++top]=st[i];
		int tmp=3;
		for(int i=1;i<=tot;++i)
		{
			while(are(a[st[i]],a[st[i+1]],a[st[tmp+1]])>are(a[st[i]],a[st[i+1]],a[st[tmp]])) ++tmp;
			ret=max(ret,max(dis(a[st[i]],a[st[tmp]]),dis(a[st[i+1]],a[st[tmp]])));
		}
		printf("%lld",(int)ret);
	}
}
signed main()
{
	red::main();
	return 0;
}

另一个经典应用:给定两凸包求两凸包上点的最近距离

取其中一个凸包的$y$最小的点,另一凸包$y$最大的点开始枚举,方向相反,这样就有单调性了

#include<cstdio>
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
namespace red{
#define int long long
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define eps (1e-9)
	inline int read()
	{
		int x=0;char ch,f=1;
		for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
		if(ch=='-') f=0,ch=getchar();
		while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
		return f?x:-x;
	}
	const int N=5e4+10;
	int n,m;
	struct node
	{
		double x,y;
		node() {};
    	node (double _x,double _y):x(_x),y(_y){}
		inline double operator ^ (const node &t) const {return (x*t.y-y*t.x);}
		inline double operator * (const node &t) const {return (x*t.x+y*t.y);}
		inline node operator - (const node &t) const {return node(x-t.x,y-t.y);}
		inline bool operator < (const node &b) const {return atan2(y,x)<atan2(b.y,b.x);}
	}a[N],b[N];
	inline double sqr(double x) {return x*x;}
	inline double dist(node a,node b)
	{
		return sqrt(sqr(a.x-b.x)+sqr(a.y-b.y));
	}
	double multi(node A,node B,node C)//点积
	{
		return (B-A)*(C-A);
	}
	double cross(node A,node B,node C)//叉积
	{
    	return (B-A)^(C-A);
	}
	inline double disline(node A,node B,node C)
	{
   		if(dist(A,B)<eps) return dist(B,C);
   		if(multi(A,B,C)<-eps) return dist(A,C);//用点积判断是否在线段内部
   		if(multi(B,A,C)<-eps) return dist(B,C);
    	return fabs(cross(A,B,C)/dist(A,B));//点到线段距离
	}
	double mindist(node A,node B,node C,node D)
	{
    	return min(min(disline(A,B,C),disline(A,B,D)),min(disline(C,D,A),disline(C,D,B)));
	}
	inline void check(node a[],int n)
	{
		for(int i=1;i<n-1;++i)
		{
			double tmp=cross(a[i],a[i+1],a[i+2]);
			if(tmp<-eps) return;
			else if(tmp>eps)
			{
				reverse(a+1,a+n+1);
				return;
			}
		}
	}
	inline double solve(node a[],node b[],int n,int m)
	{
		int ymina=1,ymaxb=1;
		for(int i=1;i<=n;++i) if(a[i].y<a[ymina].y) ymina=i;
		for(int i=1;i<=m;++i) if(a[i].y>a[ymaxb].y) ymaxb=i;
		double tmp,ret=1e9+7;
		a[n+1]=a[1],b[m+1]=b[1];
		for(int i=1;i<=n;++i)
		{
			while(tmp=cross(a[ymina+1],b[ymaxb+1],a[ymina])-cross(a[ymina+1],b[ymaxb],a[ymina])<eps)
			{
				++ymaxb;
				if(ymaxb>m) ymaxb=1;
			}
			ret=min(ret,mindist(a[ymina],a[ymina+1],b[ymaxb],b[ymaxb+1]));
			++ymina;
			if(ymina>n) ymina=1;
		}
		return ret;
	}
	inline void main()
	{
		while("haku")
		{
			n=read(),m=read();
			if(!(n|m)) return ;
			for(int i=1;i<=n;++i) scanf("%lf%lf",&a[i].x,&a[i].y);
			for(int i=1;i<=m;++i) scanf("%lf%lf",&b[i].x,&b[i].y);
			check(a,n);check(b,m);
			printf("%.5lf ",min(solve(a,b,n,m),solve(b,a,m,n)));
		}
	}
}
signed main()
{
	red::main();
	return 0;
}

半平面交

有点恶心的东西

给定$n$条直线,每条直线保留左侧部分,求最后留下的面积

首先我们有$n^2$的暴力算法:每加入一条直线,就枚举以前的直线

如果在左侧则保留

如果在右侧把它扔掉

如果相交保留左侧端点,右侧改为交点

当然一般来说$n^2$是不够用的

我们可以先将所有直线按照与$x$轴的夹角排序,夹角相同的保留靠左的

用双端队列维护半平面交

维护方式:先保证队列中至少有两个元素

如果队尾的两个元素的交点在当前直线右侧,把队尾弹出

队首同理

但是记住一定要先弹出队尾,因为新加入的元素与一开始加入的元素围成的面积更小

如在这张图中,如果标号就是加入队列的顺序,那么当$3$加入的时候,先判断$1$就会踢$1$,先判断$2$就会踢$2$,但是新加入的元素和队首围成的面积会更小,所以应该先踢队尾

#include<bits/stdc++.h>
using namespace std;
namespace red{
#define int long long
#define ls(p) (p<<1)
#define rs(p) (p<<1|1)
#define eps (1e-8)
	inline int read()
	{
		int x=0;char ch,f=1;
		for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
		if(ch=='-') f=0,ch=getchar();
		while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
		return f?x:-x;
	}
	const int N=5000;
	int n,m,tot,sum,head,tail;
	struct node
	{
		double x,y;
		inline double operator ^ (const node &t) const{return x*t.y-y*t.x;}
		inline node operator - (const node &t) const{return (node){x-t.x,y-t.y};}
		inline node operator + (const node &t) const{return (node){x+t.x,y+t.y};}
		inline node operator * (const double &t) const{return (node){x*t,y*t};}
	}a[N],c[N];
	struct seg
	{
		node a,b;
		double k;
		seg(){}
		seg(const node &aa,const node &bb):a(aa),b(bb){k=atan2(b.y,b.x);}
		inline bool operator < (const seg &t) const
		{
			return k<t.k;
		}
	}q[N],que[N];
	inline double cross(node a,node b,node c)
	{
		return (b-a)^(c-a);
	}
	node get_node(seg A,seg B)//求交点
	{
    	node C=A.a-B.a;
    	double t=(B.b^C)/(A.b^B.b);
    	return A.a+A.b*t; 
	}
	inline int dcmp(double x)
	{
		if(fabs(x)<eps) return 0;
		return x>0?1:-1;
	}
	inline double are()
	{
		double are=0;
		for(int i=head;i<tail;++i){ are+=fabs(cross(c[head],c[i],c[i+1])); }
		return are/2;
	}
	inline void work()
	{
		head=tail=1;
		que[tail]=q[1];
		for(int i=2;i<=sum;++i)
		{
			while(head<tail&&(q[i].b^(c[tail-1]-q[i].a))<=eps) --tail;
			while(head<tail&&(q[i].b^(c[head]-q[i].a))<=eps) ++head;
			que[++tail]=q[i];
			if(fabs(que[tail].b^que[tail-1].b)<=eps) //平行保留较左的
			{
				--tail;
				if((que[tail].b^(q[i].a-que[tail].a))>eps) que[tail]=q[i];
			}
			if(head<tail) c[tail-1]=get_node(que[tail-1],que[tail]);//c[i]表示c[i]和c[i+1]的交点
		}
		while(head<tail&&(que[head].b^(c[tail-1]-que[head].a))<=eps) --tail;
		if(tail-head<=1) return;
		c[tail]=get_node(que[head],que[tail]);
	}
	inline void main()
	{
		n=read();
		for(int i=1;i<=n;++i)
		{
			m=read();
			for(int j=1;j<=m;++j)
			{
				a[j].x=read(),a[j].y=read();
			}
			for(int j=1;j<=m;++j)
			{
				++sum;
				int t=j+1;
				if(j==m) t=1;
				q[sum]=seg(a[j],a[t]-a[j]);
			}
		}
		sort(q+1,q+sum+1);
		work();
		printf("%.3lf",are());
	}
}
signed main()
{
	red::main();
	return 0;
}

自适应辛普森积分法

$simple$积分法

求积分$int_^frac{cx+d}{ax+b}dx$

先来说什么是辛普森积分法

假设有函数$g(x)=Ax^2+Bx+C$

(int_{a}^{b}g(x)dx)

微积分基本定理:

(=int_{a}^{b}frac{A}{3}(b^3-a^3)+frac{B}{2}(b^2-a^2)+C(b-a))

大力化简

(=frac{A}{3}(b-a)(a^2+ab+b^2)+frac{B}{2}(b+a)(b-a)+C(b-a))

(=frac{b-a}{6}(2A(a^2+ab+b^2)+3B(b+a)+6C))

(=frac{b-a}{6}(2Aa^2+2Aab+2Ab^2+3Ba+3Bb+6C))

(=frac{b-a}{6}(Aa^2+Ba+C+Ab^2+Bb+C+4A(frac{a+b}{2})^2+4B(frac{a+b}{2})+4C))

(=frac{b-a}{6}(g(a)+g(b)+4g(frac{a+b}{2})))

得到辛普森积分公式

(int_{a}^{b}g(x)dx=frac{b-a}{6}(g(a)+g(b)+4g(frac{a+b}{2})))

不过这是二次函数的积分公式,和我们上面那个式子有啥关系?

我们知道,一个复杂的函数可以近似拟合为一个二次函数

如果$f(x)$的拟合二次函数为$g(x)$,那么

(int_{a}^{b}f(x)dx≈frac{b-a}{6}(f(a)+f(b)+4f(frac{a+b}{2})))

当然拟合肯定存在误差,不过当定义域越小的时候,这个误差也就越小

我们可以把原函数分段,对于每一段求拟合函数$g(x)$的值再加起来,只要分得的段足够小就可以得到正确答案

但是分的太小会导致答案不正确, 太多会降低程序效率

这个时候我们可以让程序自适应

二分递归答案的段数和区间长度,精度达到要求就停止递归返回答案

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
using namespace std;
namespace red{
#define y1 qwq
	inline int read()
	{
		int x=0;char ch,f=1;
		for(ch=getchar();(ch<'0'||ch>'9')&&ch!='-';ch=getchar());
		if(ch=='-') f=0,ch=getchar();
		while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
		return f?x:-x;
	}
	double a,b,c,d,l,r;
	inline double f(double x)
	{
		return (c*x+d)/(a*x+b);
	}
	inline double simpson(double l,double r)
	{
		double mid=(l+r)/2;
		return (f(l)+4*f(mid)+f(r))*(r-l)/6;
	}
	inline double asr(double l,double r,double eps,double ans)
	{
		double mid=(l+r)/2;
		double tl=simpson(l,mid),tr=simpson(mid,r);
		if(fabs(tl+tr-ans)<=15*eps) return tl+tr+(tl+tr-ans)/15;
		return asr(l,mid,eps/2,tl)+asr(mid,r,eps/2,tr);
	}
	inline double asr(double l,double r,double eps)
	{
		return asr(l,r,eps,simpson(l,r));
	}
	inline void main()
	{
		scanf("%lf%lf%lf%lf%lf%lf",&a,&b,&c,&d,&l,&r);
		printf("%.6f
",asr(l,r,1e-6));
	}
}
signed main()
{
	red::main();
	return 0;
}
原文地址:https://www.cnblogs.com/knife-rose/p/12179026.html