数据结构做题笔记

P5309 [Ynoi2011] 初始化

修改次数与修改周期乘积 \(\le n\) 。启发我们使用根号分治。修改次数 $ \le T$ 时候直接暴力修改,修改次数 \(\ge T\) 时候说明每次修改的间隔很短。

可以维护每一个修改周期下的修改位置。其实题目本质上就是在 \(mod\) \(x = y\) 的位置上增加值,于是对于每个模数 \(x\) 维护取模后值的前后缀和统计即可。

P9371 [APIO2023] 序列

二维偏序

首先枚举中位数然后根据相对大小 \(-1\) \(0\) \(1\) 的转化。设此时前缀和为 \(s_i\),枚举数的个数是 \(c_i\)。那么对于枚举 \(l\),显然就是要找到满足要求的最大 \(c_r\)。有 \(\lvert s_r-s_{l-1}\rvert \le c_r-c_{l-1}\)。拆开绝对值号转化一下就是 \((c_l+s_l,c_l-s_l)\) 右上方的点,那么直接偏序即可。

注意细节这里不需要使用二维数据结构或三维偏序,因为要求的是最大的 \(c_r-c_{l-1}\),且 \({c_i}\) 单增,所以第一维不需要考虑大小关系。直接对于第二维排序,然后第三维树状数组即可。这样子的话时间复杂度是 \(O(n^2\log n)\) 的。

根号分治

对于出现次数大于 \(B\) 的数直接执行上述二维偏序。时间复杂度 \(O(\dfrac{n}{B}n\log n)\)

对于出现次数小于 \(B\) 的数,我们发现中位数出现位置端点组数是 \(O(\frac{n}{B}\times B \times B)\) 级别的那么直接枚举左右端点 \(pos_L\)\(pos_R\) 即可。

此时固定左右端点后的 \(c\) 是定值,我们此时不要带入二维数点的式子,而是应该带入原始式子。这样发现由于要求的 \(\max c\) 固定,所以我们只需要判断最小的 \(\lvert s_r-s_{l-1} \rvert\) 是否小于 \(c\),其中 \(l \in (pos_{L-1},pos_L-1)\)\(r\in(pos_R+1,pos_{R+1})\),这种绝对值的两边配对最小值其实很难维护,因为这是要寻找两边最接近的数。

用到性质这里的 \(s\) 每次变化量最多为 \(1\),因此是连续变化的。我们可以用线段树维护 \(s\),只需要分别找两边区间最大最小值,就可以求出两个区间的值域范围。然后找到最接近的两个数即可。

其中至于 \(s\)\(c\) 的动态维护,我们不需要每次遍历序列处理,只需要从小到大地枚举待处理中位数,每个数的 \(s\)\(c\) 只会被修改 \(O(1)\) 次。用线段树维护就行了。这一部分的时间复杂度为 \(O(nB\log n)\)

总的时间复杂度为 \(O(\dfrac{n^2}{B}\log n+nB\log n)\),取 \(B=\sqrt n\),可以得到时间复杂度 \(O(n\sqrt n\log n)\)

正解解法一

顺着根号分治的第一种情况思考,我们发现其实无用状态太多了。明明一个数能产生实际贡献的位置很少,但是我们对于每种数还需要遍历整个序列十分浪费。每个位置实际产生的贡献又有一些微妙的变化,不能直接忽视。不过变化连续都是 \(+1 -1\) 这样的变化。对于这种情况,我们考虑将上述的 \((c_i+s_i,c_i-s_i)\) 画在坐标系中,因为图象可以反应不同点之间的规律,合并相同信息。

设当前枚举的中位数为 \(x\),发现如果 \(a_i<x\),那么会往左上走,\(a_i=x\),会往右上走,如果 \(a_i=x\),会往右下走。两条直线贡献即为截距之差,思考两条直线何时可以产生贡献,只要截距大的那条直线存在一点在截距小的那条直接右上方就行了。

只需要直线的最大横坐标和最大纵坐标分别大于另一条直线的最小横坐标和最小纵坐标即可,转化一下就是二维数点。我们发现直线个数取决于 \(c\) 的变化次数,\(c\) 每次变化相当于出现一次枚举的中位数,那么对于每个中位数枚举每条直线复杂度均摊线性。

时间复杂度 \(O(n\log n)\)

正解解法二:

顺着根号分治的第二情况思考,固定区间左右端点后 \(c\) 为定值,为了最大化 \(c\),我们要尽可能扩大区间。这是一个最大化区间的过程,于是我们考虑能不能用双指针维护,发现真的有单调性。

