【洛谷5286】[HNOI2019] 鱼(计算几何)
大致题意: 给你\(n\)个点,让你求鱼形图的数量。
核心思路
首先,考虑到\(n\)这么小,我们可以枚举线段\(AD\),再去找符合条件的\(BC,EF\)。
然后,不难发现\(BC\)与\(EF\)互不影响,因此我们可以分开求对于已知\(AD\)的\(BC\)与\(EF\)的方案数,然后将其相乘。
那么我们现在的问题就在于,如何求出\(BC\)与\(EF\)的方案数了。
\(BC\)的方案数
预处理
考虑到\(AB=AC,BD=CD\),用我这点可怜的初中数学知识,都能证明出\(AD\)垂直平分\(BC\)(貌似作业里还做过这种题目)。
则,我们可以换一个角度,如果枚举\(BC\),那么符合条件的\(AD\)必然满足两个条件:
- \(\because BC\bot AD,\therefore BC\)与\(AD\)的斜率相乘为\(-1\)。
- \(\because AD\)平分\(BC,\therefore AD\)必然经过\(BC\)的中点。
而已知斜率和直线上一点,我们就可以推出这条直线的解析式啦!
设\(B(x_B,y_B),C(x_C,y_C)\),则\(BC\)斜率为\(\frac{y_C-y_B}{x_C-x_B}\),\(\therefore AD\)斜率为:
又\(\because BC\)中点\(G\)坐标为\((\frac{x_B+x_C}2,\frac{y_B+y_C}2),\therefore AD\)截距为:
这样其实我们已经求出了\(AD\)的斜率和截距,但如果你像我一样无聊,可以看一下我对于这里截距的进一步化简。
这个截距的式子显得过于冗长,因此我们可以再转化一下得到:
其中,\(B-C,B+C\)皆为向量。
然后,了解一些计算几何公式的人就可以发现,\(x_{B-C}*x_{B+C}+y_{B-C}*y_{B+C}\)其实是一个点积的形式!
于是我们最后得到一个简单的式子:
而求出了斜率和截距之后,我们就相当于求出了\(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
}