2020中兴捧月算法大赛迪杰斯特拉赛道初赛题解

摘要

这是2020中兴捧月算法大赛迪杰斯特拉赛道初赛的题解,所述程序在初赛中获得了95.132分。下文将分两个部分阐述自己的算法思路。首先介绍程序中使用的各种数据结构,这反映了我对赛题的理解,也是本文后续内容的基础;接着介绍参赛这段时间以来我的解题思路的主要演变及相关程序的实现。

1 程序中使用的数据结构

1.1 几个基本数据类型

我在程序中对车道(车箱)、道路、站点、货物进行了重新的编号,每个相应实例都有自己的id,可以通过id映射到相应的实例,比如通过站点id映射到某个站点的实例。id的类型为id_t(无符号16位整数类型)。此外,使用road_lane_t唯一的标记一个车道;同时使用src_dst_t表示一对源宿。图示如下:
在这里插入图片描述
虽然目前使用的id_t仅能标识65536个实例,但如果有需要的话,可以很方便的将其提升为无符号32位整数类型。

1.2 车道(Lane)

赛题中将其称为车箱,属于道路的一部分,是货物(链路)完成路径规划后需要扣除的系统资源。该类的成员如下:
在这里插入图片描述

1.3 道路(Road)

一条道路由多条车道构成,因此道路和组成它的车道之间数据的一致性必须得保证。所以我将Road作为Lane的友元,所有Lane的方法均为私有,访问Lane必须通过Road进行,而Road则通过访问Lane的私有方法最终实现对Lane的访问。Road的成员如下:
在这里插入图片描述

1.4 站点(Station)

站点是货物进行中转的场所,通常,站点拥有一定数量的工人用于处理货物的收发、分流以及汇聚。由于工人是和工位相联系的,而工位又和车道相联系,因此我将工人和车道联系起来,建立一个元素类型为road_lane_t的集合来保存分配出去的工人。具体的,Station的各个成员如下:
在这里插入图片描述

1.5 货物(Goods)

货物是路径规划的主体,一个货物拥有的信息较少,主要是货物的质量和必经站点。具体的,Goods的各个成员如下:
在这里插入图片描述
值得说明的是,根据题目规则,路由是无向的,因此货物的两个端点站并没有固定哪一个是源、哪一个是宿。Goods的get_src方法获取的是must_route_stations的首个元素(站点id),get_dst方法获取的是must_route_stations最后一个元素。至于为什么要实现一个方法来反转货物的必经站点,下文会有阐述。

1.6 系统资源(SystemResource)

为货物规划好路径后,是需要占据一定的系统资源的,系统资源主要是这两类:车道的容量资源、站点的工人资源。我将系统资源抽象为class SystemResource
在这里插入图片描述
将路径规划过程中消耗的系统资源使用专门的结构暂存起来有一个好处,当路径规划到后期由于资源问题无路可走时,只需要忽略暂存的系统资源即可,因为没有真的扣除相应资源,所以不存在系统状态回退的问题,这对于回溯之类的搜索方法的实现是有帮助的。下面举一个例子来进一步介绍系统资源类:
在这里插入图片描述
路径规划成功后,该路径消耗的所有系统资源都会暂存在SystemResource的容器中,然后只需遍历该容器,逐一扣除相应的资源,如此,一条路径的规划就彻底完成了。

1.7 物流系统(LogisticsSystem)

LogisticsSystem是我的程序中内容最多的一个类,这个类描述的是整个物流系统,所有其他对象,包括道路、站点、货物等都在LogisticsSystem的统筹之下。该类的数据成员全部为私有,主要如下:
在这里插入图片描述
LogisticsSystem对外开放的方法主要如下:
在这里插入图片描述
以上仅仅是对LogisticsSystem的简介,更多更具体的内容将放在下文中展开,结合算法思路来说明。

2 算法思路

初赛期间,程序有过一次较大程度的调整,这里我将调整之前的程序称为初赛初版,调整之后的程序称为初赛终版。相应的,对于本题的解题思路也有一次转变。下文将先介绍初版程序和思路,并分析其中的缺陷,以及我是如何转变解题思路的;然后介绍终版的程序和思路。

2.1 初赛初版:路由表、深度优先搜索、路径惩罚

