题目来源: 2023 ICPC 亚洲南京区域赛 M
链接: Problem - M - Codeforces
描述
和LeetCode 42. 接雨水一样,有一张由长度为n的序列 a 1 , a 2 , ⋅ ⋅ ⋅ , a n a_1,a_2,··· ,a_n a1,a2,⋅⋅⋅,an 表示的柱状图。柱状图上从左到右第 i i i 根柱子的高度为 a i a_i ai,宽度为 1 1 1。
我们会对该柱状图进行 q q q次修改。第 i i i 次修改可以记为一对整数 ( x i , v i ) (x_i,v_i) (xi,vi),表示我们会将第 x i x_i xi 根柱子的高度增加 v i v_i vi。
在每次修改之后,回答以下询问:如果下了一场大雨,雨水填满了柱状图上的每个坑洼,求这张柱状图中可以留存多少雨水。
输入
第一行包含一个整数 n n n( 1 ≤ n ≤ 10 5 1 \leq n \leq 10^5 1≤n≤105),表示直方图中柱子的数量。
第二行包含 n n n 个整数 a 1 , a 2 , ... , a n a_1, a_2, \dots, a_n a1,a2,...,an( 1 ≤ a i ≤ 10 6 1 \leq a_i \leq 10^6 1≤ai≤106),其中 a i a_i ai 表示第 i i i 根柱子的初始高度。
第三行包含一个整数 q q q( 1 ≤ q ≤ 10 5 1 \leq q \leq 10^5 1≤q≤105),表示修改操作的次数。
接下来 q q q 行,每行包含两个整数 x i x_i xi 和 v i v_i vi( 1 ≤ x i ≤ n 1 \leq x_i \leq n 1≤xi≤n, 1 ≤ v i ≤ 10 6 1 \leq v_i \leq 10^6 1≤vi≤106),表示第 i i i 次操作将第 x i x_i xi 根柱子的高度增加 v i v_i vi。
输出
输出 q q q 个整数,即每次修改后,输出一个整数,表示当前直方图能接住的雨水总量。
样例
输入
6
1 2 3 4 5 6
2
1 2
3 3
输出
1
4
思路
在LeetCode 42. 接雨水标准解法中,储水量计算为:对于位置 i i i,它能接的水量为: min ( f i , g i ) − a i \min(f_i, g_i) - a_i min(fi,gi)−ai
其中:
- f i = max ( a 1 , a 2 , ... , a i ) f_i = \max(a_1, a_2, \dots, a_i) fi=max(a1,a2,...,ai) → 左侧最大值
- g i = max ( a i , a i + 1 , ... , a n ) g_i = \max(a_i, a_{i+1}, \dots, a_n) gi=max(ai,ai+1,...,an) → 右侧最大值
所以总储水量为:
∑ i = 1 n ( min ( f i , g i ) − a i ) \sum_{i=1}^{n} \left( \min(f_i, g_i) - a_i \right) i=1∑n(min(fi,gi)−ai)
现在,设当前全局最大值为 M M M,令:
-
p L p_L pL:最小的 i i i 使得 f i = M f_i = M fi=M
-
p R p_R pR:最大的 i i i 使得 g i = M g_i = M gi=M
我们将整个求和区间 [ 1 , n ] [1, n] [1,n] 按照 p L p_L pL 和 p R p_R pR 划分为三部分:
a. 左侧部分: i ∈ [ 1 , p L − 1 ] i \in [1, p_L - 1] i∈[1,pL−1]
- 此时 min ( f i , g i ) = f i \min(f_i, g_i) = f_i min(fi,gi)=fi
- 贡献为: ∑ i = 1 p L − 1 f i \displaystyle \sum_{i=1}^{p_L - 1} f_i i=1∑pL−1fi
b. 中间部分: i ∈ [ p L , p R ] i \in [p_L, p_R] i∈[pL,pR]
- 此时 min ( f i , g i ) = M \min(f_i, g_i) = M min(fi,gi)=M( M M M 为全局最大值)
- 贡献为: ( p R − p L + 1 ) × M (p_R - p_L + 1) \times M (pR−pL+1)×M
c. 右侧部分: i ∈ [ p R + 1 , n ] i \in [p_R + 1, n] i∈[pR+1,n]
- 此时 min ( f i , g i ) = g i \min(f_i, g_i) = g_i min(fi,gi)=gi
- 贡献为: ∑ i = p R + 1 n g i \displaystyle \sum_{i=p_R + 1}^{n} g_i i=pR+1∑ngi
即:
| 区间 | min ( f i , g i ) \min(f_i, g_i) min(fi,gi) | 贡献 |
|---|---|---|
| [ 1 , p L − 1 ] [1, p_L-1] [1,pL−1] | f i f_i fi | ∑ i = 1 p L − 1 f i \displaystyle \sum_{i=1}^{p_L-1} f_i i=1∑pL−1fi |
| [ p L , p R ] [p_L, p_R] [pL,pR] | M M M | ( p R − p L + 1 ) × M (p_R - p_L + 1) \times M (pR−pL+1)×M |
| [ p R + 1 , n ] [p_R+1, n] [pR+1,n] | g i g_i gi | ∑ i = p R + 1 n g i \displaystyle \sum_{i=p_R+1}^{n} g_i i=pR+1∑ngi |
| 如何高效维护三段信息之和 | ||
| 利用单调性: f f f 非降, g g g 非增 |
- 序列 f f f 是单调不下降的(因为前缀最大值只会增大或不变)
- 序列 g g g 是单调不上升的(因为后缀最大值只会减小或不变)
这一性质至关重要:它意味着,当某个 a x a_x ax 增大时,对 f f f 和 g g g 的影响是连续的区间更新,而非零散点。
动态维护 f f f 序列:线段树
我们用一棵线段树维护整个 f f f 序列。
- 通过一次扫描得到 f i = max ( f i − 1 , a i ) f_i = \max(f_{i-1}, a_i) fi=max(fi−1,ai),建树。
- 当 a x a_x ax 增加后,若新值大于原 f x f_x fx,则从位置 x x x 开始, f f f 序列将被"抬高"。
- 由于 f f f 单调不降,存在一个最右位置 r r r,使得对所有 i ∈ [ x , r ] i \in [x, r] i∈[x,r],新的 f i f_i fi 都等于这个新值;而 f r + 1 f_{r+1} fr+1 及之后不受影响(因为已有更大的前缀最大值)。
- 在线段树上二分,找到第一个位置 r + 1 r+1 r+1 满足 f r + 1 ≥ new_value f_{r+1} \geq \text{new\_value} fr+1≥new_value。这利用了线段树节点存储的区间最值信息,可在 O ( log n ) O(\log n) O(logn) 时间内完成。
- 将区间 [ x , r ] [x, r] [x,r] 的所有 f i f_i fi 推平(区间赋值)为新值。借助懒标记,此操作也是 O ( log n ) O(\log n) O(logn)。
- f f f 序列始终保持正确,且支持高效更新与区间求和。
同理,用另一棵线段树维护 g g g 序列
-
g g g 单调不上升;
-
当 a x a_x ax 增大时,会影响 g 1 , g 2 , ... , g x g_1, g_2, \dots, g_x g1,g2,...,gx;
-
在线段树上二分,找到从 n n n 到 1 1 1 的第一个位置 l l l,使得 g l ≥ new_value g_l \geq \text{new\_value} gl≥new_value;
-
将区间 [ l + 1 , x ] [l+1, x] [l+1,x] 推平为新值。
同样可在 O ( log n ) O(\log n) O(logn) 时间内完成更新。
回答查询:三段分解 + 区间求和
∑ min ( f i , g i ) = ∑ i = 1 p L − 1 f i ⏟ 查 f 的前缀和 + ( p R − p L + 1 ) ⋅ M ⏟ 直接计算 + ∑ i = p R + 1 n g i ⏟ 查 g 的后缀和 \sum \min(f_i, g_i) = \underbrace{\sum_{i=1}^{p_L - 1} f_i}{\text{查 } f \text{ 的前缀和}} + \underbrace{(p_R - p_L + 1) \cdot M}{\text{直接计算}} + \underbrace{\sum_{i=p_R + 1}^{n} g_i}_{\text{查 } g \text{ 的后缀和}} ∑min(fi,gi)=查 f 的前缀和 i=1∑pL−1fi+直接计算 (pR−pL+1)⋅M+查 g 的后缀和 i=pR+1∑ngi
而 p L p_L pL 和 p R p_R pR 也可通过在线段树上二分快速定位。
cpp
using ll = long long;
const int maxn=2*1e5+20;
template<int N>struct Seg_tree{
#define ls (p<<1)
#define rs ((p<<1)|1)
struct{
int l,r;
ll sum,lz,l_val,r_val;
int len;
}tr[4*N];
void push_up(int p){
tr[p].sum=tr[ls].sum+tr[rs].sum;
tr[p].l_val=tr[ls].l_val;
tr[p].r_val=tr[rs].r_val;
}
void push_down(int p){
if(tr[p].lz==0) return;
ll& tag=tr[p].lz;
tr[ls].lz=tag;
tr[ls].sum=tag*tr[ls].len;
tr[ls].l_val=tr[ls].r_val=tag;
tr[rs].lz=tag;
tr[rs].sum=tag*tr[rs].len;
tr[rs].l_val=tr[rs].r_val=tag;
tag=0;
}
void Build(int p,int lo,int ro,vector<int>& _a){
tr[p].l=lo,tr[p].r=ro;
tr[p].len=ro-lo+1;
tr[p].lz=0;
if(lo==ro){
tr[p].sum=_a[lo];
tr[p].l_val=tr[p].r_val=_a[lo];
return;
}
int mid=(lo+ro)>>1;
Build(ls,lo,mid,_a);
Build(rs,mid+1,ro,_a);
push_up(p);
}
void Fix(int p,int lo,int ro,ll k){ //区间赋值
if(lo<=tr[p].l && ro>=tr[p].r){
tr[p].sum=k*tr[p].len;
tr[p].lz=k;
tr[p].l_val=tr[p].r_val=k;
return;
}
push_down(p);
if(lo<=tr[ls].r) Fix(ls,lo,ro,k);
if(ro>=tr[rs].l) Fix(rs,lo,ro,k);
push_up(p);
}
ll Ask(int p,int lo,int ro){ //区间求和
if(lo<=tr[p].l && ro>=tr[p].r) return tr[p].sum;
push_down(p);
ll res=0;
if(lo<=tr[ls].r) res+=Ask(ls,lo,ro);
if(ro>=tr[rs].l) res+=Ask(rs,lo,ro);
return res;
}
ll Value(int p,int x){ //查询单点值
if(tr[p].len==1) return tr[p].sum;
if(x<=tr[ls].r) return Value(ls,x);
else return Value(rs,x);
}
int Find_1(int p,ll k){ //找到 1->n 第一个>=k的
if(tr[p].len==1) return tr[p].l;
push_down(p);
if(tr[ls].r_val<k) return Find_1(rs,k);
return Find_1(ls,k);
}
int Find_2(int p,ll k){ //找到 n->1 第一个>=k的
if(tr[p].len==1) return tr[p].l;
push_down(p);
if(tr[rs].l_val>=k) return Find_2(rs,k);
return Find_2(ls,k);
}
};
Seg_tree<maxn> T1,T2;
ll a[maxn];
void solve(){
int n;
cin >> n;
for(int i=1;i<=n;++i) cin >> a[i];
vector<ll> pre(n+3,0),suf(n+3,0);
for(int i=1;i<=n;++i) pre[i]=max(pre[i-1],a[i]);
for(int i=n;i>=1;--i) suf[i]=max(suf[i+1],a[i]);
T1.Build(1,1,n,pre);
T2.Build(1,1,n,suf);
ll mx=*max_element(a+1,a+1+n);
ll sum=0;
for(int i=1;i<=n;++i) sum+=a[i];
int q;
cin >> q;
for(int i=1;i<=q;++i){
int x; ll val;
cin >> x >> val;
a[x]+=val;
sum+=val;
if(a[x]>T1.Value(1,x)){
int lim=T1.Find_1(1,a[x]);
if(T1.Value(1,lim)<=a[x]) T1.Fix(1,x,lim,a[x]);
else T1.Fix(1,x,lim-1,a[x]);
}
if(a[x]>T2.Value(1,x)){
int lim=T2.Find_2(1,a[x]);
if(T2.Value(1,lim)<=a[x]) T2.Fix(1,lim,x,a[x]);
else T2.Fix(1,lim+1,x,a[x]);
}
mx=max(mx,a[x]);
int lp=T1.Find_1(1,mx),rp=T2.Find_2(1,mx);
ll res=mx*(rp-lp+1)-sum;
if(lp>=2) res+=T1.Ask(1,1,lp-1);
if(rp<=n-1) res+=T2.Ask(1,rp+1,n);
cout << res << '\n';
}
}