• 部门与学生的智能匹配


    结对第二次作业

    github项目地址:

    https://github.com/chujiuling/jiuling

    结对小组:

    031502543 周龙荣

    031502632 伍晨薇

    031502639 郑秦


    一、问题描述

    编码实现一个部门与学生的智能匹配的程序。提供输入包括:

    • 20个部门:
      - 学生数上限:单个,数值,在[0,15]内
      - 特点标签:多个,字符
      - 常规活动时间段:多个,字符/日期

    • 300个学生:
      - 绩点信息:单个,数值
      - 兴趣标签:多个,字符
      - 空闲时间段:多个,字符/日期
      - 部门意愿:不多于5个的,不能空缺

    实现一个智能自动分配算法,根据输入信息,输出部门和学生间的匹配信息(一个学生可以确认多个他所申请的部门,一个部门可以分配少于等于其要求的学生数的学生) 及 未被分配到学生的部门 和 未被部门选中的学生。

    二、问题分析

    • 需求:实现部门与学生的智能匹配,尽量满足部门纳新人数,在部门纳新人数限制下尽可能多收学生
    • 编程语言:C++
    • 数据输入:文本文件
    • 数据输出:文本文件
    • 部门意愿:不能空缺,可重复
    • 标签和时间段:对于个体(学生或部门)内部不重复

    三、数据模型

    1.模型建立

    • 学生的属性
      - 编号int stu_id
      - 绩点double gpa
      - 兴趣标签vector<string> stu_tags
      - 空闲时间段vector<string> free_time
      - 志愿部门int applications_dep[5]
      - 学生平均热度double hotAve
      - 未选满的志愿部门int left
      - 志愿部门(动态删去匹配过程中被选满的部门)vector<int> stuChosen
      - 中选的志愿部门vector<int> ResChosen
      - 是否中选bool chooseYN
    • 部门的属性
      - 编号int dep_id
      - 学生数上限int member_limit
      - 特色标签vector<string> dep_tags
      - 活动时间段vector<string> event_schedules
      - 选择该部门但未中选的学生数int stuSums
      - 中选该部门的学生数int stuResult
      - 选择该部门但还未中选的学生列表vector<int> DepStuCho
      - 中选该部门的学生列表vector<int> DepChosen
      - 部门热度double hot
      - 是否已经匹配过bool visited

    2.“数据生成”程序的原理

    设置两个结构体数组,分别为学生和部门,其中包含属性如上所述。对其属性随即设置,数据生成如下:

    • 学生和部门的编号可通过循环生成信息,每轮的i值递增赋值。
    • 学生绩点位数设置5位浮点数。
    • 部门纳新人数限制0-15,即随机数对16取余。
    • 设置两个string数组(星期+时刻)并初始化,随机得到学生的空闲时间和部门的活动时间。只需随机两个数,分别为时间段的数目和第一个时间段,剩下的时间段可通过递增获得。
    • 兴趣标签也是如此。
    • 学生志愿部门设置五个,不能空缺,可重复,与时间段和标签的原理有些不同,可以每次随机的志愿部门,不用判断重复筛选。

    数据输出到txt文本,设置两个输出程序,用循环对每个学生和部门的属性进行打印。

    3.考虑因素

    • 兴趣标签和空闲时间段不能重复,数目随机,内容随机。
    • 学生和部门的标签从给定的标签集合中选,匹配度提高。
    • 部门和学生对应关系是多对多,因此要学生中要设置最终中选的部门数组,部门中要设置最终选中的学生数组。
    • 学生被选入部门后还具有与其他学生竞争的权利,考虑到不能阻止优秀的人更优秀(时间捉急),未对其进行标记区分。
    • 部门五个志愿不为空,可重复,随机结果不用考虑重复问题。
    • 部门和学生活动时间如果随机程度太高,匹配度就会很低,所以将可随机的时间段的数目减少。

    4.数据生成代码

    //标签数组、星期数组、具体时间段数组如下,学生部门属性从中选择有利于匹配度提升
    string alltags[10] = {"reading","programming","film","English","music","dance","basketball","chess","running","swimming"};
    string week[7] = {"Mon.","Tues.","Wed.","Thurs.","Fri.","Sat.","Sun."};
    string ttime[3] = {"15:00~17:00","18:00~20:00","20:00~22:00"};
    
    //随机生成学生信息
    for(int i = 0; i < stuSum; i++)
    	{
    		//学生id生成
    		stu[i].id = i + 1000;        
    		//绩点            
    		stu[i].gpa = (rand() % 5000) / 1000.0;  
    		//五个志愿部门生成,可能重复
    		for(int j = 0; j < 5; j++)
    		{
    			stu[i].applications_dep[j] = rand() % depSum;
    		}
    		//兴趣标签生成
    		stu[i].tags_num = rand() % 10 + 1;
    		int t = rand() % 10;
    		stu[i].stu_tags[0] = alltags[t];
    		for(int j = 1; j < stu[i].tags_num; j++)
    		{
    			if(t == 9) t = 0;
    			else t ++;
    			stu[i].stu_tags[j] = alltags[t];
    		}
    		//空闲时间段生成
    		stu[i].times = rand() % 5 + 1;
    		int r = rand() % 7;
    		for(int j = 0; j < stu[i].times; j++)
    		{
    			int r2 = r + j;
    			int r3 = rand() % 3;
    			if (r2 >= 7) r2 -= 7;
    			stu[i].free_time[j] = week[r2] + ttime[r3];
    		}
    	}
    
    //打印生成的学生信息
    void PrintStu(Student *stu, int stuSum, int depSum)
    {
    	for(int i = 0; i < stuSum; i++)
    	{
    		printf("%d %.5lf
    ", stu[i].id, stu[i].gpa);
    		for(int j = 0; j < 5; j++)
    		{
    			printf("%d ", stu[i].applications_dep[j]); //志愿
    		}
    		printf("
    "); 
    		for(int j = 0; j < stu[i].times; j++)    //空闲时间
    		{
    			printf("%s ", stu[i].free_time[j].c_str());
    		}
    		printf("
    ");
    		for(int j = 0; j < stu[i].tags_num; j++)    //标签
    		{
    			printf("%s ", stu[i].stu_tags[j].c_str());
    		}
    		printf("
    ");
    	}
    	printf("
    ");
    }
    

    5.数据

    输入样例:import.txt

    四、算法设计

    1.设计原则

    • 热度优先原则
      部门热度=志愿该部门的学生总数/该部门纳新数上限
      学生平均热度=该学生志愿的部门热度之和/志愿数
      优先级:热度>绩点>兴趣/活动时间
      - 以热度为竞争力,选拔竞争力最小的同学。不断调整竞争力。
      - 计算各部门的热度以及学生的平均热度。
      - 对各部门进行热度排列,志愿该部门的先找出低热度的部门,然后找到报人数和需求人数中更小的那个值作为限制while的循环次数。
      - 在while循环体中遍历查找到热度最低的那个学生,若出现同热度,选择绩点高的进行匹配,若绩点相同,进行时间段和兴趣标签的匹配。
      - 如果合适就把他加入部门选中学生的数组里,循环结束即该部门招满了,将不再考虑该部门纳新,且把被选入这个部门的学生的平均热度都更新。
    • 绩点优先原则
      - 以绩点和通过次数之和为竞争力,选拔竞争力最大的同学。
      - 将通过部门次数不同的同学进行分层,避免绩点低的同学失去机会。每通过一个部门,竞争力增加一个梯度。
      - 大体的匹配过程同热度优先原则。

    2.流程图

    3.代码说明

    1)热度优先匹配

    void StuAdv(Department *Dep, int DepSum, Student *stu, int stuSum) {    //按低热度优先原则开始匹配
    	int tmpDep = DepSum;
    	while (tmpDep--) {    //循环轮数为部门总数
    		int minHot = -1;    //查找并记录热度最低的部门的序号
    		for (int i = 0; i < DepSum; i++) {
    			if (Dep[i].visited == false && (minHot == -1 || Dep[i].hot < Dep[minHot].hot)) {
    				minHot = i;
    			}
    		}
    		Dep[minHot].visited = true;    //将该部门标记为已匹配
    		int tmp = min(Dep[minHot].member_limit, Dep[minHot].stuSums);    //待选该部门的学生总数
    		while (tmp--) {
    			int minId = 0;    //记录平均热度最低的学生在该部门学生数组中的下标
    			int minHotSum = stu[Dep[minHot].DepStuCho[minId]].hotAve;    //记录平均热度最高的学生 热度 
    			int getHotSum;    //平均热度临时值
    							  //低热度优先原则 > 高绩点优先  
    			for (int i = 1; i < Dep[minHot].DepStuCho.size(); i++) {
    				getHotSum = stu[Dep[minHot].DepStuCho[i]].hotAve;
    				if (getHotSum < minHotSum) {
    					minHotSum = getHotSum;
    					minId = i;
    				}
    				else if (getHotSum == minHotSum) {
    					if (stu[i].gpa > stu[minId].gpa) {
    						minHotSum = getHotSum;
    						minId = i;
    					}
    				}
    			}
    			int minHotStu = Dep[minHot].DepStuCho[minId];    //当前平均热度最低的学生的序号
    															 //进行时间匹配判断
    			bool flag = false;
    			for (int t = 0; t < stu[minHotStu].free_time.size(); t++)
    			{
    				for (int k = 0; k < Dep[minHot].event_schedules.size(); k++)
    				{
    					if (Dep[minHot].event_schedules[k] == stu[minHotStu].free_time[t])
    					{
    						flag = true;
    						Dep[minHot].stuResult++;    //中选个数 
    						Dep[minHot].DepChosen.push_back(stu[minHotStu].stu_id);//部门的中选学生列表增加该学生的记录
    						Dep[minHot].stuSums--;    //未中选个数 
    						Dep[minHot].DepStuCho.erase(Dep[minHot].DepStuCho.begin() + minId);//部门的未中选学生列表删除该学生的记录
    						stu[minHotStu].ResChosen.push_back(Dep[minHot].dep_id); //学生的中选部门列表增加该部门
    						if (stu[minHotStu].left != 1) //热度更新 
    							stu[minHotStu].hotAve = (stu[minHotStu].hotAve * (stu[minHotStu].left * 1.0) - Dep[minHot].hot) / (stu[minHotStu].left * 1.0 - 1);  // - Dep[minHot].hot 
    
    						for (int j = 0; j < stu[minHotStu].left; j++)//为中选部门列表 删除该部门 
    						{
    							if (stu[minHotStu].stuChosen[j] == Dep[minHot].dep_id) {
    								stu[minHotStu].stuChosen.erase(stu[minHotStu].stuChosen.begin() + j);
    								break;
    							}
    						}
    						stu[minHotStu].left--;
    						goto here;
    					}
    				}
    			}
    		here:
    			if (!flag)	//匹配失败
    				tmp++;
    		}
    		//删除未中选同学记录,更新热度 
    		for (int i = 1; i < Dep[minHot].stuSums; i++) {
    			int tag = Dep[minHot].DepStuCho[i]; //为该未中选同学下标
    			if (stu[tag].left != 1)
    				stu[tag].hotAve = (stu[tag].hotAve * (stu[tag].left * 1.0) - Dep[minHot].hot) / ((stu[tag].left * 1.0) - 1) - 1000; //
    			for (int j = 0; j < stu[tag].left; j++) {    //初始stuChosen为所有部门,现删除匹配失败的部门 
    				if (stu[tag].stuChosen[j] == minHot) {
    					stu[tag].stuChosen.erase(stu[tag].stuChosen.begin() + j);
    					break;
    				}
    			}
    			stu[tag].left--;
    		}
    	}
    }
    
    

    2)绩点优先匹配

    void StuAdv(Department *Dep, int DepSum, Student *stu, int stuSum) {    //按低热度优先原则开始匹配
    	int tmpDep = DepSum;
    	while (tmpDep--) {    //循环轮数为部门总数
    		int minHot = -1;    //查找并记录热度最低的部门的序号
    		for (int i = 0; i < DepSum; i++) {
    			if (Dep[i].visited == false && (minHot == -1 || Dep[i].hot < Dep[minHot].hot)) {
    				minHot = i;
    			}
    		}
    		Dep[minHot].visited = true;    //将该部门标记为已匹配
    		int tmp = 0;
    		if (Dep[minHot].member_limit > Dep[minHot].stuSums)
    			 tmp = Dep[minHot].stuSums;
    		else
    			 tmp = Dep[minHot].member_limit;
    		//int tmp = min(Dep[minHot].member_limit, Dep[minHot].stuSums);    //待选该部门的学生总数
    		while (tmp--) {
    			int maxId = 0;    //记录竞争力最强的同学的下标  
    			double maxHotSum = stu[Dep[minHot].DepStuCho[maxId]].hotAve;    //最强竞争力 
    			double  getHotSum;    //临时变量 
    							  //高绩点优先  
    			for (int i = 1; i <(int) Dep[minHot].DepStuCho.size(); i++) {
    				getHotSum = stu[Dep[minHot].DepStuCho[i]].hotAve;
    				if (getHotSum > maxHotSum) {
    					maxHotSum = getHotSum;
    					maxId = i;
    				}
    			}
    			int maxHotStu = Dep[minHot].DepStuCho[maxId];    //最强竞争力下标 
    															 //进行时间匹配判断
    			bool flag = false;
    			for (int t = 0; t < (int)stu[maxHotStu].free_time.size(); t++)
    			{
    				for (int k = 0; k <(int) Dep[minHot].event_schedules.size(); k++)
    				{
    					if (Dep[minHot].event_schedules[k] == stu[maxHotStu].free_time[t])
    					{
    						//		stu[maxHotStu].free_time.erase(stu[maxHotStu].free_time.begin() + t);
    						flag = true; //合格了 
    						Dep[minHot].stuResult++;    //部门中选个数 
    						Dep[minHot].DepChosen.push_back(stu[maxHotStu].stu_id); // 部门的中选学生列表增加该学生的记录
    						Dep[minHot].stuSums--;    //部门未中选学生个数 
    						Dep[minHot].DepStuCho.erase(Dep[minHot].DepStuCho.begin() + maxId);//部门的未中选学生列表删除该学生的记录
    						stu[maxHotStu].ResChosen.push_back(Dep[minHot].dep_id); //学生的中选部门列表增加该部门 
    						stu[maxHotStu].hotAve -= 100; //竞争力分层 
    						for (int j = 0; j < stu[maxHotStu].left; j++)//未中选部门列表 删除该部门 
    						{
    							if (stu[maxHotStu].stuChosen[j] == Dep[minHot].dep_id) {
    								stu[maxHotStu].stuChosen.erase(stu[maxHotStu].stuChosen.begin() + j);
    								break;
    							}
    						}
    						stu[maxHotStu].left--;
    						goto here;
    					}
    				}
    			}
    		here:
    			if (!flag)  //匹配失败 
    				tmp++;
    		}
    		//删除未中选同学记录
    		for (int i = 1; i < Dep[minHot].stuSums; i++) {
    			int tag = Dep[minHot].DepStuCho[i]; //未中选同学下标  
    			for (int j = 0; j < stu[tag].left; j++) {  //初始stuChosen为所有部门,现删除匹配失败的部门  
    				if (stu[tag].stuChosen[j] == minHot) {
    					stu[tag].stuChosen.erase(stu[tag].stuChosen.begin() + j);
    					break;
    				}
    			}
    			stu[tag].left--;
    		}
    	}
    }
    

    五、结果分析

    优先条件 部门期待招收人数 部门实际招收人数 未匹配到部门的学生数 实际耗时 输出文件路径
    热度优先 155 113 187 1.2s https://github.com/chujiuling/jiuling/blob/master/clubProject/output_condition1.txt
    绩点优先 155 155 145 1.2s https://github.com/chujiuling/jiuling/blob/master/clubProject/output_condition2.txt

    由上面测试可得出结论:

    • 1)热度优先匹配率73%,绩点优先匹配率100%。
    • 2)热度优先的算法还有待完善,匹配率相对较低。
    • 3)在部门需求的人数限定下,部门招收到的人采用绩点优先原则反而会更多。

    六、结对感受

    周龙荣LL
    自己和队友各自都有理解,但是都不完善,通过讨论,沟通,一步步的扩充,才慢慢变得完整。结对学习时,和队友分工学习不同的方面,然后进行整合讨论,相互引导比自己摸索来得容易的多。但是在这次作业讨论算法的过程中,不够积极大胆的提出自己想法,使得讨论效率不太高,影响了完成作业进度,对自己的表现不太满意。开始工作前,尽量先构思好大致的流程和采用的方法,不然会因为考虑不周到临近截止日期之前还不断地修改,匆匆忙忙做很多无用功。


    伍晨薇VV
    国庆的时候没太在意这个作业,觉得一两天就做完了,没具体的计划。还好队友督促,及时安排好任务,带动我。期间我们进行了远程+语音,发现效率及其高,这种方式希望以后有机会继续尝试!写匹配算法遇到很多bug,有几点结论:
    1.在宿舍,尽量不要熬夜debug,存有影响舍友的担心时,debug效率低下。
    2.学会一步步覆盖,输出,查看与计划中是否相符。
    3.提问时应尽量说明问题产生的情况,情绪要稳定,以免影响后续debug。
    4.结对中,应该合理分配工作,但我好像不太会分配,未来结对应该共同探讨清楚分工。


    郑秦Fang
    第一次结对打代码,有点新奇。假期中,通过了远程和通话与对友一起写代码,从中感觉被监督着写代码会使得注意力更集中,也能够及时改正一些自己没发现的错误,但是与此同时,当两个人意见不一时也使得效率下降。
    很多细节没有考虑好,比如目标是要从学生的角度来匹配尽可能全中还是从部门角度在部门需要的人数限定下尽可能满足,当部门限选人数为0时,学生的志愿部门是否该排除它等等。时间比较赶,想法就会比较“粗糙”。
    在这过程中,对友写代码改bug很用心,给了我能完成它的信心。虽然第一次结对写代码,没有达到理想的效果,但我们还是尽力地去完成它,认真对待。


    分工

    • 共同探讨、修改
    • VV:匹配算法
    • FANG:输入程序+博客
    • LL:输出+博客
  • 相关阅读:
    克如斯卡尔 P1546
    真正的spfa
    第四课 最小生成树 要点
    关于vscode中nullptr未定义
    cmake学习笔记
    python学习笔记
    (BFS 图的遍历) 2906. kotori和迷宫
    (图论基础题) leetcode 997. Find the Town Judge
    (BFS DFS 并查集) leetcode 547. Friend Circles
    (BFS DFS 图的遍历) leetcode 841. Keys and Rooms
  • 原文地址:https://www.cnblogs.com/jiuling/p/7641928.html
Copyright © 2020-2023  润新知