• 第二次结对编程作业——毕设导师智能匹配


    031402402 曹鑫杰
    031402428 鄢继仁

    第二次结对编程作业——毕设导师智能匹配

    项目链接:https://coding.net/u/Shepard_y/p/Homework/git

    问题描述:

    编码实现一个毕设导师的智能匹配的程序。提供输入包括:30个老师(包含带学生数的要求的上限,单个数值,在[0,8]内),100个学生(包含绩点信息),每个学生有5个导师志愿(志愿的导师可以重复但不能空缺)。实现一个智能自动分配算法,根据输入信息,输出导师和学生间的匹配信息(一个学生只能有一个确认导师,一个导师可以带少于等于其要求的学生数的学生)及未被分配到学生的导师和未被导师选中的学生。

    问题分析:

    • 算法的目标?
      确保最后没分到符合志愿的剩下的学生数越少越好。
    • 随机的数据,是不是一定要老师所带学生总数数大于学生数?
      考虑到实际情况所有学生最后都要分配到导师,所以要求导师所带学生数大于学生总数。
    • 随机的数据,要不要考虑实际中学生志愿会集中在热门老师的问题?
      也是为了符合实际情况,不让数据太水,要加入这个条件。
    • 要不要固定学生总数和导师总数
      为让程序更灵活,不把学生总数和导师总数固定
    • 选择的语言?
      C++
    • 导师可不可选择0个学生?
      有可能导师不想要带学生或因为自身原因无法带学生,我们可以让导师选择带的学生数为0。
    • 文件输入采用txt,数据库,格式如何?
      经过讨论,采用更加熟悉的txt文件输入,格式经讨论后确定,具体可见项目中format.txt。
    • 学生与老师之间的权重要怎么定义(分配规则,志愿梯度、平行?)
      分配规则:采用绩点排名90%+梯度志愿10%。权重分配公式:具体看代码实现部分
    • 如果一个学生多个志愿重复选同一老师怎么办?
      随机生成的数据可能会出现这种情况,考虑过为该学生增加权重,但增加过多是不合理的,过少使得学生浪费了自己的机会。后来发现在实现中,学生重复选择是没有太大意义的。
    • 变量名命名规范和注释
      这次使用的C++,决定采用下划线命名法,注释的话尽量多注释。

    算法设计:

    算法目标

    确保最后没分到符合志愿的剩下的学生数越少越好。

    题目分析

    学生和导师可以看做一个二分图,导师拆成多个点,数量等于其所期望学生数,每个学生填报的每个志愿就相当于学生与老师之间存在一条边。如果不考虑分配的优先程度(即边的权重)的话,那就是求一个二分图的最大匹配。如果考虑上绩点等多个因素(边有权值),就是求一个二分图的带权匹配

    算法选择

    解决二分图的带权匹配可以用KM算法或者费用流。两者差距不大,KM算法在二分图上效率比较高,但却不能再一般图上跑。我们在这里决定采用KM算法。

    算法介绍

    KM算法解决的是二分图最大权完备匹配,我们给每一个边设定一个权重,KM算法可以得到一个完备匹配(完备匹配一定是最大匹配,所以匹配数最大,即学生选上导师的人数最多)并且权值总和最大(从整体上让大家都满意)。

    Kuhn-Munkras算法流程:
    (1)初始化可行顶标的值
    (2)用匈牙利算法寻找完备匹配
    (3)若未找到完备匹配则修改可行顶标的值
    (4)重复(2)(3)直到找到相等子图的完备匹配为止

    随机化数据设计

    为了更贴合实际,在随机化数据的同时也添加了一些条件

    1.规定导师所期望学生总数要大于等于学生总数。
    2.根据实际情况设定热门、普通、冷门导师,各占比20%、60%、20%,选中概率分别为50%,40%,10%(概率不绝对,具体看随机的数据,但相差幅度不大)。

    权重设计

    设某一学生绩点排名为Rank(最高1,最低m),选择某一导师为第 i 志愿。

    权重 = ( ( ( m+1 - Rank ) / m * 0.4 + 0.6 ) * 0.9 + ( ( 6 - i ) * 2 / 100 ) ) * 10000

    即 绩点占比90%( 60%的底分+40%的排名分) + 志愿占比10% ,最高分10000,但最低分也不会太低,让彼此间有差异,但又不会太大。

    代码实现:

    随机化导师数据

    while(true)
    {
    	int sum=0; //导师所带学生总数 
    	memset(tea,0,sizeof(tea));
    	for(int i=1;i<=n;i++)
    	{
    		tea[i].id=i; //导师编号 
    		tea[i].num=r(0,8); //学生人数 
    		sum+=tea[i].num; 
    	}
    	if(sum>=m) break; //导师所带学生总数应大于学生总数 
    }
    for(int i=1;i<=n;i++)
    {
    	printf("%-8d%-8d
    ",tea[i].id,tea[i].num);
    } 
    

    随机化学生数据

    for(int i=1;i<=m;i++)
    	{
    		stu[i].id=i; //学生编号 
    		stu[i].GPA=((double)r(100,500)/100.0); //绩点 
    		for(int j=1;j<=5;j++) 
    		{
    			stu[i].aspiration[j]=rand_tea(); //志愿 
    		}
    		printf("%-8d%-8.2lf",stu[i].id,stu[i].GPA);
    		for(int j=1;j<=5;j++) printf("%-3d%c",stu[i].aspiration[j],j==5?'
    ':' ');
    	}
    

    随机化函数

    int r(int x,int y) //随机取[x,y]中的一个数 
    {
    	int t=y;
    	y=x;
    	x=t-x+1;
    	return rand()%x+y;
    }
    int rand_tea() //50%选中热门导师(占比20%),10%选中冷门(20%),40%选中一般(60%)
    {
    	int tmp=r(1,10000);
    	if(n==1) return 1; 
    	if(tmp<=5000)
    	{
    		while(1)
    		{
    			tmp=r(1,n);
    			if(v1[tmp]) return tmp;
    		}
    	}
    	else if(tmp>=9001)
    	{
    		while(1)
    		{
    			tmp=r(1,n);
    			if(v2[tmp]) return tmp;
    		}
    	}
    	else
    	{
    		while(1)
    		{
    			tmp=r(1,n);
    			if(v3[tmp]) return tmp;
    		}
    	}
    }
    

    核心算法

    bool DFS(int x) //匈牙利算法寻找増广路
    {
    	stu_vis[x] = true;
    	for(int y = 1; y <= tot; y++)
    	{
    		if(tea_vis[y]) continue;
    		int tmp = stu_l[x] + tea_l[y] - g[x][y];
    		if(tmp == 0)
    		{
    			tea_vis[y] = true;
    			if(linker[y] == -1 || DFS(linker[y]))
    			{
    				linker[y] = x;
    				return true;
    			}
    		}
    		else if(slack[y] > tmp)
    		slack[y] = tmp;
    	}
    	return false;
    }
    
    int KM()
    {
    	memset(linker,-1,sizeof(linker));
    	memset(tea_l,0,sizeof(tea_l));
    	for(int i = 1;i <= stu_num;i++) //设置顶标
    	{
    		stu_l[i] = -INF;
    		for(int j = 1;j <= tot;j++)
    		{
    			if(g[i][j] > stu_l[i]) 
    			{
    				stu_l[i] = g[i][j];
    			}
    		}
    	}
    	for(int x = 1;x <= tot;x++)
    	{
    		for(int i = 1;i <= tot;i++) slack[i] = INF;
    		while(true)
    		{
    			memset(stu_vis,false,sizeof(stu_vis));
    			memset(tea_vis,false,sizeof(tea_vis));
    			if(DFS(x)) break; //寻找到增广路,进行下一个点的増广 
    			int d = INF; 
    			//修改顶标 
    			for(int i = 1;i <= tot;i++)
    			{
    				if(!tea_vis[i] && d > slack[i]) 
    				{
    					d = slack[i];
    				}
    			}
    			for(int i = 1;i <= tot;i++)
    			{
    				if(stu_vis[i]) 
    				{
    					stu_l[i] -= d;
    				}
    			}
    			for(int i = 1;i <= tot;i++)
    			{
    				if(tea_vis[i]) tea_l[i] += d;
    				else slack[i] -= d;
    			}
    		}
    	}
    	int res = 0; //计算总权重 
    	for(int i = 1;i <= tot;i++)
    	{
    		if(linker[i] != -1)
    		{
    			res += g[linker[i]][i];
    		}
    	}
    	return res;
    }
    

    具体代码可见项目链接 https://coding.net/u/Shepard_y/p/Homework/git

    结果分析:

    下面为程序在一些极端条件下的测试

    固定导师数为30,学生数为100,热门导师占20%,6人,冷门导师占20%,6人。
    横坐标为学生志愿随机到热门导师的概率,概率越高,学生就越会集中选取热门导师。
    纵坐标为测10组数据所得的平均数。
    由图可知,随着概率的上升,未分配到学生的导师数量和为分配到导师的学生数量也随着上升。

    分析

    • 由于学生集中选部分导师,其他导师填报人数很少,甚至没有,这会导致未分配的情况出现的。
    • 在非极端情况下,该程序表现情况不错。若是不考虑导师热不热门,采用原版随机化程序,基本上不会出现学生未选上老师的情况。
    • 每次只测了10组数据,可能不够客观。

    结对感受:

    曹鑫杰: 本次作业对友主要负责算法设计,对友编码时,在旁边提出问题和算法可能存在的bug,讨论并排出这些可能,两个人分析问题会更全面一点,对友提出的算法很优,后面在测试的时候我们都比较满意。

    鄢继仁: 这一次结对编程,由于一开始我们讨论了许多问题,就很快确定了算法。对友也给了很多建议,所以进行的比较顺利。同时也遇到了不少困难,比如在编码的时候要多注意变量名的规范和多加注释,向对友分析算法的同时由于自己的表达能力不佳,导致交流遇到了障碍。

    对彼此结对中的闪光点或建议的分享

    1.进行合理的分工,进度会更快些,比如确定好输入格式后,随机化数据和算法实现可以分开来写。
    2.开工前问题讨论的细致些,能给后续的工作避免不少麻烦。

  • 相关阅读:
    darknet实时识别无法显示在窗口解决
    C# 获取当前打开的文件夹2
    C# 如何调试安装包
    C# 自定义文件格式并即时刷新注册表 非关闭explorer
    C# 获取当前打开的文件夹
    SQL Server里面导出SQL脚本(表数据的insert语句)
    windows平台安装redis服务
    C# 默认参数/可选参数需要注意
    webstrom使用
    office密匙
  • 原文地址:https://www.cnblogs.com/cccddd/p/5923644.html
Copyright © 2020-2023  润新知