最近抱着试水的心理参加了2020中兴捧月算法大赛傅里叶派赛题。从4.19号由旁观者转变为参赛者,到5.8号提交完成最后的文档和代码,前后算起来也有20天了。虽然自己比较菜,但毕竟是第一次参加这种比较正式的程序算法类竞赛,自己也学到了一些东西,所以还是写点东西纪念一下吧,留待日后回首。当然这也是第一次在CSDN写博客,一下解锁两个第一次,想想心里也是有点小激动。
2020中兴捧月算法赛道傅里叶派赛题菜鸡回顾
- 0.原始赛题描述
- 1.赛题分析与算法建立
- 1.1问题重述
- 1.2问题抽象
- 1.3算法建立
- 2.程序源码
- 3.参考博客
0.原始赛题描述
完整赛题直接复制进来吧,怕以后忘记到底做了一道什么题(手动笑哭表情)。
情景描述:
在某片遥远的大陆上,居住着两个世代友好的部落,分别是部落A和部落B。他们一起耕耘劳作,互相帮助,亲如一家。久而久之,部落里的每个人都在对方部落里找到了志趣相投,互相欣赏的好朋友。有的人性格热情开朗,好朋友很多;有的人性格沉稳内敛,好朋友相对少一些。
每到秋天丰收的季节,这两个部落的人民都会聚集在一起举行盛大的“丰收祭”,来祈祷下一年的风调雨顺。今年的丰收祭马上又要举行了。为了进一步增进两个部落的友谊,也为了明年能有一个好收成,这两个部落的祭司们商量后决定:在今年的丰收祭前举办一场特别的“击鼓传花”游戏。只不过游戏中并非有人真的击鼓,并且所传递的“花”也不是真的花,而是等待在丰收祭上献上的祭品。
游戏规则如下:
- 两个部落的所有人都可以事先准备自己的祭品,且每个人的祭品样式都不同,每一个祭品都分别盛放在一个相对应的木托盘里;准备此祭品的人熟悉自己的祭品;
- 每个人可以准备的祭品数量不限;祭品的最小不可分割单位是1份;
- 游戏开始后,在整个游戏过程中,每个人都能且只能将祭品(包括木托盘)传递给自己在对方部落里的好朋友们,每个好友可以接收的祭品数量不限;
- 收到祭品的人必须在盛放此祭品的木托盘上刻上自己的名字(代表留下自己美好的祝愿),随后按照上一条规则,继续传递;
- 如果祭品回到最初准备此祭品的人手中,此人也在木托盘上刻上自己的名字之后,终止传递;
- 木托盘上不允许出现重复的人名,如果无法满足此条件,则不再继续传递该祭品;
- 当所有的祭品都不再传递后,游戏结束;
游戏开展得非常顺利。游戏结束后,祭司们将收集同时满足如下三个条件的祭品用于接下来的丰收祭活动:
- 此祭品回到了最初准备它的人手中;
- 盛放此祭品的木托盘上至少有4个名字,至多有14个名字;
- 如果有多个木托盘上的名字完全一样(不区分名字的排列顺序),则从其中随机选择一个木托盘所对应的祭品。
问题:
已知这两个部落里的所有人都不重名,并且部落A的人和部落B的人之间的好朋友关系以附件的csv数据表格文件给出,其中行索引代表部落A中的人,列索引代表部落B中的人,表格中的数字“1”代表他们两人是好朋友,“0”代表他们两人不是好朋友。请问:
如果以木托盘上的名字的数量对用于丰收祭的祭品分类,每一类分别最多有多少个祭品?
请参赛者答题:
木托盘上有4个名字的祭品最多有()个;木托盘上有6个名字的祭品最多有()个;木托盘上有8个名字的祭品最多有()个;木托盘上有10个名字的祭品最多有()个;木托盘上有12个名字的祭品最多有()个;木托盘上有14个名字的祭品最多有()个;
1.赛题分析与算法建立
1.1问题重述
本题要求祭品在A、B两部落的朋友间互相传递,祭品传递过程中除祭品发起人外,其他人不能重复接收该祭品,祭品再次回到祭品发起人手中时该祭品传递结束。
求经手4人,6人,8人,10人,12人,14人(含祭品发起人)的祭品分别最多能有多少个,对于经手人员相同,只是传递顺序不同的祭品,均算作1个有效祭品。
1.2问题抽象
如果把A、B部落的每个成员都看成一个节点,则A、B部落的朋友关系,可以理解为A类节点和B类节点的连接情况,A类节点内部、B类节点内部不存在连接情况,祭品只能在有连接情况的节点间传递。以经手14个人的祭品为例,可以理解为连接了14个节点的环路。对应1.1中的问题重述,环路连接过程中,除起点外,其他节点不允许重复经过,这种环路称为简单环路,之后描述的环路不做特别说明均指简单环路(图的概念都是参加这个比赛才学到的,后面附上几个参考博客链接)。经手14人的祭品最多能有多少个,则可理解为不同的14节点环路最多能有多少个,对于经过节点相同,只是连接顺序不同的环路均算作是1个有效环路。
假设A,B部落成员节点分别有M个和N个,对应CSV数据表格的行数与列数,为它们统一编号:A部落成员节点ID为0~~(M-1),B部落成员节点ID为M~~(M+N-1)。则根据CSV文件可以得到这些节点间的连接情况,进而构建得到一张无向图。问题抽象为根据给定无向图,求其中节点长度为4,6,8,10,12,14的环路分别有多少。
1.3算法建立
根据前述分析,本题抽象为根据给定无向图,求其中节点长度为4,6,8,10,12,14的环路分别有多少。
建立具体算法前,几点为了提高搜索环路效率的思考。
a.由于寻找的是环路,环路中的每个节点都可以作为起点,为此设环路的起点都从A类节点开始,对应节点ID为0~~(M-1)。因此依次从编号为0~~(M-1)的节点出发,遍历寻找节点长度为4,6,8,10,12,14的环路分别有多少。
b.直接进行环路的遍历寻找,当搜索的环路较长时,如12、14环,会导致搜索的速度较慢,为此采用先搜索单向路径再进行组合拼接的方式进行环路的间接遍历寻找(大佬们在群里提供的思路)。如14节点环路,可以先遍历搜索全部的8节点单向路径,如果其中2条单向路径起止点相同,且中途没有重合点,那么这2条路径可以组合拼接成1条14节点环路,这样可以从一定程度上提高搜索长节点环路的速度。
c.假设从i号节点出发,完成了全部的4,6,8,10,12,14节点环路的遍历寻找,下一轮则从(i+1)号节点出发继续遍历寻找满足长度要求的环路。由于含有i号节点的环路在上一轮遍历中已经全部找出,因此从(i+1)号节点出发继续寻找满足长度要求的环路时,应将i号节点排除在环路节点的搜索范围之外。
d.对于经过遍历寻找初步得到的满足长度要求的环路,多条环路可能由相同的节点组成,只是连接顺序不同而已,这些环路按照赛题要求只能算作一条有效环路。因此对于遍历寻找后初步得到的满足长度要求的环路,还需要进一步去重,计算出其中的有效路径数量。
主体算法步骤如下:
(1)设置最大搜索深度MAXDEEP为8节点(通过修改msic.h文件中的MAXDEEP 宏定义实现),i=0;
(2)从节点i出发,基于图的DFS深度优先遍历(DFS也是现学的),在搜索深度不超过MAXDEEP条件约束下,不断遍历图中的节点,在遍历的过程中将节点长度为3,5,6,7,8的单向路径保存到对应长度的routem_flexarry (m取3,5,6,7,8)二维路径数组中。routem_flexarry二维路径数组的每一行存储一条长度为m的单向路径的全部节点,routem_flexarry二维路径数组的行数代表长度为m的环路的条数;
寻找满足长度要求的单向路径流程图
在搜索深度不超过MAXDEEP条件约束下,不断遍历图中未访问过的节点,在遍历的过程中将节点长度为3,5,6,7,8的单向路径保存起来
void adjlistDFSRoute(ALGraph *G, int startVertax)////从start节点出发探索新路径,用于遍历单向路径
{
// printVisitedVertax(startVertax);
setVisitedFlag(startVertax, 1);//标记start节点被访问
int nextVertax;
push_stack(&loop_stack, startVertax);//被访问的节点存入路径序列堆栈
innerStep++;//路径节点数+1
if (innerStep == 3)
FlexibleArrayAppend(&route3_flexarry, loop_stack.base);
else if (innerStep == 4)
FlexibleArrayAppend(&route4_flexarry, loop_stack.base);
else if (innerStep == 5)
FlexibleArrayAppend(&route5_flexarry, loop_stack.base);
else if (innerStep == 6)
FlexibleArrayAppend(&route6_flexarry, loop_stack.base);
else if (innerStep == 7)
FlexibleArrayAppend(&route7_flexarry, loop_stack.base);
else if (innerStep == 8)
FlexibleArrayAppend(&route8_flexarry, loop_stack.base);
if (innerStep >= MAXDEEP)//达到最大允许搜素深度 ,不允许再继续探索新节点
{
//isRecall =1;
return;
}
//没有达到允许搜索的最大深度,继续深入搜素
nextVertax = FirstNeighbor(G, startVertax);//寻找节点
while (1) //keep looping
{
if (nextVertax != -1)//找到了一个节点
{
if (visitedFlag[nextVertax] == 0)// 是未访问过的新节点
{
adjlistDFSRoute(G, nextVertax);//从新节点出发继续探索新节点、新路径
//if (isRecall == 1)
{
innerStep--;
pop_stack(&loop_stack, &pop_value);
setVisitedFlag(nextVertax, 0);//清除访问标记
//isRecall = 0;
nextVertax = NextNeighbor(G, startVertax, nextVertax);//新节点可达的新路径被探索完,重新寻找新节点(路口)进行探索
continue;
}
}
else//是已经访问过的节点
{
nextVertax = NextNeighbor(G, startVertax, nextVertax);//重新寻找新节点(路口)进行探索
continue;
}
}
else //没有找到节点
{
//isRecall = 1;
return;//返回上一层被调用点
}
}
}
(3)i++,如果i<M成立,转步骤(2),否则转步骤(4);
(4)长度为m (m取3,5,6,7,8)的routem _flexarry二维路径数组中含有大量相同起止点的单向路径。通过比较2条单向路径的起止点,得到2条单向路径的大小关系,进而对routem _flexarry二维路径数组的全部行(一维数组)使用堆排序法(堆排序时空复杂度低,排序不稳定,现学了一波常用排序算法特点),则所有长度为m的单向路径按照升序排列后重新存入routem_flexarry二维路径数组。经过本步骤,相同起止点的单向路径将被相邻排列在一起;
(5)遍历长度为m的routem _flexarry二维路径数组,进行单向路径的组合拼接。其中起止点相同且中途没有重合点的2条长度为m节点单向路径可以组合拼接为1条长度为n=2m-2的环路。拼接得到的长度为n的环路保存到对应长度的cyclen_combinedbyroute_flexarry (n取4,6,8,10,12,14)二维环路数组中。cyclen_combinedbyroute_flexarry二维环路数组的每一行存储一条长度为n的环路上的全部节点(起点不重复存储),cyclen_combinedbyroute_flexarry二维环路数组的行数代表长度为n的环路的条数;
(6)将步骤(5)得到的每条环路,使用堆排序法,将节点ID升序排列后重新存入cyclen_combinedbyroute_flexarry二维环路数组。经过本步骤,相同节点组成只是连接顺序不同的环路将存储为相同的一维数组序列;
(7)长度为n的cyclen_combinedbyroute_flexarry二维环路数组中含有大量相同节点组成的重复环路。通过从高位到低位(大端模式)比较2个长度为n的一维数组,得到这2个一维数组间的大小关系,进而对cyclen_combinedbyroute_flexarry二维环路数组的全部行(一维数组)使用堆排序法,将所有长度为n的环路按照升序排列后重新存入cyclen_combinedbyroute_flexarry二维环路数组。经过本步骤,相同节点组成只是连接顺序不同的环路将被相邻排列在一起;
(8)对于整体排好序的cyclen_combinedbyroute_flexarry二维环路数组,使用1个基准指针和1个游标指针遍历得到其中的非重复行数量,非重复行数量便是长度为n的有效环路的数量。对应原始问题中经手n个人的祭品最多有多少个。
2.程序源码
最近再学习下怎么用GITHUB,然后再把仓库链接贴过来。
3.参考博客
CSDN是个好地方,大佬们技术牛皮,还喜欢写博客交流分享——理论讲解配合代码案例,我超喜欢这里的。这次参加比赛现学了一些图这种数据的基本概念与知识,放在下面,留待以后用到时再来翻。
1.[数据结构]–图(图的遍历,最小生成树,最短路径算法)
2.数据结构:图的存储、图的遍历、最小生成树、最短路径、拓扑排序
3.图的遍历、最小生成树、最短路径
4.找出无向图中所有的环的算法
5.邻接矩阵转邻接表
6.数据结构 图的邻接表 深度优先遍历(DFS)(C语言版)