华为2020软件精英挑战赛成渝赛区初赛赛后方案分享

队伍介绍

我们是来自UESTC的选手。成渝赛区初赛成绩为0.1806。在这里分享我们的方案和代码。本篇博客对于前排大佬毫无用处,仅适用于后排同学学习进步使用。犹豫很久还是决定开源了,因为复赛题目数据量增大了,读取方式和数据结构也可能发生一些变化,我的代码即便开源,也不会影响正常的比赛。
在这里插入图片描述

框架

  • 第一步:mmap读取并解析数据,做好**id映射**
  • 第二步:构建图并使用**拓扑排序**剪掉出度和入度为0的节点
  • 第三步:并行寻环,顺便并行往char数组中写入内容
  • 第四步:写入文件

约定符号

  • N N N: 有N个节点
  • i i i: 节点ID值映射到1-N之间, i ∈ { 1 , 2 , . . . , N } i \in \{1, 2, ..., N\} i{1,2,...,N}
  • I I I: 节点原始ID,就是来自原始数据,是不连续的数字

1. 前菜

总的来说,重点在第2步和第3步,然后再配合上一些细节。我们先提出几个问题:

  1. 能不能在寻环的过程中,就保证环的起点是最小ID值
  2. 能不能寻环结束之后,就能保证全部的环是按照长度为第一优先级,字典序为第二优先级排好序。

这两个问题能省掉很多多余的操作,并且大部分选手都能解决,不失完整性,还是先从这两个问题开头。

  • 针对第一个问题,如果dfs遍历到4->3->5->2, 我们应该输出2->4->3->5。 当我们开始寻找以2为起点的环时,只要控制遍历的ID值是大于2的,在环中2总是最小值。
  • 针对第二个问题, 我们最开始的做法是对所有环(300W个)做一个排序。但后来发现,其实遍历输出的环本身是可以有序的。首先先对ID值做一个排序,要保证ID值的相对顺序和ID映射之后的相对顺序一致。 比如说有4个 I I I 4 , 10 , 7 , 12 4,10,7,12 4,10,7,12, 如果按照先后出现的顺序做映射,那么 I I I-> i i i: { 4 : 1 ; 10 : 2 ; 7 : 3 ; 12 : 4 } \{4:1; 10:2; 7:3; 12:4\} {4:1;10:2;7:3;12:4},试想,从寻找 i i i为起点的环, i i i 1 − N 1-N 1N变化,当i为1时,开始找以4为起点的环,然后是找以10为起点的环。那这样子,全部的环排列当然是无序的。但是如果先对所有的 I I I排个序,再做映射, I I I-> i i i: { 4 : 1 ; 7 : 2 ; 10 : 3 ; 12 : 4 } \{4:1;7:2;10:3;12:4\} {4:1;7:2;10:3;12:4} { 4 , 7 , 10 , 12 } \{4,7,10,12\} {4,7,10,12} { 1 , 2 , 3 , 4 } \{1,2,3,4\} {1,2,3,4}的相对顺序是相同的。那么挨个寻欢的过程中,环的起点是按照顺序的,但是环长度却不能保证,所以还需要为5个长度的环,单独建立一个容器。例如检测到3环,就写到对应的容器中。这个容器可以是int数组,也可以是char数组。

2. 中菜

2.1.1 构图

图的数据结构用二维数组当做邻接表。最大出入度设置为50(为何我要提入度?)。
从数据解析出节点连接对(pair)之后,就可以往二维数组中装东西了,并且统计节点入度。构建的图肯定是冗余的,因为有些节点入度为0,这类节点当然会有环;同样出度为0的节点也是。而拓扑排序能去除掉出入度为0的节点(在线上,使用拓扑排序带来的增益是恐怖的,和线上数据分布有关,因为好多节点是无效的)。以入度为0为例子:

在这里插入图片描述
1这个节点是没有入度的。我们把和他连接的边全部删除,发现2的入度也为0了;接着2对寻欢也没有帮助了,然后继续删除。出度为0的节点也可以使用该方法删除,但在线上好像提升没有删除入度的提升大。
我们已经知道哪些节点入度为0,根据邻接表,走到其后继节点,同样把他们的入度减1。如果后继节点的入度为0,继续对其后继节点的入度减1…。 然后引出反向图

反向图: 如果i直接连接到j, 那么记录下有 哪些节点和j存在以j为终点的直接连接,同样是一个邻接表,只不过是反向记录的。反向图的好处是很大的,可以帮助dfs执行的更快。

