【持续更新】集训队作业 CF DS 题目选做
本博客收录集训队作业 2020 的 CF 题中的大部分数据结构题目和一些不是数据结构的题目。
CF526F \((\texttt{Medium} \ 4 / 2)\)
发现我们只要确定这个正方形包含行的范围是 \([l, r]\),那么满足条件的正方形至多有一个。
所以我们可以枚举 \([l, r]\),然后就变成点对贡献,考虑分治。
设第 \(i\) 行的棋子列数为 \(p_i\),那么 \([l, r]\) 可行就等价于 \(\max\limits_{i \in [l, r]}\{p_i\} - \min\limits_{i \in [l, r]} \{p_i\} = r - l\),于是在分治的时候,我们对于 \([l, mid]\) 预处理后缀 \(\max\)、后缀 \(\min\);对于 \((mid, r]\) 预处理前缀 \(\max\)、前缀 \(\min\)。然后我们枚举 \(i \in [l, mid]\),那么我们可以根据 \(\max\) 和 \(\min\) 取左边还是右边把右半区间分为若干个部分,每个部分开个桶维护即可。
CF568E \((\texttt{Medium} \ 5 / 2)\)
好神啊但是好像和 DS 没有什么关系。
首先考虑如何求最长 LIS 的长度。不妨令 \(a_n = \infty\),设 \(f_i\) 表示长度为 \(i\) 的上升子序列末尾数的最小值;\(l_i\) 表示以 \(a_i\) 结尾的 LIS 长度(\(a_i \ne -1\))。
注意到两个位置填相同的数实际上是不可能同时出现在一个上升子序列中的,所以可以暂时忽视一个数只能用一次的条件,在构造的时候再考虑一下。
那么对于 \(a_i \ne -1\),转移就是直接二分 \(f\);对于 \(a_i = -1\),转移就从小到大枚举填什么数,用一个指针记录位置即可做到 \(\mathcal{O}(k(n + m))\)。
考虑如何找到对应的方案。我们需要额外记录 \(g_i\) 表示 \(f_i\) 在哪里取到、\(p_i\) 表示 \(l_i\) 的上一个位置在哪里,这两个东西也可以在 dp 的时候一并处理出来。那么在还原的时候,我们从后往前还原,维护一下 \(1 \sim i\) 中 LIS 的长度和最后一个数。假设填到了 \(i\),\(a_i\) 填的数为 \(x\),那么分情况讨论:
- 如果 \(a_i \ne -1\),那么可以确定上一个数为 \(p_i\)。
- 否则,如果可以找到一个 \(j\),使得 \(l_j = i - 1\) 并且 \(a_j < x\),那么令上一个数为 \(a_j\) 即可。否则说明上一个数也是空,找 \(i\) 前面的第一个空即可。
总的时间复杂度是 \(\mathcal{O}(m \log m + k(n + m))\)。
CF571D \((\texttt{Medium} \ 3 / ?)\)
对于两类操作分别建立一棵类似于 Kruskal 重构树的操作树,每次用第二棵树求出最近一次清零的时间,然后对应到第一棵树上就是一段区间,将询问离线以后再进行一次 dfs 即可。时间复杂度 \(\mathcal{O}(n \log n)\)。
代码待补。
CF573E \((\texttt{Hard} \ 6 / 3)\)
好神啊,我真的不会贪心。
贪心的策略是维护每个数加进去的贡献,每次加贡献最大的,直到贡献为负,证明可以见 cz_xuyixuan 的博客。
那么每个数的贡献实际上可以写成 \(ka_i + b\) 的形式,需要支持区间加 \(k\)、区间加 \(b\) 和全局最大值。
直接上分块,对于每个块维护 \(a\) 不升、\(ka_i + b\) 不降的栈,发现每次加 \(k\) 肯定是弹出栈顶若干个;加 \(b\) 没有影响;选的数所在的块暴力重构即可。时间复杂度 \(\mathcal{O}(n \sqrt n)\)。
CF575A \((\texttt{Easy} \ 0 / 4)\)
矩乘,用线段树维护一段矩阵的乘积。
代码真 tm 难写,太丑了不放了。
CF575I \((\texttt{Medium} \ 3 / 4)\)
发现等腰直角三角形可以拆成 \(x + y = k\) 或 \(x - y = k\) 的区域减去两个平行四边形,分别旋转坐标系后用二维树状数组维护即可。
CF576E \((\texttt{Easy} \ 1 / 2)\)
线段树分治板子题。dfs 到叶子的时候判断加的边是否合法,如果合法可以看作这条边一直保持到下一次修改;否则可以看作修改后的边一直保持到下一次修改。
CF603E \((\texttt{Medium} \ 2 / 7)\)
考虑有解的条件,发现是每个连通块的大小均为偶数。
再考虑最优解,即每个连通块内最小生成树最大边权的最大值。
把一条连接 \(u\) 和 \(v\) 的边 \(e\) 拆成 \((u, e)\) 和 \((e, v)\) 两条边,然后查询路径边权最大值就可以做了。具体地,在每插入一条边 \((u, v)\) 时:
- 如果 \(u, v\) 不连通,那么直接加。
- 如果 \(u, v\) 连通,那么替换 \(u \to v\) 路径上的最大边更优就替换。
当然,为了保证最优解,我们需要在保证大小为奇数的连通块个数为 \(0\) 的前提下尽量删去边权最大的边,这个用一个全局大根堆 + 懒惰删除法可以做到。
最后就是维护大小为奇数的连通块个数的问题了。这个在每个连通块中需要维护的信息不仅仅局限于一棵 Splay 树内,一般的方法是实儿子和虚儿子分开维护,再修改一下 access
、link
、cut
这些涉及虚实边变换的函数,就可以做了。
虽然上面的思路很清楚,但这终归是 LCT,还要两个大小分开维护,所以代码极其难写 /kk
时间复杂度 \(\mathcal{O}(n \log n)\)。
CF757G \((\texttt{Medium} \ 5 / 5)\)
本题有很多做法,这里记录其中的两个。
做法一
设 \(d_u\) 表示 \(u\) 的深度,把贡献转化为 \(\sum_u d_u + d_v - 2d_{\text{LCA}(u, v)}\)。显然我们只需要考虑最后一项。如果把 \(u\) 到根和 \(v\) 到根的路径上的点都打上标记,那么 \(d_{\text{LCA}(u, v)}\) 就是被打两次标记的点的数量 \(- 1\),可以用树剖配合可持久化线段树统计这些点的数量。
时空复杂度 \(\mathcal{O}(n \log^2 n)\)。
做法二
这是一个基于边分树的做法。
先讲解一下边分树的构造,其和点分树并不相似,分为两个阶段。
第一阶段:边分树是由若干二叉树链组成的森林。我们先对原树进行一次边分治,然后在每个点上维护一条二叉树链。初始时,每个节点的二叉树链都是单独的一个点;在边分治的过程中,设当前边的两个端点为 \(u, v\),且 \(u\) 的深度较浅,我们把 \(u\) 所在的那一部分每一个节点的二叉树链都加上一个左儿子、\(v\) 则是加上右儿子。边分治完成后所有的二叉树链构成了边分树。
第二阶段:边分树的合并。考虑两个点 \(u, v\) 会在什么时候产生贡献,如果把 \(u, v\) 的边分树合并起来,那产生贡献的点就是出现分岔前的最后一个点。边分树的合并与线段树合并完全相同,所以我们可以按照自己想要的顺序合并并统计贡献,最后会形成一棵极大的边分树,这就是边分树的最终形态。
回到此题。同理,边分树也是可持久化的,所以我们按照 \(p\) 的顺序依次合并,并把合并的过程改为可持久化的,交换操作就直接重构第 \(x\) 棵边分树即可。
时空复杂度 \(\mathcal{O}(n \log n)\)。
CF671E \((\texttt{Medium} \ 4 / 6)\)
可能是我没想清楚吧,这个题的代码调了很久。
如果我们能找到 \(l < r\),使得
那么 \([1, n]\) 这段区间就不行,因为两个 \(\sum\) 分别表示 \(1 \sim l\) 和 \(r \sim n\) 至少要加的数量。我们可以证明,若对于所有 \([l, r]\) 不等式都不成立,则 \([1, n]\) 合法。
不难想到分治。分治的时候考虑跨过中点的区间 \([L, R]\),记 \(S(l, r) = \sum_{i = l} ^ r a_i - \sum_{j = l} ^ {r - 1} b_j\),则不等式可以转化为
我们需要枚举选出的 \(l, r\) 是否跨过中点,有三种情况:
- 区间全部在左半部分,我们需要求出 \(\max_{L \le l < r \le mid} S(l, r)\)。
- 区间全部在右半部分,我们需要求出 \(\max_{mid < l < r \le R} S(l, r)\)。
- 区间跨过中点,我们需要求出 \(\max_{L \le l \le mid} S(l, mid)\) 和 \(\max_{mid < r \le R} S(mid + 1, r)\)。
求出上述最值之和,我们需要加上一些其他的东西,得到三个不等式,问题被我们转化为了三维偏序问题,我们再套一个分治即可。
时间复杂度 \(\mathcal{O}(n \log^3 n)\),但是两个 \(\log\) 来自分治,一个 \(\log\) 来自树状数组,常数极小,可以通过!!!
实现上可能和我上面讲的有略微不同,并且细节较多。
CF643G \((\texttt{Easy} \ 2 / 5)\)
随机化是没有前途的,使用类似摩尔投票法的方法维护即可。
记 \(k = \left\lfloor \frac{100}{p} \right\rfloor\),时间复杂度 \(\mathcal{O}(n \log n \times k^2)\)。
CF697E \((\texttt{Easy} \ 1 / 3)\)
暴力修改即可,维护每个数到最近的幂次的差值,如果子树的值全部一致或者差值全部 \(\ge k\) 就停止递归。
看上去应该是对的,但是时间复杂度不会证。
CF1491H \((\texttt{Medium} \ 4 / 1)\)
我是先在 Global Round 看到的这题,如果在 Ynoi 看到说不定就会了(
把 \(1 \sim n\) 分为 \(\sqrt n\) 个块,考虑和 弹飞绵羊 类似的做法,维护每个点 \(u\) 最后一个在块内的祖先 \(fail_u\),此时我们发现一个性质:如果一个块进行了不少于 \(\sqrt n\) 次修改,那么对于其中的所有 \(u\) 都有 \(fail_u = u\),所以在以后的修改中都没有必要修改它了,直接打标记即可。于是一个块至多被修改 \(\mathcal{O}(\sqrt n)\) 次就可以 \(\mathcal{O}(1)\) 打标记了,修改的时间复杂度是 \(\mathcal{O}(n \sqrt n)\)。
询问的话直接跳即可,时间复杂度也是 \(\mathcal{O}(n \sqrt n)\)。
所以总的时间复杂度就是 \(\mathcal{O}(n \sqrt n)\),代码十分好写。
CF1479D \((\texttt{Easy} \ 2 / ?)\)
给每种颜色赋一个随机的权值,使用主席树维护 \(u\) 到根节点出现奇数次的颜色权值和,查询时主席树上二分即可。注意特殊处理一下 LCA 处的颜色。
代码是简单的,就不写了(
CF1458E \((\texttt{Medium} \ 5 / 5)\)
感觉跟 DS 没什么关系,主要是代码难写(
如果 \(n = 0\),显然 \((x, y)\) 先手必败当且仅当 \(x = y\)。
如果 \(n = 1\),设关键点为 \((a, b)\),我们分类讨论:
- \(a < b\),此时我们考虑 \(n = 0\) 时的策略:在 \(x \ne y\) 时把 \(x, y\) 中的较大者变为较小者,如果忽略这个关键点,游戏会一直这样进行下去。观察发现这个关键点所带来的影响就是我们不能落在 \((b, b)\) 这个点上。于是 \((b, b + 1)\) 就变成先手必败的情况了;对应地,\((x, x + 1) \ (x \ge b)\) 都变成先手必败;反而 \((x, x) \ (x \ge b)\) 变为先手必胜了,还有 \((a, b)\) 变成先手必败了,除此以外没有变化。除去最后一种情况,我们可以得到 \((a, b)\) 的作用其实是把第 \(j\) 列删去了。
- \(a = b\),此时没有影响,因为落在 \(y = x\) 上本来就意味着接下来操作的人必败。
- \(a > b\),类似第一种情况,我们可以得到它的作用是把第 \(i\) 行删去了。
当有多个关键点时,其实是一样的,并且这些删行 / 列的操作是同时进行的。所以我们按照 \(x\) 从小到大的顺序加入关键点回答询问即可。
CF1458F \((\texttt{Medium} \ 6 / 5)\)
1458 这场真是神啊。
树上圆理论,题解咕了。
CF1446D2 \((\texttt{Medium} \ 4 / 2)\)
看到题解第一句话就会了!!!
有一个重要结论:原序列的众数在答案区间里依然是众数。证明可以考虑反证法,考察原序列众数和区间众数的差,左右端点移动 \(1\) 位其变化量 \(\le 1\),而左右端点扩到整个序列时其 \(\ge 0\),所以至少有一个位置为 \(0\),即存在一个更长的满足条件的区间。
然后就是经典的众数问题考虑根号分治了。对于出现次数 \(\le \sqrt n\) 的数,我们可以枚举众数的出现次数 \(c\),然后 \(\mathcal{O}(n)\) 扫一遍判断;对于出现次数 $ > \sqrt n$ 的数,我们可以枚举另一个众数,此时我们把两个数的权值设为 \(1\) 和 \(-1\)、其余数的权值设为 \(0\),然后求最长的和为 \(0\) 的子段即可。在求出的子段中两数不一定是众数,但是根据结论这种情况必然是不优的,所以可以忽略。
时间复杂度 \(\mathcal{O}(n \sqrt n)\)。
CF1413F \((\texttt{Easy} \ 2 / 2)\)
可以证明一点:最优解的一个端点必然是直径的一个端点,方法和证明到一个点距离最远的点一定是直径端点差不多。
于是分别以两个直径端点为根,维护到它有奇数 / 偶数个 \(1\) 的点的深度最大值即可。
时间复杂度 \(\mathcal{O}(n \log n)\)。
CF526G \((\texttt{Medium} \ 5 / 3)\)
很厉害的题。
首先如果没有要求经过某个点,那这题就就是经典题,选取直径的一个端点为根进行长链剖分,答案就是前 \(2k - 1\) 长的长链边权和。
钦定经过一个点的情况其实是不平凡的,我们分类讨论:
- 若 \(u\) 所在的长链在前 \(2k - 1\) 长的长链中,直接输出和即可。
- 否则,我们需要改变一条长链,设 \(v\) 为 \(u\) 第一个在前 \(2k - 1\) 长的长链中的祖先,此时显然有两种选择:
- 去掉第 \(2k - 1\) 长的长链,加入 \(u\) 所在的长链和 \(u \to v\) 的路径;
- 去掉 \(v\) 所在的长链的一部分,加入 \(u\) 所在的长链和 \(u \to v\) 的路径。
于是我们分类讨论即可,时间复杂度 \(\mathcal{O}(n \log n)\)。
CF1408H \((\texttt{Medium} \ 5 / 3)\)
看到 flow
的 tag 和官方题解的 observation 2 就会了这不是都提示完了吗。
设 \(0\) 的个数为 \(c0\),注意到答案至多为 \(m = \left\lfloor \frac{c0}{2} \right\rfloor\),我们可以考虑把原序列分裂为前缀 \(L\) 和后缀 \(R\),使得 \(L\) 中恰好有 \(m\) 个 \(0\)。这么分有什么用呢?假如我们现在选出了 \(k \le m\) 个互不相同的整数,那么我们只需为 \(L\) 中的数选取左边的 \(0\)、\(R\) 中的数选取右边的 \(0\) 即可,因为剩下的一定可以匹配上。通过分出 \(L, R\),我们现在只用考虑一边的 \(0\) 了。
显而易见地,对于 \(L\) 中的重复的数,我们只用保留最右边的那个;对于 \(R\) 中的重复的数,我们只用保留最左边的那个。于是我们可以建出网络流模型:为每个不同的正数建立一个点,为每个位置建立一个点;源点向数连边,数向其位置连边,\(0\) 向汇点连边,\(L\) 内的边从右往左连,\(R\) 内的边从左往右连。肯定是不能直接网络流的,我们考虑如何优化它。
模拟最大流感觉没什么思路,但是最大流等于最小割,我们考虑如何求最小割。因为位置之间的边流量是 \(\infty\),所以不能割去它们;因为割数向位置连的边不如割源点向数连的边,所以不会割去它们。剩下两种边:
- \(0\) 连向汇点的边;
- 源点连向数的边。
对于前者,以 \(L\) 为例,发现割前面的边比割后面的边要更优,所以最终割掉的一定是一段前缀里所有的 \(0\) 向汇点连的边。这下就简单了,我们可以枚举 \(L\) 里割了多少 \(0\),然后用线段树维护 \(R\) 里割一段后缀所需的总代价即可。线段树需要支持区间加、整体 \(\min\)。
时间复杂度 \(\mathcal{O}(n \log n)\)。注意这样算出来的答案要和 \(m\) 取 \(\min\)。