- 代码源挑战赛 Round 68
代码源挑战赛 Round 68 题解
- @ 2026-9-12 16:20:15
T1
100%
如果 正好是某一班车的发车时刻,那么答案就是 。否则,设 距离最近的上一班车还差 ,那么还需要等待的时间就是 。把这两种情况合并起来,答案可以写成:
直接按这个式子计算即可,当然也可以用 if 语句进行分类讨论。
时间复杂度 。
T2
100%
由于题目保证只有 [说明] 的两侧有括号,因此从左到右扫描字符串,找到第一个 [ 和第一个 ] 的位置,输出中间的子串即可。
时间复杂度 。
T3
100%
设原串中 0 的个数为 ,1 的个数为 。最终要求两者数量相同,所以最少需要修改的字符数 为:
接下来考虑字典序最小。
- 如果
0的数量更多,那么需要把一部分0改成1。为了让字典序尽量小,应该尽量保留前面的0,所以选择修改最靠右的 个0。 - 如果
1的数量更多,那么需要把一部分1改成0。为了让字典序尽量小,应该尽量让前面变成0,所以选择修改最靠左的 个1。
因此只需统计两种字符的数量,再按上述规则从左或从右挑出需要修改的位置即可(但输出下标时,需要从左往右输出)。
时间复杂度 。
T4
100%
这是一个树上自底向上的贪心。
题目要求对任意祖先 和后代 ,满足最终高度 。由于我们只能让高度增加,所以一个节点最终至少要比它所有儿子的最终高度都大 1。也就是说,如果一个节点 的儿子中的最终高度最高的是 ,那么这个节点 最终至少要变成 ;如果它原本更大,则不需要额外修改,即:
因此,从根节点开始做后序 DFS:
- 先递归处理所有子节点;
- 取所有子节点最终高度的最大值 ;
- 当前节点最终高度应当更新为 ;
- 需要执行的操作次数就是这个新值减去原值,累加到答案里。
这样处理后,当前节点会自动满足“比所有后代都高”,并且对子树以外的部分没有额外影响,因此这是最优的。
时间复杂度 ,空间复杂度 。
T5
100%
为了击败邪教徒,我们至少需要总共打出 张打击牌。
接下来考虑如何在保证总打击牌数至少为 的前提下,让机宝剩余血量尽可能多。
对于每一回合,我们贪心地尽可能打出多的打击牌。
具体来说,第 回合打出 张打击牌,剩余的牌全部用于防御,即 。此时该回合机宝受到的伤害是 。
如果按照这个贪心策略打到某一回合,机宝的总打击牌数超过了 ,且承受的总伤害没有让机宝死亡,那么超出的打击牌其实就是“冗余”的。我们可以把这些冗余的打击牌“撤回”,替换成防御牌,从而进一步减少受到的伤害,提高最终剩余血量。
当我们将一张打击牌改为防御牌时(即反悔操作),该回合防御值增加 。观察伤害 随 的增加,减小量(即反悔收益)有两阶段特征:
如果当前 (即该回合收到伤害了):
- 在伤害降为 0 之前,每多 1 张防御牌,伤害稳定减少 。
- 当伤害即将从 降至 0 时,这 1 张防御牌,只能减少 点伤害。
由于每个回合可选的反悔操作的收益是单调不增的(先是若干个 ,再是至多一个 ,最后都是 0),因此我们可以使用 大根堆(优先队列) 来维护所有可用的反悔操作。
具体流程:
- 初始化:计算必须打出的打击牌数量 。维护当前打出的打击牌总数 ,和当前累计受到伤害 。
- 模拟每一回合的操作:
- 贪心打出 张打击牌, 张防御牌。
- 令 ,。
- 如果当前回合防御牌未打满(),且仍在掉血(),计算可反悔的次数和收益,压入大根堆(按收益从大到小排序):
- 可反悔操作首先受打击牌和防御牌数量的限制,即 。
- 实际受到伤害为 ,因此收益为 的次数 为 ,压入大根堆。
- 若 且 ,说明还可以有一次收益为 的操作,压入大根堆。
- 反悔过程:只要当前总打击牌数量 且堆不为空,说明我们有冗余的打击牌可以用来反悔。弹出堆顶操作(收益 ,次数 ),将尽可能多的打击牌换成防御牌,并更新 和 。
- 若在某一回合的调整结束后,(机宝存活)且 ,说明当前是一种可行的获胜状态,更新最大剩余血量。
- 输出最大剩余血量或
-1。
时间复杂度约为 。
T6
100%
我们需要计算:
$$S = \sum_{a' \in \mathrm{perm}(a)} \sum_{b' \in \mathrm{perm}(b)} F(a', b')$$其中
$$F(x, y) = \sum_{1 \le i_1 < \dots < i_m \le n} \prod_{j=1}^{m} x_{i_j}^{y_j}.$$将 代入并交换求和顺序:
$$S = \sum_{a'} \sum_{b'} \sum_{1 \le i_1 < \dots < i_m \le n} \prod_{j=1}^{m} (a'_{i_j})^{b'_j}.$$重新组织求和:对于固定的 的大小为 的子序列(保持原下标顺序),以及固定的 的一种排列,计算这些值在所有 中出现多少次。
- 从 中选出 个元素,并按选出的顺序放入子序列位置,有 种方式。
- 中剩余的 个元素可以任意排列在剩下的位置上,有 种方式。
- 两者相乘,表示每种“有序的 元子序列 + 剩余元素排列”恰好对应一个完整的 ,出现次数为 。
因此, 取遍所有 种排列,而 的求和可以转化为对 的所有大小为 的有序子序列求和,再乘上 :
$$S = n! \sum_{1 \le i_1 < \dots < i_m \le n} \sum_{b' \in \mathrm{perm}(b)} \prod_{j=1}^{m} a_{i_j}^{b'_j}.$$这样我们就把问题分解为两部分:
- 计算 $T = \sum_{1 \le i_1 < \dots < i_m \le n} \sum_{b' \in \mathrm{perm}(b)} \prod_{j=1}^{m} a_{i_j}^{b'_j}$;
- 最终答案 。
其中 的含义是:在序列 中按原顺序挑选 个元素构成子序列,同时将 的所有排列作为指数分配给这些元素,求所有乘积之和。
这可以通过动态规划解决。我们按顺序遍历 的每个元素,并逐步决定它是否被选入子序列,以及如果被选中,它对应 中的哪一个指数。为了不重不漏地枚举 的所有排列,我们还需要记录 中哪些位置已经被分配,由于 ,因此可以通过二进制状态来表示。
状态定义:
用二进制掩码 表示当前已经使用了 中的哪些元素()。
表示在当前已经考虑过的 的前缀中,选出的子序列已经占用了 中的 元素时,所有合法方案的乘积之和。
转移: 顺序遍历 中的每个元素 (代码中保持原始顺序)。对于当前的 ,我们有两种选择:
- 不选 : 保持不变。
- 选 ,并把它分配给 中某个尚未使用的指数 (): 此时新状态为 ,贡献要乘上 。
写成递推式,对于当前 :
$$\mathrm{new\_dp}[\mathrm{mask}] = \mathrm{dp}[\mathrm{mask}] + \sum_{j \in \mathrm{mask}} \mathrm{dp}[\mathrm{mask} \setminus \{j\}] \cdot a_i^{b_j}$$其中 等价于“在上一步状态 的基础上,选用 并匹配 ”。
初始化: 未考虑任何 元素时,空集状态乘积为 :,其余为 。
最终结果: 处理完 的全部 个元素后, 即为所需的 ——它恰好枚举了所有大小为 的 子序列,以及 的所有排列与之匹配的情况。
由前面的推导,答案即为:
$$S = n! \cdot \mathrm{dp}[(1 \ll m) - 1] \bmod 998244353.$$时间复杂度为 (幂运算可以提前 预处理并保存到数组中),空间复杂度为 (滚动数组优化空间,否则为 )。