loj.578 小球进洞题解-从单侧递归到贡献预存

loj.578 小球进洞题解-从单侧递归到贡献预存

本题做法很多,本题解讲的是一个特判稍少一点的 $ \log^2 $ 做法。

本题思维链较长,题解会分为多个 part,每个 part 之间有依赖关系但相对独立,可以作为提示。强烈建议自己思考一会再看题解,可以看一眼每个 part 的标题后,自己思考一会后在看。

简单来说,分段存档?

还有,强烈建议一次性做完,时间拉太长容易忘了前文,别问我怎么知道的qwq

题面

有若干个小球放在数轴上,第 \(i\) 号小球的坐标参数为 \(a_i\)。

有两种操作:

  1. 输入 \(i, v\),修改第 \(i\) 号小球的坐标参数为 \(a_i \leftarrow v\);
  2. 输入 \(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);
		}
	}
}
posted @ 2026-10-14 20:10  FarrisL  阅读(6)  评论(0)    收藏  举报