[BZOJ 2957] 楼房重建 线段树/分块(两种做法)

题目传送门:【BZOJ 2957】

题目分析见下。


题目大意:小 A 的楼房外有一大片施工工地,工地上有 N 栋待建的楼房。为了简化问题,我们考虑这些事件发生在一个二维平面上。
小 A 在平面上 ( 0 , 0 ) 点的位置,第 i 栋楼房可以用一条连接 ( i , 0 ) 和 ( i , Hi ) 的线段表示,其中 Hi 为第 i 栋楼房的高度。如果这栋楼房的最高点与 ( 0 , 0 ) 的连线没有与之前的线段相交,那么这栋楼房就被认为是可见的。
施工队的建造总共进行了 M 天。初始时,所有楼房都还没有开始建造,它们的高度均为 0。在第 i 天,建筑队将会将横坐标为 Xi 的房屋的高度变为 Xi (高度可以比原来大—修建,也可以比原来小—拆除,甚至可以保持不变—建筑队这天什么事也没做)。

请你帮小A数数每天在建筑队完工之后,他能看到多少栋楼房?( 1≤ N,M ≤10 5 ,1≤ Xi ≤N,1≤ Yi ≤10 9 )


题目分析:

这道题一开始我并没有想到线段树的做法,只用了分块,后来听到学长讲了之后,瞬间明白了这道题 Orz ——线段树太巧妙了,而且还很快

由题,我们把每个房子的最高点与(0 , 0)相连,得到的这个线段有一个斜率 k
而根据定义,只有当第 i 条线段的斜率大于第 i1 条线段斜率 k 时(即后一座房子斜率大于前一座),后面的那座房子才能被看到。

1.线段树

首先对于每个点和区间,维护关于这个点或区间中能被看到的房子的数量 ans,以及斜率的最大值 kmax

我们将两个区间合并时(点算作长度为 1 的区间),会出现两种情况:左边的区间最大斜率大于或等于右边的区间的最大斜率,或左区间斜率小于右区间斜率。出现前一种情况时,合并后的区间的 ans kmax 直接等于左区间的 ans kmax ,因为此时左区间的房子把右区间的房子挡住了,右区间对于答案没有任何贡献。后一种情况时,需要进行分类讨论。

这里写图片描述

为了方便起见,我们把斜率称作“高度”。

对于后一种情况,我们把右区间分为左子区间和右子区间,此时有三种不同的取法:

