1.问题描述
一个石油传送网络可由一个加权有向无环图G表示。该图中有一个称为源点的顶点S ( 保证S的入度为0 ) , 从S出发 , 石油流向图中的其他顶点 。G中每条边上的权重为它所连接的两点间的距离。
在输送石油的过程中 , 需要有一定的压力才能使石油从一个点到达另一个点,但压力会随着路程的增加而降低 ( 即压力的损失量是路程的函数 ) 。
因此为了保证石油在网络的正常运输,在网络传输中必须保证在任何位置的压力都要不小于最小压力Pmin。为了维持这个最小压力,可在G中的一些或全部顶点放置压力放大器 , 压力放大器可以将压力恢复至该网络允许的最大压力Pmax。
可以设d为石油从压力Pmax降为压力Pmin所走的距离 , 在无压力放大器的情况下石油可运输的距离不超过d .
在该问题中 , 需要在G中放置最少的压力放大器 , 使得该传输网络从S出发,能够将汽油输送到图中的所有位置。
2.相关假设
①为了简化计算 , 每走一个单位长度就会降低一个单位压力且Pmin=0 , 即d=Pmax - Pmin = Pmax
② 起点S处的压力为Pmax
③如果存在一个点x , x有多条入边e[1]…e[n] , x处的压力p[x] = max(p[x], p[e[i].from] - e[i].cost)
即x处的压力为到达x处的压力中的最大值
④图中每条边上的任何一点都要能到达: 例如 如果存在一个点x , x处的压力>Pmin , 但存在一条边 e , p[e.from] - e.cost < Pmin 这也是不合法的。
3.基本要求
①给出两种方法以解决上述问题,思考如何验证两种方法的正确性.
②比较两种方法的时间和空间性能,用图表显示比较结果。
4.数据集描述
①该数据集提供100组石油网络的数据 , 可自行选择进行测试 , 输入数据在input文件夹下, 标准输出数据在outputSTD文件夹下
②在Image文件夹下存放了每组结果的可视化图片 , 标注了边权 , 压力 , 是否放置放大器 . (注 : 仅供参考 , 可能存在多种方案)
③该数据集保证加权有向无环图 , 保证只有一个入度为0的源点
④根目录下的make.py为数据生成器 , 数据生成器依赖于库cyaron , 你可以阅读该文档以了解其使用方法和原理 cyaron并没有指出生成的dag只有一个源点 , 但他的写法确实只有一个源点
5. 数据集格式
输入数据 :
第一行包含三个整数 [n, m, l]
, 源点均为 1
号节点
n
表示节点个数m
表示边的个数l
表示最大边权 ( 可以认为是Pmax )
接下来 m
行 , 每行三个正整数[x, y, c]
用来标识一条有向边
x
表示起点y
表示终点c
表示边权
输出数据 :
一行一个整数 , 表示最少的放大器的个数
题解
以图形化输出solve12.jpg为例
输入建立了从结点i到j的多条路径,其中有用的是最短路径和最长路径。用Pi 表示nodei 的压力,对于边nodei -> nodej,nodej 的压力等于nodei 的压力减去路程耗费cost。初始时只有node1的压力是确定的,其它所有结点都根据石油运送过程中的路径损失、压力放大器的设置点来更新。
根据题意要求,对此题分别按最短边和最长边建图,nodei -> nodej 的最短边用来更新nodej 的压力,最长边用来作边界条件,检测该放大器设置是否合理。即,在保证最长边(最大cost)能到达的前提下,用最小边(最小cost)来更新nodej 的压力。
例如solve12.jpg,如果 node7 不放增压器,pre7 = 19,虽然pre8 的值是用 node6 -> node8 的耗费来更新的,但此时 node7 无法到达 node8 ,因此 node7 必须要放增压器。
方法
本题方法众多,已了解到的同学们使用的方法就包括枚举、剪枝、prim、Floyd、分支定界等,在此介绍利用bfs贪心和利用dfs分支定界两种方法。
①bfs贪心:
首先,对于所给的图,记录它的拓扑序列,把源点 node1 插入到队列中,按照拓扑序列对图进行bfs遍历。
例如solve12.jpg:
按照插入边的顺序,它的拓扑序列为:[1, 2, 3, 4, 5, 6, 7, 8]。用p来表示当前结点的压力大小。此时检查1,由于1是源点,故p = Pmax = 51。然后,检查所有与1相邻接的点,即2、3、6,由于1到2、3、6的最大花费的边都能够成功到达,因此1不必放加压器。于是,用最小花费的边来更新2、3、6结点的压力值。此时pre2 = 1,pre3 = 11,pre6 = 8。
然后考察2,此时p = 1。与2相邻接的点有4、5、6,按照结点序列先检查4,从2可以到达4,更新pre4 = 0;但发现从2不可以到达5,于是给结点2放上加压器,重新按照序列检查、更新2能到达的所有结点,也就是再从4开始更新一遍,置p = Pmax,pre2 = Pmax,pre4 = 50,pre5 = 10,pre6 = 29。
接着,按照上述步骤,依次检查图中的所有结点。总体思想是,按照拓扑序列检查到nodei 时,按顺序检查nodei 能到达的所有结点,如果发现从nodei -> nodej 不能到达,那么给nodei 放上加压器,重新按顺序检查、更新一遍 nodei 能到达的所有结点。检查时用最大边,更新时用最小边。
由于每次只是贪心地考虑当前结点,因此结果集可能会比标准结果集大一点,但经过测试,绝大多数情况下贪心的方法即是标准结果。
②dfs分支定界:
本题使用dfs进行递归也比较直观。使用一个结果集Vset[]表示当前图中加了放大器的点,初始时都是0。从源点1开始到结点n进行dfs,每搜索到一个新的结点,就对这个点进行左递归和右递归,左递归表示这个点不加压力放大器,右递归表示这个点要加压力放大器。递归完毕后保证每个点都被验证过0和1两种状态,有点类似穷举的方法,在全部可行解中找出最小的一个可行解。
在每次递归后,判断在当前加压情况下,从s出发是否能到达每一个点,如果能到达,说明这是一个可以考虑的分支,反之则说明这不是一个可考虑的分支。
如果这是一个可考虑的分支,那么要做对应的修改:如果得到的结果集比之前得到的小,即有更小的加压方式,那么把加压方式的数组和加压点个数进行更新。在所有修改操作后,筛选出结果集最小的一种情况。
反之,如果不是一个可考虑的分支,那么不做任何修改。判断该分支是否可行的方法也是利用了拓扑序列进行bfs遍历,可参考代码中相应部分。由于拓扑序列记录了每个点的入度,因此也可以顺便记录其出度,对于此方法,可以进行剪枝优化:把出度为0的点全部剪掉。
更新一个同学的做法:
③全排列:
每一个结点根据放置放大器和不放放大器可有1和0两种状态,求出所有结点状态的全排列,即列出所有可能的放置情况。然后,对于每一种放置情况,都对图进行一次遍历,看这种放置方法是否可行,对于所有的可行解维护一个最小值。
这种方法也是暴力枚举的思想,复杂度比较高,但确实写起来比较简单,遍历图也只需要一个函数就能完成。
文件下载地址
https://github.com/zj23333/CDDSA/