BZOJ 4311: 向量(线段树分治+凸包+三分)

Description

你要维护一个向量集合,支持以下操作:
1.插入一个向量(x,y)
2.删除插入的第i个向量
3.查询当前集合与(x,y)点积的最大值是多少。如果当前是空集输出0


Input

第一行输入一个整数n,表示操作个数
接下来n行,每行先是一个整数t表示类型,如果t=1,输入向量
(x,y);如果t=2,输入id表示删除第id个向量;否则输入(x,y),查询
与向量(x,y)点积最大值是多少。
保证一个向量只会被删除一次,不会删没有插入过的向量


Output

对于每条t=3的询问,输出一个答案


Sample Input

5
1 3 3
1 1 4
3 3 3
2 1
3 3 3


Sample Output

18
15


HINT

n<=200000 1<=x,y<=10^6


Solution

这题原本是我在JZOJ上找到的,现在由于我没有再在JZ充饭卡,我就被踢了……QAQ

话说这题想了我好久,首先从代数性质入手,死磕 x1x2+y1y2 这条式子,发现根本不行。每项不独立该怎么整啊?于是开始联系计算几何的相关性质。首先, a⃗  b⃗  的点积是 |a⃗ ||b⃗ |cosα ,这是一个向量的模乘上另一个向量在此向量上的投影。而读入的待求向量已经确定了一个模,剩下一个向量在上面的投影。我们要使其最大。

对于一个待求向量,我们做一条垂线,从无穷远处挪向原点。根据投影的定义,明显垂线第一个遇到的点所代表的向量就是我们要找的。然而,这个向量必然处于所有向量的最外侧,就是在所有点外围的凸包上。

由于本题向量都在第一象限,所以只用保存上凸壳即可。由于上凸壳斜率单调,然后我们在上凸壳上三分极值即可。现在的问题在于我们每插一个,删一个都去合并凸包显然不是行的。于是我们考虑线段树分治

我们保存每个向量出现的时间区间 [L,R] 。明显它只会对处于这段时间里的询问起贡献。我们将每个向量插入到线段树的 logN 个节点上,然后在每个节点都开个链表储存出现在这个节点代表时间区间上的向量。然后我们遍历整颗线段树,在每个节点都用扫描法求出对应的上凸壳。我们可以从下往上处理,也可以从上往下做。在处理完当前节点后,我们要对能贡献的询问贡献答案。这里就在上凸壳上三分,寻找点积最大值更新答案就行了。

考虑这样分治的时间,对于每个询问,都要自顶向下查询 logN 个节点,每个节点都需要排序求凸包并三分一次,虽然一起处理询问可以同时求凸包,但三分的时间省不掉。所以时间复杂度是 O(Nlog2N) 的。

另外网上有人的做法是将向量与询问提前排序,根据决策点单调什么的,又归并排序,时间复杂度就会降为 O(NlogN) ,我也不太懂,米娜有兴趣的话可以自己上网查阅=。=


Code

#include <cstdio>
#include <cstdlib>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <iostream>
#define maxn 200010
#define maxc 18

using namespace std;

typedef long long LL;
int n, tot, top, cur, head[maxn<<2], st[maxn];
struct Data{
    int obj, nxt;
}List[maxn*maxc];

struct Ask{
    LL x, y;
    LL ans;
}q[maxn];

struct Vector{
    LL x, y;
    Vector() {}
    Vector(int _x, int _y):x(_x), y(_y) {}

    bool operator < (const Vector& Q) const{
        return (x < Q.x) || (x == Q.x && y < Q.y);
    }
    friend Vector operator - (Vector A, Vector B){
        return Vector(A.x - B.x, A.y - B.y);
    }
    friend LL Dot(Vector A, Vector B){
        return A.x * B.x + A.y * B.y;
    }
    friend LL Det(Vector A, Vector B){
        return A.x * B.y - A.y * B.x;
    }

}p[maxn], Ch[maxn], tmp[maxn];


void Insert(int root, int L, int R, int x, int y, int k){
    if(x > R || y < L)  return;
    if(x <= L && y >= R){
        List[++cur].nxt = head[root];
        List[cur].obj = k;
        head[root] = cur;
        return;
    }

    int mid = (L + R) >> 1, Lson = root << 1, Rson = root << 1 | 1;
    Insert(Lson, L, mid, x, y, k);
    Insert(Rson, mid+1, R, x, y, k);
}

void query(int k){

    Vector me = Vector(q[k].x, q[k].y);

    int L = 1, R = top;
    while(L + 2 < R){
        int mid1 = L + (R - L) / 3, mid2 = R - (R - L) / 3;
        LL Val1 = Dot(Ch[mid1], me), Val2 = Dot(Ch[mid2], me);
        if(Val1 == Val2)  L = mid1, R = mid2;
        else if(Val1 < Val2)  L = mid1;
        else  R = mid2;
    }

    for(int i = L; i <= R; i++)  q[k].ans = max(q[k].ans, Dot(Ch[i], me));
}

void Solve(int root, int L, int R){

    if(L < R){
        int mid = (L + R) >> 1, Lson = root << 1, Rson = root << 1 | 1;
        Solve(Lson, L, mid);  Solve(Rson, mid+1, R);
    }

    int cnt = 0;
    for(int i = head[root]; i; i = List[i].nxt)  tmp[++cnt] = p[List[i].obj];

    if(!cnt)  return;
    sort(tmp+1, tmp+cnt+1);

    top = 0;
    for(int i = 1; i <= cnt; i++){
        while(top > 1 && Det(tmp[i] - Ch[top], Ch[top] - Ch[top-1]) <= 0)  top --;
        Ch[++top] = tmp[i];
    }

    for(int i = L; i <= R; i++)  if(q[i].x)  query(i);
}

int main(){

    scanf("%d", &n);

    int op, k;
    LL x, y;
    for(int i = 1; i <= n; i++){
        scanf("%d", &op);
        if(op == 1){
            scanf("%lld%lld", &x, &y);
            p[++tot] = Vector(x, y);
            st[tot] = i;
        }
        else if(op == 2){
            scanf("%d", &k);
            Insert(1, 1, n, st[k], i, k);
            st[k] = 0;
        }
        else  scanf("%lld%lld", &q[i].x, &q[i].y);
    }

    for(int i = 1; i <= tot; i++)  if(st[i])  Insert(1, 1, n, st[i], n, i);

    Solve(1, 1, n);

    for(int i = 1; i <= n; i++)  if(q[i].x)  printf("%lld\n", q[i].ans);

    return 0;
}

这里写图片描述

为你闯出的前方
贯穿世界的消亡
将弱小的自己藏匿抹杀

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值