刚刚看到题的时候,我觉得可能会用到迪杰斯特拉算法,毕竟赛题组就是以迪杰斯特拉为名,但仔细思考以后,决定还是放弃使用该算法规划路径。我是这样考虑的,迪算法寻找的是最小跳数的路径,但由于系统资源的约束,最小跳数的路径不一定是最优的,当然这可以通过对路径进行加权来解决。然而,加权总得有依据,不能随意加,对于本题,通常都会想到将路径的权值锚定系统资源,即考虑站点工人使用情况和车道占用情况来确定权值。而在路径规划的过程中,资源是不断被消耗的,系统资源是变量,进而导致权值也是变量。这会有什么问题呢?执行一次迪算法,可以得到以某一站点为根的最短路径树,即可以得到好多对源宿的最短路径,如果这个结果可以留到后面用,就可以避免很多重复计算。然而,由于权值是变量,我们无法使用之前迪算法找到的路径。这样一来,每次为货物规划路径都要更新权值,用迪算法跑一遍。再考虑到,每条道路有着几十条车道,整个地图边的数量非常大,因此使用迪算法的程序的复杂度可能会较高。为了避免超时,我就没有采用迪算法。当然,不排除采用迪算法的程序也能得到很好的解,以上只是我个人的一些考虑。接下来介绍我在初版程序中使用的一些策略。

2.1.1 搜索策略

决定采用其他路径搜索算法之后,我首先想到了深度优先搜索,通过深度优先搜索回溯的算法来搜索路径。当然,单纯的满地图的dfs在庞大的解空间面前存在着不可忽视的效率的问题,需要优化。我的优化策略受到路由器转发信息的启发,建立路由表:
在这里插入图片描述
当搜索到某一节点时,下一步该往哪个节点搜会受到路由表的影响,搜索倾向于跳数少的方向。当然,路径的抉择不能只考虑跳数,还要考虑系统资源的消耗,否则程序就退化成了最短路径优先,简单的最短路径优先几乎肯定不会取得好成绩的。

2.1.2 路径惩罚策略

为了在搜索时做出好的路径选择,我设置了一个惩罚函数,为所有能到达未访问的下一跳站点的车道计算一个惩罚因子,然后排序,取惩罚因子最小的前k条车道,顺次尝试,不行则回溯。期间花费了很多时间在如何计算惩罚因子上,考虑了很多因素:
在这里插入图片描述

2.1.3 货物发送策略

对于如何发送货物,即决定哪些货物先发哪些货物后发,这对结果的影响也比较大。对此,赛题组的各位老师给出了按照同源同宿的原则组织货物的建议,我觉得这个思路非常的棒,也就照着实现了。首先将货物分成两份,一份不含必经站点,一份含有必经站点,再将这两份货物按照同源同宿的原则组织,即对每份货物建立一个容器,容器的每个元素是一个链表(list),链表上串着的货物都拥有相同的源宿(源宿位置可以反过来),如下图所示:
在这里插入图片描述
在发送时,先发送不含必经站点的货物,这样做的理由是不含必经站点的货物可以不用绕路,更可能按照最短路径到达,减少系统资源消耗,同时少了必经站点的约束,也更可能规划成功。发货前,先对给链表进行排序,货物数量多的在前,然后遍历存有货物id的各同源同宿链表。对每个链表又按照货物的质量从大到小排序,遍历排序后的链表,从头开始取出货物,累加货物质量,若超出车道容量则跳过当前货物(一个比较粗糙的打包策略),然后对取出的所有货物联合寻路。

处理完不含必经站点的货物后,处理含有必经站点的货物,由于必经站点的存在,因此不能向之前那样对货物进行打包(实际后面会设计含有必经站点的货物的路径统合策略,只是初版还没有考虑到),因此选择了逐个货物进行寻路,当然顺序上仍然是同源同宿的且货物数量多的在前,这样做是考虑到,尽管不能打包,但含有必经站点的同源同宿货物终究会有一些共同路径,将它们放到一起比较整齐,能够提高部分车道的利用率。

2.1.4 效果与问题

