真的是好久没写 BLOG 了。现在只有最后的几个月为我最后一届 ACM 比赛而奋斗了,所以我想尽量多做几道题。上星期又是一年一度的复旦、东华、上大和少科站的练习赛。一连比了两场,我们队都是做出 6 题。题目都比较简单,但是有几道比较难的题,我觉得确实出得挺好的。
第一场比的是
TUDPC 2008 的题目,共 8 道题,我就挑几道稍微难一点的回忆一下吧。
D. Rain on your Parade
看似不难,是典型的二部图匹配,左、右各 3000 个点,我觉得用匈牙利算法应该过得了。这道题是我的两个队友负责的,我没有过问。它们却一直 TLE,原因不明。当时他们一直在置疑匈牙利算法能不能应付得来左、右各 3000 个点的复杂度,我说我也不知道更快的算法。事后我也用匈牙利算法打了一遍这道题目,我决得并不慢。奇怪……
E. Olympic Games
有 50,000 个整数区间,问最多能选出几个,使它们两两不相交(允许两个区间的首、尾相连)。如 [1, 3]、 [2, 5] 和 [3, 4] 这三个区间,最多可以选出 [1, 3] 和 [3, 4] 这两个区间是不相交的。
我的队友一下就想到了最大上升子序列(LIS),我一听有道理,正好我有 O(N logN) 的 LIS 代码,于是我就负责这道题了。可是这不是单纯的 LIS ,因为每个区间不是单纯的比大小,如果两个区间有相交或包含的关系,那该认为谁比较小?
其实是这样的,通常的 O(N logN) 的 LIS 算法是用一个数组 T[] 记录长度为 1 ~ L 的上升子序列(IS)的尾巴的位置,L 是目前找到的最长的 IS 的长度。对于每个数组元素 a[i],都用二分搜索找到最大的 x,满足 a[ T[x] ] < a[i];也就是说,找到最长的尾巴比 a[i] 小的 IS;那么此时 a[i] 就是新的长度为 x + 1 的 IS 尾巴,或者说 i 是新的 T[x + 1]。
但是这道题目不是说找 LIS,而是要构造一个 LIS,所以要先把所有区间递增排序,按区间的头部(左边界)递增,头相同的尾巴递增。然后再找 LIS。问题在于二分搜索这一步,就算找到了最大的 x,满足 a[i] 可以跟在 a[ T[x] ] 后面,此时 i 也不一定是新的 T[x + 1]。必须要 a[i] 的尾巴在原来的 a[ T[x + 1] ] 前面,才更新 T[x + 1]。
举例说,a[i] = [3, 5],找到了 a[ T[x] ] = [2, 3],此时 a[i] 可以跟在 a[ T[x] ] 后面;但是如果原来的 a[ T[x + 1] ] 是 [3, 4],它的尾巴在 [3, 5] 的前面,显然把 [3, 4] 作为 T[x + 1] 更有可能找到更长的解,所以此时不能更新 T[x + 1]。比赛时我就是没注意到这一点,吃了好几个 Wrong Answer。幸好后来我们想到了另一个算法,原来这道题可以贪心解的,如下。
首先仍然是要把所有区间递增排序。假如已经找到了前 i 个元素的最优解,它的最后一个元素是 a[i],此后的每一步都可以保证最后得到最优解:
- 如果 a[i + 1] 的头可以跟在 a[i] 的尾巴后面,则前 a[i + 1] 个元素的最优解当然就是以 a[i + 1] 结尾。
- 如果 a[i + 1] 的头不能跟在 a[i] 的尾巴后面,或者说 a[i + 1] 和 a[i] 有重叠,则再分两种情况(别忘了已经是排好序的,a[i + 1] 的头不会在 a[i] 的头前面):
- 如果 a[i + 1] 包含于 a[i],则与 a[i + 1] 冲突的区间肯定更少,所以此时舍去 a[i],保留 a[i + 1]。
- 如果 如果 a[i + 1] 不包含于 a[i],也就是说 a[i + 1] 的尾巴在 a[i] 的后面,则之后与 a[i] 冲突的区间肯定比 a[i + 1] 少,所以此时舍去 a[i + 1],保留 a[i]。
如此一来,每一步就可以保证总体的最优解。走许多弯路后,我们总算把这题 AC 了。
G. Robotic Invasion
这可以算是这套题里最难的一道吧,比赛当场也很少有人碰。赛后我才想到是动态规划,题目确实出得不错。说是有一个 100 × 100 的方格地图,其中有一些格子是墙,不能走上去;有一个起点和若干个终点,走到任意一个终点都算到达目标;给出一条指令,是一个由
上、
下、
左和
右组成的字符串,表示一条路径,长度最多 100,但是它不一定能从起点走到终点;现在需要修改这条指令,使得指令长度不变,而且可以走到目标;如果走到指令的一半就已经到目标了,则后半部分指令可以被乎略;如果有多个解,则输出满足下列条件的解: a. 修改的字符数最少。 b. 如果还有平局,则输出最先到达目标的指令。 c. 如果还有平局,则输出字典顺最先的指令。一看题目要求就挺吓人的吧,还好题目中的数据规模提示了我,令我发现是 DP。
DP 的状态是这样的,需要三个数组: minChange[i][j][k]、 minTime[i][j][k] 和 newCmd[i][j][k]。 [i][j] 表示当前位于的格子,[k] 表示进行到了指令的第 k 个字符; minChange 表示从这一步开始的最优解需要改变后面指令的几个字符; minTime 表示从当前步开始的最优解离目标有几步; newCmd 表示最优解的当前这一步的指令是什么字符。有了状态,相信算法已经很清楚了吧,我就不多说了。
这套题其余都是水题,连我们这种队都能秒杀,也就没什么好说的了。
第二场比赛比的是
2007 年 Pacific Northwest 赛区的题目(好像是美国的某个赛区),今年
浙江省某比赛的一场热身赛也引用了这套题目。让我也来回忆一下其中一些有意思的题目。
B. Missile Command
最少有人做出来的一题,不仅比较综合,而且题目比较难读懂。我们当时没碰,但是很高兴赛后我做出来了。说的是二维坐标系中,有最多 20 颗导弹从空中的(y > 0)某些点射出;导弹都是均速直线运动;各颗导弹的出发点、出发时间、运动方向和速度各不相同;导弹到达地面(y = 0)就算击中目标;防御方向空中发射了最多 20 颗高射炮,每颗高射炮于不同的时间在空中的不同点爆炸;爆炸呈圆形,持续 2 秒钟,圆的半径 r 服从时间 t 的函数 r = sqrt( 1 - (t - 1)
2 ),也就是说半径随着时间从 0 增至 1,又降至 0;如果导弹碰到了爆炸区域,则被阻挡。问最少采用其中的几颗高射炮,就足以阻挡最多的导弹。
可以看出数据规模很小,是典型的搜索题,要找出最少的子集,让它们能覆盖整个全集。搜索之前需要几何的预处理,需要解一元二次不等式来判断各条直线和各个圆的相交。另外,题目还有一些歧义的地方,使过题更加困难:
- 即使是向上飞的导弹,虽然它不会击中地面,但是也要尽可能阻它。也就是说,如果一颗高射炮只挡住了向上飞的导弹,不要认为它是多余的。
- 题目中说“爆炸区域的半径为 0 时无法阻挡导弹”,这是个晃子,其实这种情况不会出现。因为通过求导可以知道,sqrt( 1 - (t - 1)2 ) 在 t = 0 和 t = 2 处的斜率都是无穷大。也就是说,圆的半径在一开始和最后的变化都是很快的。一颗导弹不可能在一开始位于圆心,而后一瞬间立即飞出圆外;也不可能本来一直在圆外,只在最后时刻到达圆心,因为它的速度没有圆的半径变化得快,除非导弹的速度是无穷大。
即便是那天比赛的冠军队,在做出这题时也欢呼了起来,可见它有多烦人。
C. Palindromic Primes Category in Jeopardy
问形如这样的问题:“小于 2
31 的 24 进制的
回文素数有几个?” 提问的进制数包括 2 到 36 进制。看似有点难,但是我们队的打表王和素数王,各大 OJ 的风云人物, TRY 同学打表秒杀了此题。
D. Pebbles
一个 15 × 15 的矩阵,每格上都有一个大于 0 的得分;要求选取一些格子,使总得分最大;要求选取的任何两个格子都不能相邻。看上去应该是典型的 DP,状态数组的下标就是一行的各种选法,有 2
15 个状态;对每一行,枚举该行和上一行有所有选法,如果有不冲突的,则可以更新状态值。但是我们一开始想,每一行的状态有 2
15 个,那总的时间复杂度就是 O(15 × 2
30),是不行的。可是后来恍然大悟,一行的选法不是 2
15 种,因为 0 ~ 2
15 之间二进制没有相邻的 1 的数只有 1000 多个,所以总的时间复杂度一下子就降低了,于是做出了此题。
E. Rubik's Cube
比较复杂的模拟题,当场很少有人碰,我们也不例外。可是我们学校的一支新队员队伍却做出了这道题,精神可嘉,令众人十分佩服。
G. Frogger
一个 30 × 30 的矩阵,有一个起点,若干个终点,走到任意一个终点就算到达目标;每一步可以上、下、左、右走,或者原地不动;有些格子是墙,不能走上去;有些墙是固定的,有些墙则是不断地横向循环移动的(移动到最左边则回到最右边);要求算出从起点到终点的最短时间。
由于墙是不断循环移动的,所以整个地图是周期性变化的,循环的周期长度就是地图的宽度。因此需要保存 30 张不同的地图, BFS 的每一步都走相应的地图即可。另外,比赛当时我们没把题目读懂,原来题目里说的是只有走到指定的一些格子上才耗费时间。所以我们当时没搞懂 SAMPLE OUTPUT,没来得及做出这题,赛后才做出。
J. Cover Up
我一开始就分配到读这道题,之后就一直在想它(除了当中被队友打岔,帮他们出点主意,或者自己分心想了想别的题目等等)。我全场比赛就做了这一道题,我的两个队友也很信任我,没有来过问。到第 4 小时左右的时候终于想出来了,这是我们队做出的最后一道题。
说的是在一次抽奖活动中,中奖号码是 7 位数;每位数上都有 1 ~ 10 个不同的数供抽奖者选择;抽奖者在各位上随便选一个数,如果全选对了就中奖了;如果全选错了就淘汰;如果只选对了一部分,则选错的部分可以重选(当然,重选的时候,原来选错的数就排除掉了);如此不断循环,直到全选对或者淘汰为止。问中奖率是多少?
我一开始就觉得是 DP,可是 7 位数,每位上可选的数有 1 ~ 10 个,则状态有 10
7 种,太多了。后来我才想到,和 D 题一样,有很多状态是没有必要的。比如有 3 位上的可选数分别有 1、 2、 3 个,和 3 位分别有 3、 2、 1 个是等价的。所以状态数其实就是各位都递增的 7 位数的个数,我记得当时算出来大概只有 50,000 多个。于是就好解啦,状态转移就是一个不算难的概率计算,枚举各位选对还是选错的所有情况嘛。不久之后便提交通过了。
K. Bridge Marble Rings
图 1
如图 1 所示,有两个环,其中各有 13 个球,总共有 13 个黄的和 13 个灰的。中间有一个小环连接,小环可以旋转 180 度,使两个环上的各 3 个球互换。问最少把小环旋转几次,可以把所有的灰球移到左边(两个大环本身任意旋转是不需要代价的)。
比赛时我最先读的就是这道题(因为我喜欢最先读最后一题),第一感觉就是双向 BFS,因为目标状态是唯一的嘛。可是乍一看状态好像太多了,当时就没敢多想,放在一边了。反正就算当时会做,比赛时间也来不及。今天我回过头又想了想,终于把它做了出来。
双向 BFS 的思路是没错的。首先, SAMPLE OUTPUT 提示了我,如果两个环全都是一黄一灰间隔排列的,则答案是 6。我想,这组 CASE 应该是离目标很远的吧,怎么只要 6 步!难道所有的解都这么小?当我编好代码后试验了一下,果然如此,一般情况下都是 4、 5 步就搜到了。
虽然我的双向 BFS 代码计算少数几组数据不慢,差不多半秒就能搜出一个,但是这道题目的输入数据量比较大,好像有上百组。所以我还需要一些优化。
两个环总的排列好像有很多,有 26
C13 ≈ 10
7 种,但是实际上没那么多,因为一个环的 13 种旋转都是一样的。所以总的排列数应该是 26
C13 / (13 × 13) ≈ 60,000 种。我在程序里是这样处理的,用 13 位二进程表示一个环,我把每种环都循环移位 13 次,找出其中最小的一个来代表它,这个预处理起来是不需要多少时间的。既然状态这么少,是不是可以索性在预处理的时候,把所有可能的状态的解都算出来?不是的,因为扩展状态的时候还是要把两个环各转 13 次,需要 O(13 × 13) 的时间,这样总的复杂度就又变回 O(10
7) 了;再加上这道题的内存有限制,不能开数组保存解,只能用 map,那么常数级的复杂度也会很大,全部预处理就会超时了。
但是后来我想到,每次双向 BFS 的反向搜索那部分,其实都是重复的。我干嘛不先反向搜索 3 步,把这些状态保存好,然后每次只要搜索正向的 2、 3 步就好了。依此修改了代码之后,立即变得飞快了。
其它的都是水题,就不说了。
总地来说,这两场比赛嘛,让我们都做出 6 题了,肯定是难度不大。其实每年的暑期练习赛都是这样,看似做出的题挺多,其实真正到了地区赛,我们还是只能做出 1 题。所以这种比赛的成绩并不重要,重要的还是抓紧时间多训练训练吧。