【JZOJ4117】lhxsb(三角函数+凸壳+CDQ分治)

Problem

  给定一个山脉,把山脉的下部看作xOy平面直角坐标系中y=0的直线,用N个来顶点表示山脉。如图:
这里写图片描述
  Lwins_Y uki有Q个操作,操作有两种:对某个点用魔法(即删点)或询问某个点对空视野的张角(可以超过180°)。譬如上图第3个点的张角是:
这里写图片描述

Input

第一行包含两个正整数N;Q,分别表示刚开始的时候山的顶点个数以及Lwins_Y uki 的行动个数。
接下来N 行,包含N 对数xi; yi,表示山的顶点的坐标(xi; yi)。
接下来Q 行,每行的格式有如下两种可能:
• 0 k,表示当下时刻Lwins_Y uki 的魔法释放在了一开始的从左往右数的第k 个顶点上。
• 1 k,表示Lwins_Y uki 想立即知道住在第k 个顶点上的生物的对空视野的张角。
输入数据保证Lwins_Y uki 的行动和询问合法,即不会询问或把魔法释放在一个已经消失的点上。

Output

你应该按照询问的顺序,每行回答一次LwinsY uki 的询问,张角用弧度制,保留6 位小数。

Hint

• 对于20% 的数据,N;Q <=1000。
• 对于另外55% 的数据,N;Q<= 30000。
• 对于100% 的数据,N;Q <= 100000。
注意,Lwins_Y uki 不会询问在两侧的顶点的对空视野的张角,但是它有可能使用魔法抹去它们。

Solution

20points:三角函数+(暴力 or 凸壳)

  刚看这题我就懵逼了,然后马上意识到——这题我不可能得分,于是就没管这题。。。
  先思考一下怎么求张角。
  你可以推一推看一看夹角公式,然后学习一下c++的acos()、atan()、atan2()这些函数来解决这个问题。
  当然,我们有一种更简单的做法。
  先让我介绍一下c++的atan2()这个函数。假设某个角 θ θ 的三角函数 tanθ=yx t a n θ = y x ,那么atan2(x,y)就会帮你求出 θ θ 这个角的弧度。而根据tan的定义,我们可以把atan2(x,y)理解为原点至点(x,y)的方位角,即与 x 轴的夹角。
  那么,如何用atan2()来求夹角呢?我们可以举个栗子:
这里写图片描述
  如上图,在平面直角坐标系xOy中,已知a、b的坐标,我们想求∠aOb。那么,我们可以用atan2()先求出∠bOx,再求出∠aOx,两者相减即可。
  解决了张角的求法问题,又有一个问题接踵而至——对于每个询问点O,如何确定其a、b两点?
  以b为例,我们可以画画图发现,我们要求的点b其实就是在O左边的所有点中,满足构成的直线Ob的斜率最小的一个点;点a恰恰相反。这样,我们就可以 O(nq) O ( n q ) 暴力求出每个询问点的a、b两点。
  当然,我们也可以求个上凸壳。
这里写图片描述
  但是,囿于他会无耻地删点,所以:
  时间复杂度: O(nq) O ( n q )

Code

  我并没有打啦╮(╯▽╰)╭

100points:CDQ分治

  我们发现他会删点,用数据结构不好维护,所以考虑CDQ分治。
  如果单纯地将操作进行分治,那么时间复杂度不好把握。因为我们的操作是删点,如果没有删点,就一直会有n个点,然后所有询问都要花费巨额时间,这样甚至会退化到比暴力还慢的 O(nqlog2q) O ( n q l o g 2 q )
  那么,我们可以将删点转化为加点。如果有些点没有被删过,就把它在最后删掉;然后将操作数组反转过来。
  打到一半,我又发现一个问题:询问点不能插入到那个凸壳里,所以我们对于每个询问点,都要暴力扫一遍栈,找到询问点在凸壳中的上一个点。
  不过,囿于数据是随机的某种原因,这样也过了,而且还很快:
这里写图片描述
  其实这个可以二分。
  以求点p的张角的左边为例,我们先将所有x坐标小于p的点做个上凸壳,然后加入一个栈中。我们可以发现:栈中有一半的点满足向量p-sta[i-1]在直线sta[i]-sta[i-1]左边,这样我们看的时候,sta[i-1]这个点会被sta[i]遮住;而有另一半的点满足向量p-sta[i-1]在直线sta[i]-sta[i-1]右边。具象地说,有一半的点不能被看见,有一半的点能被看见。这显然满足二分性质,所以我们二分那个中间点。
  然后,求左右手向就可以用叉积。
  时间复杂度: O((n+q)(log2(n+q))2) O ( ( n + q ) ∗ ( l o g 2 ( n + q ) ) 2 )
  尽管时间复杂度分析起来是比上面的方法快的,然而这种方法的代码竟然比上面的方法还慢:
这里写图片描述

Code

  囿于此题代码过于高级,所以我会作下一些批注。

#include <cstdio>
#include <cmath>
#include <algorithm>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
using namespace std;
typedef double db;

const int N=1e5+1,Q=N<<1;
const db pi=acos(-1);//c++的三角函数是弧度制,而cos(π)=-1
int i,n,q,type[Q],query[Q];
bool del[N];
db left[Q];//Solution中所述的∠bOx
db right[Q];//Solution中所述的∠aOx
struct DOT//定义一个点或一个向量
{
    db x,y;
    DOT(db _x=0,db _y=0){x=_x,y=_y;}
}a[N];
DOT operator-(DOT a,DOT b){return DOT(a.x-b.x,a.y-b.y);}
inline bool cmpx(const DOT &x, const DOT &y) {return x.x < y.x;}
void scan()
{
    scanf("%d%d", &n, &q);
    fo(i,1,n)scanf("%lf%lf", &a[i].x, &a[i].y);
    sort(a + 1, a + 1 + n, cmpx);

    fo(i,1,q)
    {
        scanf("%d%d", &type[i], &query[i]);
        del[query[i]]|=!type[i];
    }

    fo(i,1,n) if (!del[i]) query[++q] = i;//将没有被删的点在最后删掉

    fo(i,1,q/2)
    {
        swap(type[i], type[q - i + 1]);
        swap(query[i], query[q - i + 1]);
    }//将删点转为加点

    fo(i,1,q)left[i] = 1.5 * pi, right[i] = -0.5 * pi;//弧度制的1.5*pi即为270°,-0.5*pi即为-90°
}

int num[N],top;
DOT poi[N],sta[N];
inline bool cmp(int x, int y) {return a[query[x]].x < a[query[y]].x;}
inline db cro(DOT a,DOT b){return a.x*b.y-a.y*b.x;}//叉积
inline db cross(DOT o,DOT a,DOT b){return cro(a-o,b-o);}
void work(int pn,int qn)
{
    if(!pn||!qn)return;

    sort(poi+1,poi+pn+1,cmpx);
    sort(num+1,num+qn+1,cmp);

    top=0;int i,j=1;
    fo(i,1,qn)//顺序做一遍
    {
        DOT p=a[query[num[i]]];
        for (; j <= pn && poi[j].x < p.x; j++)
        {
            while(top>1) 
                if (cross(sta[top-1], sta[top], poi[j]) > 0)top--;
                else break;//维护上凸壳
            sta[++top] = poi[j];
        }

        if(!top)continue;
        int l=1,r=top,mid;
        while(l<r)
        {
            mid=l+r+1>>1;
            if(cross(sta[mid-1],sta[mid],p)>0)
                    r=mid-1;
            else    l=mid;
        }
        db t = atan2(sta[l].y - p.y, sta[l].x - p.x);//计算Solution中所述的∠bOx
        if (t < 0) t += 2 * pi;//atan2()的返回值是-pi~pi
        left[num[i]] = min(left[num[i]], t);//bOx要尽量小
    }

    top=0;j=pn;
    fd(i,qn,1)//逆序做一遍
    {
        DOT p=a[query[num[i]]];
        for (; j >= 1 && poi[j].x > p.x; j--)
        {
            while(top>1) 
                if (cross(sta[top-1], sta[top], poi[j]) < 0)top--;
                else break;
            sta[++top] = poi[j];
        }

        if(!top)continue;
        int l=1,r=top,mid;
        while(l<r)
        {
            mid=l+r+1>>1;
            if(cross(sta[mid-1],sta[mid],p)<0)
                    r=mid-1;
            else    l=mid;
        }
        db t = atan2(sta[l].y - p.y, sta[l].x - p.x);//计算Solution中所述的∠aOx
        right[num[i]] = max(right[num[i]], t);//∠aOx要尽量大
    }
}
void CDQpartition(int l,int r)
{
    if(l==r)return;

    int mid=l+r>>1;

    int pn=0;
    fo(i,l,mid)if(!type[i])poi[++pn]=a[query[i]];//记录左边的加点

    int qn=0;
    fo(i,i,r  )if( type[i])num[++qn]=i;//记录右边的询问

    work(pn,qn);

    if(pn&&pn<mid-l+1)CDQpartition(l    ,mid);
    if(qn&&qn<r-mid  )CDQpartition(mid+1,r  );
}

void print()
{
    fd(i,q,1)
        if(type[i])
        {
            db t=left[i]-right[i];
            if (t < 0) t += 2 * pi;
            if (t > 2 * pi) t -= 2 * pi;
            printf("%.6f\n",t);
        }
}

int main()
{
    scan();
    CDQpartition(1,q);
    print();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值