决赛的过程颇为坎坷,深深的感受到了自身能力的不足,团队的力量有时要远胜个人。
开源地址:https://github.com/Chadriy/CodeCraft2020
复赛结束,还没来得及感慨,第二天一大早就放题了。实在是不想做题目介绍了,简单说明一下,仍然是给定一个有向加权图,不过解题目标变更为计算网络中节点介数中间性,并返回top100的点与其精确值,对于概念及算法可以参考https://blog.csdn.net/betarun/article/details/51168259。基本上,大家参考的都是2001年brandes在论文《A faster algorithm for betweenness centrality》中提出的算法,一天之内算法就内卷了呢(无奈摊手.jpg)。
然后队友一天写好了base,然后造了份数据集(tql),之后几天就是debug和精度问题。
决赛后我们进入了分别solo的状态,蔡总写好base后被导师gank出差去了,于是交给我继续搞这base。第一版的最短路算法我们用的是优先队列的dijskra算法,配合brandes算法计算介数中间性。由于算法复杂度提高了非常多,之前对io的优化基本上没有作用了。因此优化思路被限制成三种:
线上开放测试,我们很快意识到针对底层的优化经过初赛复赛已经达到了瓶颈,因此一开始没有很好的方案。榜A第一天,官方公布了数据集的规模,并给了一个线上线下特性同步的数据集,根据其大小我们推测,完全随机图是根本跑不完的。之后测测数据集特性,主要是度特性、权值分布、scc这些,发现平均度为1。继续观测数据集,发现总点数约80w,而其中60w+为入度0出度1点,形状有些像刺猬。
于是根据公示的等价性,入度0点的 σ \sigma σ为其邻接表的 ∑ l r σ \sum\limits_{l~r}\sigma l r∑σ,其中 σ \sigma σ为以该点为起点的最短路条数。
于是显然,对于入度0出度1的点,其本身必不可达。于是在计算介数中间性时,可以将该点合并到其邻接点,然后在计算邻接点的介数中间性时*(1+n),n表示到该点的特殊父节点数目。通过这样处理,需要计算的点数降至约16w。对于入度0出度任意的点,存在等价性问题(先加后除还是先除后加),因此当时只想到保存中间结果的方法,实现起来很麻烦。因此最终未实现,当然通过分析可知,若实现后点数降至约7w。
不得不说,前排大佬实在是太猛了。我刚加上这个tirck线上从400s到90s,发现不小心冲得太猛0 0,于是赶紧吹一波SPFA转移视线,然而没几个小时大家就都进100了。事实上到了这个阶段,只要不藏分,甚至可以通过前排大佬分数的提升幅度来推断做了什么优化qwq。杭夏赛区真的很强,据说507和鸽鸽鸽都是ACM的金牌选手,一度支配比赛。
A榜存活时间很短,又做了点小改动,最后封榜时77s,貌似还在前五吧
B榜线上增加了随机图,直接一个scc块,扼杀了很多利用scc特性的方向。B榜对新旧两份数据集进行时间累加,作为最终成绩。由于上文trick的存在,我们线上成绩为800s,可以看出图一已经只占很少的比重了。接下来的几天,蔡总未归,我这边思路也陷入了误区。由于旧图trick提分巨大,因此我的思路一直往这如何删点、删边、找等价性替换的方向走,整整两三天没有思考出结果。
这时候很感谢同赛区的各队伍,尤其是服务器队。在观察到数据集分布中权值为100以内的正态分布,一番讨论后,由扫黑除恶小分队提出了对堆内distance的压缩想法,同天晚上服务器队完成了具体实现。事实上我们整个赛区的四支队伍都在700s这里卡了好几天,至此成为了转折点,我们的优化重点全部转移到对数据集的观测性优化。
对于第二张图,我们可以观测到:
因此,可以修改入队规则,队内只保存distance,然后每个distance对应一个点id列表,通过该tirck线上提升约200s。沿该方向继续深化,distance最高为250左右,图一要比较大约6000。于是考虑分段映射,当distance小于阈值T时,用distance做下标索引,大于阈值则用unordered map。事实上,对于图二根本不需要队列,连数组都不需要,用一个index变量自增就可以。通过这种方法简化了逻辑,抛去了优先队列,同时删掉了两个变量和一个判断,然后线下线上均负优化qwq。队友也尝试了各种堆结构以及手写vector,最后只有vector被保留下来,其余均不如std的优先队列。除此之外,又尝试了一版Neon加载邻接表,负优化+1。通过以上优化,分数来到400+s。
由于一次疏忽,distance的vector替换为手写vector后写成了uint,分数莫名提了几十s我们浑然不觉…某次三联线上0%后,我误以为是uint导致的,然后发现这玩意还能上分。于是第二个大trick,对数据结构进行压缩。一通大改,尽可能的压缩数据结构。同时与同赛区小伙伴们交流了一下数据结构方面的心得,将brandes算法反向搜索的表改成二维,同时简化构图中G.l与G.r结构体中r与l+1的等价性,只存l。这时候对赛制有些担忧,只要官方调一下权值分布,我们这些trick就完全失效了。但是如果官方不调,那如果不用这些tirck就基本凉凉。
这时候倒计时两天,我们来到了340s左右,然而还进不了第一页的样子(摊手)。
这时候由其他队想出来一个很厉害的trick,通过对邻接表BFS然后重映射,对于图一这种出度很大的局部稠密图来说,cache友好提升巨大。通过该trick图一只需要8s就可以跑完,图二没变化。线上330->315s。之后也做了一些对图二的cache优化,但是并没有起到左右。最后一天下午,我加了一点unlikely和prefetch的优化,并且把distance数组从结构体提出来,放到邻接表G旁边(调整变量顺序)。线上跑到了300s,封榜时第三。
这时候我们图二线下约126s(也可能是130s),晚上和第二天上午我们做了一些逻辑判断、变量结构的调整,譬如在for循环中,往往用如下写法
for(i = G[v].l;i < G[v+1].l;i++)
而替换成如下写法相当于prefetch了,节省了一次跳址
for(i = G[v].l,end = G[v+1].l;i < end;i++)
同样,对部分变量封装结构体,也可以减小访址代价。
struct Result{
double bc; //中介中间性
double delta;
};
通过这些细小优化,最后图二分数提升到113s,吃完午饭就开始最终的决赛了。
不得不说,对于我们这样很少打现场赛的队伍来说,决赛中我们并没有什么发挥。前半个小时我们遇到了一个很崩溃的bug,新服务器编译不过。折腾了半个多小时,其他队伍都已经出分,我们还是编译不过,几乎处于崩溃边缘。然后蔡总发现编译选项用的-o3,应该是-O3。旧服务器上一直用的-o3没啥问题,然而新服务器直接编译不过。蔡总nb,差点我们就没分了。
然后拿最新版直接提交,第一发276s。凌少分享了一个调参的trick,把反向遍历的二维表,第二个维度由250改为150提升至265s。然后是一发探测,线上ushort就能过,去掉所有切换换成ushor,同时微调了一处逻辑即263s。
关于NUMA的特性,之前做过线程绑定的尝试,由于效果很差没有备份。现场大概写了10min就感觉这个特性如果要利用上,除了绑定还要对数据做内存隔离。于是将所有精力放到了调参上,然后我贡献了两发re与一发263。封榜的时候,我们第四名,离第三名只差0.5s左右。如果这样GG掉,可能我要自闭很久。因为我的两次re,最后只有两次调参机会。
略带遗憾,最后两次机会调错了方向,没有提升,最终凭借着平淡的临场发挥拿到了全国第五。
就这样,两个月的历程拉下了帷幕,我们收拾好行李,又各自奔赴向更远的地方。
最后,再次感谢比赛途中遇到的所有人,谢谢。我们有缘再见。