初版程序的效果较为一般,调整一些参数(主要是惩罚策略用到的参数)后,成绩处于接近80分的水平,并且瓶颈明显,在上述策略下,提升分数很困难。在测试过程中,也发现了一些问题:

  • 问题1——搜索路径时个别货物出现复杂度指数爆炸
    前文说到,我会在搜索时对候选的车道进行惩罚,并取前k个惩罚因子小的车道进行搜索。即便k=2,对于跳数特别多的货物,在最为恶劣的情况下(尽管不多见)会出现复杂度指数爆炸。比如,30跳的货物,所有尝试均失败,那就是230,这样的复杂度是惊人的,为了一个货物是不值得的。对此,我的解决方案是限制递归深度。如此,仅仅限制了少部分难以路由的货物,同时也可以适当加大k的值,以提升大部分货物的搜索空间。如此,解决了程序的超时问题,对分数的提升也略有帮助。

  • 问题2——惩罚策略令人失望
    实际测试时发现,尽管我在惩罚函数中考虑了很多因素,但效果并不好,甚至可以说比较差。通过测试发现,仅考虑跳数和工人消耗时,效果是最好的,且将工人消耗对惩罚因子的影响调整到极为显著的程度时,取得了该方案下最好的成绩。此时,我认识到工人资源的制约要远大于车道资源的制约

2.1.5 转向新的思路

在初版程序遇到瓶颈后,我打算尝试新的思路。初版程序的编写和调试为后续尝试新思路打下了基础,至少它让我认识到工人资源的制约是多么的严重。为了搞清楚工人资源和车道资源各自的重要性,我做了一些测试:

  • 测试1——统计寻路失败的原因
    寻路失败无非就两个原因:没有路、没有人(缺少系统资源)。我设置了两个变量,当没有路失败时,相应的变量加1。同样,没有人失败时相应的变量加1。统计结果显示,没有人失败的次数和没有路失败的次数的比达到了8 : 2,甚至部分地图达到了9 : 1。

  • 测试2——统计路径规划完成后车道资源的利用情况
    当我规划完全部的货物,分数接近80分,侧面说明大部分货物(过半)都完成规划了,此时,我将所有道路的空闲车道数量打印出来,发现绝大多数道路的大半车道都是完全空闲的。

  • 新思路
    有了上述的测试,不难对本题的地图、系统资源、用例做一个判断:车道资源充沛,如果能减少用人,绕一绕路也无妨;工人数量较少,是主要的制约因素。为了建立新思路,我决定先分析清楚一个非常基本的问题:什么样的路由可称为最佳路由?

    根据本题的规则,这个问题不难回答,最佳路由需要具备三个特征:
    1、 跳数最少(路径最短);
    2、 车道全部沾满(车道利用率最高);
    3、 仅在发货/收货的时候用人(用人最少)。

    然而,最佳终究是难得的。我们无法对所有规划出的路径都要求最佳,一般只能规划出 一部分最佳的路径。不过,没有最佳可以追求次佳,根据之前分析的系统资源的情况, 我们可以定义次佳路由为这样的路由:
    1、用人最少,仅在发货/收货的时候用人;
    2、尽可能的对货物进行打包,提高车道利用率,本质上是为了降低货均用人量,由于小质量货物的存在,当一组小质量货物仍然不能充分利用车道时,只要这组货物数量足够多,仍然值得为其独占一条道路规划路径;
    3、跳数需要向用人数量妥协。

    综上,可以将新的思路归纳为:追求最佳路由,力保次佳路由。那么到底怎么做到呢,具体的实现细节将在2.2节中介绍。

2.2 初赛终版:多条路径的宽度优先搜索、货物组包、路径统合

2.2.1 搜索策略

初版采用深度优先搜索的策略进行搜索,但问题比较明显,一是依赖好的惩罚策略,二是货物跳数过多时容易出现复杂度爆炸的问题,需要用硬性限制递归深度的方式来解决,即便如此,k的值依旧不能取得太大,这虽然降低了程序复杂度,但也限制了搜索空间。因此终版决定使用宽度优先搜索的策略。