当我们构造好反向图,利用反向图在构建一次新的正向图(旧的图就无效了),这个时候图中就没有入度为0的点了。用同样的思路,可以继续删除出度为0的点。所以我的代码用了很多的connectionLeft和connectionRight,就是用来存图的。

3. 压轴菜

3.1 双向dfs

如果用dfs寻找环长度为7的环,最大递归深度为7。非常深,层次越大,遍历的节点数目呈指数级上升。能不能不遍历这么多层就可以知道有没有环呢。数组7的最大平均拆分是4+3或者3+4。 很多选手都说自己的方案是4+3或者几+几。这些数字的意义是啥。见下图
在这里插入图片描述
这里有一个环:1->2->4->3->5->6->7,起点为1。
对于环,从一个位置往后继节点位置走,和往反向走,一定会碰到一起。比如1往后继节点走4步到5,然后1往反向走3步,居然也是5。正向递归深度为4,反向深度为3。当开始寻找以 i i i为起点的环时,可以先反向走3步,构建能到i的反向路径,用 P 3 P3 P3存这样的路径,用 P 2 P2 P2存从反向走2步的路径。然后开始从 i i i出发,正向开始遍历,假设第一层dfs遍历到数组 k k k,那么目前的路径为 i i i-> k k k。假设 i i i存在一个长度为3的环 { i , k , n , } \{i, k, n,\} {i,k,n,} i i i反向走2步也是到 k k k。而我们已经用 P 2 P2 P2记录了从 i i i反向走两步的所有路径,如果这些路径中存在以k为起点的路段,那我们就找到了一个以 i i i为起点,长度为3的环。接下来,再往后继节点走一步,我们可以通过 P 2 P2 P2获得长度为4的环,通过 P 3 P3 P3获得长度为5的环;以此类推。至于如何使用 P 2 , P 3 P2,P3 P2P3,可以见开源代码部分。

3.2 并行执行

很多选手说并行执行是负优化或者提升不大,很可能是没考虑到线程的工作量是否均衡。
还有选手说使用并行就result is incorrect,也有可能是每个线程的数组不够大,有些数组被撑爆了,从而导致丢了答案。
我了解的前排大佬中,为了平衡工作量,他们开了很多的线程,有6的有8的,然后切出来8段区间,每个线程去完成自己的那块区域的寻环。有7个数值要调参。 ddd大佬使用的是自己写的调度算法,敬请期待他的代码
我们在比赛后期使用并行的时候,因为代码设计不合理,如果要使用超级多的线程,那需要对函数以及变量复制多份。我们采用的四线程。中间某个位置切一刀。
在这里插入图片描述
线程2和线程4是从大到小寻环,所以memcpy也要倒着走。这样子,我们只要调整一个参数,并且可以保证效率是双核以上

4. 各种细节

4.1 IO篇

  • 一般用映射需要使用sort和unique,但是ID值全部都是在20w数字一下,那么就可以使用一个20w大小的数组,标记出现的数组为1,然后遍历这个数组,同时用cnt记录当前ID映射到的index值即可。
  • 读数据的时候,顺便把 i i i对应的ID值的字符形式表示出来。有两种办法,第一种,直接从mmap映射到的字符串中截取出来。第二种,自己写个to_char函数,把整型转换为char*,方便写入。我们组使用的是后者。
  • 写入数据直接往字符数组中写,如果还利用了多线程,那就相当于同时并行处理了写入数据。
  • fwrite比mmap还快了一点。多用数组,比vector和new出来的数组快很多。

4.2 DFS

  • 前向dfs要走4步,用5层for循环嵌套的效率 远远低于 递归写法
  • 调整if else if的顺序, 把经常发生的情况写在前面;另外switch case的效率高于if else if
  • 遍历下一层的时候,先用引用一下出度的值,避免在for循环中的条件判断,每次都做指针位移操作,相当于减少一次加法。
  • 函数中尽量不要带形参,需要带形参的可以设置为全局变量。
  • 根据官方微信推送的优化思路,用一些临时变量有助于减少寻址。

赛后感受

  • 细节决定排名
  • 算法虽好,也要看实现方式(数据结构尽量用数组,new和vector是真的慢)
  • ddd, chier 永远滴神
  • C++强无敌

代码地址点击我

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 15
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值