第十二届成都信息工程大学ACM程序设计竞赛 题解
A. 欢迎来到算法竞赛的世界!
签到题,直接写即可。
fun fact:题目背景并非虚构,希望大家以后做题能够一发即过,永远不吃罚时。
I. 你是人类吗?
按题意模拟即可。
一种实现方法是,我可以枚举 个像素格,判断其在哪一个单元格中。如果以
作起始下标,那么第
个像素格就属于第
个单元格。开一个二维数组进行记录即可。
时间复杂度
L. 平衡串
我们记 子序列个数为
,
子序列个数为
。
对于一次交换:
,则
减一,
加一;
,则
减一,
加一。
所以我们只需要统计原串的 与
,答案即为
。
无解的情况显然是 为奇数的情况。
D. 平方序列
构造方法不唯一,std 的构造方法是:
- 对于前
个数:
。
- 否则:
。
前 个数的和显然为平方数,后者的构造在于
的观察,维持了和为平方数的性质。
这个序列显然是严格递增的,因为每一项都为正且增加了一个 。
时间复杂度 。
J. 分隔
这道题的题解其实一句话就能说完:求一条由最左侧走到最右侧的最短路即可。
赛时,大部分的选手认为从最左侧到最右侧是不能走“回头路”的,于是使用了 解法,这显然是不正确的,其实是可能出现需要“回头”的情况的,例如存在一条“回头”路径上所有点的权值都为
,其他点权值都为无穷大。
这道题同样可以用网络流中的最小割求解,但是最小割的时间复杂度太大,无法通过本题。
建立八方无向图(即八个邻侧方向都可以走),用 dijkstra 算法跑最短路即可。
时间复杂度 。
E. 迷宫制造者
我们先把迷宫中所有不存在墙的位置建墙,把开销记录进答案里。
然后,把迷宫看成一个图,仔细观察题目给出的信息,显然就变成了求最小生成树,被选择的边代表把这堵墙给拆除。
边权的赋值是需要注意的:如果我们拆除墙的位置原来本来就存在墙,那么边权即为其施工开销 ;如果本来不存在,那么边权应该为其边权开销的相反数
,因为我们是把这堵墙建立起来统计进答案中再拆的,显然一开始就不建立是优的,所以要把这部分开销扣去。
使用 Kruskal 求最小生成树即可。
时间复杂度 。
F. 我们之间的相交线
对于本题,一种比较好的处理方法是把 设为根,先按顺序找到给出路径
上的所有点,找到的序列记为
序列,
的长度另外记作
。
然后,我们可以把 序列的点在原树中删去,然后找到
上每个点可以抵达的最深的长度,对于点
,我们把该值记为
。以上操作均可以在一遍 dfs 内完成。
然后,我们在 序列上作一个长度为
的“滑动窗口”操作,取窗口上左右两侧端点
,只需要找到一个窗口,满足
,那么就存在一个解。如果找到最后都没找到合法解即为无解。
找到解后,顺着向下找到对应的点即可,可以在 dfs 时处理出每个点的 点,这样找答案时会更简单一些。
可以看出,这道题目的思路比较简单,难点在于实现,需要比较精细的处理。
时间复杂度 。
B. 小游戏 III
首先有一个显然的结论:如果一个人走到 ,那么另一个人一定会输。那么当一个人操作时,如果能使
变为
,那么其一定会将
变为
。
什么时候 能够变成
呢?答案是
的时候,也就是
与
不互质的时候,这个时候只需要取
即可。
那么一个人操作时,如果当前 ,这个人就只能选取与
互质的数
。因为一旦选取的数
与
不互质,那么新的
一定有
,这相当于送对方胜利。
所以能选取的 一定是与
互质的数,相对的,每选取一个与
互质的
,就会消耗一个与
互质的
。可以证明的是,所有与
互质的
都能取到,谁先把与
互质的数消耗完,那么谁就赢了。
所以我们只需要找到不在 中且与
互质的数的个数,判断其奇偶即可。
时间复杂度 ,
来源于求
。
G. Satori and Koishi's game
区间乘积大于 就不统计是一个很好的性质,如果一个区间里的乘积不大于
,那么该区间内非
数的个数不会超过
个。
由于数字 会让区间乘积直接变成
,所以我们把序列按
断开。然后把
压缩,实际实现上可以求出每个非
数的
与
,也就是下一个/上一个非
数的位置,跳跃地访问。
我们采用离线的方式统计答案,即先求出 的所有答案,再
回答每个询问。
在只关注非 数的情况下,我们可以暴力的检查长度为
的所有区间,对于一个区间
,其对答案有贡献的区间是
,我们只需要对有贡献的区间取
即可。这个部分可以用线段树大力做,但因为是在做完所有操作后询问答案,所以也可以把每个贡献区间记录下来,最后统一做一个“线段覆盖”类问题。
由于产生贡献的区间个数有 个,所以总复杂度为
。
H. XOR and SUM
我们可以把每一个二进制位分开来看,这样序列与操作就都变成了 序列与翻转操作。
直接去用原序列计算某个子数组的价值是比较困难的,我们可以直接对序列做一个前缀异或,接下来对这个新的前缀序列进行操作与计算。
考虑一个操作,把某一个区间的数全部异或上 ,那么对于前缀序列,体现的是在这个区间的奇数位被翻转,偶数位不被翻转。特别地:如果区间长度为奇数,那么这个操作区间后的所有位置都需要被翻转。
考虑一个询问,我们只需要记录询问区间内 的个数
。对于询问区间
,如果
的位置是
,则需要翻转一下
的个数。若记录的是第
位,产生的价值即为
。
上述操作对每个位都开一个线段树维护即可。
时间复杂度 。
C. 你是奶龙?你是奈龙!
我们可以建出主串的 border 树,树上的每个结点代表一个字符串,实际上求出来的是一个森林。
那么,对于当前的串,我们只关心最后一次添加的点到树根的这条链,该链上的结点对应的字符串首先一定是主串 的前缀与后缀,这些串的区别在于在主串
中出现了多少次。
那么一个结点 对应的串
在主串
中出现了多少次呢?这个出现的次数实际上是子树
的大小。
所以我们在不考虑操作 1(即添加操作)的时候,对于 k-绚丽子串 的询问,只需要在树上做一个倍增即可,去找深度最大的且子树大等于 的位置所对应的串,找深度最大的原因是,border 树的一条链上,其深度越大,字符串长度越长。
现在考虑上操作 1,正向地去做是很困难的,因为每次添加一个点都需要对维护该点的倍增数组,所以我们可以离线所有操作,倒过来去做。先建出所有操作后最终的 border 树,然后倒过来遍历每个操作,每遇到一个操作 1 就删掉相对应的点,这样就保证了正确性与时间复杂度。类似的技巧在 Codeforces Round 907 F Growing Tree 中也有体现。
时间复杂度 。
赛时选手和验题人中也有大手子直接写的 SAM 在线处理操作,
可惜我不会。
K. 树上转移
这题本来有一个序列的版本,转移只能从左转移到右边,我们可以先考虑序列版本怎么做。
为了使方差最小,从数学的意义上来看,实际上是把数字分配地更加“均匀”。
对于一个序列,我们可以开一个单调栈维护。栈中维护两个信息 ,代表总和与包含的数字个数。最开始,我们依次取出序列中的数
,初始信息为
,然后考虑能否与栈顶信息合并。记栈顶信息为
,如果
,代表我栈顶的元素向该序列的数进行转移是更优的,因为这样能使数字更加平均,于是我们就可以把栈顶的信息合并进初始信息,即
。
单调栈做完一遍的复杂度是 的。
现在我们来考虑树上的版本怎么做。类似于序列的版本,我们也要考虑不断合并相邻的信息。我们从每个叶子向根合并。
与序列版本不同的是,一个结点可能有多个儿子,首先就有一个合并顺序的问题,我们显然需要先在所有儿子结点中,取最小的 值向上合并,直到没有儿子可以合并为止,这样做才能保证合并的正确性,不会出现兄弟之间发生转移的情况。
所以,对于每个结点,可以开一个优先队列,用来维护其儿子的 信息,每次选取最小的信息,观察其能否与之合并。需要注意的是,如果结点
与儿子结点
发生了合并,那么
需要继承
优先队列中的信息,以保证正确性。
显然,暴力地继承信息时间复杂度太高了,但是由于一个点包含的信息大小至多为其子树的大小,所以我们可以使用启发式合并,这样就保证复杂度的正确。
时间复杂度 ,两只
分别来源于优先队列与启发式合并。