Chs97's Blog

结对项目--第二次作业

结对项目--第二次作业

标签(空格分隔): 软工实践


这里是:作业的传送门

结对情况:248 文航、205 汉森
generator的传送门
match地址

一、设计说明

1.匹配算法设计

这是一个开放性的题目,题目给了两个开发性的思考点,

4、需要为匹配算法确立几条分配或排序原则,比如 绩点优先、或兴趣优先、或活动时间优先、或其他等等,请你们结对讨论确定。
5、对不同策略做出评价,并在博客中展示测试结果。提醒:对于同一组输入,输出的未被导师选中的学生数越少越好

(a、学生优先

一开始我看到这个题目要求的时候,特别是看到第五点的提示的时候,以为题目的要求是:
确定某种匹配原则,来使得尽可能多的学生都能加入部门,并且以此处为出发点,思考学生优先的匹配方案。

  • 网络流思想

    拿出反例,反驳zwh的网络流想法: 网络流的初始化建边在跑的过程中不能删边,也就是说如果A报名了X部门,然后和Y部门的时间冲突了,就不能报Y部门。但是在建边的时候A与X Y都连边了,就会导致结果错误。

  • 贪心---优先级思想

利用贪心的思想,类比高考填报志愿,给每个学生的部门申请投以优先级:第一志愿>第二志愿>第三....
利用三层循环:第一层枚举部门,第二层枚举学生,第三层获取该学生对这个部门的匹配度,如:判断是否时间冲突、是第几个志愿等,然后贪心从匹配度高的开始一个部门一个部门的来

(b、部门优先

经过激烈的讨论后,我的学生优先的两个想法先后被自己和别人hack了。于是乎,我恍然大悟:没有必要以学生优先,实际的情况是,部门之间的选人是相对独立的,处于竞争状态,应该都是尽可能的选取最优秀的生源为主要目标,这个想法会比较合理

  • 贪心
    利用某个方法计算出 学生--部门 的匹配度,然后每次选取匹配度最高的 学生--部门 ,然后让这个学生加入对应的部门,同时更新这个学生对其他部门的匹配度(有些部门对部员加入的部门数有要求)

最终设计

最终,我们假设的场景是:

新生入学,准备加入部门,部门积极准备纳新,各个部门都争相向选取更满足部门条件的部员,新生们也想通过自己的实际情况和部门的一些介绍来想加入对自己更有意义的部门

经过详细的考虑后,我们一致选择了部门优先的想法。
部门优先的情况下:zwh认为兴趣是关键,为了让兴趣标签更加符合第一优先级的原则,我们设定每个部门的兴趣标签数量相同,以匹配部门的兴趣标签数为匹配度的第一考量,这样子可以保证不会出现,拥有更多兴趣标签的部门更具优势。但是我不这样认为,我认为,这个部门标签多,代表部门的实力更强,所以更有选择权,也就是说,必须要更有优势。但是,最终向zwh妥协了,用了他的想法,在造数据的时候,让部门的标签数量一致。
接下来,就是可以参与部门的活动时间,由于两个人参与部门的经历不同,我认为部门例会很重要,要保证有时间参与,文航认为参与部门的例会不是关键,那些不定期的部门活动才是加入部门的意义所在,所以综合考虑,时间就分成了两块,学生的每周固定时间和每周的不定期时间,以总的空闲时间作为第二考量,当学生匹配了一个部门后,因为例会的关系,总空闲时间有所下降,因此要更新该生对其他部门的匹配度,若是考虑到部门除了例会以外还有其他活动,会使得匹配算法过于复杂,因此总空闲时间的减少量为部门例会的时间。

至于绩点,个人认为及时是大二大三的学生,成绩与部门的影响比较小,兴趣标签已经足以表现这方面的情况。

还有其他的,如:给部门定一些属性的比例,如:不同兴趣标签之间还有优先级等,就算能实现,实际情况是:不存在属性的考量标准,我们思考的其他方面的东西,要么真实很难定量,要么想不出来,我们觉得匹配只是一个初步标准,其他东西需要面试才能体现

匹配细化成量的原则:
原则1

2.内部实现设计(类图)(1)

根据上述想法,在不考虑输入输出使用json的情况下,仅由C++完成整个过程设计的类图
类图2

3.接口设计(API)

算法实现后,为了实现json的输入与输出,定下了相应的输入要求
(经过算法设计难度和json设计难度的考量,输入输出要求版本已经改进了3次)
输入输出

类READDATA:

  • 构造函数:传入文件名
  • getStudents() 返回vector
  • getDepartment() 返回vector
struct student {
	int id;
	vector<times> times;
	int actives;
	vector<string> tags;
	vector<int>applications;
};
struct department {
	int id;
	times meeting;
	vector<string> tags;
	int limit;
};	

类MATCHING

  • void getStudent(vector &S) //初始化学生
  • void getPartment(vector &P)//初始化部门
  • void matching()//匹配
  • void Print()//输出到文件

4.内部实现设计(类图)(1)

完成了接口设计后,已经可以让代码正常运行了,类图也就完整了
类图

5.测试数据的实现

其实测试数据的实现基本上就是根据拟定的输入输出的要求来的,由汉森实现。

package main

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"math/rand"
	"strconv"
	"time"
)

type times struct {
	Start int `json:"start"`
	End   int `json:"end"`
}

type department struct {
	Id         int      `json:"id"`
	HasMeeting int      `json:"hasMeeting"`
	Meeting    times    `json:"meeting"`
	Tags       []string `json:"tags"`
	Limit      int      `json:"limit"`
}

type student struct {
	Sn           int      `json:"sn"`
	Times        []times  `json:"times"`
	Actives      int      `json:"active"`
	Tags         []string `json:"tags"`
	Applications []int    `json:"applications"`
}

func generatorByWeek(day, time int) int {
	return (day-1)*24*60*60 + time*60*60
}

var timesList = []times{}
var tags = []string{"film", "English", "reading", "music", "dance", "basketball", "chess",
	"programming", "drawing", "writing", "football", "coding", "running",
	"moning", "testing", "talking", "food"}

func createTime(start, len int) times {
	time := times{}
	time.Start = start
	time.End = start + len
	return time
}

func generatorTimes() {
	for i := 1; i <= 5; i++ {
		timesList = append(timesList, createTime(generatorByWeek(i, 18), 60*60))
		timesList = append(timesList, createTime(generatorByWeek(i, 21), 60*60))
	}
}

func readName(filename string) ([]string, error) {
	bytes, err := ioutil.ReadFile(filename)
	if err != nil {
		fmt.Println("ReadFile: ", err.Error())
		return nil, err
	}
	names := make([]string, 0)
	if err := json.Unmarshal(bytes, &names); err != nil {
		fmt.Println("Unmarshal: ", err.Error())
		return nil, err
	}

	return names, nil
}
func generatorStudents(n, m int) []student {
	rand.Seed(time.Now().UnixNano())
	var students = []student{}
	for i := 0; i < n; i++ {
		t := student{}
		tagsCount := rand.Intn(len(tags)-1) + 1
		timesCount := rand.Intn(len(timesList)-2) + 2
		permTags := rand.Perm(len(tags))
		permTimes := rand.Perm(len(timesList))
		Tags := []string{}
		for i := 0; i < tagsCount; i++ {
			Tags = append(Tags, tags[permTags[i]])
		}
		Times := []times{}
		for i := 0; i < timesCount; i++ {
			Times = append(Times, timesList[permTimes[i]])
		}
		appCount := rand.Intn(5) + 1
		permApp := rand.Perm(m)
		t.Sn = i
		t.Tags = Tags
		t.Times = Times
		t.Actives = rand.Intn(12) * 60 * 60
		t.Applications = permApp[:appCount]
		students = append(students, t)
	}
	return students
}
func generatorDepartments(m int) []department {
	rand.Seed(time.Now().UnixNano())
	var departments = []department{}
	for i := 0; i < m; i++ {
		t := department{}
		t.Id = i
		t.Limit = rand.Intn(40) + 20
		perm := rand.Perm(len(timesList))
		t.Meeting = timesList[perm[0]]
		tagsCount := 5
		permTags := rand.Perm(len(tags))
		Tags := []string{}
		for i := 0; i < tagsCount; i++ {
			Tags = append(Tags, tags[permTags[i]])
		}
		t.Tags = Tags
		departments = append(departments, t)

	}
	return departments
}
func init() {
	generatorTimes()
}
func int2String(x int) string {
	return strconv.Itoa(x)
}
func main() {
	departments := flag.Int("department", 0, "the numbers of department")
	students := flag.Int("student", 0, "the numbers of students")
	flag.Parse()
	S := generatorStudents(*students, *departments)
	D := generatorDepartments(*departments)
	data := make(map[string]interface{})
	data["students"] = S
	data["departments"] = D
	fileName := "tests/s" + int2String(*students) + "-d" + int2String(*departments) + "-in.json"
	studentsJSON, err := json.Marshal(data)
	if err != nil {
		fmt.Println("data 2 json error")
	}
	ioutil.WriteFile(fileName, studentsJSON, 0x644)
}

思路: 人工生成几个可能的空闲时间段(周一到周五晚上 6 - 7点,9 - 10 点)、人工生成兴趣标签tags,随后随机学生的空闲时间段数和空闲时间段,标签数和标签。部门的例会时间为生成的空闲时间段中的某一段时间。

由go语言实现编码,然后利用的go语言自带导出json的功能,导出生成json。
通过对比,现在的某些语言对json的支持比C/C++好得多。

二、代码实现

1.核心代码

(a 匹配过程


struct P {

	int sid;        //学生ID
	int pid;        //部门ID
	int sameTag;    //部门与学生的相同兴趣标签数
	int time;       //学生的空闲时间
	P(int _sid = 0,int _pid = 0,int _sameTage = 0,int _time = 0):sid(_sid),pid(_pid),sameTag(_sameTage),time(_time){
	}
	//重载运算符: 兴趣第一原则,时间第二原则
	bool operator < (const P &t) const {
		if(sameTag ==t.sameTag) return time < t.time;
		else return sameTag < t.sameTag;
	}
};
//匹配算法的实现
void MATCHING::matching()
{
    /*-------init-------*/
    STUDENT *stu;
    PARTMENT *par;
    //用优先队列来贪心每次选取最大的匹配度
    priority_queue<P>que;   
    P t;
    //记录每个学生的空闲时间
    int *leisureTime = new int[studentCount+10];
    
    //初始化匹配度和空闲时间
    //枚举学生
    for (int i = 0; i < studentCount; ++i){
    	stu = Student + i;
    	leisureTime[i] = stu->leisureTime;
    	//枚举部门
    	for (int j = 0; j < stu->applicationCount; ++j){
    		int k = stu->application[j];
    		if (k >= partmentCount) continue;
    		par = Partment + k;
    		if (timeconflict(stu, par)) continue;   //判断是否冲突
    		int sameTag = getSameTag(stu, par);
    		int time = stu->leisureTime;
    		t = P(stu->studentId, par->partmentId,sameTag,time);
    		que.push(t);
    	}
    }


	/*-------solve-------*/
	while (!que.empty()){
		t = que.top(); que.pop();
		stu = Student + t.sid;
		par = Partment + t.pid;
		//--判断当前的 学生--部门 信息是否已经过时
		if (leisureTime[t.sid] != t.time) continue;
		if (stu->matchCount == stu->applicationCount) continue;
	    
	    //部门获取人数已经达到上限
		if (par->matchCount == par->studentLimit) continue;


		//匹配成功
		par->matchCount++;
		par->mathStudent.push_back(t.sid);
		stu->matchCount++;
		stu->matchPartment.push_back(t.pid);
	
		t.time -= (par->regularTime.second - par->regularTime.first + 1);
		leisureTime[t.sid] = t.time;
	
	    //更新 学生--部门 信息
		for (int j = 0; j < stu->applicationCount; ++j){
			int pid = stu->application[j],k;
			if (pid >= partmentCount) continue;
			par = Partment + pid;


			if (timeconflict(stu, par)) continue;
			//判断 当前部门是不是已经加入了,加入了就不用更新
			for (k = 0; k < stu->matchCount; ++k){
				if (pid == stu->matchPartment[k]) break;
			}
	
			if (k==stu->matchCount||k < stu->matchCount && pid == stu->matchPartment[k]) break;
	
			int sameTag = getSameTag(stu, par);
			int time = t.time;
			
			que.push(P(stu->studentId, par->partmentId, sameTag, time));
		}
	}
}

(b 冲突判断条件

//判断学生与部门的冲关系
bool MATCHING::timeconflict(STUDENT * stu, PARTMENT * par1)
{
    //判断是否已经加入了该部门
	for (int i = 0; i < stu->matchCount; ++i)
	{
		int j = stu->matchPartment[i];
		if (j == par1->partmentId) return true;
	}
	
	//判断学生加入的部门是否与该部门例会上时间冲突
	PARTMENT *par2;
	for (int i = 0; i < stu->matchCount; ++i)
	{
		int j = stu->matchPartment[i];
		par2 = Partment + j;
		int mx = max(par2->regularTime.first, par1->regularTime.first);
		int mi = min(par2->regularTime.second, par1->regularTime.second);
		if (mx <= mi) return true;
	}
	
	//判断学生的固定时间中是否包含了整个部门的例会时间
	for (int i = 0; i < stu->timeCount; ++i)
	{
		if (stu->stableTime[i].first <= par1->regularTime.first
			&&stu->stableTime[i].second <= par1->regularTime.second)
		{
			//stu->matchCount++;
			return false;
		}
	}
	
	return true;
}

三、算法评价

1.运行及测试结果展示

测试200位同学,20个部门的情况

测试500位同学,30个部门的情况】

测试1000位同学,50个部门的情况

测试5000位同学,100个部门的情况

输出1

输出效果图

输出3

2.效能分析报告

直接测试5000student 和 100 department的性能

1.纯C++ 的输入输出

性能分析1

发现getSudent函数占了一半的时间?

性能分析2

原来是vector的输入耗时很大,STL在没有开O2优化的情况,是很耗时的,然后vector是很好用的东西,直接5000个student的数组是acm行为,new的话指针太多不好处理,所以最后还是保留vector这种方式

性能分析3

getSameTag函数耗时也很多?由于兴趣标签很少,两重循环的方式不会耗时太多兴趣标签是字符串类型,存储方式是指针,排序然后用O(n)的方法也不会减少太多,反而增加代码的复杂度。所以没有想到什么优化的方法,唯一能做的就是限制一下每个人和部门标签数不要太多(这样也符合实际)

2.完整实现后的分析度

性能分析5
果然不出所料,getStudents()的耗时增加了,vector还是大头
性能分析4

算法评价:

(a 学生优先

(b 部门优先

四、遇到的困难及解决方法

困难描述
golang识别命令行输入的参数

做过哪些尝试

google之。

是否解决

使用flag库完美解决。

有何收获

golang的各种库真多。

五、对队友的评价

有哪些好的地方值得学习

毕竟是第一,无可挑剔的认真。对于每个细节都深入思考。本作业贡献了最重要的算法部分。

有哪些不好或者需要改进的地方

附件:
PSP表格

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划 5 5
· Estimate · 估计这个任务需要多少时间 5 10
Development 开发 500 310
· Analysis · 需求分析 (包括学习新技术) 60 30
· Design Spec · 生成设计文档 0 15
· Design Review · 设计复审 (和同事审核设计文档) 0 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 10 5
· Design · 具体设计 120 30
· Coding · 具体编码 180 120
· Code Review · 代码复审 120 30
· Test · 测试(自我测试,修改代码,提交修改) 120 60
Reporting 报告 210 340
· Test Report · 测试报告 180 300
· Size Measurement · 计算工作量 10 10
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 20 30
合计

学习进度条【5分】

第N周 新增代码 (行) 累计代码(行) 本周学习耗时(小时) 累计学习耗时(小时) 重要成长
1 1000 1000 30 30 GoLang语法 xormCURD操作 go-iris路由设置 团队项目前端配置
2 700 1700 31 61 **考试系统前端部分完成60%,后台部分完成70%。
posted @ 2017-10-15 19:51  Chs97  阅读(181)  评论(1编辑  收藏  举报