浅谈操作分块:从 Div.3 F 到 Ynoi
前言
分块是一种常见的处理信息的思想。
序列分块通常以
而在答案具有可合并性、修改和询问并不是复杂操作时,我们还可以考虑对操作进行分块,控制块的大小并结合暴力统计与快速统计,做到在同样
这样的 trick 就叫操作分块(又名询问分块)。
本文配套题单:Link
正文
操作分块的主要的思想就是合并贡献。
操作分块的主要策略是这样的:在某一块开始处理前先得到每个询问在该块前的答案或信息,因此对于块内的询问可以快速得知该询问由块之前操作造成的贡献或答案,而对于块中直接进行
伪代码形式如下(极度不严谨并且中式英语):
第
除了上面那种直接合并贡献的方式,为了得到某块前的全部答案信息,还可能需要对前面的所有操作扫一遍或者进行暴力重构,因此操作分块一般还适用于整体重构复杂度比较低的操作,比如单点修改和虚树(只对部分关键点操作)。
而对于询问,操作分块一般用于单点询问或全局询问,因为这两类询问比较容易计算贡献,并且复杂度通常较低。
举个例子,不能操作分块的一个经典问题是:在不将问题转化到
下面以几道题目为例,介绍操作分块的一些经典应用与配置。操作分块主要还是一种思想,只有结合题才能更深刻地理解,所以你就说我是不是差不多写了一整篇的题解吧。
CF342E Xenia and Tree:多源 BFS
本题正解是动态点分治,但注意到只对树上单点进行操作并且操作简单,不妨考虑操作分块。
首先我们考虑如何在块中
然后考虑在本块之前就已经是红色的点,怎么去计算他们的贡献。于是我们不得不思考:如何得到块前的全部答案信息?如何在本块处理结束后将本块操作的贡献加入全部答案信息?
考虑维护一个 queue,对这些点跑一遍多源 BFS 并更新
核心代码如下:
fr1(i,1,q){
cin>>query[i].fi>>query[i].se;
if(i%B==0||i==q){//新块
fr1(j,lst,i){
if(query[j].fi!=2){
continue;
}//先处理询问
ans[j]=minn[query[j].se];//获取询问点在块之前与最近的红点的距离
fr1(k,lst,j-1){//计算询问之前的块内操作对询问的贡献
if(query[k].fi==1){
ans[j]=minn[query[j].se]=min(minn[query[j].se],dis(query[j].se,query[k].se));
}
}
}
fr1(j,lst,i){
if(query[j].fi==2){
cout<<ans[j]<<endl;
}
}
fr1(j,lst,i){
if(query[j].fi==1){
minn[query[j].se]=0;
qq.push(query[j].se);
}
}
bfs();//用多源BFS在可接受的时间内将本块的操作的贡献算入minn数组,保证下一块的minn数组符合其定义
lst=i+1;//将块头改为下一块块头
}
}
取
因为多源 BFS 是一种常见的在
一道类似的题目:CF1790F Timofey and Black-White Tree
CF925E May Holidays:虚树
本题在 CF 上的 std 采用的就是操作分块做法,因此本题也常被作为操作分块的模板题。感谢这篇题解在写作时的帮助。
首先考虑转化题意,限制本质上等价于黑点总数减去
注意到修改单个节点,查询全局,考虑操作分块。
先考虑如何解决块内操作对询问的贡献。对于到根的链
不会虚树可以移步这里。
然后你考虑如何在虚树上暴力操作却可以达到原树上操作的效果,可以尝试下放节点,使得虚树上一个节点
因为询问是询问权值
于是不妨考虑不再实时维护所有数的权值,而维护这个分界点与目前该点上的增减量
于是现在我们就做到了操作分块中快速计算块内操作对询问贡献的步骤,再观察刚刚的过程,可以发现每块的操作结束后我都可以暴力走一遍原树,利用记录的
用图总结一下我们到现在的过程:
在上图中,如果再对
但是我们还没有做完这道题。还有一个给点反色的操作我们没有实现。可以发现,如果一个点由黑变白,那么我们就要删去这个点的贡献,我们不妨在它管理的所有点的权值序列中找到它自己的权值,然后把这个权值删去(直接扣掉一次出现次数)。注意如果这个权值的位置在指针前面,则还需要给答案扣掉
所以实际上,我们的虚树节点要管理的权值序列,是它所管理的点中的白点构成的权值序列,黑点我们是不用关心的。因为除了这个点本身,是不可能在它管理的其他节点中出现黑变白的情况的,否则那个点也会成为一个虚树节点。
talk is cheap,这里有一份注释过的正解代码。
本题中,每块的复杂度为
将操作分块中的操作点设置为关键点,建立虚树,因为虚树大小是
CF1866F Freak Joker Process:操作分块大乱炖
题目大意:
有n 名选手组成了一个 OI 队。第i 名选手的思维能力是A_i ,他的思维能力排名\text{RankA}_i 被表示为A_j>A_i 的j 的个数+1 ;第i 名选手的代码能力是B_i ,同理定义了他的代码能力排名\text{RankB}_i 。第i 名选手的综合实力为\text{RankA}_i+\text{RankB}_i ,综合实力排名为\text{RankA}_j+\text{RankB}_j<\text{RankA}_i+\text{RankB}_i 的j 的个数+1 。你需要维护将某位选手的思维或代码能力\pm 1 ,以及查询某位选手的综合实力排名两种操作。
官方题解在这里,一上来就是一坨公式糊我一脸,我理解了一晚上才大概搞明白。
这道题看上去是一道困难的数据结构题目,注意到单点修改,查询全局排名,考虑操作分块。为了防止变量名冲突,在这一部分,我们把块长定义为
我们仍然先考虑块内操作对询问的影响。发现
很自然可以想到计算询问的选手
首先根据操作分块的思维方式,我们可以想到计算要改变
这样是不是就做完了呢?当然不是。比如 @StayAlone 场切黑题,他的思维能力从普及三等上升到了(国家)队爷,但是 @Shunpower 还是只会做提高组的题,那么虽然 @Shunpower 没有被操作,但是他的
于是我们再考虑一下怎么维护这些像 Shunpower 一样的选手。首先可以发现,这样的选手一定是在
首先很明显所有啥也不是选手的
先考虑某一项能力是特殊值的选手。假设他们的能力
然后考虑最后一种选手,这是最困难的部分。注意到每一次变化,对于这些选手都只有
下一块需要的变量只有在本块
此处
这道题也引出了操作分块中拆贡献的思想。在这道题里面我们经过一通对选手的情况的讨论,干了拆贡献的工作,再在操作分块中算贡献,最后合并拆出去的贡献,然后搞得麻烦得要死才解决了这道题。
因为这玩意太难写了,代码暂时先咕着有空再更。CF 上的提交记录也没什么好东西,除了 好像最优解都是指令集。这份代码可读性还算比较好,在我补上这里的代码之前可以先将就用着。
小结
上面我们讲的操作分块主要分为以下步骤:
- 获取一些初始信息
- 计算块中的操作对答案的贡献(一般是枚举询问和前面的操作
\mathcal O(B^2) 暴力) - 计算块前的操作对答案的贡献(可能是一种全局数据结构,也可能是扫一遍前面所有的操作或者信息)
- 重构或者维护信息
在计算贡献的途中需要把贡献合并。当然,也有可能是把答案拆成子贡献,在操作分块里合并子贡献,再把子贡献合并成总贡献。
如何看出题目需要采用操作分块呢?除了有一些做过就知道的 trick 以外,还有一种想法是得到两种暴力并且考虑结合他们。例如 CF342E Xenia and Tree,你可以想到一种针对询问的
还可以从操作分块的本质入手。操作分块的本质就是把块内转成一个可以直接上
这里应当出现 lxl:P7711 [Ynoi2077] 3dmq。这个操作和询问很容易转成点加和三维数点,能够发现贡献是好拆的,并且这两种操作都比较全局因此可以考虑操作分块。因为实在太复杂了,还没有研究具体做法,如果以后有时间会在这里加上这道题的内容的。把题挂在这里欢迎读者研究。
一道比较容易的练习题:[ABC256F] Cumulative Cumulative Cumulative Sum,单点修改,查询三次前缀和的单项。题解区可以找到我的操作分块题解。
一道稍微有点难的练习题:CF1254D Tree Queries,其实也不是很难吧……认真想一下很容易可以通过操作分块得到一个
[APIO2019] 桥梁:贡献不删除
这是一种在写法和外观上和上面所有各种操作分块都有所不同的操作分块,果然 APIO 是 trick 制造机吗。我们上面所做的事情其实都是预先算好每个询问的块前贡献,在询问的时候直接拿出块前贡献就完事了。但是有的操作分块,询问可能不只是某个单点,可能还有附加的权值等等,这种情况我们没办法直接全部预先处理出来块前的贡献,也不能每个询问都扫一遍前面的所有操作。有没有什么办法可以干掉这个附加的权值呢?
我们考虑“每个询问都扫一遍前面的所有操作”这个方向。每个询问都扫一遍是不是有点太浪费了?因为有了权值,所以有一些操作可能对某个询问是无效的!不然权值的意义是什么
(
然而即便是这样,如果只扫那些对这个询问有影响的操作,在这道题里面渐进复杂度完全没有任何变化。权值能够使一些操作对询问无效,并不代表总是可以使一些操作对询问无效。
我们再往下想,我们已经只想考虑那些只对这个询问有影响的操作了,能不能快速得到这些操作的贡献呢?还是说,我可不可以维护它们,让我在计算这个询问的时候这些操作正在某种数据结构中,我可以直接考虑呢?
前者比较难办,我们考虑后者。然后你就发现,假设我确实存在这样一种数据结构,那我在算完这个询问之后,我要清空掉整个数据结构,然后把影响下一个询问的操作的贡献又加入这个数据结构。你发现这个清空数据结构又加回去的操作也太憨了,我可以把这次的给留下来,这样我下次只有加,不用撤销或者清空了。放宽一点,我允许一些撤销和清空,但是只要我能保证每个操作的贡献都加入数据结构常数次,或者次数比较少,我的复杂度就是对的啊!
那怎么减少撤销和清空的次数呢?我只要想办法尽量让影响可以被继承下去就好了。举个例子,如果某个操作同时对询问
当然也可以反过来,一开始所有操作的贡献都在数据结构里,我想减少加入的量,让每个操作撤销一次或者很少的次数,让先算的询问总是受这个操作影响,后算的询问总是不受这个操作影响也是可以的。
需要注意,这种操作分块不一定必须只撤销或者加入一次,
接下来回到这道题,我们发现假设存在两个询问的车的重量分别是
当然前提是要先移除那些在块内被修改重量限制的桥。
然而还有一个问题,虽然这样我避免了一座桥的贡献被撤销,但我每次处理询问的时候,还有一些桥梁是要被加入的,怎么找到这些要加入贡献的桥呢?
很容易想到,还可以把桥按照块前最终的重量限制也从大到小排个序。并维护一个指针表示目前加入的最后一座桥。当我切换下一个询问时,我直接后移指针,将重量限制大于等于询问车重量的桥加入贡献,直到下一座桥的重量限制小了或者没有桥了。这个过程和归并排序其实是比较像的。
在这道题里,答案是询问的点在仅经过重量限制大于等于车重量的桥的情况下,能够到达的点的数量,转换一下就是在图中只有重量限制大于等于车重量的桥的情况下,询问点所在连通块的大小。这个直接并查集就能轻易做到加入一座桥,并且不方便撤销任意一座桥,这也正好对应了我们这类询问分块“不撤销、少撤销”的特点(所以其实从并查集入手来想也能想到我们这种不撤销类型的操作分块,因为很难维护图的连通性)。
而剩下的在块内有修改的桥是好做的。对于每个询问,你需要考虑在它前面被修改的桥的最终重量限制,再往前是怎么改的根本不重要,直接把满足条件的桥加入并查集就好了。还有一些桥块内有修改,但不在询问前面,我们还没有把它们算进贡献当中,需要得到这些桥在块之前的最终重量限制然后再把满足条件的桥加入并查集。
然而还有一点小问题,因为块内操作的顺序被打乱了,所以块内操作加的桥不具有继承性,必须要撤销掉。不过我们加这些桥是连续的一段过程,并且这些桥的数量是块长的,我们直接用可撤销并查集暴力撤销掉就行了。你也可以理解成,我们通过破坏块内操作的继承性换来了块前操作的继承性,以保证复杂度的正确。
注意可撤销并查集不能路径压缩,只能启发式合并所以并查集复杂度是 但是 LOJ 上总时间要一百多秒并且会出现有一个点差六毫秒 TLE 的情况,而且洛谷过不了,当然不影响我们在 LOJ 上用神机加持碾题。
此外经过测试,使用非常憨的主席树大力可持久化并查集去撤销块内桥的贡献会被卡常,在洛谷上只能获得八分的好成绩,我尽力调整块长使得主席树的常数得到平衡,然而事实是最快也只能做到(这是
我在本地偶尔能做到 在 LOJ 的超算上还是 TLE 得很死的。使用常数比较小的可撤销并查集再卡卡常就能过啦(用可撤销并查集跑上面的数据只需要
我的最终代码常数比较大,但是写得应该还是很好理解的。这份代码选择了前人得出的较快的块长
开头已经提到,这种操作分块与我们之前说的几乎都不太相像。它的询问确实符合一般操作分块的“块前贡献+块内贡献”的模式,但在算块前贡献的时候遇到了障碍,我们需要找到块前贡献的特殊性。比如这题就需要我们注意到贡献边与询问权值必须存在的偏序关系,然后自然地想到两边排序进行一个类似双指针的操作,使得一条边的贡献不会被多次删除、插入,也就保证了并查集的正确性与复杂度。
大多数操作分块的难点其实都在于怎么复杂度正确的计算块前贡献。把这些贡献插入全局数据结构,再对询问依次查询是最常见的解决办法,但是考虑本质就会发现,导致任何复杂度接近 没关系,它当然被 cdq 套 cdq 的四维偏序爆踩了。
所以做操作分块题还是不要被 trick 束缚,操作分块并不是很强劲的算法,甚至可能也无法带来很强的性质,它本质上始终只是弱化了问题,新的问题可能仍然很困难,也仍然具有多样性。
[Ynoi2019] 魔法少女网站
Ynoi 这不就来了吗!
先暂且咕一下,不久后会进行添加。
一点闲话
操作分块的题好像在网上还是很难找。但是最近至少我看到题解包含操作分块的题越来越多了,这个 trick 或许正在逐渐普及开来?
我所知道的题大多都写在博客里面了,如果有的题我没有写,只是想过认为可以操作分块但是实际上不可以的话可以在评论区指出。如果还有别的题可以操作分块也可以告诉我,我会看看虽然我应该做不出来就是了。
如果文章有问题也可以在评论区指出~
完结撒花!qwq
Shun.