这里的单调性并不显然,因为最后我们选取的产生中位数的区间和枚举的左右端点并不完全一样。但是更小的区间意味向外扩展着更多的选择,如果包含它的大区间满足,那么小区间必然可以通过向外扩展满足条件。

双指针配合线段树即可求解,时间复杂度为 \(O(n\log n)\)

P6688 可重集

这种难以维护信息显然是要用哈希判断相等的。

还要在加 \(k\) 之后判断相等,于是我们可与先考虑确定下来所加数\(k=\min\limits_{i \in [l2,r2]}a_i-\min\limits_{i \in [l1,r1]}a_i\)

这其实就是需要我们设计出一个可以进行加法的哈希。

对于值域很小的 subtask 2 我们可以对每一种取值都维护一个树状数组。判断两个区间是否相等,直接看区间内某种数出现次数是否相等。这样子时间复杂度为 \(O(nV\log n)\)。由于上述做法中询问和修改的复杂度极其不平衡,我们可以考虑分块,维护每种值下所有块间的前缀和。对于修改直接对于所涉及的两种值进行块间前缀和的暴力修改。对于询问直接枚举取值,然后大块用块间前缀和解决,小块暴力扫描。时间复杂度 \(O(n\sqrt n+nV)\)

考虑一个可以支持区间自变量加法的函数,这里可以考虑指数函数 \(f(x)=a^x\),直接将 \(x \to a^x\),当我们想要加的时候,只需要对于指数函数的求和式子乘上一个数就行了。小细节,树状数组可以维护区间和,但是无法维护最值,可是用线段树维护区间最小值太麻烦了。于是我们考虑用另一种方式求出 \(k\):把两个区间和的差除以区间长度。

P8421 [THUPC2022 决赛] rsraogps

考虑扫描线,线段树维护 \(f_i\) 表示 \([i,r]\) 的贡献,考虑加入 \(r+1\),可以发现其能更新一段后缀,因为如果有一次合并值后不变,就代表后续也不会变了,同时由于这些变化每次至少都是 \(/2\) 的变化,所以最多变化 \(\log V\) 次。回答问题就是每次求出区间历史版本之和即可。时间复杂度 \(O(n\log n\log V+m\log n)\) 无法通过。

可以发现多的那个 \(\log\) 是因为我们维护了单点,但是要求区间和。考虑直接维护前缀和形式。

\(s_i\) 维护的是 \(l'\le i\)\(r'\le r\) 的权值和。这样子单次回答的答案就是 \(s_r-s_{l-1}\)。同时维护每个位置合并到 \(r\) 之后的 \(a,b,c\) 值。

每次加入 \(r+1\),对于 \(s_i\) 的影响就是加入了 \([1,r+1],[2,r+1]...[i,r+1]\) 这些区间的贡献。对于大部分 \(i\),其 \(s_i\) 是不变的,于是这就是一个历史和的形式,维护当前版本时间和每个位置上次被更新的版本时间,还没每个版本新增的值 \(d_i\)\(d_i\) 也是一个类似于前缀和的东西,\(d_i\gets d_{i-1}+a_i\times b_i\times c_i\)。于是每次加入 \(r+1\) 之后,直接计算出被影响部分当前的 \(s_i\),并且重新对于改区域递推贡献函数,重置版本信息。时间复杂度 \(O(n\log m+m)\)

P3792 由乃与大母神原型和偶像崇拜

我们可以发现如果区间值域连续,我们是可以确定下来值域就是区间极值所包含的区间,即 \([\min a_i,\max a_i]\)

等于是判断区间内数所构成的集合跟值域集合 \([l,r]\) 是否相等,可以直接映射+异或解决。或者我们可以用线段树维护区间 \(k\) 次方之和。

P5445 [APIO2019] 路灯

发现联通 \(x\to x+1\),就等于把左右两段区间给联通了,设这段区间为 \([l,r]\),那么本次联通对于跨过中点的点对都有贡献,这相当于对于一个左下角为 \((l,x+1)\),右上角为 \((x,r)\) 的矩形进行了一个矩阵加。

矩阵加的贡献,就取决于何时断开连接,是一个 \(T-t\) 的贡献。这是一个三维偏序,可以用 CDQ 解决。

这题把区间信息放到二维平面上很巧妙。

P4065 [JXOI2017] 颜色

首先对问题进行一个等价转换,其实就是左边选一段右边选一段,那我们考虑中间一段,不就是求合法区间数目吗?
法一:随机化,就是如果某个颜色出现于该区间内,那么该区间应该包含这个元素的所有位置。假设元素 \(x\) 出现了 \(z\) 次,那么我们对于前 \(z-1\) 个赋一个随机权值,对于第 \(z\) 个赋值为前 \(z-1\) 个的异或和。这样就可以保证了合法区间异或和为 \(0\)。另外本题可以双哈希,提高准确度。

