本题是2015年提高组复赛第二天的最后一题,也就是说,是整体而言最难的题目。而其思路也证明了这一点。但是,在江老师一句“LCA”的启发后,我历尽了半节课,终于想了出来一个做法。
【题意】
给定一棵无向的连通的一共有n个节点的树,并给出m个“运输计划”,每个运输计划要从一个点一路将货物运输到另一个点,费用为沿途所有边的权值之和。
现在,可以允许你将某一条边的权值设为0,问最小的总费用和是多少?
【分析】
DFS+LCA+线段树。我想到的方法大概要用到这三个结构。当然,直接上树链剖分也是很好的方法,但是可能比较难写。
首先,很容易看出,每一个“运输计划”的路径必然是一条所谓的LCA路径,即先到两点的LCA,再到剩下来的一个点。
因此,我们维护某一条路径被“运输计划”经过的次数ti,容易看出,ti就是说所有“运输计划”路径的交于第i条边的次数。然后,我们可以看出,这一道题就是主要求ti*di的最大值!于是,di无需维护,那么只需要维护ti了。
首先,LCA可以仅用O(log2n)的时间求出,所以,我们首先设某个“运输计划”是从点i运到点j,也就是点对(i,j)。那么,我们所需要的就是覆盖两条线段(i,LCA(i,j))和(LCA(i,j),j)。线段覆盖怎样做呢?线段树!所以,我们可以在O(log2n)的时间内处理每一个“运输计划”。当然,我们还需要求出LCA,所以我们怎样都应该用到DFS,总的时间复杂度为O(n+mlog2n+n)。大概不会怎么超时。
但是,这一道题更高级的做法是树链剖分,具体的解法好像《高级数据结构》上类似USACO背景的一道题类似,而且好像比那道题目还要简单很多。
从另一个角度来想,我们也可以用树状数组!想想看,我们要求修改一个区间,查询一个点,那我们可以更改sum数组的含义,使sum(n)=a1+a2+a3+……+an,那我们要修改[i,j]区间,就只需将ai减去1,aj-1加上1即可。这可以很容易用树状数组来维护,只需要两次点状的修改即可。
【实际做法】
行了,我终于有点时间来写了。
首先,DFS一遍,求出每一个遍历到的节点(包括回溯!),并打成一个表。打成的这个表就用线段树来维护,或者说树状数组应该也可以。这个DFS应该是很轻松的。
接着,对于每一个“运输计划”,我们首先求出(i->j)这一计划中(i,j)的LCA,这可以在O(log2n)的时间内求得,相信各位印象十分深刻,倍增、时间戳以及并查集都可以做到这一点,但是我们要求一个离线的算法,并查集可能是最好的选择了。但是实际上在线算法在这里更加直观。如果我们求出了(i,j)的LCA(i,j),那么我们可以用线段树或树状数组在[i,LCAfirst(i,j)]和[LCAend(i,j),j]间加上1。注意,在这里LCAbegin和LCAend是指在[i,j]区间内,LCA(i,j)所出现的第一次和最后一次的相应坐标。不过同样要注意的是(i,j)的次序并没有关系,所以可以用swap()将两种情况归为一种。
总计而言,就是DFS先求出表(和RMQ+时间戳的那个LCA方法差不多,在这里RMQ可以直接用线段树做),然后将每个运输计划的路线覆盖上去,也就是线段覆盖。最后只需要扫一遍就可以了。
至于树状数组的区间修改+点状查询,可以这么理解:用树状数组来直接维护原来的数,但是换了一种构造方法。比如说某个数i的总出现次数ti,我们就可以将ti设为一系列辅助数组aj的和,令得ti=a1+a2+a3+……+ai,那么,让t数组在[i,j]区间内都加上1,只需将a[i]加上1,并将a[j+1]减去1即可。
如此这般,最终的时间复杂度不算常数大概是O(n+mlog2n+nlog2n)。应该可以过……但是好像又听说有可以直接用线段树的方法,不知道有多简单?
【总结】
LCA真的是一个很好的工具,特别是在树中。但是实际上,树上还有很多非常有用的算法和结构,大大多于图中的算法,而且往往更为简便。因为树的天生无后效性,我们有树型DP,因为树的特殊结构,我们有LCA即是最短路,还有Tire、平衡树、Splay、Treap这些图根本就难以实现的东西,限制一下,变成树就简单多了。