2017华为软件挑战赛总结
2017华为软件挑战赛总结
这次比赛是去年做的, 自己之前没有总结,现在才开始总结,很多东西快想不起来了,真是惭愧
赛题主要内容和目的
初赛题目和内容
- 给你一个流网络(边有容量和单位流量费用),已知有一些节点有流量需求(消费节点),现要选一些节点部署服务器(服务节点),给消费节点传输流量,使得在满足所有消费节点流量需求的条件下,最小化成本(服务器购买成本+线路流量费用)
- 服务器输出能力无上限,一个服务节点可以服务多个消费节点,一个消费节点也可以从多个服务节点获取流量
- 每台服务器的购买成本均相同
- 输出结果中每条边上的流量必须为整数
数据规模
- 网络节点数量不超过1000,每个节点出度不超过20,消费节点数量不超过500
- 边容量和单位流量费用为[0,100]的整数,服务器与消费节点的带宽需求为[0,5000]的整数
- 非常好的一个NP-hard问题,数据规模和时限的设置比较合理,没有现成的模型可以立即套用
- 可以对整个比赛分为两个部分: 服务器选址(启发式搜索) + 最小费用流(计算网络流的最小费用)
- 最小费用流可以认为是求解算法的底层基础设施,费用流越快,基本上可以搜索的空间就越大,可以实施的操作也越多,得到更优解的可能性也越大
- 费用流的速度决定了算法能力的上限
- SPFA增广路算法--->原始—对偶算法("zkw"算法)--->Cost Scaling--->增量式zkw算法--->网络单纯形
最小费用流问题
- SPFA增广路算法--->原始—对偶算法("zkw"算法)--->Cost Scaling--->增量式zkw算法--->网络单纯形
设施选址问题,搜索算法
- 采用局部搜索的方法(模拟退火), 从一个初始可行解出发,不断改进当前解,如果到了局部最优,尝试用一些策略跳出它,再去改进,如此循环
- 全局搜索(遗传算法和粒子群算法), 参数太多, 更新速度较慢,大Case因为费用流的速度回降低, 很容易遇到瓶颈
- 与邻居以及当前可行解的路由上的点进行启发式交换,可以进一步从质上提高解的质量,而且速度是遗传和粒子群所远不能比
复赛变化点
- 在初赛赛题的基础上,加入了服务器的差异化约束条件——服务器有10个档次
- 每个档次的服务器购买成本和容量都不同,每个网络节点的部署费用也可能不同。
- 也就是说,在一个网络节点部署一台服务器的成本 = 该服务器的购买成本 + 网络节点的部署成本
数据规模: - 网络节点数量不超过10000,每个节点的出度不超过10000,消费节点数量不超过10000
- 边容量与单位流量费用为[0, 100]的整数
- 服务器档次不超过10个
- 服务器成本为[0, 5000]的整数
- 服务器的输出能力、节点的部署成本与消费节点的流量需求为[0,10000]的整数
- NP-hard的双层叠加: 除了要选择比较好的部署服务器的点集之外,还要确定服务器的档次,而一台服务器可选的档次就有10个之多,因而相当于面对两个NP-hard问题
- 30个节点部署服务器, 加入档次的条件后,即便确定了部署的服务器数量、在哪部署,还有10^30种可能
- 每次计算完费用流后,根据服务节点的输出流量确定相应的档次,可以有效避免成本的浪费
- 在搜索算法的前期,我们基本不考虑服务器档次,执行可行解中服务器的减少、增加和交换等操作的组合,和初赛算法类似;后期将档次的调整引入这些操作算子,并在算法的最后把降档和升档作为两个相对独立的操作过程又执行了一遍
- 只能用C/C++/Java自己实现算法(在我看来,理解原理是一回事,用高效的数据结构实现又是一回事。比如,多少人能手动把修正单纯形、割平面等算法撸出来)
ZKW算法 (算法复杂度是多少没有分析过) -- 最小费用可行流
- ZKW算法详解
- 增广路径不机械使用源点的标号, 应该是源点汇点标号之差
- 重赋权技术, 即通过对每个节点合适的顶标, 使reduced cost非负, 这个顶标使用到汇点的距离
- 根据流量平衡条件, 根据新的费用算出原费用, 相当于一次SPFA操作
- 将所有负边强制满流, 称为推流操作
- 什么是负圈?
- 存在一个环(从某个结点出发又回到自己的路径), 而且这个环上所有路径之和是负数
- 负权边, 一条路径的和为负数
- 连续最短路径算法, 弄懂SPFA算法和Dijkstra算法;
- dijkstra算法讲解参考
- Dijkstra算法的特点: 以起点为中心向外层层扩展, 直到扩展到终点为止; 贪心算法, 不能处理负边的情况
- 算法思想: 设G=(V顶点, E边)是一个带权有向图, 把顶点集合分成两组, 第一组为已经求出最短路径的顶点集合(用S表示, 初始时S只有一个源点, 以后每求得一条最短路径,就将其加到S中, 只到全部顶点加入到S中, 算法就结束)
- 第二组为其余未确定最短路径的顶点集合(用U表示)
- 按最短路径长度的递增次序依次把第二组的顶点加入S中;
- 在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度
- 每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度
- SPFA可以处理负权边的情况:
- SPFA算法的精妙之处在于不是盲目的做松弛操作(),而是用一个队列保存当前做了松弛操作的结点
- 只要队列不空,就可以继续从队列里面取点,做松弛操作
- 初始时将源加入队列;每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,如果该点没有在队列中,则将其入队。 直到队列为空时算法结束
- 动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止
- dijkstra算法讲解参考
- 什么是拓扑排序?
- 将有向图中的顶点以线性方式进行排序。即对于任何连接自顶点u到顶点v的有向边uv,在最后的排序结果中,顶点u总是在顶点v的前面(打怪升级)
- 一个有向图能被拓扑排序的充要条件就是它是一个有向无环图(DAG:Directed Acyclic Graph)
- 拓扑排序的实现步骤:
- 在有向图中选一个没有前驱的顶点并且输出
- 从图中删除该顶点和所有以它为尾的弧(删除与它有关的边)
- 重复上述两步, 直达所有顶点输出, 或者当前图中不存在无前驱的顶点为止, 后者代表我们的有向图是由环的
- 可以通过拓扑排序来判断一个图是否有环
- 拓扑排序的实现算法
- Kahn算法
- 使用一个栈保存入度为0的顶点, 然后输出栈顶元素并将和栈顶元素有关的边删除, 减少和栈顶元素有关的顶点的入度数量并且把入度减少到0的顶点也入栈
- 基于DFS的拓扑排序算法
- DFS深度优先搜索, 它每次都沿一条路径一直往下搜索, 直到某个顶点灭有了出度时, 就停止递归, 往回走, DFS很像Kahn算法的逆过程
- Kahn算法
- 定义 Di 为点 i 的距离标号(离起点的最短路径), 任何一个最短路算法保证, 算法结束时对任意指向顶点 i 、从顶点 j 出发的边满足 Di <= Dj + cij (条件1), 且对于每个 i 存在一个 j 使得等号成立 (条件2). 最短路径算法结束后, 恰在最短路上的边满足Di = Dj + cij
- 在最小费用流中的计算中, 我们每次沿Di = Dj + cij的路径增广后, 都不会破坏条件1, 但是可能破坏条件2, 使我们找不到每条边都满足Di = Dj + cij新的增广路, 只好利用Dijkstra, SPFA等算法重新计算新的满足条件2的距离标号
- 什么是路径增广?
- 路径增广是来自网络流的一种概念: 一定能找到一条路上每段路还允许流过流量的最小值delta(容量 - 流量); 把这条路上每一段流量都加上这个delta值, 一定可以保证这个流依然是可行流, 这样我们可以得到更大的流, 它的流量是之前的流量加上这个delta, 这条路就叫做增广路。
- 增广路径是找出在残留网络中从源点到汇点的有向路径
- 增广路径的残留容量是路径中任意边所形成的最小残留容量, 我们可以沿着增广路径从源点到汇点发送额外的流
- 流x是最大流当且仅当这个残留网络中不包含其他增广路径
- KM算法中可以不断修改可行顶标,不断扩大可行子图
- 什么是KM算法?
- KM算法是对匈牙利算法的一种贪心扩展, 为了高校求解二分图最佳完美匹配问题的一种算法
- Kuhn - Munkras算法流程:
- 初始化可行顶标的值
- 用匈牙利算法寻找完备匹配
- 若未找到完备匹配则修改可行顶标的值
- 重复2,3直到找到相等子图的完备匹配为止, 完美匹配是最大匹配
- KM算法的核心部分即控制修改可行顶标的策略使得最终可到达一个完美匹配
- 什么是匈牙利算法?
- 匈牙利算法是为了解决二分图的最大匹配(二分图的最大匹配也可以转换为一个网络流的问题), 二分图等价于不含奇数条边的环的图
- 交错路
- 增广路: 起点和终点都为未匹配点的交错路为增广路(这里的增广路和网络流中的增广路意义不同)
- 增广路一定有奇数条边, 而且未匹配边一定比匹配边多一条
- 如果找到了一条增广路,那么将未匹配点与匹配边的身份调换,那么匹配的边数就多了一条,这样直到找不到增广路为止,那么整个图的匹配的边数一定最大,也就是找到了二分图的最大匹配
- ZKW的核心就是在始终满足条件1的距离标号上不断修改, 直到可以继续增广(满足条件2)
- 初始标号为0, 不断增广, 如果不能增广, 修改标号继续增广, 直到彻底不能增广(源点的标号已经被加到了正无穷)
- ZKW的程序中, 所有的cost均表示reduced cost, 即 cij = cij - Di + Dj; 另外这个算法不能直接用于有任何负权边的图, 更不能用于负权边的情况
- ZKW算法只需要增广, 改标号, 不需要队列, BFS, SPFA等复杂的操作
- ZKW算法的精髓在于修改顶标, 然后不断增广, 不能增广了, 在看能不能修改顶标, 如果能就继续不断增广, 如果不能的话就返回函数结果
- 实际增广是沿最短路进行的, 时间复杂度与SPFA等连续最短路算法一致, 但节约了SPFA或Dijkstra的运行时间, 算法常数很小, 速度较快
- Bellman-Ford算法
- Dijkstra的贪心算法的本质是如下条件要成立:如果存在某条路径p,使得p是从顶点u到v的最短路径:
- p = u->v1->v2...->vn->v, 则对于任意的1<=k<=n, 需要满足 d(u,vk) < d(u,v)。
- 很显然,这个条件要满足的话,那么需要图中无负权边才行
- Dijkstra和Bellman-Ford的区别是:
- 前者过于贪心,负权的情况他不考虑,也无能力考虑。
- 后者反复调整,保证精确,顺便可以探知无解的情况。
- Floyd算法:
- Floyd-Warshall算法(Floyd-Warshall algorithm)是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。Floyd-Warshall算法的时间复杂度为O(N3),空间复杂度为O(N2)
- Floyd算法是一个经典的动态规划算法;
- 从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j;
- 所以,我们假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。
- Floyd算法描述:
- 从任意一条单边路径开始。所有两点之间的距离是边的权(求和),如果两点之间没有边相连,则权为无穷大;
- 对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它;
- Floyd算法过程矩阵的计算----十字交叉法
服务器选址算法
- 近邻算法的大致思路
- 第一步: 初始化一个bool型数组, 有800个网络结点就声明大小为800的bool型vector v,
v[i]
为false表示不选择第i
号网络结点放置服务器,v[i]
为true表示选择i
号网络结点放置服务器; 可以随机初始化那些结点放置服务器,那些结点不放置服务器; 也可以与消费结点相连的结点初始化为放置服务器; - 第二步: 循环迭代一次数组v(这个相邻是id相邻, 1,2,3,4,5···), 针对每一个结点会存在两种情况: 选与不选
- 针对
v[i]
之前本来是true的情况(也就是遍历的时候节点i已经被选择当做服务器)- 把
v[i]
设置为false(即从服务器集合中删去第i
个网络结点), 再计算一次最小费用流;删掉一个结点会存在三种情况- 不能满足消费结点网络流的需求, 把第
i
号节点的v[i]
设置为true, 并加入服务器集合中 - 能满足消费结点的需求, 但是总费用变得更高, 把第
i
号节点的v[i]
设置为true, 并加入服务器集合中 - 能满足消费结点的需求, 而且总费用变得更低, 只有这种情况才真正的删除这个第
i
号网络节点
- 不能满足消费结点网络流的需求, 把第
- 把
- 针对
v[i]
之前本来是false的情况(第i
号节点不是服务器)- 先把第
i
号节点加入服务器集合,v[i]
设置为true, 并重新计算一次最小费用流; 添加一个结点也会存在三种情况- 不满足消费节点网络流的需求, 证明添加第
i
号节点之前和之后, 服务器集合都是无解的, 所以继续添加下一个结点知道有解 - 能满足消费结点的需求, 但费用更高, 把第
i
号节点设置为false, 并从服务器集合中删除 - 能满足消费结点的需求, 而且费用更低, 将全部最小费用更新为总费用值, 并继续迭代
- 不满足消费节点网络流的需求, 证明添加第
- 先把第
- 针对
- 第三步: 遍历服务器集合中的所有结点, 与原来不是服务器结点的相邻结点进行交换(这里的相邻结点是指邻接表中的相邻, 即在图中连接的相邻)
- 存在两种情况: 造成更好的结果(满足消费节点的需求, 并且总费用还降低了)和更坏的结果(总费用增高, 或者是不满足消费结点的需求)
- 如果结果变好则继续进行交换, 如果结果变坏则还原成之前的情况
输入输出
- 输入输出要求都以文件的形式输入输出
- 实现了两种不同的构图方式: 邻接矩阵和邻接表
- 输出利用DFS递归搜索所有有效的路径, 并把其保存在一个二维数组中