loj.578 小球进洞题解-从单侧递归到贡献预存
loj.578 小球进洞题解-从单侧递归到贡献预存
本题做法很多,本题解讲的是一个特判稍少一点的 $ \log^2 $ 做法。
本题思维链较长,题解会分为多个 part,每个 part 之间有依赖关系但相对独立,可以作为提示。强烈建议自己思考一会再看题解,可以看一眼每个 part 的标题后,自己思考一会后在看。
简单来说,分段存档?
还有,强烈建议一次性做完,时间拉太长容易忘了前文,别问我怎么知道的qwq
题面
有若干个小球放在数轴上,第 \(i\) 号小球的坐标参数为 \(a_i\)。
有两种操作:
- 输入 \(i, v\),修改第 \(i\) 号小球的坐标参数为 \(a_i \leftarrow v\);
- 输入 \(l, r\),询问下述内容:
按照 \(a_i\) 从小到大的顺序(\(a_i\) 相等时按 \(i\) 从小到大的顺序)依次将小球放在数轴上。第 \(i\) 号小球放在 \(\le a_i\) 的没有被之前放置的小球占据的最大的整点处,设第 \(i\) 号小球放置的位置为 \(b_i\)。
请你输出 \(\sum_{i=1,2,\dots,n,\ [l,r] \subseteq [b_i, a_i]} (a_i + b_i)\) 的值。也即,所有满足 \(b_i \le l,\ a_i \ge r\) 的小球的 \(a_i + b_i\) 之和。
$ 1\le n \le 10^5,1\le q\le 2\times 10^5,n<a_i,v\le 2n,1\le l\le r\le 2n $
part 1,维护“不变量”
定义值域 $m=2n $。
首先,不难注意到小球的编号没有作用,我们直接拍到值域桶 \(cnt\) 里,接下来我们按值域讨论。
先思考一下我们要干什么:支持 \(a\) 的单点修改,同时维护 \(b\),同时计算具有两个自由量的查询。
两个自由量太难维护了,不论是查询区间还是两个数组,我们必须想办法改为一个自由量。
先考虑数组,方便寻找本题性质。
\(a_i\) 和 \(b_i\) 绑定太烦了,想办法给他们拆开。我们要尝试维护一个东西,支持单点修改,并且能够将 \(a\) 和 \(b\) 拆开。真的存在这种东西吗?
观察性质,我们发现,将小球看做 ),洞看做 (,二者的匹配满足括号序列的性质。考虑用括号序列来描述这个过程。
我们使用 \(c_i\) 维护此时的栈的高度,也就是这个点前的空位数。把这个推平到值域上,\(c_i\) 就表示:将所有 $a\le i $ 的球放完后前 \(i\) 个位置还有多少个空位,不难得出递推 $ c_i=c_{i-1}+1-cnt_i $
\(a\) 的单点修改等于 \(c\) 的两次后缀加。
处理区间一个简单的想法是维护“差分”,我们思考一下这种“差分”具有什么性质。这就要开始用 \(c\) 来反推合法的 \(a\) 和 \(b\) 了。
part 2,反推数组 \(b\)
考虑一个位置何时为空位。
手玩一下,不难想到,仅当这个位置是一个极长后缀最小值的开头。我们将后缀最小值记为 \(s_i\)。
用数学语言描述一下,\(i\) 为空位当且仅当 $[c_i=s_i] \land [c_i=c_{i-1}+1] $
由于 \(c_i\) 最大是 \(c_{i-1}+1\),因此后缀最小值一定“平滑上升”。也就是说,每次 \(s_i\) 变大,都意味着一个空位产生。我们可以维护空位的位置。换句话说 \(i\) 为空当且仅当 $s_i=s_{i-1}+1 $。
但是,等一下,考虑一下差分后的查询是什么样子。这个无疑是很简单的,直接拿“将 \(r\) 以内的小球放完后前 \([2,l-1]\) 中被占的空位”,减去“将所有球放完后前 \([1,l]\) 中被占的空位”,即可得出 $ [l,r] $ 占的位置了。
用数学语言表示,定义 $q_B(l,r) $ 表示将 \(r\) 以内的小球放完后 \([1,l]\) 以内的空位编号之和,所求即 $ q_B(l,r-1)-q_B(l,m) $。
重新定义 $s_i(r) $ 表示将 \(r\) 作为后缀开始的地方,做的后缀最小值数组,也就是 \([i,r]\) 这个区间的最小值。有 $q_B(l,r)= \sum_{i=1}^l i\times [s_i(r)=s_{i-1}(r)+1] $。
\(s_{i-1}(r)\) 不好记录,但是在叶子处我们知道下一个位置的最小值(即外部最小值),于是我们考虑在 \(i\) 处统计 \(i+1\) 的贡献,就相当于给所有区间平移 \(-1\)。由于题目约束保证了不存在 \(b_i=1\) 因此不用考虑边界。
考虑如何计算 $q_B(l,r) $。我们考虑 \(r\) 会造成什么影响。对于一个区间,唯一的影响就是区间右边的最小值会变。那我们额外传入一个“外部最小值”即可。
先考虑查询覆盖整个区间。不难发现,要么最小值比右儿子最小值小,右儿子完全没贡献;要么右儿子有贡献后,最小值变为右儿子最小值,而这个值是固定的。我们可以记录在根节点记录“左儿子的外部最小值是右儿子的最小值”的值(称之为 sumB ),能够用单 \(\log\) 做到一次计算,乘上 push_up 的区间数量就是 $ \log^2 $ 了。
考虑单论这个计算函数,如何做到单侧递归。如果最小值比右子树最小值还要小,那么直接递归左子树即可。反之,我们递归右子树,并调用本节点存储的预存值即可。
伪代码如下
calcB(rt,mn)//传入根节点和外部最小值
if(mn<=rt.mn) return 0
if(rt is leaf) return 区间左端点+1
if(mn<=rc.mn) return calcB(lc,mn)
else return calcB(rc,mn)+rt.sumB
对于一个没被完全覆盖的节点,定义一个函数 queryB 计算 \([1,l]\) 的答案,用与之类似的逻辑,额外处理一下区间端点即可。对于一个询问 $q_B(l,r) $,其外部最小值应为 $ min_{i=l}^{r} c_i $,处理范围为 $ [1,l-1] $。
注意,部分覆盖时,更新外部最小值只能使用已经扫描的查询部分,不能直接使用整个儿子的最小值。
另外,改变参考系,发现区间整体加不会影响区间内最小值结构,只会影响区间外最小值,也就是说可以无视懒标记直接用预存值,查询时需要 push_down(因为要使用儿子区间的最小值)但不用 push_up 了。
part 3,反推数组 \(a\)
有了反推数组 \(b\) 的基础,可以试着自己反推数组 \(a\)。先自己想想?
写在前面:
我们虽然是维护值域,但是实际上可能有多个球满足 $ a_i=j $,这时候如果我们想要对每个球进行单独考虑会产生混淆。
为了方便,我们重新定义球的顺序:按 \(a\) 从小到大排序,然后按顺序依次匹配每个球,不难注意到这等价于原题。为了防止混淆,我们可以将其看做为新数组 $c_a $。这时候,我们的 \(c_{a_i}\) 看做随着放球不断降低。
好的开始题解。
类似的,我们从 \(c\) 的最小值来反推 \(a\)。
手玩一下,考虑什么时候一个小球的匹配在 \(l\) 前面。不难发现,对于一个 \(a_i\) 的匹配在 \(l\) 前面,当且仅当不存在 $l\le j\le a_i,c_j<c_{a_i} \(。换句话说,\)c_{a_i}$ 即 $ [l,a_i] $ 的最小值。
再拍到值域上,也就是如下公式:
\(k_i(l) = max(min_{l\le j < i} (c_j-c_i),0) (l\leq j<i)\)
定义 $ q_A(l,r)=\sum_{i=r}^m i\times k_i(l) $,即所求。
考虑如何实现。发现我们要维护的,仍然是和外部最小值有关的一个东西,唯一的区别就是从后缀最小值变为前缀最小值。考虑和维护 \(b\) 类似的思路。定义一个节点的预存值为 sumA,表示外部最小值恰为左儿子的区间最小值时,右儿子的贡献,有如下伪代码:
calcA(rt,mn)//传入根节点和外部最小值
if(mn<=rt.mn) return 0
if(rt is leaf) return (mn-rt.mn)*区间左端点
if(mn<=lc.mn) return calcA(rc,mn)
else return calcA(lc,mn)+rt.sumA
类似的,我们再定义一个函数 queryA,额外处理区间端点即可。对于一个 $ q_A(l,r) $,其外部最小值为 $ min_{l\le i < r} c_i $,处理范围为 $[r,m] $。
综上,我们做到了 $ O(q\log^2 n) $ 的复杂度。
part 4,其他细节与实现
不过,仍有一些细节问题。比如说,我们的外部最小值形如 $ min_{l< i \le r} c_i $ 一类,那么如果 $ l=r $,这就变成空区间了,怎么办呢?
还记得前面对 $ c_{a_i} $ 的定义吗?在这里,不难发现,\([l,r]\) 的最小值的真实意味实际是“找到从放置 \(a_i=l\) 的球前”到“放置完所有 \(a_i=r\) 的球后”的最小值。只不过相同值域一定放完更优,所以没有考虑值相同的 \(c_{a_i}\)。
那么,“空区间” \([r+1,r]\) 的实际意义就是“找到从放置 \(a_i=r+1\) 的球前”到“放置完所有 \(a_i=r\) 的球后”的最小值,现在不就有定义了吗?其值就是 $c_r+1 $。
AC Code
int a[N];
int c[N];
#define lc (rt<<1)
#define rc (rt<<1|1)
#define mid ((tl+tr)>>1)
#define RT rt,int tl,int tr
#define ME rt,tl,tr
#define LC lc,tl,mid
#define RC rc,mid+1,tr
namespace Seg{//维护数组 C,并计算\sum A,以及\sum b
struct Tree{
int mn,tag;
LL sumA;//预存的 \sum A
LL sumB;//预存的 \sum B。sumB 是经过平移的。
void operator += (const int &w){mn+=w,tag+=w;}
}t[N<<2];
int root,L,R;
void push_down(int RT){
int &tag=t[rt].tag;
if(tag&&tl!=tr){
t[lc]+=tag,t[rc]+=tag;
tag=0;
}
}
LL calcA(int mn,int RT){//当左边最小值为 mn 时,预处理本区间的 \sum A
push_down(ME);
if(t[rt].mn>=mn) return 0;
if(tl==tr) return 1ll*(mn-t[rt].mn)*tl;//返回有多少个 a 占用了这里。
if(mn<=t[lc].mn) return calcA(mn,RC);
else return calcA(mn,LC)+t[rt].sumA;
}
LL calcB(int mn,int RT){//当右边最小值为 mn 时,预处理本区间的 \sum B
push_down(ME);
if(t[rt].mn>=mn) return 0;
if(tl==tr) return tl+1;//区间整体平移。
else if(t[rc].mn>=mn) return calcB(mn,LC);
else return calcB(mn,RC)+t[rt].sumB;
}
void push_up(int RT){
t[rt].mn=min(t[lc].mn,t[rc].mn);
t[rt].sumA=calcA(t[lc].mn,RC);
t[rt].sumB=calcB(t[rc].mn,LC);
}
void build(int RT,int c[]){
if(rt==1) root=rt,L=tl,R=tr;
if(tl==tr) return t[rt].mn=c[tl],void();//要求在外面预处理好 c。
build(LC,c), build(RC,c);
push_up(ME);
}
void update(int l,int w,int RT){
if(l<=tl) return t[rt]+=w,void();
push_down(ME);
if(mid<l) update(l,w,RC);
else update(l,w,LC),t[rc]+=w;
push_up(ME);
}
void update(int l,int w){update(l,w,root,L,R);}//后缀加
int queryMin(int l,int r,int RT){
if(l<=tl&&tr<=r) return t[rt].mn;
push_down(ME);
if(l<=mid)
if(r<=mid) return queryMin(l,r,LC);
else return min(queryMin(l,r,LC),queryMin(l,r,RC));
else return queryMin(l,r,RC);
}
// int queryMin(int l,int r){return queryMin(l,r,root,L,R);}//普通区间最小值
int queryMin(int l,int r){if(r==l-1) return queryMin(r,r,root,L,R)+1;return queryMin(l,r,root,L,R);}//不普通区间最小值
LL queryA(int l,int mn,int RT){
push_down(ME);
if(t[rt].mn>=mn) return 0;
if(tl==tr) return 1ll*(mn-t[rt].mn)*tl;
if(mid<l||mn<=t[lc].mn) return queryA(l,mn,RC);
LL res=queryA(l,mn,LC);
tomin(mn,queryMin(l,mid,LC));
res+=calcA(mn,RC);
return res;
}
LL queryA(int l,int mn){return queryA(l,mn,root,L,R);}//查询在区间 [l,n] 内,满足条件 a_i 的和
LL queryB(int r,int mn,int RT){
push_down(ME);
if(t[rt].mn>=mn) return 0;
if(tl==tr) return tl+1;
if(r<=mid||mn<=t[rc].mn) return queryB(r,mn,LC);
LL res=queryB(r,mn,RC);
tomin(mn,queryMin(mid+1,r,RC));
res+=calcB(mn,LC);
return res;
}
LL queryB(int r,int mn){return queryB(r,mn,root,L,R);}//查询在区间 [1,r] 内,满足条件 b_i 的和
}
#undef mid
#undef lc
#undef rc
#undef RT
#undef ME
#undef LC
#undef RC
int Test=1;
void Austring_awa()
{
int q;
read(n),read(q);
m=n<<1;
For(i,1,n) read(a[i]),c[a[i]]++;
c[0]=0;
For(i,1,m) c[i]=c[i-1]+1-c[i];
Seg::build(1,1,m,c);
while(q--){
int op;
read(op);
if(op==1){
int x,v;
read(x),read(v);
Seg::update(a[x],1);
a[x]=v;
Seg::update(a[x],-1);
}
else{
int l,r;
read(l),read(r);
if(l==1){puts("0");continue;}
auto E=[](int l,int i){//传入原数组的位置
return Seg::queryB(l-1,Seg::queryMin(l,i));
};
LL suma=Seg::queryA(r,Seg::queryMin(l,r-1));
LL sumb=E(l,r-1)-E(l,m);
LL res=suma+sumb;
printf("%lld\n",res);
}
}
}

浙公网安备 33010602011771号