省选数据结构专题 2 做题记录
省选数据结构专题 2 做题记录
A P9342 [JOISC 2023] Bitaro's Travel (Day4)
先找出位置 \(x\) 第一个到达的点,接下来会发现我们走的路径一定是一段向右、一端向左,我们考虑何时会发生转折,假如当前我们在向右走,左边第一个没有走的点是 \(a_l\),那么找出右边第一个 \(i\) 满足 \(a_i-a_l\le a_{i+1}-a_i\),即 \(2a_i-a_{i+1}\le a_l\) 时我们要向左走到 \(i\)。
此时会发现一点,当我们转折后,\(i+1\) 将会变成右边第一个没有走的点,\(l\) 成为了新的起始点。此时考虑两者的差值,由于我们转折的时候要满足 \(a_{i}-a_l\le a_{i+1}-a_i\),所以 \(a_{i+1}-a_l\ge 2(a_i-a_l)\),那么每一次起始点和下一个未到达点之间的距离都会扩大一倍,显然总转折次数此时是 \(O(\log V)\) 的。
那么有了这个条件我们就不用担心复杂度了,对于每一次转折利用上面的式子直接二分求出最远能走到的点,最后特判一下边界情况即可。复杂度 \(O(n\log n\log V)\)。
B P9530 [JOISC 2022] 鱼 2
容易发现每一条鱼能够扩展出一个区间,并且我们可以想到,由于我们每一次的操作是先把能吃的吃掉,然后去吃两边更大的鱼,所以每一次阻挡我们的值都会扩大一倍,那么显然扩大次数是 \(\log V\) 的。
所以对于每一条鱼我们维护它可以吃完的区间 \([l_i,r_i]\)。然后考虑原题,我们有单点修改和区间查询,不难想到线段树。我们现在希望在线段树上的每个区间维护当前所有鱼的区间,不过这样显然是不可行的。容易想到的是此时不经过左右端点的区间一定都是无用的,它们不可能作为答案。所以我们只维护经过左端点或经过右端点的所有区间。根据上述结论,这些区间最多 \(O(\log V)\) 个。
然后考虑如何合并,显然合并的是左儿子中经过右端点的区间和右儿子中经过左端点的区间,合并之后我们会有新的区间经过左右端点。暴力的想法是直接按照上面的做法 \(\log V\) 扩展一个区间,然后存进答案,这样的话单次合并复杂度就是 \(O(\log^2 V)\) 的了,不是特别优秀。
更好的做法是考虑双指针,因为此时我们还有一个性质是合并后的区间端点一定在原先的区间端点中。以扩展左儿子区间为例,我们不断扩展右区间直到被堵住,然后再扩展左区间。这样只要保证区间端点有单调性即可,而这么做是天然满足这个条件的。此时复杂度就可以降到 \(O(\log V)\) 了。
所以总复杂度是 \(O(n\log n\log V)\) 的,可以通过。实现合并的时候比较困难,需要仔细一点。
C CF1442D Sum
首先观察到一个重要的性质:最多存在一个数组不选满。
证明考虑调整法。我们假设有两个数组不选满,设它们最后一个选的元素是 \(a,b\),不妨设 \(a>b\),那么此时 \(a\) 的下一个元素依然是比 \(b\) 大的,所以我们可以不选 \(b\) 而去选那个元素,如此第一个数组必然选满。
如此调整之后至多有一个数组不选满。
知道这个性质之后我们有一个简单的思路,即枚举没有选满的数组,然后做一次背包。但是这样做的复杂度还是 \(O(n^2k)\) 的,难以通过。
考虑分治,我们对于区间 \([l,r]\),先将 \([l,mid]\) 的元素加入背包,递归到右半边;然后撤销这部分贡献,加入 \([mid+1,r]\) 的元素,递归到左半边。如此一来,当我们递归到区间 \([l,r]\) 时,所有不在该区间内的数字都被加入背包,我们在叶子节点处统计一下贡献即可。
这样分治的复杂度是 \(T(n)=2T(\tfrac{n}{2})+O(nk)\) 的,根据主定理复杂度为 \(O(nk\log n)\),可以通过。
D P6109 [Ynoi2009] rprmq1
首先建出 \(t-x-y\) 三维坐标系,然后发现由于总是先修改再查询,所以 \(t\) 这一维其实是无用的,所以实际上这是一个 \(x-y\) 两维坐标系问题。此时两维是完全一样的,所以考虑先处理一维。
先将修改沿 \(x\) 轴差分,这样修改会变为 \(x\in[0,p],y\in[l,r]\)。先尝试对 \(x\) 轴扫描线,从后往前加入贡献;但是区间最大值不是一个可减贡献,所以扫描线不可行。考虑到区间最大值是一个可合并贡献,所以考虑利用分治求解。
对 \(x\) 轴进行猫树分治,处理 \([L,R]\) 经过当前 \(mid\) 的询问。当我们递归到区间 \([l,r]\) 时要保证其右侧的所有贡献已经加入。接下来按照如下流程操作:
- 先递归 \([mid+1,r]\) 区间,然后加入 \([mid+1,r]\) 中的所有贡献。
- 从右向左扫描 \([l,mid]\),处理 \([L,mid]\) 的最大值;此时我们会对一个区间进行区间加,然后查询一个区间从 \(mid\) 开始所有值的最大值,也就是历史最值。对 \(y\) 轴建立线段树,用广义矩阵乘法维护一下即可。
- 撤销 \([l,mid]\) 的贡献,递归 \([l,mid]\) 区间。
- 从左向右扫描 \([mid+1,r]\),处理 \([mid+1,R]\) 的最大值。和上面同样的,我们此时需要在撤销贡献后求出区间历史最值。
- 此时该区间所有贡献撤销完毕,返回。
不过值得注意的是上述过程中我们需要在统计贡献前将历史最值赋值为当前最大值,不然会统计到之前的答案。这个同样可以打一个矩阵标记实现。这样的话总复杂度是 \(O(n\log^2 n+q\log n)\),可以通过。
E P8496 [NOI2022] 众数
首先题面定义了众数为绝对众数,所以容易想到摩尔投票。而又由于摩尔投票是有结合律的,加之题目中出现合并操作,容易想到利用权值线段树维护每个序列。
具体的,用链表维护每个序列从尾到头的每个元素,并且对每个序列维护一棵权值线段树,线段树上每个节点维护当前区间投票后剩下的数字以及对应次数,pushup 显然可以做到 \(O(1)\)。合并的时候直接做一下线段树合并即可,查询的时候直接把 \(m\) 个序列的信息合并起来投票,再用权值线段树确定一下是不是答案即可。注意序列为空时链表的维护即可,复杂度 \(O(n\log n)\)。
F P6780 [Ynoi2009] pmrllcsrms
首先考虑一个经典套路,对序列按照 \(c\) 分块,这样的话我们的答案会分为两种情况:块内或者块间。先把序列补齐成 \(c\) 的倍数,这样就不用考虑散块情况了。然后接下来分类讨论:
-
对于块内情况:
显然我们只需要求出块内最大子段和,用一棵线段树直接维护即可。然后我们还需要求出一个区间内所有整块的最大子段和的最大值,所以我们还需要用一棵线段树维护块答案的最大值。
-
对于块间情况:
这个情况比较复杂,假如现在我们要求区间左端点不超过 \(L\),右端点不超过 \(R\),我们要在这个区间中选出一个子区间 \([l,r]\)。排除掉 \(R-L<c\) 的平凡情况,此时我们求出 \(R'=R-c\),\(L'=L+c\),也就是将这两个块上下叠放在一起后端点延长后的对应点。然后接下来除了子区间在同一块的平凡情况,我们的答案会分成三种:
- \(l\in[L,ed_i],r\in[st_{i+1},L')\),此时直接求出后缀和以及前缀和的最大值相加即可。
- \(l\in(R',ed_i],r\in[st_{i+1},R]\),此时依然直接求出后缀和以及前缀和的最大值相加即可。
- \(l\in[L,R],r\in[L',R']\),这个我们需要采用线段树维护。需要发现的一个性质是,由于 \(r-l<c\),所以 \(l>r-c\),也就是叠放后 \(l\) 在 \(r\) 的右边。采用标记合并,要么直接取左区间答案,要么直接取右区间答案,要么左区间里面选一个前缀和、右区间里面选一个后缀和,然后逐次向上传递。当然我们在线段树上就可以顺便维护出前后缀和最大值,这样上面两个信息也可以顺便处理。
这样块间答案就可以直接算了。同理,由于我们需要求出一个区间内所有块间的答案最大值,所以我们还需要开一颗线段树维护这个最大值。
这样我们就可以处理所有情况了,维护四棵线段树即可,复杂度 \(O(n\log n)\),常数可能会比较巨大,需要轻微卡常。
G P7125 [Ynoi2008] rsmemq
首先考虑求出以每个下标 \(i\) 为中心的合法区间。令合法区间 \([l,r]\) 的长度为 \(r-i\),则不难发现对于同一个 \(i\),合法区间的长度是若干段区间。并且容易发现的是,对于同一个 \(i\),合法区间长度的区间段数和 \(i\) 的出现总数是同阶的,所以这样的区间总段数是 \(O(n)\) 的。对于每个 \(i\),每次二分找到下一个区间,利用分块统计区间众数出现次数,复杂度最优可以做到 \(O(n\sqrt{n\log n})\),无法通过。
由于区间总个数是 \(O(n)\) 的,所以对于考虑对于区间段数,也就是 \(i\) 的出现次数根号分治。
- 对于出现次数 \(\ge \sqrt n\) 的数,这样的数不超过 \(\sqrt n\) 个,暴力扩展找合法区间即可,复杂度 \(O(n\sqrt n)\)。
- 对于出现次数 \(<\sqrt n\) 的数,我们需要对于每一个 \(i\),找出以他为中心,众数出现次数 \(\le 1,2,\cdots,\sqrt n\) 的最长区间,枚举一下这个出现次数,双指针求最长区间即可。复杂度 \(O(n\sqrt n)\)。
这样我们可以在 \(O(n\sqrt n)\) 的复杂度内完成区间的预处理。现在考虑查询。我们现在相当于有若干三元组 \((i,x,y)\),表示 \(l\in[i-y,i-x],r\in[i+x,i+y],r-i=i-l\) 的所有区间都是合法的。考虑它对于查询 \([L,R]\) 的贡献,容易发现,贡献总数相当于两个区间 \([i-y,i-x],[i+x,i+y]\) 和 \([L,R]\) 交的较小值。拆开讨论什么时候取最小值。
令 \(mid=\lfloor\frac{L+R}2\rfloor\),当 \(i\le mid\) 的时候较小值由 \([i-y,i-x]\) 取得,否则由 \([i+x,i+y]\) 取得。那么此时问题就是一个朴素的扫描线了,线段树维护区间加、区间和即可,复杂度 \(O(n\log n)\)。
综上我们可以在 \(O(n\sqrt n+n\log n)\) 的复杂度内解决这个问题,实际实现的时候需要注意根号分治时的阈值 \(B\) 的取值,简单卡常后即可通过。
H P3642 [APIO2016] 烟花表演
首先考虑朴素 dp,令 \(f(i,j)\) 表示让 \(i\) 子树内所有烟花距离 \(i\) 都为 \(j\) 的最小花费,则有转移方程:
显然直接做是 \(O(nV^2)\) 的,非常炸裂。考虑优化,发现转移中有一个绝对值函数,而这是一个凸函数。而 \(f\) 的转移实际上就是将子树所有 \(f\) 与这个绝对值函数做闵可夫斯基和后再相加。根据归纳法可得,\(f\) 也是一个凸函数,考虑利用 Slope trick 优化。
具体的,我们采用维护拐点的方式,维护所有斜率变化点。然后考虑怎样做闵可夫斯基和,暴力显然是不可行的,考虑发现一些性质。实际上,假如 \(f(i)\) 最小值所在区间为 \([L,R]\),那么我们可以知道,它和 \(f(x)=|w-x|\) 作闵可夫斯基和之后得到的函数有如下性质:
- 对于 \(0\le x\le L\) 的部分,将其整体上移 \(w\)。
- 对于 \(L< x \le R\),将其整体右移 \(w\),并在这个部分和上一个部分中间加入一段斜率为 \(-1\) 的线段。
- 对于 \(x>R\),直接插入一个斜率为 \(1\) 的直线。
而对于我们维护的拐点,实际上操作很简单。首先把后面斜率 \(>1\) 的删掉,由于合并时每个儿子传上来的拐点的最后一段斜率一定为 \(1\),所以我们删去最后的 \(cnt-1\) 个拐点即可(\(cnt\) 为 \(i\) 的儿子个数)。然后接下来最后的两个点就是 \(L,R\),弹出后加入 \(L+w,R+w\) 即可。合并的时候直接将所有拐点合并,利用可并堆合并即可,可以直接利用 pb_ds 库里的配对堆,复杂度 \(O(n\log n)\)。
I P5284 [十二省联考 2019] 字符串问题
我们考虑一个简单的暴力,对于每个 \(A\) 串向其支配的 \(B\) 串连边;然后对于每个 \(B\) 串,向所有使得该串是这个 \(A\) 串前缀的 \(A\) 串连边。把所有 \(A\) 串对应点点权赋值为其长度,然后在图上跑最长路就是答案,如果有正环就输出 \(-1\)。
暴力建图复杂度显然比较劣,考虑优化建图。发现复杂度瓶颈在于 \(B\to A\) 的连边,而合法连边会保证 \(A,B\) 之间有前缀关系,所以考虑利用后缀树优化建图。
将字符串反转后建出后缀树,对于每个 \(A\),找到其在后缀树上对应节点,并从该节点连向 \(A\);此时对于一个 \(B\),找到它的对应节点后,子树内所有的 \(A\) 都是合法的连边,所以直接树上优化建图即可。然后另外一种情况就是同一个节点对应多个 \(A\) 和多个 \(B\) 的情况,此时 \(B\) 依然有可能有合法的连边。按照长度降序进行排序,然后做一个前缀和优化建图即可。
这样的话我们的总点数只有 \(4n\),并且建树复杂度是 \(O(n\log n)\) 的,所以可以通过。最后拓扑排序求出 DAG 上最长链即可。复杂度 \(O(n\log n)\)。
J CF1458F Range Diameter Sum
首先看到题目中出现双重求和,考虑分治求解。现在问题是怎样求出所有跨过当前分治中心 \(mid\) 的区间的 \(f\) 值总和。
容易观察到,\(f\) 代表的实际上就是点集 \([l,r]\) 中的直径,而我们的目标是在较短的复杂度内合并两个区间的直径信息。传统的方法自然是维护直径两端点,然后用经典结论合并直径。不过这在本题中不是特别适用,因为这无法让我们在固定一个端点的情况下快速求出其与另一个区间合并后的贡献。
引入一个新的方法叫做树上圆理论, 记 \(f(u,r)\) 为以 \(u\) 为圆心,距离 \(u\) 不超过 \(r\) 的所有点构成的点集。对每个点集 \(S\) 维护最小的能覆盖它的圆 \(f(u,r)\)。那么此时直径就是 \(2r\)(注意此时我们先要对每条边建一个虚点向端点连边,这样可以避免左右 \(r\) 不一致的情况)。而这种方法的优势在于,其合并后的直径更好求,具体的,如果我们要合并 \(f(u_1,r_1)\) 和 \(f(u_2,r_2)\):
- 若 \(r_1\ge r_2+\text{dis}(u_1,u_2)\),则 \(f(u_1,r_1)\) 包含 \(f(u_2,r_2)\),答案还是 \(f(u_1,r_1)\)。另一种包含情况同理。
- 若两者没有包含关系,令 \(d=\text{dis}(u_1,u_2)\),则合并后结果是 \(f(\text{mov}(u_1,u_2,\tfrac{d-r_1+r_2}{2}),\tfrac{d+r_1+r_2}{2})\),其中 \(\text{mov}(u,v,x)\) 表示将 \(u\) 向 \(v\) 方向挪 \(x\) 步后的点,这个可以用树上 \(k\) 级祖先求出。
此时考虑左半边的一个区间 \([i,mid]\),右半边的区间的贡献有三种:被 \([i,mid]\) 包含、包含 \([i,mid]\) 或其他情况。前两种情况分别对应一段前缀和一段后缀,可以双指针维护。对于中间的情况,我们要求出的就是 \(\tfrac{d+r_1+r_2}2\) 的和,显然里面唯一比较困难的就是 \(\sum\limits \text{dis}(i,j)\) 的值,这个实际上可以直接用点分树简单解决。至此我们可以在 \(O(n\log^2n)\) 的时间复杂度内解决这个问题。可以用 \(O(1)\) LCA 和长链剖分 \(O(1)\) 求 \(k\) 级祖先,不过一波炫技下来并不能优化复杂度,不如全部用树剖解决。

浙公网安备 33010602011771号