zoukankan      html  css  js  c++  java
  • 【洛谷5286】[HNOI2019] 鱼(计算几何)

    点此看题面

    大致题意: 给你(n)个点,让你求鱼形图的数量。

    核心思路

    首先,考虑到(n)这么小,我们可以枚举线段(AD),再去找符合条件的(BC,EF)

    然后,不难发现(BC)(EF)互不影响,因此我们可以分开求对于已知(AD)(BC)(EF)的方案数,然后将其相乘。

    那么我们现在的问题就在于,如何求出(BC)(EF)的方案数了。

    (BC)的方案数

    预处理

    考虑到(AB=AC,BD=CD),用我这点可怜的初中数学知识,都能证明出(AD)垂直平分(BC)(貌似作业里还做过这种题目)。

    则,我们可以换一个角度,如果枚举(BC),那么符合条件的(AD)必然满足两个条件:

    • (ecause BCot AD, herefore BC)(AD)的斜率相乘为(-1)
    • (ecause AD)平分(BC, herefore AD)必然经过(BC)的中点。

    而已知斜率和直线上一点,我们就可以推出这条直线的解析式啦!

    (B(x_B,y_B),C(x_C,y_C)),则(BC)斜率为(frac{y_C-y_B}{x_C-x_B}),( herefore AD)斜率为:

    [-1÷frac{y_C-y_B}{x_C-x_B}=frac{x_B-x_C}{y_C-y_B} ]

    (ecause BC)中点(G)坐标为((frac{x_B+x_C}2,frac{y_B+y_C}2), herefore AD)截距为:

    [frac{x_B-x_C}{y_C-y_B}*frac{x_B+x_C}2-frac{y_B+y_C}2=frac{(x_B-x_C)(x_B+x_C)-(y_C-y_B)(y_B+y_C)}{2(y_C-y_B)} ]

    这样其实我们已经求出了(AD)的斜率和截距,但如果你像我一样无聊,可以看一下我对于这里截距的进一步化简。


    这个截距的式子显得过于冗长,因此我们可以再转化一下得到:

    [frac{(x_B-x_C)(x_B+x_C)-(y_C-y_B)(y_B+y_C)}{2(y_C-y_B)}=frac{(x_B-x_C)(x_B+x_C)+(y_B-y_C)(y_B+y_C)}{2(y_C-y_B)}=frac {x_{B-C}*x_{B+C}+y_{B-C}*y_{B+C}}{2(y_C-y_B)} ]

    其中,(B-C,B+C)皆为向量。

    然后,了解一些计算几何公式的人就可以发现,(x_{B-C}*x_{B+C}+y_{B-C}*y_{B+C})其实是一个点积的形式!

    于是我们最后得到一个简单的式子:

    [frac{(B-C)cdot(B+C)}{2(y_C-y_B)} ]


    而求出了斜率和截距之后,我们就相当于求出了(AD)的函数表达式。

    则可以考虑把(AD)函数表达式相同的(BC)的信息全部扔入同一个(vector)存储下来,方便后面求答案。

    还有要注意的就是对于(B,C)两点(y)相等的情况要特判,因为此时(AD)就不是一次函数了。

    求答案

    枚举(AD)时,我们首先就是求出(AD)的函数表达式,然后到对应(vector)里去求答案。

    此时,我们主要是要满足题目中给出的“(∠BAD,∠BDA)(∠CAD,∠CDA)都是锐角”的要求。

    也就是说,线段(AD)需要与线段(BC)有交点。

    我们先前已经提过要开(vector),但其实不需要直接存下(BC),因为这样不太方便。

    实际上,我们只要存下中点某一坐标的两倍(两倍是为了可以直接用(int)存储),就可以直接在(vector)中通过(upper\_bound)的方式,分别用(A,D)两点的相应坐标查找出两个范围,然后利用前缀和思想来用大的减小的即可得出答案。

    至于应该选择哪一坐标,需要根据具体函数解析式来分析了。

    如形如(x=a)的表达式必须选纵坐标,形如(y=a)的表达式必须选横坐标(因为固定的无法判),否则任选。

    不过反正我们要特判(B,C)纵坐标相等,即(A,D)横坐标相等的情况,则我们干脆对于横坐标相等的取纵坐标,不相等的取横坐标,也方便我们判断应用哪个坐标去(upper\_bound)

    这一过程有一定细节,具体实现详见代码。

    (EF)的方案数

    对于这个,我们可以考虑,过点(D)(AD)的垂线(l),则我们将整个平面划分成了两个半平面,而根据题目要求,点(E,F)必须在点(A)所不在的那个半平面内。

    则如果我们枚举点(D),然后将其余点按极角排序,我们就可以用双指针,来维护对于每个点(A)可能作为点(E,F)的所有点。

    那么什么情况下两个点才能作为一对点(E,F)呢?

    正如题目中说的,需要(DE=DF)

    因此我们开个(map),维护每种到点(D)的距离出现的次数,然后就可以用类似于莫队的方式进行维护了,这应该还是比较简单的。

    注意,这里我对于相同的两个点只统计了一次,因为最后我将答案乘上了(4),即(BC,EF)互换的情况。

    这一过程同样有一定细节,具体实现详见代码。

    代码

    #include<bits/stdc++.h>
    #define Tp template<typename Ty>
    #define Ts template<typename Ty,typename... Ar>
    #define Reg register
    #define RI Reg int
    #define RL Reg LL
    #define Con const
    #define CI Con int&
    #define CL Con LL&
    #define I inline
    #define W while
    #define N 1000
    #define LL long long
    #define DB long double
    #define eps 1e-10
    #define swap(x,y) (x^=y^=x^=y)
    using namespace std;
    int n,val[N+5][N+5];Con DB Pi=acos(-1); 
    Tp I Ty gcd(Con Ty& x,Con Ty& y) {return y?gcd(y,x%y):x;}
    struct Fr//存储一个分数
    {
    	LL x,y;I Fr(CL a=0,CL b=1) {RL g=gcd(a,b);x=a/g,(y=b/g)<0&&(x=-x,y=-y);}//注意约分
    	I bool operator != (Con Fr& o) Con {return x^o.x||y^o.y;}//不等于
    	I bool operator < (Con Fr& o) Con {return x^o.x?x<o.x:y<o.y;}//这个小于不是真的小于,只是用于区分不同分数来存储到map中
    };
    struct Line//存储一条直线,只存斜率和截距
    {
    	Fr Slope,Incre;I Line(Con Fr& s=Fr(),Con Fr& i=Fr()):Slope(s),Incre(i){}
    	I bool operator < (Con Line& o) Con {return Slope!=o.Slope?Slope<o.Slope:Incre<o.Incre;}//用于区分
    };
    struct Point//存储一个点
    {
    	#define Dot(A,B) (1LL*(A).x*(B).x+1LL*(A).y*(B).y)//点积
    	#define Cro(A,B) (1LL*(A).x*(B).y-1LL*(A).y*(B).x)//叉积
    	#define Len(A) Dot(A,A)//求出长度的平方,之所以不开放是为防炸精度,而且不影响比大小
    	int x,y;I Point(CI a=0,CI b=0):x(a),y(b){}
    	I Point operator + (Con Point& o) Con {return Point(x+o.x,y+o.y);}//点相加
    	I Point operator - (Con Point& o) Con {return Point(x-o.x,y-o.y);}//点相减
    }p[N+5];
    struct Data//存储极角和对应点编号用于排序
    {
    	int pos;DB ang;I Data(CI p=0,Con DB& a=0):pos(p),ang(a){}
    	I bool operator < (Con Data& o) Con {return ang<o.ang;}
    }s[N<<1];
    map<Line,int> pos;vector<int> v[N*N+5];map<LL,int> cnt;
    int main()
    {
    	RI i,j,x,y,z,tot,Pc=0,H,T;RL ans=0;Line w;Point t;
    	for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d%d",&x,&y),p[i]=Point(x,y);
    	for(i=1;i<=n;++i) for(j=i+1;j<=n;++j)
    	{
    		if(p[i].y==p[j].y)//特判y相等
    		{
    			w=Line(Fr(1,0),Fr(p[i].x+p[j].x,2)),//求出截距
    			v[pos.count(w)?pos[w]:pos[w]=++Pc].push_back(p[i].y<<1);//存储中点纵坐标两倍
    			continue;
    		}
    		t=p[j]-p[i],w=Line(Fr(-t.x,t.y),Fr(Dot(t,p[i]+p[j]),2LL*t.y)),//求出斜率和截距
    		v[pos.count(w)?pos[w]:pos[w]=++Pc].push_back(p[i].x+p[j].x);//存储中点横坐标两倍
    	}
    	for(i=1;i<=Pc;++i) sort(v[i].begin(),v[i].end());//排序,用于之后的upper_bound
    	for(i=1;i<=n;++i)//枚举点D
    	{
    		for(tot=0,j=1;j<=n;++j) i^j&&(s[++tot]=Data(j,atan2(p[j].y-p[i].y,p[j].x-p[i].x)),0);//存下极角
    		for(sort(s+1,s+n),j=1;j^n;++j) (s[j+n-1]=s[j]).ang+=2*Pi;//排序,然后将每个点复制一份,方便后面的双指针
    		for(cnt.clear(),tot=H=T=0,j=1;j^n;++j)//双指针
    		{
    			W(s[T+1].ang<s[j].ang+1.5*Pi-eps) ++T,tot+=cnt[Len(p[s[T].pos]-p[i])]++;//加入新进入半平面的点
    			W(s[H+1].ang<s[j].ang+0.5*Pi+eps) ++H,tot-=--cnt[Len(p[s[H].pos]-p[i])];//删除新离开半平面的点
    			val[s[j].pos][i]=tot;//记下结果
    		}
    	}
    	for(i=1;i<=n;++i) for(j=i+1;j<=n;++j)//枚举AD
    	{
    		p[i].x^p[j].x?(x=p[i].x,y=p[j].x):(x=p[i].y,y=p[j].y),x>y&&swap(x,y),t=p[i]-p[j],//求出当前情况下对应哪种坐标
    		w=(p[i].x^p[j].x?Line(Fr(t.y,t.x),Fr(Cro(t,p[i]),t.x)):Line(Fr(1,0),Fr(p[i].x,1))),//求出解析式
    		#define UB(x) upper_bound(v[z].begin(),v[z].end(),x)
    		pos.count(w)&&(z=pos[w],ans+=(UB((y<<1)-1)-UB(x<<1))*(val[i][j]+val[j][i]));//如果该解析式存在,更新答案
    	}return printf("%lld",ans<<2),0;//注意将答案乘4
    }
    
  • 相关阅读:
    原生JS实现日历
    重复的事情让机器来做,简化的思想
    Ext3.1的一些使用讨论
    JS使用Crypto实现AES/ECS/zero-padding加密
    一些提升效率的小知识
    一些很有意思的JS现象
    Tiny Linux -- tce-load
    python sqlalchemy mysql 自动映射
    python 反射
    python 动态导包
  • 原文地址:https://www.cnblogs.com/chenxiaoran666/p/Luogu5286.html
Copyright © 2011-2022 走看看