[省选联考 2021] 解题报告
这两天(2023-3-12/13)开了一场省选 VP,感触比较大,同时也有颇多要总结的地方,因此写下这篇博客。
省选 \(-20\) 多天,我还在补一些没有仔细学的新算法,虽然感觉新学了很多东西,但是感觉在实战中并没有用到什么,所以感觉自己学得并没有很大意义。最近的做题计划比较混乱,基本上是想到什么做什么,再有就是跟着群友的刷题记录,但是自己的做题步骤一般为:开题 \(\to\) 思考 \(\to\) 两分钟没有任何思路 \(\to\) 看题解 \(\to\) 发现做法不难 \(\to\) 开贺。感觉对自己的分析与思考能力没有提升。
趁着周末来了一次 VP,感觉还是要考试性质的做题对自己的实力有一定帮助。
以上均为个人想法。
Day 1
卡牌游戏
不考虑正反面,求一个长度为 \(2n\) 的数列删掉至多 \(m\) 个数的极差是平凡的,显然从两端删起,枚举分别在两边删掉的数的个数。
现在考虑加入限制,那么可以转换为牌有两种颜色,其中一种颜色最多被删去 \(m\) 张,且同一编号的牌不能全部删除,问极差。
思考方式仍然相同,我们将 \(2n\) 个数打上正反面和编号标记,按照值的大小进行排序。
我们维护一个双指针 \(l,r\) 表示我们已经删除了 \([1,l],[r,2n]\) 中的所有数。
假设我们先让 \(l\) 走到最大的位置。那么现在 \(l\) 往回走的时候,\(r\) 就可以移动更新答案。
这题有一定的思维难度,主要是想到通过打标记的方式让所有的数变成一个序列再操作会简单很多。
代码不难,有一定细节。
矩阵游戏
2021年真的很喜欢游戏。
有一堆限制的矩阵并不好直接构造,赛时我本来的想法是四个相邻的数中取 \(\min\),然后用四周的格子填补。这个想法显然是有问题的,因此考虑换一个方式构造。
采用调整法,不考虑限制,先构造出一个符合条件的矩阵,去掉限制后答案非常简单:
规定 \(a_{n,i} = a_{i,n} = 0\) 即可倒序推出。
满足条件的矩阵有一个很好的性质:如果一个矩阵 \(A\) 满足条件,那么
图函数
先是一些常规的转化:因为求的是 \(\forall i \in [1,m],h(G_i)\),肯定不会用 \(f \to h\),我们转而思考有多少点对 \((u,v)\) 对 \(h(G_i)\) 有影响。
先给出一些性质:
- \((u,v)\) 的贡献是一对前缀
- 如果是 \(u \to v, v \to u\),则 \(u \ge v\)
- \(u \to v, v \to u\) 的路径上只存在 \(\ge v\) 的点
那么问题转化成:对于 \(\forall G_i\),分别求 \((u,v)\) 点对的数量,存在 \(u\to v, v \to u\) 且不经过 \(<v\) 的顶点的路径。
\(u\to v, v \to u\) 上经过的边的编号的最小值最大是我们希望它产生贡献的前缀大小。最后统计答案的差分数组,后缀和求解。
使用 Floyd 和 Dijkstra 均可求解。
Bonus:采用 BFS,倒着加边,对于一条新边 \((s,t)\),如果 \(s\) 被遍历过而 \(t\) 没有,那么这条边就可以造成贡献,直接从 \(t\) 开始 BFS。如果 \(s,t\) 都没有被遍历过,那么它可能在后面造成贡献,先加入图中,否则这条边不会造成贡献。在 BFS 时,我们将经过的边删去,因为这条边不会再被更新到,总复杂度更优。
Day 2
宝石
DS 好题。
从题解中看到的一种比较清晰的做法。
我们先按照颜色在容器中的顺序对颜色进行重编号,问题就转化成:对于一条路径 \(u\to v\),找到最大的 \(x\),使得 \(1\) 到 \(x\) 在这条路径上顺次出现。
我们将 \(u\to v\) 的路径转换成 \(u \to \operatorname{lca}(u,v)\),\(\operatorname{lca}(u,v)\to v\)。对于树上路径的一般套路是倍增/树剖,这题我们要维护的是一个点的前继后继关系,因此考虑用倍增。我们记录 \(f_{u,i}\) 表示 在 \(1\to u\) 的这条链上,深度最深的颜色为 \(w_u+2^i\) 的位置,\(g_{u,i}\) 表示 在 \(1\to u\) 的这条链上,深度最深的颜色为 \(w_u-2^i\) 的位置。这两个数组分别是为了处理 \(u \to \operatorname{lca}(u,v)\) 与 \(\operatorname{lca}(u,v) \to v\) 的路径关系。
那么思路就很明晰了,对于向上跳的这条链,我们先找出颜色为 \(1\) 的节点,然后向上跳到深度不小于 \(\operatorname{lca}(u,v)\) 的节点,这就是前半条链的最长匹配长度。向下跳的链采用二分的思想,利用 \(g\) 向上跳维护出最大的可行值。根据贪心的思想,收集的起点越靠近 \(u\),终点越靠近 \(v\),答案不会更劣。因此还要维护出距离 \(u\) 最近的 \(1\) 和距离 \(v\) 最近的二分的那个值。
对于这样的树上问题采用主席树,问题迎刃而解。
当然你也可以用任何你喜欢的方式。
滚榜
\(n\) 很小,考虑状压 DP。
先考虑一个比较自然的 dp:\(f_{S,i,j,k}\) 表示已经得过第一的队伍状态 \(S\),现在得第一的队伍 \(i\),总共已经通过了 \(j\) 题,上一个队伍通过了 \(k\) 题。时间复杂度 \(\mathcal{O}(2^nnm^2)\),显然需要优化。
一种可行的想法是,我们只需要知道一个排列是否合法即可,而不关心真正的分配方式,因此我们可以贪心的分配通过的题目数。先给前面的队伍分配最少的能获得第一的的题目数量,剩下的分配给最后一支队伍即可。
此时假设给每个队分的题目数量为 \(b_i\),如果 \(a_i > a_{i - 1}\),为了维护 \(b_i\) 不降,\(b_i = b_{i - 1}\),否则 \(b_i = b_{i - 1} + (a_{i-1} - a_i)\)。拆贡献可得 \(\sum b_i = \sum \max(a_{i - 1} - a_i,0)\times(n - i + 1)\)。因此我们并不需要注意上一个元素的贡献。
同时还要注意,通过题数相同时,编号小的优先。
支配
不会支配树。/kk
支配的定义:\(x\) 支配 \(y\) 当且仅当删除 \(x\) 这个点后 \(1\) 和 \(y\) 不可互达。
以下用 支配集 表示 支配某个点的所有点。
先从询问思考如下性质:
- 加入一条边,\(u\) 有可能和 \(1\) 联通的路径变多了,因此支配集可能变小。但这条路径可能还是会经过支配集中的点,所以也有可能不变。
再考虑支配有哪些性质: - 如果 \(x\) 支配 \(y\),\(y\) 支配 \(z\),那么 \(x\) 支配 \(z\)。
显然,如果 \(1\) 和 \(y\) 不联通,\(1\) 和 \(z\) 不联通。 - 如果 \(x\) 支配 \(z\),\(y\) 支配 \(z\),那么 \(x\) 和 \(y\) 之间存在支配关系。
考虑反证,如果删去某一个点 \(x\),因为不存在支配关系,所以必然有 \(1\to y\) 的路径,同理有 \(y \to z\) 的路径,那么 \(x\) 不支配 \(z\)。与题设不符。
根据上述结论,我们可以发现支配的关系应该是一个树形结构,事实上确实如此。支配关系形成的树叫做支配树。支配树有如下性质:
- 设 \(x\) 的支配集为 \(D_x\),则有且仅有一个点 \(y \in D_x\) 且 \(y\) 被 \(D_x\) 中所有点支配,这个点是 \(fa_x\)。
根据树的形态推导。 - \(x\) 的支配集为支配树上 \(x\) 的所有祖先。
- \(x\) 的支配集中,\(fa_x\) 是支配集大小最大的那个点。
我们可以依此来构建支配树。 - 加入一条边后,如果 \(x\) 的支配集发生改变,只有可能是 \(fa_x\) 的支配集改变或 \(fa_x\) 不再是 \(x\) 的支配点。
- 加入一条边 \(s\to t\) 后,\(fa_x\) 不再是 \(x\) 的支配点,当且仅当删除 \(fa_x\) 后,\(1\to s\),\(t \to x\)。
这道题可以 \(\mathcal{O}(n^2)\) 方式建立支配树:对于某个点,求出删除这个点后 \(1\) 还能到达哪些点。无法到达的属于支配集,再根据支配树的性质2建树即可。
查询时,单次在树上暴力跳,复杂度单次 \(\mathcal{O(n)}\)。
总时间复杂度有点卡,这里给出建树代码。
点击查看代码
inline void dfs1(int u, int del) {
vis[u][del] = true;
if (u == del) return;
for (int v : e[u]) if (!vis[v][del]) dfs1(v, del);
}
inline bool cmp(int x, int y) {
return domi[x].size() < domi[y].size();
}
inline void build() {
dfs1(1, 0);
for (int i = 1; i <= n; ++i) {
rnk[i] = i;
dfs1(1, i);
for (int j = 1; j <= n; ++j)
if (vis[j][0] && !vis[j][i])
domi[j].emplace_back(i);
}
sort(rnk + 1, rnk + n + 1, cmp);
for (int i = 1; i <= n; ++i) {
for (int v : domi[i]) {
if (domi[i].size() - 1 == domi[v].size()) {
fa[i] = v;
break;
}
}
}
}
其中,domi 是一个点的支配集,rnk 表示按照支配集大小排序后的各点编号。
本篇 blog 涉及所有代码。
后记
说是后记,其实是个人的一些想法。整场考试唯一用到的我比较不熟悉的算法就是 Day2T3 的支配树,但其实考场上略微手推也能得出一些关键结论。这场考试让我想起之前看到的一句话,
学的算法多 \(\not=\) 能力强
算法的本质是理解运用,因此在省选前学新算法可能不如好好复习自己的所学,而且 2022 赛季其实在我所学范围内的分有很多,在考场上,总是因为 做题做急了/没仔细思考/看到长题面不想看/学过的算法没有理解导致考场上忘记 这种主观原因出问题,感觉我的 OI生涯 可能需要积淀一段时间,先把学过的算法复习好再学习新知,这也是我开始记录自己所学的初衷。

浙公网安备 33010602011771号