P4198 楼房重建 (线段树)
求从 \((0,0)\) 往上看能看到多少栋没被挡住的楼房,带修改。
对于带修改的题目,我们需要快速维护,就需要用到数据结构。这时候通过直觉可以想到,问题是可以分为子问题然后合并得到的,所以我们考虑线段树。
观察到能被看到的楼房,从左到右斜率递增,即我们需要维护斜率递增的序列。
考虑子区间需要维护的信息,发现该区间对其他区间的影响只跟其斜率最大值有关,所以一个是维护区间斜率最大值 \(\max\)。然后再维护一个区间答案 \(cnt\),表示从该区间的左端点能看到的楼房数量。
怎么合并?记 \(u\) 为合并后的区间,\(ls\) 和 \(rs\) 为左右区间。首先显然左区间 \(ls.cnt\) 一定能贡献到 \(u.cnt\) 中;右区间的答案显然不能马上得出,我们在把右区间细化成更小的问题,分类讨论一下。记右区间的左区间为 \(rsl\),右区间的右区间为 \(rsr\)。
如果 \(rsl.max<ls.max\),那么 \(rsl\) 所有点都看不到,舍去,然后继续考虑 \(rsr\)。
否则 \(rsr\) 的贡献只和 \(rsl.max\) 有关,可以发现贡献即为 \(rs.cnt-rsl.cnt\),然后继续考虑 \(rsl\)。
这部分思考变成代码也很简单,由于每次只会递归一个区间,复杂度单次是 \(O(\log n)\)。
所以是 update 的同时套一个 \(O(\log n)\) 的合并区间,总复杂度是 \(O(n\log^2 n)\)。
#include <bits/stdc++.h>
using namespace std;
int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c - '0');
c = getchar();
}
return x * f;
}
int n, m;
struct node{
double max;
int cnt;
}t[100010 << 2];
void pushup(int u){
t[u].max = max(t[u << 1].max, t[u << 1 | 1].max);
}
int calc(int u, int l, int r, double pre){
if(l == r) return t[u].max > pre;
int mid = (l + r) >> 1;
if(t[u << 1].max <= pre) return calc(u << 1 | 1, mid + 1, r, pre);
return calc(u << 1, l, mid, pre) + (t[u].cnt - t[u << 1].cnt);
}
void update(int u, int l, int r, int x, int y){
if(l == r){
t[u].cnt = 1;
t[u].max = (double)y / x;
return;
}
int mid = (l + r) >> 1;
if(x <= mid) update(u << 1, l, mid, x, y);
else update(u << 1 | 1, mid + 1, r, x, y);
pushup(u);
t[u].cnt = t[u << 1].cnt + calc(u << 1 | 1, mid + 1, r, t[u << 1].max);
}
int main(){
n = read(), m = read();
for(int i = 1; i <= m; i++){
int x = read(), y = read();
update(1, 1, n, x, y);
printf("%d\n", t[1].cnt);
}
return 0;
}