当然,我希望宽度优先搜索不只是简单的宽搜了事,而是能够在一次搜索中找到多条路径。考虑到工人数量的制约,我希望bfs搜索出来的多条路径都记录有总的用人数,这样可以从中选出用人数量最少的那条(多条选最优),并且复杂度还要能够接受,不能超时。为了实现这样的宽度优先搜索,我设计了一个类:class BFSPath,以及基于这个类的一个新的类型:bfspath_map_t
在这里插入图片描述
这两个类的作用可由下图解释:
在这里插入图片描述
如此一来,有了BFSPath就可以在宽度优先搜索时记录多条路径及相应的累计用人数,有了bfspath_map_t类型的which_station_to就可以从宿站一路往回追溯到源站,进而恢复出整个路径。

LogisticsSystembfs_for_weight方法就是上述bfs搜索策略的具体实现,而另外一个方法calc_system_resource_from_bfspath可以完成从which_station_to中恢复出完整的路径,并保存在系统资源的容器中,进而方便方法deduct_system_resource扣除相应的系统资源。

2.2.2 货物组包策略

在初版程序中,我实现了一个较为粗糙的打包程序,这里需要对原来的打包策略进行改进。我使用了降序最佳适应算法(BFD)对货物进行打包,这是一个经典的装箱算法,具体在方法pack_goods_with_bfd中实现,对于打包的具体算法实现此处不再赘述。这里我重点介绍的是货物的组包策略,即将哪些货物放在一组交由打包程序打成1个或多个包。具体策略如下:

  1. 将所有的货物都组成一个个的包,路由系统对货物的管理(寻路)以包为单位
  2. 不含必经站点同源同宿货物按照车道利用率施以一定的约束组包,由于约束的存在,必然有一部分质量小的货物未能满足约束,这部分不满足条件的货物会添加到含有必经站点的对应同源同宿组中,与含有必经站点的货物一同组包,如果对应同源同宿组不存在,则这部分货物即便不满足约束也进行组包;
  3. 含有必经站点的货物(包括上一步添加进来的不含必经站点但与之同源同宿的货物)在组包之前先进行路径统合,之后交由打包程序打包,关于路径统合,在下文中会有阐述。

这里解释一下第2步中设置车道利用率约束的原因。基于这样一个观察,大部分含有必经站点的同源同宿组货物在打包后无法填满整个车道,这时如果把第2步中车道利用率低的一包货物与同源同宿货物组合,可以提高车道利用率,减少包的总数。具体的组包程序在方法make_system_packages中实现。

此外,再介绍一个类:class Package。这个类用于管理一包货物,内容比较简单,这里直接给出其定义:
在这里插入图片描述
物流系统的所有包存放在LogisticsSystemsystem_packages成员中。

2.2.3 货物路径统合策略

上文提到的路径统合是指对同源同宿的含有必经站点的多个货物,找到一条道路使得这些货物按照找到的道路进行路由时均能满足其必经站点要求。统合的主要策略概括如下:

  1. 将一组含必经站点的同源同宿货物按照必经站点的个数排序,必经站点个数多的在前,然后取出其中第一个货物,即必经站点数量最多的货物,为其规划一条可行(这是为了保证统合后路径可行)的路径,并记录下沿途的站点,以该路径为基础统合剩下的货物;
  2. 对于某些路径特殊的货物,如case5的G1337(Z46==>Z68(must route)==>z138),宽度优先搜索为其规划路径会失败(具体原因看图可知),此时反转货物的必经站点,倒着来一次(实测这是比较实用的技巧),这时一般都能成功规划;
  3. 对于仍然不能成功规划路径的货物,则将其归入失败组。

路径统合的具体的程序实现在LogisticsSystemmerge_must_route_stations方法中。

2.2.4 以包为单位发货的策略

这部分程序实现在LogisticsSystemroute_for_goods方法中,策略比较简单:

  1. 将物流系统中所有的包按其中货物数量的大小,从大到小排序,即优先为货物数量多的包规划路径;
  2. 路径规划主要分两轮,第一轮通过限制每个站点使用的工人数量最多为1以及仅搜索空车道来实现最佳或次佳路由,这一步仅针对车道利用率高或者货物数量多的包,不满足条件的包将放在本轮弃规划。第二轮,对上一步放弃规划或规划失败的包进行最后的尝试,此时不再限制仅搜索空车道,并逐步放开对每个站点的用人数量的限制。

3 源码

源码下载地址:就是这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值