CSP2019 题解
D1T1 格雷码(code)
题目传送门
题解
按照题意模拟就可以了。
对于第 \(i\) 位,如果 \(k \geq 2^i\) 那么这一位就是 \(1\),然后把 \(k\) 变成 \(2^{i + 1} - k - 1\)。否则这一位为 \(0\),\(k\) 不变。
代码
https://loj.ac/submission/687508
D1T2 括号树(brackets)
题目传送门
题解
考虑在每一个点 \(i\) 处求出从根到 \(i\) 的括号序列中的所有后缀的合法数量 \(f_i\),其余的子串的贡献可以直接从父亲处得到。
维护一个栈存储从根到 \(i\) 的没有被匹配的左括号的位置 \(x\),那么显然如果 \(i\) 上是右括号,那么 \(f_i = f_x + 1\),否则 \(f_i = 0\)。
记得在每一个点的处理中记录被弹掉了哪个点,方便回溯。
代码
https://loj.ac/submission/687509
D1T3 树上的数(tree)
题目传送门
题解
作为从根本上把 CSP 难度推向弱胜省选难度的题目,这道题需要分成好几块来讲。
菊花图
菊花的部分分很简单(我又没想出来)。
很容易发现把某一个连接根的边断掉以后,这个点的点权会和根交换,于是这个点的点权就变成了上一个被断的边的点权。
于是可以发现所有的点之间存在这样的关系:
\[
\text{根} \to v_1 \to v_2 \to \cdots \to v_{n - 1} \to \text{根}
\]
也就是说,点权的移动形成了环的关系。我们只需要构造出这个环。
维护很多个链,从小到大枚举每一个点权,找到对应的点,它应该连向目前是链底的一个点中编号最小的点。注意只有在一条链的点数已经满了的时候才可以形成环。
这个可以用并查集或者把链缩成"链底 \(\to\) 链顶"的格式来维护。
这个部分分的代码:https://loj.ac/submission/687489 的 Task2。
链
在链上,如果想要把 \(x\) 上的点权移动到 \(y\),那么需要满足如下条件:
- 那么对于 \(x\):两个邻边中朝向 \(y\) 的边必须是第一个断掉的。
- 对于中间的点:两个邻边中朝向 \(x\) 的边必须要比 \(y\) 先断。
- 对于 \(y\):两个邻边中朝向 \(x\) 的边必须是最后一个断掉的。
于是我们只需要维护每个点的两个邻边的断边顺序即可。从小到大枚举点权,从对应的点上来一次 \(dfs\),可以一边扫一遍判断合不合法,找到最小的可以移动到的点就可以了。
这个部分分的代码:https://loj.ac/submission/687489 的 Task3。
Full
可以发现,对于一个点来说,和菊花类似,以它为根的整棵树中,每一个子树中的点权的去往顺序也依然是形成了一个链的关系:
\[
\text{根} \to v_1 \text{的子树} \to v_2 \text{的子树} \to \cdots \to v_{n - 1} \text{的子树} \to \text{根}
\]
这个链的关系既是点权的移动顺序,也可以表示删边顺序,就是先删 \(v_1\),再删 \(v_2\)……
对于一条路径 \(x \to y\),要把 \(x\) 上的点权移动到 \(y\),那么和链类似,我们可以做出如下的限制:
- 对于 \(x\):邻边中朝向 \(y\) 的边必须是第一个断的;
- 对于中间的点:领边中朝向 \(x\) 的点必须恰好在 \(y\) 前一个断。
- 对于 \(y\):领边中朝向 \(x\) 的边必须是最后一个断的。
我们考虑如何用上面的链的关系来表达断边顺序:
- \(e\) 是第一个断的:\(rt \to e\);
- \(e\) 是最后一个断的:\(e\to rt\);
- \(e_1\) 恰好比 \(e_2\) 前一个断:\(e_1 \to e_2\)。
于是我们就可以对于每个点维护一堆链表来操作,和菊花一样,同样是只有在链满了的时候才可以形成环。
代码
https://loj.ac/submission/687959
D2T1 Emiya 家今天的饭(meal)
这道题没做出来是我一辈子的耻辱。
题目传送门
题解
看上去就很像容斥对吧。
如果我们不考虑“每种主要食材至多在一半的菜(即 \(\lfloor \frac k2 \rfloor\) 道菜)中被使用”的限制的话,那么这个题目显然就是每种烹饪方法的可以做出来的菜的数量 \(+1\) 的乘积再 \(-1\)。
想要如果解决这个限制可以考虑容斥。一半这个特殊的限制非常优美,它使得容斥只需要进行一层就可以了(因为不存在有两个食材同时超过一半)。
于是我们枚举哪一个食材超过了一半,假设这种食材为 \(A\)。那么我们维护这样的东西 \(dp[i][j][k]\) 表示前 \(i\) 个烹饪方式中,其中 \(j\) 个烹饪方式用了食材 \(A\),\(k\) 个没有使用的方案数。\(dp\) 的时候直接类似 \(01\) 背包转移就可以了。
最终的限制条件就是 \(j > k\)。
但是这样做是 \(O(mn^3)\) 的。可以获得 \(88pts\)。
因为我们只需要保证 \(j > k\) 即 \(j - k > 0\),所以我们可以直接维护 \(j - k\) 而不是分开维护 \(j\) 和 \(k\)。
时间复杂度 \(O(mn^2)\)。
代码
https://loj.ac/submission/687509
D2T2
题目传送门
题解
并不会证明,只能抽象理解。
可以发现每一个数的贡献就是要乘上它的所在段的和。所以我们要尽量最小化每一段的和,也就是尽量最小化最后一段的和(因为和是递增的)。
然而这个理解方式非常不严谨。
也许可以这样考虑:
把一个子段划分为两段,如果存在两种可以行的划分方式:前面一段的和为 \(s_1\),后面一段的和为 \(s_2\),中间夹着一个数 \(a\),我们考虑应该把 \(a\) 放进前面还是后面。其中 \(s_1 + a \leq s_2\)。
于是
\[
(s_1 + a)^2 + s_2 ^ 2 = s_1^ 2 +s_2 ^ 2 +2as_1 + a^2\s_1^2 + (a + s_2)^2 = s_1 ^ 2 + s_2 ^ 2 + 2as_2+a^2
\]
因为 \(s_1 < s_2\) 所以显然选择前者更优,也就是让后面的那一段小一些更优。
然而这样考虑还是很不严谨,算了自闭了不证了。
这样我们的目标就很明确的:最小化最后一段的和。
令 \(f_i\) 表示前缀 \(1..i\) 的最后一段的和的最小值。转移的时候我们需要保证 \(f_j \leq s_i - s_j\) 其中 \(s_i\) 表示前缀和。
\(f_j \leq s_i - s_j\) 等价于 \(f_j + s_j \leq s_i\)。当满足这个条件时,应该尽量取 \(j\) 大的。
所以如果对于 \(j, k\) 满足 \(f_j + s_j > f_k + s_k, j < k\) 那么 \(j\) 就可以被舍弃了。
同时因为 \(s_i\) 是递增的,所以我们可以维护一个单调栈,其中只保留最后一个满足要求的 \(j\),这个 \(j\) 就是 \(i\) 的决策点。
最后因为答案爆炸了 ll
,所以需要开一个 \(128\) 个字节的高精度(使用两个 \(ll\) 实现)。
但是如果直接开的话会把 \(1G\) 的空间限制开炸了,所以我们维护前面的每一个点的决策点,最后只需要用一个 \(128\) 字节的变量统计答案就可以了。
代码
我比较懒,不想写高精度,所以用了 int128
(其实高精度也很好写)。
https://loj.ac/submission/687514
D2T3 树的重心(centroid)
题目传送门
题解
考场上想到一个非常繁琐的做法,不敢写。
因为两个联通块并的重心应该在原来的两个块各自的重心之间的链上,所以可以直接在链上二分或者倍增。
但是这样还需要特判一对东西,比如连通块不包含哪个子树啊之类的。很麻烦。
所以考场还是乖乖地写了 \(75pts\)。
后来因为我的 \(75pts\) 算法最后的二叉树内容本身就非常古怪,所以想到了一个和一个可行的正解有不小的交集的做法。
我的二叉树做法大概就是发现一个点的重心只能在这个点为整棵树的根以后的最重的儿子中,(显然)所以在完全二叉树上可以直接暴力跳(每个点深度不超过 \(\log n\))。
后来发现了一个可行的正解也用了这个思路。
既然在最重的儿子的子树里面,那么实际上对于一棵树,重心只能在那条从根连下去的重链里面。
于是我们考虑动态维护每一个点为根的重链,然后在上面倍增跳就可以了。
维护重链的时候,每次选择包括其父亲在内的点中最重的儿子作为重儿子。然后因为我们需要避开一个子树,所以顺便维护一下次重的儿子,在需要避开重儿子时使用。
代码实现中需要先预处理每一个子树的重链的倍增数组,然后换根的时候再维护一个倍增数组(这个倍增数组就可以把父亲作为重儿子)。然后在重链上倍增找到最后一个子树大超过一半的点。
(感觉我讲得语无伦次
代码
https://loj.ac/submission/687516
原文地址:https://www.cnblogs.com/hankeke/p/csp2019_sol.html