法二:经典套路:求区间个数可以转化为枚举右端点,然后用数据结构统计有多少满足条件的左端点。
对于每种颜色 \(c\) 求出 \(l_c~r_c\),分别表示该颜色最左边的位置和最右边的位置。设选择的左右端点为 \(L~R\),那么显然是要在 \(r_c>R\)\(c\) 中求 \(l_c \le R\) 的最大值,然后 \(L\) 必须大于这个值。而且对于 \(r_c \le R\) 的点需要满足 \(L \le l_c\) 或者 \(L>r_c\),直接将中间不合法区域线段树赋值即可。过程 \(1\) 我们可以反过来求 \(\max\limits_{j<R且r_j>R} j\),只需要维护一个栈即可。

ZROI2836.雷克雅未克

我们发现一个极大正方形肯定是被至少三个点卡住。可以发现每个点会扩展出 \(O(1)\) 个极大正方形,于是总共有 \(O(n)\) 个极大正方形。

如果我们对于 \(y\) 轴建立可持久化线段树,然后对于每个点二分一个 \(l\),在可持久化线段树的 \([y,y+l-1]\) 区间内寻找 \(x\) 的前后驱记为 \(x_1\)\(x_2\),然后直到找到 \(x_2-x_1=l\)。如果找不到说明不存在或者有一个卡着上界左右只存在一个点。这是 \(2\log\) 的,如果我们左边扫一下,右边扫一下再在两颗线段树上同时二分是 \(1\log\) 的。

然后就是对于每个询问点和矩形的匹配了,如果传统套路线段树上节点放 set 是 \(2\log\) 的,发现如果一个矩形比另一个小还更早消失,那么是不优的。我们可以通过节点上维护单调队列来完成这个操作,复杂度也是 \(1\log\) 的。总的时间复杂度 \(O(n\log n)\)

注意要通过设置无限远的点来制造无限大的矩形,从而判断无解。

ZROI2841.秋海棠

在值域不不大的时候,线段树二分是显然的。

值域过大的时候其实并不是找规律,这种东西都随便给的询问也很难找规律维护,也还是是线段树二分,但是需要动态开点就行。

线段树一大重点就是维护信息。现在考虑如何快速修改,直接对于每个节点打上标记 \((x,y)\) 表示下标 \(\bmod 2^x=y\) 的位置保留,假如新传入标记 \((1,1)\),原先的保留下来的是 \(2^xk+y\),注意区间剩余位置奇偶性只和 \(k\) 有关,和 \(y\) 无关。现在就是 \(2^x(2k+1)+y=2^{x+1}k+y+2^x\),对应修改即可。

附带剪枝,防止 \(x\) 过大,当区间个数为 \(0\) 的时候我们不改变 \(x\),当区间个数为 \(1\) 的时候,新标记保留首个元素,不改变 \(x\),这样子 \(x<2^{60}\)

XYD4290.笛镭特

可以用线段树维护 dfs 序,然后改变处理顺序,每次找到最值之后先对其所有子树处理,而不是同时对于所有分裂出来的联通块处理,处理完一个点后立刻删除其在线段树上的信息,时间复杂度 \(O(n\log n)\)

还有一种更加普适的方法,之前没见过这个 trick,就称之为树上启发式分裂吧。

这种树上联通块行走的问题肯定要联系到重心上,这样子才能保证复杂度。本题由于不是直接跳到重心,所以有点难办。但我们可以对于信息的利用联系到重心上。具体来说我们用 \(\mathrm{solve(u,0/1)}\) 表示处理 \(u\) 的一个联通块,而 \(0/1\) 表示我们是否需要进行遍历处理。初始肯定是要传入 \(1\) 的也就是要预处理,我们算出重心,然后将所有点的信息加入堆中,找到极值点 \(c\)\(u\gets c\)。我们现在删点 \(u\) 点,那就需要处理 \(u\) 的所有相连点下方的联通块。对于所有非重心子树 \(\mathrm{subtree(v)}\),我们进行 \(\mathrm{solve(v,1)}\),并且删点这些子树在堆中的信息。对于重心所在子树,\(\mathrm{solve(v,0)}\),因为我们已经在堆中保存了 \(v\) 子树的信息,不需要重复处理了。但是需要注意由于我们不能遍历该子树,所以新重心不能直接求出,这里很巧妙,直接设老重心为新重心,可以发现需要新处理的子树大小还是 \(\le \dfrac{sz_u}{2}\) 的,只不过这是上一轮的 \(u\),但复杂度还是对的。最后一个细节就是我们可以维护一个 \(to_u\),表示 \(u\) 点往哪个相邻点走可以走到重心,来得到重心和 \(u\) 点相对位置关系。