1.当左子区间的高度小于右子区间的高度时,此时左子区间对答案 ans 没有任何贡献,仅右子区间的房子能被看到。在计算时,我们对右区间再进行和这两步一样的递归操作。(注意递归的时候斜率判断条件仍然是这里的左区间

2.当左子区间高度大于左区间,小于右子区间时,此时我们需要同时计算两个子区间能够被看到的房子的数量。此时的答案 ans 为左子区间的合法数量加上右子区间的合法数量,因此,我们需要递归处理左子区间,然后加上右子区间的合法数量。这里我们不能直接加上右子区间的答案(因为左子区间挡住了一部分),增加的答案应为整个右区间的 ans 减去左子区间的 ans

3.当左子区间高度既大于左区间,又大于右子区间的时候,此时我们仅需递归处理左子区间即可(因为右子区间的房子被左子区间的挡住了)。

这里写图片描述

最后统计线段树根节点的 ans ,根节点的 ans 即为答案。

下面附上线段树操作的代码:

/* 线段树做法 */
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int MX = 200005;

struct Node{
    int ans;
    double slope;
    Node *ls,*rs;
};
Node node[2*MX],*tail = node,*root;
int n,m,xi;
double s;

Node *build(int lf,int rt){
    Node *nd = ++tail;
    if (lf == rt){
        nd->ans = 0;
        nd->slope = 0.0;
    } else {
        int mid = (lf + rt) / 2;
        nd->ls = build(lf,mid);
        nd->rs = build(mid + 1,rt);
    }
    return nd;
}
int calc(Node *nd,int lf,int rt,double curSlope){       //curSlope:左区间的斜率 
    if (lf == rt)
        return nd->slope > curSlope ? 1 : 0 ;
    int mid = (lf + rt) / 2;
    if (nd->ls->slope < curSlope)
        return calc(nd->rs,mid + 1,rt,curSlope);
    return calc(nd->ls,lf,mid,curSlope) + nd->ans - nd->ls->ans;

    // if-语句对应情况 1,之后的语句对应情况 2,3。
    // 对于情况 3,此时左子区间的答案和整个右区间的答案相等,
    // 即 nd->ans = nd->ls->ans,最后就等于 0,因此相当于直接递归左子区间 

}
void modify(Node *nd,int lf,int rt,int pos,double val){
    if (lf == rt){
        nd->slope = val;
        nd->ans = 1;
        return;
    }
    int mid = (lf + rt) / 2;
    if (pos <= mid) modify(nd->ls,lf,mid,pos,val);
    else modify(nd->rs,mid + 1,rt,pos,val);
    nd->slope = max(nd->ls->slope, nd->rs->slope);
    nd->ans = nd->ls->ans + calc(nd->rs,mid + 1,rt,nd->ls->slope);

    //和上面讲的一样,答案分情况求 

}
int main(){
    scanf("%d%d",&n,&m);
    root = build(1,n);                      //预先建树 
    for (int i = 1;i <= m;i++){
        scanf("%d%lf",&xi,&s);
        s = (s / xi) * 1.0;                 //直接将 s 表示为斜率大小 
        modify(root,1,n,xi,s);
        printf("%d\n",root->ans);
    }
    return 0;
}

2.分块

其实这道题分块做法十分好想……

把所有的点划分成许多的区块,每个区块内的房子数量最大值和区块的总数近似相等。将所有的房子放在一个个区块内,对于这个区块维护一个严格上升的高度序列(或者是斜率,甚至标号都可以,只要能知道它对应的是哪些房子);每次需要修改一座房子的高度(斜率)时,重建这个区块,重新维护这个区块的上升序列,然后再暴力统计所有区块提供的答案之和。

统计答案的时候,对于任意的一个区块,如果它里面最大的斜率都没有之前所有区块的最大斜率 k 大,那么就直接跳过这个区块;否则,二分查找出这个区块内斜率刚好比 k 大的那个位置,然后将这个位置及之后的所有房子数量加起来就是这个区块的答案。(由于区块内斜率严格递增,所以这样做是正确的)

时间复杂度略高,据说分块的方式很有讲究,应该是 12nlogn2 ,不过本人嫌麻烦,所以干脆直接分成 n 块,差别很小。
(分块数量都要向上取整)

下面附上分块做法的代码:

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int MX = 320;

int n,m,xi,geshu[MX],fsize = 0;              //geshu:每个区块内的合法答案个数 
double line[MX*MX],map[MX][MX];              //line:原序列,map:按区块存每个位置的斜率 
int hefa[MX][MX];                            //hefa:存每个区块的上升序列 (本人存的标号) 
double s,zuida[MX];                          //zuida:存每个区块斜率的最大值 
                                             //上述命名方式极为鬼畜233 

int mod(int x,int MOD){                      //每次分块时的判断函数,无关紧要 
    if (x == 0) return 0;
    return x % MOD == 0 ? MOD : x % MOD ;
}
void check(int p){                           //暴力重建的过程 
    double slope = 0.0;
    int now = 0;
    geshu[p] = 0,zuida[p] = 0;
    memset(hefa[p],0,sizeof(hefa[p]));
    for (int i = 1;i <= fsize;i++){
        if (map[p][i] > slope){
            slope = map[p][i];
            geshu[p]++;
            hefa[p][geshu[p]] = i;
            if (map[p][i] > zuida[p]) zuida[p] = map[p][i];
        }
    }
}
int findpos(double val,int p,int lf,int rt){ //二分查找位置 
    if (lf == rt) return lf;
    int mid = (lf + rt) / 2;
    if (val < map[p][ hefa[p][mid] ])
        return findpos(val,p,lf,mid);
    return findpos(val,p,mid + 1,rt);
}
void find(){
    int tot = 0,q = 0;
    double maxs = zuida[1];
    tot += geshu[1];
    for (int i = 2;i <= fsize;i++){
        if (zuida[i] <= maxs) continue;         //斜率小于之前的最大值,跳过 
        q = findpos(maxs,i,1,geshu[i]);
        tot += geshu[i] - q + 1;
        maxs = zuida[i];
    }
    printf("%d\n",tot);
}
int main(){
    scanf("%d%d",&n,&m);
    fsize = (int)ceil((sqrt(n)));               //ceil:向上取整,<cmath>自带的函数 
    for (int i = 1;i <= m;i++){
        scanf("%d%lf",&xi,&s);
        s = s / xi * 1.0;                               //直接将 s 表示为斜率 
        line[xi] = s; 
        int p = (xi - 1) / fsize + 1,q = mod(xi,fsize); //p为行数,q为列数 
        map[p][q] = s;                          //把斜率存放到对应区块的位置上 
        check(p);
        find();
    }
    return 0;
}

最后实测线段树效率大概是分块的两倍。
所以这道题推荐大家用线段树做。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值