题解:P13963 [ICPC 2023 Nanjing R] 接雨水

· · 题解

提供一个不需要数据结构就可以解决的方法。

思路

考虑给这个式子化简,我们知道 \max(x,y)=x+y-\min(x,y)

所以可以给这个式子化简成:

\sum_{i=1}^n f_i +\sum_{i=1}^n g_i - n\times\max_{i=1}^n a_i -\sum_{i=1}^{n} a_i

后两项很好维护,我们把重点放到 fg 的求和上。

我们先考虑 fg 就可以把 a 反过来一样的方法求出。

注意到一个数 a_i 的贡献可能是一个区间,直到下一个比它大的数出现(如果前面有比它大的,那它暂时不会产生贡献)。

那么我们可以考虑给 f 维护集合 s,每个存 \{id,val\} 表示一段相同数值的起始点和相同的数值。

拿出相邻的两个元素,记作 \{id_1,val_1\}\{id_2,val_2\},显然 [id_1,id_2-1] 这个区间内的 f 值全部是 val_1

考虑目前要在 a_x 加上 v

先找到 x 所在的段,即最后一个 id \le x 的段,如果现在的 a_x+v 还没有超过所在段的 val,那么不会影响最大值。

如果大于,说明影响到了最大值,需要改动。

说明在 x 在这个段的起点之后,我们先减去这整个段的全部贡献,再加上这个段被斩断后的贡献。

此时加入 \{x,a_x+v\} 这个段,这个段可能后面会有 val 比其小的,要删去。

说明在 x 在这个段的起点,那这个段就直接没了,换成了 \{x,a_x+v\}。减去原来的贡献,再把后面小于新段的删掉,再加上贡献就好。

因为 $g$ 是单调不增的,和 f 的单调不减不一样,但反转后就一样了。 --- 以上是思路,不过具体细节尚多,详细的见代码与注释。 时间复杂度 $\text{O}((n+q)\log (n+q))$。 ## 代码 ``` #include<bits/stdc++.h> #define int long long using namespace std; const int MAXN=1000000; const int INF=1e18; int n,a[MAXN+5]; int maxx,sum; int sum_f,sum_rev; // f 的和,g 的和 set<pair<int,int>> sf,srev; // 计算 set 中 it 指向的这个段的贡献 int solve(set<pair<int,int>>& s, set<pair<int,int>>::iterator it, int n) { auto nxt=next(it); // 找到 it 下一个迭代器 int r=(nxt==s.end()?n:(*nxt).first-1); //取迭代器的 id,如果 it 本身就是最后一个那就是 n return (*it).second*(r-(*it).first+1); // val*(r-id+1) } // 维护一个前缀最大值 set:位置 pos 的值变成了 val,更新 tot void change(set<pair<int,int>>& s,int pos,int val,int n,int& tot) { // 找到 pos 所在的段 auto it=s.upper_bound({pos,INF}); it--; //先找到第一个大于的,再减一就是最后一个小于等于的 int st=(*it).first; //段起点 int old_val=(*it).second; //旧的 val 值 if(val<=old_val) { return; //没超过当前前缀最大值,不变 } if(st<pos) //如果 pos 在 st 之后 { // pos 在段内部(不是段起点) tot-=solve(s,it,n); // 减掉旧段的贡献 tot+=old_val*(pos-st); // 旧段缩短为 st~pos-1,补回 auto now=s.insert({pos,val}); // 插入新段 auto new_it=now.first; // 新的迭代器 auto nxt=next(new_it); // 找到新迭代器的下一位,往后遍历寻找有没有需要更新的 while(nxt!=s.end() && (*nxt).second<=val) // 删掉后面值不够大的段 { tot-=solve(s,nxt,n);//减去这一段的贡献 nxt=s.erase(nxt); //并删除 } tot+=solve(s,new_it,n); // 加上新段的贡献 } else { // pos 正好是某段的起点 tot-=solve(s,it,n); // 减掉旧段全部贡献 s.erase(it); // 删掉旧段 auto now=s.insert({pos,val}); // 插入新段 //与 st<pos 部分同理 auto new_it=now.first; auto nxt=next(new_it); while(nxt!=s.end()&&(*nxt).second<=val) // 删掉后面值不够大的 { tot-=solve(s,nxt,n); nxt=s.erase(nxt); } tot+=solve(s,new_it,n); // 加上新段的贡献 } } signed main() { ios::sync_with_stdio(0); cin.tie(0); int T; cin>>T; while(T--) { cin>>n; sf.clear(); srev.clear(); maxx=sum=sum_f=sum_rev=0; for(int i=1;i<=n;i++) { cin>>a[i]; sum+=a[i]; maxx=max(maxx,a[i]); } // 建 sf:从左往右扫,前缀最大值变化时插入新段 int res=0; for(int i=1;i<=n;i++) { res=max(res,a[i]); if(sf.empty() || (*(sf.rbegin())).second!=res) {//如果空,或者上一个最大值不等于自己,说明开始了一个新段 sf.insert({i,res}); } } for(auto it=sf.begin();it!=sf.end();it++) { sum_f+=solve(sf,it,n);//记录 f 的和 } // 建 srev:反转数组的前缀最大值(其实就是 g 数组的集合) // 原数组 a[i] 反转后 b[k]=a[n-k+1] // g[i]=max(a[i..n])=max(b[n-i+1..n])=fb[n-i+1](fb 是 b 的前缀最大值) // 所以 sum_g = sum_rev res=0; for(int i=n;i>=1;i--) { res=max(res,a[i]); // 从右往左扫,相当于反转数组从左往右 if(srev.empty() || (*(srev.rbegin())).second!=res) { srev.insert({n-i+1,res}); // 反转数组中的位置 } } for(auto it=srev.begin();it!=srev.end();it++) { sum_rev+=solve(srev,it,n); // 同理 } int q; cin>>q; while(q--) { int x,v; cin>>x>>v; a[x]+=v; maxx=max(a[x],maxx); sum+=v; // 更新 sf(原数组位置 x) change(sf,x,a[x],n,sum_f); // 更新 srev(反转数组位置 n-x+1) change(srev,n-x+1,a[x],n,sum_rev); int ans=sum_f+sum_rev-n*maxx-sum;//按公式算 cout<<ans<<'\n'; } } return 0; } ```