数据结构 提高组刷题
以下题目大部分来自 https://www.luogu.com.cn/training/1010,少数题作为题单的前置知识用于巩固数据结构。
并查集
连通性判断
P1197 [JSOI2008] 星球大战
先建图再一个一个去掉点显然难以维护,我们考虑倒着操作,每次删去一个点的答案就是加这个点之前的答案。
1 |
|
[P3958 NOIP 2017 提高组] 奶酪
由于老鼠只能在空洞之间穿梭,我们把能够穿梭的空洞合并(圆心间距离和半径比较),然后判断每一个连通块能否从最下面到最上面即可。
1 |
|
P1783 海滩防御
有了上一题的基础,我们这里额外的操作就是二分答案(半径),但是由于半径是浮点数,我们可以选择把所有长度乘 1000,最后将答案除以 1000。这里不要乘100,会有精度问题。
1 |
|
非加权维护
[P1525 NOIP 2010 提高组] 关押罪犯
扩展域并查集。我们从贪心的角度思考,要让最小值尽量小,我们就排序从大到小地将两个点隔开,这样可以避免最大值出现。我们建立大小为 \(2n\) 的并查集,对于每对冲突的人 \(x,y\),连边 \((x,y+n)\) 和 \((x+n, y)\) ,代表 \(x,y\) 不同时出现。但是,一旦 \((x,x+n)\) 或 \((y,y+n)\) 连通,说明此时不论如何都会冲突了,直接输出答案。
稍微说一下扩展域并查集,有点像 2-Sat 的思想,\(a,b\) 不能同时出现就把 \(a\) 和 \(b\) 的反状态 \(b+n\) 连边,\(b\) 和 \(a\) 的反状态 \(a+n\) 连边,这样一旦出现冲突代表不满足条件。
1 |
|
P2024 [NOI2001] 食物链
同样使用扩展域并查集,不过这里是 ABC 三类关系,我们并不具体清楚三者的关系,所以选择在三个区域都进行操作。
1 |
|
加权维护
P1196 [NOI2002] 银河英雄传说
本地区别于普通并查集维护连通性和连通块大小的地方在于:我们需要回答连通块内两点的距离。由于该题目是以链的形式首位合并的,我们可以维护两点到根节点的距离。利用前缀和的思想,答案就是 \(\mid v_x-v_y \mid - 1\)。
注意实现带权并查集和普通并查集的区别:在 find
函数中我们用递归的思路更新
v[x] += v[p[x]]
,在合并时,儿子节点到根节点的距离额外加上根连通块的大小(注意操作的顺序)。
1 |
|
[P5092 USACO04OPEN] Cube Sta
跟上题几乎一模一样。
1 |
|
树状数组 / 线段树
P2574 XOR的艺术
区间赋值其实就是把区间的 0 1 翻转,tag 用异或维护,info 的 apply 就是用区间大小减去 1 的数量。
1 |
|
[P2894 USACO08FEB] Hotel G
由于需要找到最左边的满足条件的区间,需要用到线段树的查找前驱操作。但是这里不能简单地调用线段树模版的
findFirst
函数,因为那个函数本质就是在线段树上二分,只找单点满足条件,而这里不仅要考虑答案在左右区间,答案还有可能就在中间,且答案不为单点的形式,而是一个区间。
我们懒标记记录上次操作的类型(1 代表退房,此时整个区间都可以住,答案就是区间长度,2 代表入住,此时清空区间长度)。
对于查找前驱操作,需要注意特判一下全局的长度是否满足条件;而懒标记尤其要注意不能把空的懒标记(值不是 1 或 2 下传)。考虑上次操作大区间下传一个标记 1 给小区间,自身被清空为 0 ,如果再次操作这个大区间,下传标记 0 给小区间就会有问题。
1 |
|
P4145 上帝造题的七分钟 2 / 花神游历各国
开根号操作是难以维护的,但是 \(10^{12}\) 的数据范围下最多开 6
次根号就会变为 1,对于全是 1
的区间,我们并不需要再一个一个进行操作,因此区间维护的次数只有 \(O(n)\) 次,总复杂度还是 \(O(m\log
n)\),可以接受。需要维护区间和和区间最大值。实现上,可以采用单调修改线段树或懒标记线段树,但是不要从
\(l\) 遍历到 \(r\),这样做的常数还是太大了,写一个区间修改函数,在终止条件
l == r
下再停止操作即可。
还有一种并查集 + 树状数组的做法,并查集连向下一个不为 1 的位置,在给定区间内的祖先修改即可。
单点修改线段树
1 |
|
懒标记线段树
1 |
|
树状数组 + 并查集
1 |
|
P4513 小白逛公园
处理区间内最大子段和,需要分三种情况:左儿子最大值,右儿子最大值,中间最大值(取左儿子右边最值和右儿子左边最值),所以维护区间和(更新很简单)、从左边开始的最大子段和(取左儿子的或者左儿子的区间和 + 右儿子左边最大子段和)、从右边开始的最大子段和(同理)、全局最大子段和。单点修改线段树即可。
1 |
|
[P2572 SCOI2010] 序列操作
如果前面都做了,这里肯定没有多大问题了,维护区间和,区间 1 和 0 的左连续,右连续,连续最值。但是要注意下 Tag 的更新不是覆盖,对于区间取反要根据之前的情况来定。
1 |
|
P5889 跳树
我们需要维护向上、向左、向右三种操作,但是操作的顺序会有影响。注意到我们可以先向上再向下,而二叉树的编号是有规律的(左儿子为 \(2p\),右儿子为 \(2p+1\),向上移动就是整除 2 ),因此我们可以维护一个二进制数(从低位到高位每一位都代表该位置向下的信息,为 0 的话就是向左,为 1 的话就是向右),这样,如果最终会向下移动,我们就能根据这个二进制数知道每次移动左右的距离。
那么我们节点维护向上的次数(最高跳到第几辈祖先)、向下的次数(从最高祖先到达最低儿子)和这个二进制数,合并区间就先向上再向下,考虑右区间对左区间的贡献:如果右区间向上的数值大于或等于左区间向下的数值,此时就是从 1 号节点重新操作,不受左节点的影响,否则用二进制的移位操作更新贡献。
1 |
|
[P6186 NOI Online #1 提高组] 冒泡排序
首先在不考虑交换的情况下,考虑进行一轮冒泡排序的影响:这一轮的实质就是对于该轮的所有前缀最大值,一直往后移动,直到遇到下一个前缀最大值或结尾,这样操作之后,这几个前缀最大值与前面的数形成的逆序对数不会改变,但是其他数与前面的数形成的逆序对数都会减少 1,因此,设前缀最大值有 \(x\) 个,每轮操作后减少 \(n - x\) 个逆序对数。
因此,我们可以用树状数组来统计逆序对数(这里均指与前面的数形成的逆序对数):如果一个数逆序对数为 0,它就是第一轮的前缀最大值,为 1 的话就是前两轮的前缀最大值,因此类推... 这样,我们通过树状数组 + 差分的思想可以求出每一轮结束后剩余的逆序对个数。下面的实现中由于把最初始的全部逆序对数放在了编号 1,第 0 次的结果应该是 \([1, 2]\),区间修改放在编号 2。
接着考虑交换的情况:交换实际上对别的数不会造成任何影响,如果 \(a_x > a_{x+1}\),那么总逆序对数实际上会增加 1,否则会减少 1,同时需要更新前面树状数组,注意改变消失的位置。
1 |
|