QOJ7599.The Jump from Height of Self-importance to Height of IQ Level

很巧妙的一道性质题。

看到这个区间移动肯定是用平衡树维护,每个节点的需要维护该点的值,区间最大值,区间最小值,是否存在三元上升,二元上升的第一个点最大值,二元上升的第二个的最小值。

大部分维护和合并都是显然的。后两个是需要思考的,二元上升第一个点的最大值,你需要在右边找到一个最大值,到左边去寻找小于它的最大值,按理说按照位置而非值域建立的平衡树的是无法维护寻找这个信息的。但是需要根据性质,我们在区间不存在三元上升的时候来维护这个信息(存在的时候维护也没有意义了),在这个前提条件下,我们会发现对于右边区间的最大值 \(p_k\),不存在 \(i<j<k\),使得 \(p_i<p_j<p_k\),对于左儿子的两个子节点考虑,也就是说在左边存在点 \(p_i<p_k\) 的情况下,右边肯定不存在一个比 \(p_i\) 更接近 \(p_k\) 的数,因此可以直接向左递归。这是一个平衡树二分的形式,单次 pushup 可以在 \(O(\log n)\) 的时间内通过平衡树二分完成。

总的时间复杂度 \(O(n\log^2 n)\)

本题还存在分块做法。由于每次会多出 \(2\) 个块,所以需要根号重构。每次修改,对于块的操作形式类似于块状链表。

AH省集 D3T2 信心花舍

给定一个排列和若干次询问,每次询问求对于该序列经过 \(c\) 轮冒泡排序之后,区间 \([l,r]\) 的前 \(k\) 小值之和。

考察冒泡排序的性质的好题。本题的弱化版本是 P12865 [JOI Open 2025] 冒泡排序机 / Bubble Sort Machine

冒泡排序若干轮后的结果是可以直接刻画的,前缀 \([1,l]\) 经过冒泡排序 \(c\) 轮之后的前 \(k\) 小值为原序列中区间 \([1,l+c]\) 的前 \(k\) 小值。区间 \([l,r]\) 经过 \(c\) 轮冒泡排序之后的前 \(k\) 小值就是前缀 \([1,r+c]\) 中去除前缀 \([1,l+c-1]\) 中的前 \(l-1\) 小值之后的前 \(k\) 小值。

这个是可以直接通过主席树在线维护的。本质还是对于两个前缀进行差分,需要设置一个阈值,意思是 \([1,l+c-1]\) 这个区间最多贡献 \(l-1\)。具体可以看看代码。

调用 solve(rt[min(l+c-1,n)],rt[min(r+c,n)],1,n,l-1,k) 即可,其中 \(\rm query\) 函数就是对于某个版本的主席树区间求和。

ll solve(int p,int q,int l,int r,int up,int cnt){
	if(l==r) return 1ll*cnt*l;
	int z=val[ls[q]]-min(val[ls[p]],up);
	if(z>=cnt) return solve(ls[p],ls[q],l,mid,up,cnt);
	int del=min(up,val[ls[p]]);
	return sum[ls[q]]-query(0,ls[p],l,mid,del)+solve(rs[p],rs[q],mid+1,r,up-del,cnt-z);
}

P11993 [JOIST 2025] 迁移计划

看到深度相关肯定会想到 bfs 序,每次就是把一段区间里面的数加到另一段区间里面。但是你会发现由于是比较整体的一个操作,所以你维护这个 bfs 序其实是没啥用的。所以考虑对于每个深度整体维护一个集合。

这样子每次就是集合合并累加的过程。考虑对于每个深度维护一个线段树,下标为 dfs 序,向上合并就使用线段树合并。然后对于单点查询就是直接对于该深度进行 dfs 序子树查询即可。

时间复杂度 \(O(n+m)\log n\)

待完成

下面的内容暂时鸽了。

P11585 [KTSC 2022 R1] 直方图

P4688 [Ynoi2016] 掉进兔子洞

P5355 [Ynoi2017] 由乃的玉米田

P4692 [Ynoi2016] 谁的梦

P7447 [Ynoi2007] rgxsxrs

posted @ 2024-01-06 23:53  Mirasycle  阅读(40)  评论(0)    收藏  举报