题目传送门:【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
,
而根据定义,只有当第
1.线段树
首先对于每个点和区间,维护关于这个点或区间中能被看到的房子的数量
我们将两个区间合并时(点算作长度为 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
大,那么就直接跳过这个区块;否则,二分查找出这个区块内斜率刚好比
时间复杂度略高,据说分块的方式很有讲究,应该是
12n∗logn2−−−−−−√
,不过本人嫌麻烦,所以干脆直接分成
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;
}