目录
一.写在前面的话
二、语言选择
三、判题器实现
四、回溯解死锁
四、发车时间设计
五、寻路设计
六、写在最后的话
第一次参加软件精英挑战赛,复赛最后打了第六名,队名是行车不规范亲人两行泪,我们上台时候,主持人都说这队名真应景啊…就差两名就可以进决赛了,但是不得不说前面的大佬真的是太强了,把我们甩的贼老远。不过也没什么遗憾,复赛练习赛时候也差不多是5-8名左右,所以正式比赛就想着要保住前十,别拿第五(感谢第五老铁抗雷…),最好第六,也算是如愿以偿了。
代码和数据在git上面:https://github.com/zhangbaodan/2019HUAWEI
主要分享下解死锁思路以及寻路那边的一些特征提取。如果您觉得有帮助的话,记得git上面点个小星星哇~
再放张队名图片(美工小姐姐真的是太用心了)
和往年一样,今年也开放了三种语言c++,java,python。最开始接触这个题时候,感觉用不到机器学习的东西(我们太菜,想了半天没想到,大佬估计用得到)。所以就把python给pass掉了,毕竟这玩意运行起来太慢了,后面也有很多队吃了这个语言的亏。c++写起来太复杂(主要是我c++写的太菜,我的c++ = c + stl),所以把c++也pass掉了。而且感觉这个比赛car,road,cross刚好三个类,java写起来会比较顺手,而且很多封装的类写起来也比较方便,运行时间上面也和c++差距不是很大,所以最后选择了java(西北复赛32个队里面,只有三个java,四个pyhon,看来c++大佬是真的多)。
周五放的比赛,晚上时候经过讨论,决定先把判题器撸出来,做出这个决定着实不容易,光规则就有19页,能不能撸出来真的不好说,但还是决定赌一把(真的感谢当初这个决定)。那么,对于计算机来说,每次我只能更新一辆车,哪以什么样的方式把所有车都更新完呢?那时候,官方还没有更新任何有关判题器逻辑的东西,所以都只能自己想。我们决定按ID顺序遍历所有的车辆,对于某一个车辆,如果有车辆影响到它的更新,那么就一直深度遍历更新下去(官方采用按cross和road方式遍历,事实证明他们的思路更清晰,实现起来也更容易),这是我们一开始写的逻辑图:
最开始打算按照车辆去遍历,如果遇到影响当前车辆更新的车辆,那么就一直DFS更新下去,看上去漏洞百出,和官方给出的逻辑完全不一样,但也就还是按着这玩意撸了。差不多撸了两三天,一个看上去能用,实际完全用不了的判题器就横空出世了。然后路径规划那块搞了搞,算是能用了。过了一周以后,官方可以线上提交代码了,第一天晚上提交时候各种死锁。把发车时间打的很散以后终于跑通了,两张图跑了80000左右…
实现这玩意着实不容易,尤其最开始官方啥都没说的时候,每天就是一边撸,一边看官方的论坛回复,看看有没有自己没有注意到的问题,而且是一边看官方回复,一边怀疑官方回复是不是错的,因为在有点地方,我们按照他的逻辑改,反而更不准了,最后索性都不看论坛了(事实证明,永远不要怀疑官方...)。
很遗憾的是,官方在初赛后期几乎完全公布了判题器逻辑时候,由于那时候我们判题器逻辑已经定型了(不太准),看了他们的逻辑,感觉差不多,就没有完全按照他们的逻辑去改。这一下子偷懒,直接导致我们一个星期死在判题器上面。后面实在找不到BUG了,就痛定思痛,把直接写的逻辑全删,完全按照官方的逻辑去写(连命名都和官方一模一样),结果很神奇的就一致了。
然而,我们一直偷了一个懒。官方之前一直强调cross的ID不保证连续,直到初赛比赛的早上,我才火急火燎的改成不连续读取,但是判题器按照路口ID升序遍历那块忘记改了,导致判题器不准。再加上我们回溯方案很耗时间,初赛练习赛时候没有表现出来,正式比赛时候,数据量一上去,我们立马就超时了。只能换回一个星期以前的思路(没有回溯),胡乱调参,还好苟进复赛了(24/32)。
复赛时候,由于增加了优先级车辆以及预置车辆,所以判题器需要大改。官方索性放出了完整的逻辑导图(这逻辑图是真的清晰,什么时候才能写出这么优雅的逻辑)。有了初赛踩坑经历,这次我们决定一模一样复现他的逻辑(名字都一样)。差不多在复赛前一个星期,线上线下完全一致了(总时间后几位不一致,这是因为四舍五入的关系,所以没太在意),提交上去一页都是提交成功(绿色)的感觉是真的爽。
总之,对于我们队来说,复现别人的东西也是一个不小的难题。一定要坚信官方是对的(就像代码给你报bug,绝对不能怀疑编辑器,一定要有对不起,都是我的错,我一定改的态度一样),坚持弄下去,一定能搞出来!
当发生死锁的时候,我们可以获得死锁对应的车辆以及对应的路和路口。如果我们能够事先对这些车辆以及路口做一定的管控的话,死锁就不会发生了。
最简单的方法是,当发生死锁后,让时光倒流,回到过去,然后做一定的调整,也就是回溯解锁的办法。首先,我们来说下如何回溯,不管采用何种回溯解锁思路,前提条件是能回的去。比如在第100秒时候发生死锁,如何回到90秒?一种很暴力的思想是重头仿真到90秒,这种方案肯定用不了,太耗时了。我们在初赛时候,采用的方案是,对于每一秒,我们都记录下该时刻对应的所有仿真信息。如果想回到90秒,只要读取对应实现记录的信息,做一个替换就好了。(这里在工程上需要注意的是深拷贝和浅拷贝的问题,我们一定要做深拷贝,浅拷贝会导致地址共用,会出很多BUG)。
然而...每一次记录都需要话很大的时间(如果车辆数很多的话,代价更大),这也直接导致我们初赛代码超时。复赛时候,我们采用每隔10秒记录一次信息,这样整个耗时相比于原来就小了10倍,而且这样的操作对回溯并没有产生很大的影响,发生死锁后,我们只需要退回到最近的记录的时间片就可以了。
回溯后,一般采用的是回溯改路或者回溯不发车的思想,我们采用的是后者,比如在第100秒时候发生死锁,记录发生死锁车辆以及发生死锁路口(以及路口周围的路口)。然后回到90秒(有一个类用于保存对应时间片的所有仿真信息,将90秒到100秒记为forbidTime。期间有三个策略来尽量保证再次回到100秒时候,不会发生死锁:
1、100秒发生死锁的车辆不允许在这期间发车(如果它原来实在这期间发车话);
2、死锁路口以及周围路口不允许发车;
3、全图允许存在的车辆数按百分比减小,因为我们有个很大的参数是控制当前地图下最大允许存在的车辆数,把这个参数减小以后,也会降低死锁可能性。
当然,在这三种策略下也不能百分百保证第100秒时候不发生死锁。我们的策略是如果不行,就再往会倒10秒,回到80秒,如果还不行就继续往回倒...,当然,往回倒多少次取决于当前最大记录了多少时间片信息。往回倒的时间片越多,在第100秒时候,整个车辆数就越小,发生死锁可能性就越小。
有一个很重要的点是控制车辆的发车时间。我目前知道的两种普遍的做法是:方案1.每隔几秒发一波车;方案2.控制全图车辆总数,如果当前地图上车辆总数小于这个阈值,就立马发一些车作为补充。我们初赛采用的是方案1,复赛采用的是方案2。
方案1:每隔几秒钟就发一波车,这种策略可以使得等到下一次发车时间到的时候,上一次发的车很大一部分就已经到达终点了。这样的发车策略,在一定程度上预防了死锁。这种策略下有如下若干问题:
同一个发车批次下,应该发哪些车。有些做法是让同一个发车批次下,车辆的始发地尽可能的分散。还有一种做法是,让车辆的速度尽可能一致。在初赛时候,我们采用了后者,因为同样速度下,车与车之间的空隙会尽可能的小。这样道路容纳量会比较大。那么,先发速度大的还是小的?我发现很多人的做法是先发速度大的,他们认为速度大的会尽快到达终点,不会对后面速度小的车辆产生过大的影响。但是,不晓得为啥,我们模型里面先发速度小的结果会比较好。而且,我认为先发大或者小结果应该是一样的。
每次应该发多少车?如果同一个发车批次下,车辆速度都尽可能一致的话,我认为速度小的时候,可以多发一些车,速度大的时候,可以少发一些车。因为速度越大,越容易死锁。如果速度是16的话,假设其要跨越路口(当前在路的最前面),那么下一条路的可以进入的车道中,如果从后往后数16个格子内存在一个等待车辆,那么它就肯定是等待状态了。而如果速度是4的话,只看4个格子。所以速度越大,越容易死锁,地图容纳总数就越小。
发车间隔是多少?发车间隔其实也控制这地图车辆总数,因此,速度大的时候发车间隔大一点,速度小的时候,发车间隔小一点会比较好。
方案2是我们复赛采用的方案,首先,按速度发车的思想没有变。我们不去控制发车间隔以及发车量。而是直接去控制地图上总车辆数,这样两个参数简化为一个参数,而且控制的效果更好。在不同速度下,发车的总量也是不一样的(和方案1一样)。速度越大,发车总量越小。
在复赛中,对于优先级车辆,能发就发,不按照速度来发,我们会控制一个优先级车辆发车时候的总地图容纳量。对于普通车辆,会控制一个允许普通车辆加入的最早时间。在最早加入时间到了以后,按照速度有小到大依次发车,不同速度下设置不同的车辆总数。
寻路无非有两种寻路,一种是没有仿真之前,就事先规划好他的路,还有一种是,等到它真正要出发的时候,在去寻路。我们采用的是后者,比如在仿真到第100秒时候,有一辆车要出发,我们就把第100秒的地图以及那辆车扔给寻路算法,返回一条路径来。这样会比事先规定路线要准确一些。
寻路主要采用的是dijkstra算法。其核心是对于路的权重设定。我们在寻路这一块做的比较烂,很大一部分精力都放在判题器和解锁以及发车上面了,在我们很烂的寻路模型中一共有如下几个参数:
1.行驶完这条路所需要的时间
这个参数应该是大家都能想到的一个参数,但在我们模型中,因为是分速度调参的,在前面几个速度中,这个参数的权重并不是很大,如果很大的话,车辆很容易立马发生死锁。当然,我们会在最后两三个速度下,把这个权重调的大一点,因为后面已经没有多少车加入了,所以尽可能让他早走完比较好。
2.当前车道拥挤程度
道路拥挤程度通过(道路总容纳量 – 道路可加入量)/道路总容纳量计算得到,这里我们计算的不是道路已有车辆数,而是道路可加入量,这可能和其他队伍的计算方式不太一样。这个参数对应权重比较大,因为我们希望车辆能够均匀的分散开来。即使这样做会绕比较远的路,但我们认为这样也是可以接受的。
3. 速度差异
我们希望车和道路之间的速度尽可能接近,如果车速高于路速的话,对于车来说本身是个损失;如果路速高于车速的话,会导致该车后面的车有可能损失速度;如果车速等于路速的话,该路上车的速度都一直,速度损失将会降到最小。
4. 已经选择确尚未到达该路的普通车辆数
这个对应权重设的比较小,也不晓得有没有用~
5. 已经选择却尚未到达该路的优先级车辆数
这个权重就设的比较大了,因为优先级车辆本身计算权重就比较大,我们尽可能让非优先级车辆不去选择优先级车辆已经选择的路。这样即使在最开始优先级车辆行驶中,我们加入一些非优先级车辆,也不会对优先级车辆运行的总时间产生过大的影响(如果设置的无穷大的话,优先级车辆总时间只可能降,不会升)。
关于动态改路的问题,首先,做动态改路肯定是能够降低总时间的,但是,我们寻路模型可能太烂了,改了反而不如不改…
这次比赛称为华为软挑有史以来最变态的一次,代码量要求真的是很大。通过这次比赛,也使得自己对代码风格和代码管理这一块有了很大的提升。感觉我们失利的主要原因是在寻路那一块做的很烂,尤其是在看了别人开源的代码以后。总之,长路漫漫,来年再战!