软件工程实践 2017 第二次结对作业
部门与学生数据生成及智能匹配
结对成员
- 吴媛媛 031502336
- 李永盛 031502517
Github 地址
https://github.com/ladit/Associator
数据生成
示例数据
https://github.com/ladit/Associator/blob/master/example_input_data.txt
生成原理与考虑因素
生成示例遵守的最优先规则应该是符合生活中的实际情况。
除非特别指出,以下 部门的特点标签
和 学生的兴趣标签
统称为 标签
,部门的常规活动时间段
和 学生空闲时间段
统称为 时间段
,工作日
指周一至周五,周末
指 周六和周日
。
标签生成
我们将用于生成的标签全集数量限制在了 30 个,定义如下:
const string TAGS[] = {
"reading", "travelling", "programming", "film", "English", "music", "chess", "painting", "photography", "food",
"cycling", "traditional culture", "tea", "nature", "health", "psychology", "volunteer", "career", "ACG", "Japan",
"fashion", "computer", "technology", "liquor", "Kungfu", "dance", "basketball", "Yoga", "NBA", "e-sports"
};
这些标签在定义时都应该是较为宽泛的内容,否则如果一个标签太过细化,例如 考研讲座
,将导致标签没有多部门复用的意义。
我们设置部门的特点标签为 2 到 4 个,学生的兴趣标签为 8 到 12 个。
虽然以上的标签已经较为宽泛,但有一些标签不太可能同时是某一个部门的,例如 liquor
和 basketball
,为了避免这样的情况,我们建立了一个图,每一个标签是一个节点,两个节点若有联系(即可能同时是某一个部门的),则它们之间存在一条双向边。每个部门选取的所有标签应当两两间存在一条边。
部门选取兴趣标签时,先随机选取一个兴趣标签,然后再继续随机,并检测是否符合上述关系。
学生兴趣标签随机选取即可。
学生空闲时间段生成
我们把学生工作日的空闲时间段数量设置为5到8个,在较为固定的八大块时间内选取,定义如下:
一二节课 | 三四节课 | 午饭及午休 | 五六节课 | 七八节课 | 晚饭时间 | 九到十一节课 | 晚上下课后 |
---|---|---|---|---|---|---|---|
8:00~10:10 | 10:10~12:00 | 12:00~14:00 | 14:00~15:45 | 15:45~17:30 | 17:30~19:00 | 19:00~21:30 | 21:30~23:30 |
const string STUDENT_TIME_PERIOD[] = {
"19:00~21:30", "8:00~10:10", "10:10~12:00", "14:00~15:45",
"15:45~17:30", "12:00~14:00", "17:30~19:00", "21:30~23:30"
};
上述字符串数组这么设置是因为我们把这八个时间段再分为三个块:
- 19:00~21:30 如果没课(一般是没课)是最可能的空闲时间;
- 8:0010:10、10:1012:00、14:0015:45、15:4517:30 没课则空闲,是较为可能的空闲时间;
- 12:0014:00、17:3019:00、21:30~23:30 是午饭、晚饭和晚上下课后三个时间段,空闲可能最小;
按 3:2:1 的可能性随机分配到这三个块上,再随机选取具体的时间段。
学生周末的空闲时间段和工作日的类似,把空闲时间段数量设置为 13 到 16 个,也在较为固定的八大块时间内选取,但分为两个块:
- 8:0010:10、10:1012:00、14:0015:45、15:4517:30、19:00~21:30 没课则空闲,是较为可能的空闲时间;
- 12:0014:00、17:3019:00、21:30~23:30 是午饭、晚饭和晚上下课后三个时间段,空闲可能小;
按 3:1 的可能性随机分配到这两个块上,再随机选取具体的时间段。
部门常规活动时间段生成
设置部门常规活动时间段数量为 2 到 3 个,一天最多一个,按 1:3 的可能性随机分配到工作日和周末,再随机生成工作日或周末中某一天的某一段时间作为常规活动时间段。
随机设置活动的起始时间为 8:00~21:00 中 30 分钟整数倍时间点,活动时间长度的选取方式如下:
部门的常规活动一般可能有:
活动名称 | 可能的时间长度(min) | 每周次数 |
---|---|---|
例会(或类似的固定活动) | 30 45 60 75 90 | 一次 |
周末聚餐(或类似的偏向周末的活动) | 90 120 | 零次到一次 |
学习、讲座(或类似的偏向工作日的活动) | 60 90 120 | 零次到两次 |
综上,部门的常规活动每周约2到3次,30、45、75 分钟的活动大概每周零次到一次,60、90、120 分钟的活动占据剩下的次数。定义如下:
const int DEPARTMENT_TIME_PERIOD[] = { 30, 45, 75, 60, 90, 120 };
学生部门意愿生成
现实生活中,当我们面对很多部门纳新安利时,会优先选取有兴趣、自己空闲时间较符合这个部门活动时间的,但有的人会害怕这些部门太热门(我们假设学生不知道任何部门的热门程度),又不想空手而归,因此报名一些其他部门。也有的人就是喜欢猎奇,报名了喜欢的部门后同样报名一些其他部门。
有的人比较奇怪,不想报部门还填写申请表(部门意愿空缺),这部分人设置为0到10个。
剩下的有部门意愿的人中,学生A的总部门意愿数量为随机0到5个,将学生A的空闲时间段与所有部门常规活动时间段进行比较,若有许多部门的常规活动学生A都有空,则将学生A的标签与这些部门的标签进行比较,按重合数量排序,取排序后的部门列表前随机0到3个作为部门意愿(优先选取有兴趣、时间符合)。总部门意愿数量减去前面已经添加的部门意愿数量的差若为正数,随机选取差额部门作为部门意愿(不想空手而归与猎奇)。
其他
其他的 部员数量限制、部门号、学生号 使用简单的随机或递增即可。
数据建模及匹配程序
部门和学生建立为两个类(仅列出重要成员):
class Department
{
public:
string departmentID; // 部门号
int memberLimit; // 部员数量限制
vector<string> scheduledEventsTimeList; // 常规活动时间段
int scheduledEventSize; // 常规活动时间段数量
vector<string> tags; // 部门的特点标签
int tagSize; // 部门的特点标签数量
};
class Student
{
public:
string studentID; // 学生号
vector<string> freeTimeList; // 学生空闲时间段
int freeTimeListSize; // 学生空闲时间段数量
vector<string> tags; // 学生的兴趣标签
int tagSize; // 学生的兴趣标签数量
vector<string> applyingDepartmentList; // 部门意愿
int applyingDepartmentSize; // 部门意愿数量
};
我们使用 RapidJSON
来处理 json。
我们称 一个部门的常规活动时间学生A都有空
为 时间符合
,称 一个部门的特色标签和学生A的兴趣标签重复数量
为 标签符合
。
匹配算法建立一个类(仅列出重要成员):
class Matcher
{
public:
void Work(); // 供主程序调用
private:
static const int DAYS_PER_WEEK = 7;
static const int HOURS_PER_DAY = 24;
static const int MINUTES_PER_HOUR = 60;
static const string WEEK[DAYS_PER_WEEK];
map<string, int> weekIndex;
static const int DEPARTMENT_SIZE = 20;
Department department[DEPARTMENT_SIZE]; // 存储部门信息
map<string, int> departmentIndex;
/// int:兴趣交集 int:学生下标
vector<pair<pair<bool,int>, int> > validStudent[DEPARTMENT_SIZE]; // 部门可选的学生
vector<string> selectedStudent[DEPARTMENT_SIZE]; // 部门已选的学生
static const int STUDENT_SIZE = 300;
Student student[STUDENT_SIZE]; // 存储学生信息
bool isStudentSelected[STUDENT_SIZE]; // 学生是否被选取
bool isDepartmentSelected[DEPARTMENT_SIZE]; // 部门是否被分配过
vector<string> unluckyDepartments; // 未被分配到学生的部门
vector<string> unluckyStudents; // 未被部门选中的学生
vector<string> matched[DEPARTMENT_SIZE]; // 匹配信息
Document inputDocument; // RapidJSON 输入 DOM
Document outputDocument; // RapidJSON 输出 DOM
pair<int, int> StringTimeToDayTime(const string & str); // 检测时间符合
pair<int, int> StringTimeToWeekTime(const string &str); // 检测标签符合
bool IsTimeMatched(const int & studentIndex, const int & deparmentIndex);
// string时间转换为一天中的时间
int TagsIntersection(const int & studentIndex, const int & deparmentIndex);
// string时间转换为一周中的时间
void SelectStudentByTimeOrderByTag(); // 部门选取学生算法
void Read(); // 读取 json 并转换为 Department 和 Student 类的对象
void Write(); // 生成 outputDocument 并写入文件
void Match(); // 匹配算法:选取学生并存到待输出 string 数组
};
从 input_data.txt
导入解析后,用 Department department[DEPARTMENT_SIZE]
和 Student student[STUDENT_SIZE]
分别存储所有的部门与学生信息。
匹配算法目的是尽量让一个部门收取尽可能多的学生。
有学生申请的部门的申请列表中,将学生先按时间符合排序,再按标签符合排序,取所有可取的,且少于部员数量限制的学生,加入 matched
数组,否则再将申请列表剩下的学生按时间符合排序,取所有可取的,且少于部员数量限制的学生,加入 matched
数组。
做完上述步骤后,未被分配到学生的部门加入 unluckyDepartments
数组,未被部门选中的学生加入 unluckyStudents
数组。
最后将 unluckyStudents
数组、unluckyDepartments
数组、matched
数组 Phrase
并输出到 output_data.txt
。
代码规范
- 面向对象风格,即使用
class
,例如Department
、Student
、Matcher
、MockDataGenerator
等 - 命名
- 类名以大写字母开始,每个单词首字母均大写,不包含下划线
- 函数名以大写字母开始,每个单词首字母均大写,不包含下划线,例如
GenerateDepartmentTags
、GenerateDOM
、RandomPeriodInADay
、IsTimeMatched
等 - 普通变量的首个单词小写,其他每个单词首字母均大写,必要时使用下划线作为前缀。例如
tagsAssociation
、memberLimit
、endMinute
、_scheduledEventsTimeList
等 - 静态常量全部字母大写,单词间用下划线间隔,例如
USAGE
、STUDENT_TIME_PERIOD
等
stdafx.h
中按照系统头文件(例如vector
)、外部头文件(json处理,例如rapidjson/document.h
)、自身class
头文件(例如MockDataGenerator.h
)排序,在整个项目中统一引用- 大括号换行风格,更加清晰易读
- 允许
if
后只有一行代码时不使用大括号 - 过长的一行应当换行以适合阅读
- 函数中添加适当空行以分清结构
- 适当注释
结果评估
根据我们上面生成的示例数据生成算法和匹配算法运行多次,有如下结果分析:
生成算法还算符合预期,部门的标签是有关联的,不会有热衷泡茶的电竞部门出现。学生和部门的时间段也大致模拟真实情况,周末较多,学生空闲时间较固定,部门活动时间长度较固定。重点在于学生部门意愿,现实生活中我们一般不了解想要报名的部门的常规活动时间段,只是按兴趣标签来报,但这里我们假设学生可以阅览所有部门的常规活动时间段,并相应做出选择,这更有利于精确匹配。
匹配结果中,比较不错的是几乎不会有未被分配的部门出现,但未被分配的学生大概在130-160个左右,这比较尴尬。原因在于,学生的空闲时间都挺多的,可能有许多学生填写同一些部门,但这些部门限制只收10-15个,这些学生被收取后,剩下的也选这些部门的学生就被拒之门外,又因为他们填写的其他「冷门」部门较少,所以就被扔进了unluckyStudents
。因此,想要改进的话应该还是要从两个算法入手,例如匹配要求放宽(时间不完全匹配也行),生成部门意愿时对时间符合和标签符合按某个权值相加,放宽时间符合要求。
结对感受,闪光点或建议
这次作业遇到国庆,我去浪了而对友在忙别的事,所以没有尽早开始做,后来加班加点,效率又不高疯狂写bug。对友是 ACM 的,所以很多概念与算法都是理解、思考、实现迅速,这大大促进了我们的进程。所以要特别感谢对友,承担起了大部分的算法实现重任,才有办法强行完成。