11.5 竞赛题目选讲
例题11-11 有趣的赛车比赛(Funny Car Racing, UVa 12661)
在一个赛车比赛中,赛道有n(n≤300)个交叉点和m(m≤50000)条单向道路。有趣的是:每条路都是周期性关闭的。每条路用5个整数u, v, a, b, t表示(1≤u,v≤n,1≤a,b,t≤105),表示起点是u,终点是v,通过时间为t秒。另外,这条路会打开a秒,然后关闭b秒,然后再打开a秒,依此类推。当比赛开始时,每条道路刚刚打开。你的赛车必须在道路打开的时候进入该道路,并且在它关闭之前离开(进出道路不花时间,所以可以在打开的瞬间进入,关闭的瞬间离开)。
你的任务是从s出发,尽早到达目的地t(1≤s,t≤n)。道路的起点和终点不会相同,但是可能有两条道路的起点和终点分别相同。
【分析】
本题是一道最短路问题,但又和普通的最短路问题不太相同:花费的总时间并不是经过的每条边的通过时间之和,还要加上在每个点等待的总时间。还记得第9章中的例题“基金管理”吗?该题的决策不仅依赖于状态本身,还依赖于该状态下现金的最大值。本题也是一样:仍然调用标准的Dijkstra算法,只是在计算一个结点u出发的边权时要考虑d[u](即从s出发达到u的最早时刻)。计算边权时要分情况讨论,细节留给读者思考。
例题11-12 水塘(Pool construction, NWERC 2011, UVa1515)
输入一个h行w列的字符矩阵,草地用“#”表示,洞用“.”表示。你可以把草改成洞,每格花费为d,也可以把洞填上草,每格花费为f。最后还需要在草和洞之间修围栏,每条边的花费为b。整个矩阵第一行/列和最后一行/列必须都是草。求最小花费。2≤w,h≤50,1≤d, f, b≤10000。
图11-16 水塘问题示意图
例如,d=1,f=8,b=1,则图11-16中的最小花费为27,方法是先把第一行的洞填上草(花费16),然后把第3行第3列的草挖成洞(花费1),再修10个单位的围栏)。
【分析】
围栏的作用是把草和洞隔开,让人联想到了“割”这个概念。可是“割”只是把图中的结点分成了两个部分,而本题中,草和洞都能有多个连通块。怎么办呢?添加源点S和汇点T,与其他点相连,则所有本不连通的草地/洞就能通过源点和汇点间接连起来了。
由于草和洞可以相互转换,而且转换还需要费用,所以需要一并在“割”中体现出来。为此,规定与S连通的都是草,与T连通的都是洞,则S需要往所有草格子连一条容量为d的边,表示必须把这条弧切断(割的容量增加d),这个格子才能“叛逃”到T的“阵营”,成为洞。由于题目说明了最外圈的草不能改成洞,从S到这些草格子的边容量应为正无穷(在这之前需要把边界上的所有洞填成草,累加出这一步所需的费用)。
同理,所有不在边界上的洞格子往T连一条弧,费用为f,表示必须把这条弧切断(割的容量增加f),才能让这个洞变成草。相邻两个格子u和v之间需要连两条边u->v和v->u,容量均为b,表示如果u是草,v是洞,则需要切断弧u->v;如果v是草,u是洞,则需要切断弧v->u。
这样,用最大流算法求出最小割,就可以得到本题的最小花费。
例题11-13 混合图的欧拉回路(Euler Circuit, UVa10735)
给出一个V个点和E条边(1≤V≤100,1≤E≤500)的混合图(即有的边是无向边,有的边是有向边),试求出它的一条欧拉回路,如果没有,输出无解信息。输入保证在忽略边的方向之后图是连通的。
【分析】
很多混合图问题(例如,混合图的最短路)都可以转化为有向图问题,方法是把无向边拆成两条方向相反的有向边。可惜本题不能使用这种方法,因为本题中的无向边只能经过一次,而拆成两条有向边之后变成了“沿着两个相反方向各经过一次”。所以本题不能拆边,而只能给边定向,就像第9章的例题“一个调度问题”那样。
假设输入的原图为G。首先把它的无向边任意定向,然后把定向后的有向边单独组成另外一个图G'。具体来说,初始时G'为空,对于G中的每条无向边u-v,把它改成有向边u->v,然后在G'中连一条边u->v(注意这个定向是任意的。如果定向为v->u,则在G'中连一条边v->u)。
接下来检查每个点i在G中的入度和出度。如果所有点的入度和出度相等,则现在的G已经存在欧拉回路。假设一个点的入度为2,出度为4,则可以想办法把一条出边变成入边(前提是那条出边原来是无向边,因为无向边才可以任意定向),这样入度和出度就都等于3了;一般地,如果一个点的入度为in(i),出度为out(i),则只需把出度增加(in(i)-out(i))/2即可(因为总度数不变,此时入度一定会和出度相等)。如果in(i)和out(i)的奇偶性不同,则问题无解。
如果把G'中的一条边u->v反向成v->u,则u的出度减1,v的出度加1,就像是把一个叫“出度”的物品从结点u“运输”到了结点v。是不是很像网络流?也就是说,满足out(i)>in(i)的每个点能“提供”一些“出度”,而out(i)<in(i)的点则“需要”一些“出度”。如果能算出一个网络流,把这些“出度”运输到需要它们的地方,问题就得到了解决(有流量的边对应"把边反向"的操作)。
细节留给读者思考。相信经过了前面题目的锻炼,读者一定可以解决这个问题。
例题11-14 星际游击队(Asteroid Rangers, ACM/ICPC World Finals 2012, UVa1279)
三维空间里有n(2≤n≤50)个匀速移动的点,第i个点的初始坐标为(x,y,z),速度为(vx,vy,vz)。求最小生成树会改变多少次。输入保证在任意时刻最小生成树总是唯一的,并且每次变化时,新的最小生成树至少会保持10-6个单位时间。
【分析】
不难发现:最小生成树切换的时刻一定对应着某两条边(u1,v1)和(u2,v2)的权值相等。一共有O(n2)条边,因此有O(n4)种可能的切换时间(称为事件点)。
最容易想到的做法是把所有可能的事件点按照时间从小到大排序,依次计算每个事件点之后0.5*10-6时刻的最小生成树(题目保证了这期间最小生成树不会发生变化),判断它是否和上一个最小生成树相等。假设使用O(n2)时间复杂度的prim算法,总时间复杂度为O(n6),需要优化。
一个行之有效的优化是:假设一个事件点对应(u1,v1)和(u2,v2)的权值相等。只有当(u1,v1)和(u2,v2)恰好有一个在当前最小生成树,且在该事件点之后这条边会变得比另一条边大时,才有可能发生切换。实践中满足这个条件的事件点非常少,运行效率大幅度提高(1)。
例题11-15 帮助小罗拉(Help Little Laura, Beijing 2007, UVa1659)
图11-17 涂色方法示意图
平面上有m条有向线段连接了n个点。你从某个点出发顺着有向线段行走,给沿途经过的每条线段涂一种不同的颜色,最后回到起点。你可以多次行走,给多个回路涂色。可以重复经过一个点,但不能重复经过一条有向线段。如图11-17所示是一种涂色方法(虚线表示未涂色)。
每涂一个单位长度将得到x分,但每使用一种颜料将扣掉y分。假定颜料有无限多种,如何涂色才能使得分最大?输入保证若存在有向线段u->v,则不会出现有向线段v->u。n≤100,m≤500,1≤x,y≤1000。
【分析】
本题的模型是:给出一张有向图,从中选出权和最大的边集,组成若干个有向圈。这里的边权等于题目中的dx-y,其中d为边的两个端点的欧几里德距离。
由于每个点并不一定只属于一个有向圈,因此例题“最优巴士路线设计”中“匹配后继”的方法不再适用。尽管如此,还是可以建立一个费用流模型:在原图的基础上设每条边的容量为1,费用为边权,要求找一个流,使得所有结点都满足流量平衡(入流等于出流)条件,且总流量乘以费用的总和最大。这样的模型没有源也没有汇,而且每个结点都要满足流量平衡,所以也没有“最大流”这种说法,称为循环流(circulation)。换句话说,此处要解决的问题是最大费用循环流问题。
对于最大费用流问题,通常会把所有边权取负,变成最小费用流问题。最大费用循环也不例外:把每条边的边权改成-dx+y,则问题转化为最小费用循环流问题。这个问题的解决方法和最小费用最大流有些类似,只不过每次不是求一条s-t的最小费用增广路,而是找整个图的一个负费用增广圈。沿着负费用增广圈进行增广之后,每个结点的流量平衡不会被破坏,而整个循环流的总费用变小了。换句话说,求解最小费用循环流的伪代码就是:
while(find_negative_cycle()) augment();
根据残量网络的概念不难得出:找负费用增广圈等价于在残量网络中找负权圈——这正是Bellman-Ford算法的拿手好戏。
上述算法可以很好地解决本题,但是本题还有一个更有意思的方法,可以避开负圈:新增附加源s和附加汇t,对于原图中的每条负权边u→v变成3条边:s→v,v→u和u→t,容量均为1,但是v→u的费用为原来的相反数,其他两条边的费用为0。原图中的正权边u→v保持不变:容量为1,费用为权值。
经过这样的处理之后,所有的边都变成正权了,但是网络里出现了很多重边,需要处理一下:对于任意点u,假设s→u的弧有a条,u→t的弧有b条,则当a>b时只保留一条s→u的弧,容量为a-b,删除所有u→t的弧;a<b时类似;a=b时删除所有s→u和u→t的弧。处理完毕之后,只需求一次s-t最小费用最大流,则求出的最小费用值再加上原图的所有负权之和就是循环流的最小费用值。
是不是很神奇?作为本章最后的“压轴题”,请读者思考这样做的正确性。另外,这种处理负权的方法具有一定的普遍性,有兴趣的读者可以自行研究。