摘要:遗传算法(Genetic Algorithm)是一种基于自然选择过程,模拟生物进化的AI模型,它可以在模拟的生物进化过程中逐代搜索到最优解的一种方法。本文利用遗传算法实现了一个简单的程序来对课程进行排程。

本文分享自华为云社区《如何用遗传算法进行智能排课》,作者: jackwangcumt。

根据百度百科的定义,遗传算法(Genetic Algorithm)是一种基于自然选择过程,模拟生物进化的AI模型,它可以在模拟的生物进化过程中逐代搜索到最优解的一种方法。遗传算法不能直接对问题进行求解,而是需要借助编码规则,将问题中的核心要素抽象为染色体上的基因,并通过基因的交叉、变异等过程,迭代选择优良基因进行繁殖,生成下一代,直到求得最优解或者满意的优化解。目前遗传算法的使用范围非常广泛,常应用于运筹、机器学习、人工智能等领域。

1 遗传算法过程图解

遗传算法核心的任务是要通过编码体系,给出解决方案的染色体表现规则,首先需要随机初始化一定数量的种群(population),而种群则由一定数目的个体(individual)构成。每个个体实际上是染色体(chromosome),可以通过规则计算出适应度(fitness)。初代种群产生之后,按照优胜劣汰的进化原理,逐代进化产生出优秀的后代。

在每代进化过程中,根据个体的适应度大小来选择个体,并借助于自然遗传学的遗传算子(genetic operators)进行交叉(crossover)和变异(mutation),产生新的种群。末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。退出条件一般为达到最大的迭代次数,比如10000次,另外,就是适应度满足要去,比如达到0.99。基本的流程示意图如下所示:

2 课程编排要求

实际的课程编排,由于涉及到大量的老师、班级、教室和课程等要素,因此非常的复杂,借助遗传算法也可能求不出最优解,而只是求出局部最优解,但是利用遗传算法辅助课程编排仍然是一个非常好的手段。一般来说,课程编排过程中,必须满足几个限制条件,否则,给出的课程安排是无效的,具体说明如下:

  1. 同一时刻,一个教室只能开设一门课程;
  2. 一个教室是有座位个数限制的,上课的学生总数不能超过教室座位数;
  3. 同一个时刻,同一个老师或班级学生只能参与一门课程,而不能参与多个课程;
  4. 教室分多媒体教室和普通教室,有的课程需要多媒体教室,因此,教室配置必须满足课程要求;

以上4条限制,都满足的情况下,给出的课程安排才是有效的,但请注意,不一定是最优的,它并未考虑优化条件,比如同一个老师,如果在一天按照多门课程,那么显然有点超负荷工作,或者同一门课程,在同一天,连续开设多次,这样对于老师和学生来说,都有点吃不消。

3 课程编排中要素数据结构

前面提到,课程编排过程中,涉及到老师、班级(学生组),教室和课程等要素,下面给出各要素的数据结构说明:

课程 : 课程对象的实现类名为Course ,其中包含课程ID和课程名称2个字段。
教室 : 教室对象的实现类名为Room ,其中包含教室ID、教室名称、座位数、是否是多媒体教室这4个字段。
教师 : 教师对象的实现类名为Professor,其中包含教师ID、教师名称和该教室需要教授的所有课程班(CourseClass)这3个字段。
课程班 : 课程班对象的实现类名为CourseClass ,其中包含课程授课老师、教授的课程、上课的班级、需要的座位数(多个班级人数求和)、是否需要多媒体教室和上课时长这6个字段。此类还提供方法GroupsOverlap(CourseClass c)来判断自己和参数c是否有班级的重叠,同理,方法 ProfessorOverlaps(CourseClass c)可判断自己和参数c是否有老师的重叠。
班级 : 班级对象的实现类名为StudentGroup,其中包含班级ID、班级名称、班级人数和该班级需要上的所有课程班(CourseClass)这4个字段。
染色体表现(Chromosome Representation): 为了应用遗传算法,需要重点考虑如何用基因序列的方式来表示问题的解,一般来说,基因序列是一长串有序的序列,这里可以将多维的课程安排要素通过降维方式,压缩到一维数组上,而数组(后续称为插槽Slots)的长度就是 :

一周上课天数 * 每天的上课时长数 * 教室数

举例来说,一周上课天数假设为5,表示周一到周五,每日上课时长为12小时,比如早上从8点开始上课,晚上到20点结束。而教室数为了简单起见,假设是2个,因此总数组长度为 5 * 12 * 2 = 120 ,这个一维数组中的每个元素,可以放置课程班CourseClass ,而不同的课程班组合就代表了不同的课程排课方案。排课方案可以用如下示意图进行表示:

注意:上述一个插槽Slot代表一个小时单位,也可以表示课程的位置索引,其中可指向具体的课程班CourseClass实例,表示该插槽位有对应的课程安排。

4 适应度Fitness

基于上述的染色体表现,我们需要计算某一个个体的适应度,计算的方法如下:

  1. 遍历代表染色体表现的一维数组中的每个课程班信息,如果同一时刻教室不存在多个课程的重叠,那么增加适应度分值。
  2. 遍历代表染色体表现的一维数组中的每个课程班信息,如果课程对于多媒体的要求和教室匹配,那么增加适应度分值。
  3. 遍历代表染色体表现的一维数组中的每个课程班信息,如果课程参与的班级总人数小于等于教室的座位数,那么增加适应度分值。
  4. 遍历代表染色体表现的一维数组中的每个课程班信息,如果老师同一时刻不会再多个教室进行授课,那么增加适应度分值。
  5. 遍历代表染色体表现的一维数组中的每个课程班信息,如果班级同一时刻不会在多个课程班进行学习,那么增加适应度分值。

而这上述5个增加适应度分值的指标,是否满足,可以通过额外的数据结构进行表示,即一个检验规则数组表示,索引为0到4,共5个值。课程重叠,用红色R表示,不重叠,用绿色R表示。教室是否有足够的座位,不足用红色S表示,否则用绿色S表示。教室是否和课程的多媒体要求匹配,不匹配用红色L表示,否则用绿色L表示。课程班中的教师是否有重叠,重叠用红色P表示,否则用绿色P表示。课程班中的班级是否有重叠,重叠用红色G表示,否则用绿色G表示。而个体的适应度值为一个float类型的值,等于 :

score/ ( number_of_classes * 5)

范围为0到1。而对于课程排课场景来说,适应度分值越高,给出的解决方案越好。因此,进化过程中的个体选择要优选适应度分值的个体。

5 遗传算法模拟实现

而对于课程排课场景来说,适应度分值越高,给出的解决方案越好。因此,进化过程中的个体选择要优选适应度分值的个体。下面给出算法模拟的进化过程(选择、交叉和变异)的核心代码片段,示例如下:

List<Schedule> offspring = new List<Schedule>();
offspring.resize(_replaceByGeneration);
for (int j = 0; j < _replaceByGeneration; j++)
{//随机选择Schedule p1 = _chromosomes[RandomNumbers.NextNumber() % _chromosomes.Count];Schedule p2 = _chromosomes[RandomNumbers.NextNumber() % _chromosomes.Count];//交叉offspring[j] = p1.Crossover(p2);//变异offspring[j].Mutation();
}

从上述代码可知,后代offspring根据参数_replaceByGeneration来确定需要进化的个体大小,针对每一个进化的后代,首先通过随机方法选择两个父代p1和p2,首先通过p1.Crossover(p2)获取到交叉操作后的后代,然后在对其进行变异处理offspring[j].Mutation()。其中交叉操作核心代码如下:

public Schedule Crossover(Schedule parent2)
{// 根据概率确定是否需要交叉操作if (RandomNumbers.NextNumber() % 100 > _crossoverProbability)//直接返回return new Schedule(this, false);//拷贝生成新的chromosome objectSchedule n = new Schedule(this, true);int size = (int)_classes.Count;//交叉点数组初始化List<bool> cp = new List<bool>();for (int k = 0; k < size; k++){cp.Add(false);}// 随机确定交叉位置for (int i = _numberOfCrossoverPoints; i > 0; i--){while (true){int p = RandomNumbers.NextNumber() % size;if (!cp[p]){cp[p] = true;break;}}}Dictionary<CourseClass, int>.Enumerator it1 = _classes.GetEnumerator();Dictionary<CourseClass, int>.Enumerator it2 = parent2._classes.GetEnumerator();//交替用父个体组合交叉生产新的个体bool first = RandomNumbers.NextNumber() % 2 == 0;for (int i = 0; i < size; i++){it1.MoveNext();it2.MoveNext();if (first){//添加新的课程n._classes.Add(it1.Current.Key, it1.Current.Value);for (int j = it1.Current.Key.GetDuration() - 1; j >= 0; j--)n._slots[it1.Current.Value + j].AddLast(it1.Current.Key);}else{//添加新的课程n._classes.Add(it2.Current.Key, it2.Current.Value);for (int j = it2.Current.Key.GetDuration() - 1; j >= 0; j--)n._slots[it2.Current.Value + j].AddLast(it2.Current.Key);}//在交叉位置交替进行课程更新if (cp[i])first = !first;}//计算适应度n.CalculateFitness();//返回更好的后代return n;
}

由上述代码可知,其中的 _crossoverProbability表示一个交叉的概率,并不是每次调用交叉操作都要执行具体的交叉操作,当随机生成的数大于设定的概率后,才进行交叉具体的操作。其中的交叉点位置也是随机生成的,交叉点的个数通过参数_numberOfCrossoverPoints给定。交叉操作的本质是将两个父类中所指向的课程集合进行随机的组合交换,也就是说,交换的是课程信息以及课程的索引位置。而变异过程相对简单,就是对需要实行变异操作的个体,当满足变异概率时,随机选定一个课程并将其移动到另一个随机选择的插槽(Slots)中。变异过程核心代码如下:

public void Mutation()
{//按照概率决定是否需要突变if (RandomNumbers.NextNumber() % 100 > _mutationProbability)return;int numberOfClasses = (int)_classes.Count;int size = (int)_slots.Count;// 随机决定突变for (int i = _mutationSize; i > 0; i--){int count = _classes.Count;int mpos = RandomNumbers.NextNumber() % numberOfClasses;int pos1 = 0;Dictionary<CourseClass, int>.Enumerator it = _classes.GetEnumerator();if (mpos == 0){it.MoveNext();}for (; mpos > 0; it.MoveNext(), mpos--);pos1 = it.Current.Value;CourseClass cc1 = it.Current.Key;// 随机确定课程的索引位置int nr = Configuration.GetInstance().GetNumberOfRooms();int dur = cc1.GetDuration();int day = RandomNumbers.NextNumber() % DefineConstantsSchedule.DAYS_NUM;int room = RandomNumbers.NextNumber() % nr;int time = RandomNumbers.NextNumber() % (DefineConstantsSchedule.DAY_HOURS + 1 - dur);int pos2 = day * nr * DefineConstantsSchedule.DAY_HOURS + room * DefineConstantsSchedule.DAY_HOURS + time;for (int k = dur - 1; k >= 0; k--){//移除不需要的课程LinkedList<CourseClass> cl = _slots[pos1 + k];for (LinkedList<CourseClass>.Enumerator it3 = cl.GetEnumerator(); it3.MoveNext(); ){if (it3.Current == cc1){cl.Remove(it3.Current);break;}}//移动课程到新的插槽位置_slots[pos2 + k].AddLast(cc1);}// 更新课程位置_classes[cc1] = pos2;}CalculateFitness();
}

课程排程,需要提供一些基础的数据,比如教师资源情况、班级情况、课程情况、教室情况等。下面给出资源数据模板:

#profid = 1name = 李老师
#end#profid = 2name = 张老师
#end#profid = 3name = 汪老师
#end...#courseid = 1name = Web编程
#end#courseid = 4name = 电子商务
#end...#courseid = 8name = 数据库原理
#end#roomname = C101lab = falsesize = 80
#end#roomname = C102lab = truesize = 90
#end
#groupid = 1name = 电商1班size = 22
#end...#groupid = 4name = 会计2班size = 27
#end#classprofessor = 1course = 1duration = 2group = 1group = 2
#end
...#classprofessor = 12course = 8duration = 2group = 3group = 4
#end

下面给出初始化种群中的个体染色体表现型,具体代码如下,种群大小可通过参数给定,通过循环调用MakeNewFromPrototype()生成不同的个体,并添加到初代种群中。MakeNewFromPrototype方法核心代码如下:

public Schedule MakeNewFromPrototype()
{//插槽个数int size = (int)_slots.Count;//生成新的个体 chromosomeSchedule newChromosome = new Schedule(this, true);//随机获取CourseClass信息LinkedList<CourseClass> c = Configuration.GetInstance().GetCourseClasses();for (LinkedList<CourseClass>.Enumerator it = c.GetEnumerator(); it.MoveNext(); ){//随机获取课程位置int nr = Configuration.GetInstance().GetNumberOfRooms();int dur = (it.Current).GetDuration();int day = RandomNumbers.NextNumber() % DefineConstantsSchedule.DAYS_NUM;int room = RandomNumbers.NextNumber() % nr;int time = RandomNumbers.NextNumber() % (DefineConstantsSchedule.DAY_HOURS + 1 - dur);int pos = day * nr * DefineConstantsSchedule.DAY_HOURS + room * DefineConstantsSchedule.DAY_HOURS + time;//将CourseClass信息放于随机的插槽位上for (int i = dur - 1; i >= 0; i--)newChromosome._slots[pos + i].AddLast(it.Current);//添加课程class信息newChromosome._classes.Add(it.Current, pos);}//计算适应度newChromosome.CalculateFitness();return newChromosome;
}

在UI上,采用C# GDI+进行绘制,示例如下:

protected void paint(PaintEventArgs e)
{string baseFile = AppDomain.CurrentDomain.BaseDirectory;string filename = baseFile + "GaSchedule.cfg";Configuration.GetInstance().ParseFile(ref filename);Graphics gac = e.Graphics;Rectangle clientRect = e.ClipRectangle;try{this.Invoke((MethodInvoker)delegate{sx = -GetScrollPos(this.Handle, SB_HORZ);sy = -GetScrollPos(this.Handle, SB_VERT);});}catch (Exception ex){Console.WriteLine(ex.Message);sx = 0;sy = 0;}Color newColor = System.Drawing.Color.FromArgb(49, 147, 120);Color bzColor = System.Drawing.Color.FromArgb(49, 147, 120);Color errorColor = System.Drawing.Color.FromArgb(206, 0, 0);Brush bgBrush = System.Drawing.Brushes.White;gac.FillRectangle(bgBrush, clientRect);Font tableHeadersFont = new Font("Microsoft YaHei", 12);Font tableTextFont = new Font("Microsoft YaHei", 10);Font roomDescFont = new Font("Microsoft YaHei", 10);Font criteriaFont = new Font("Microsoft YaHei", 12);SolidBrush classBrush = new SolidBrush(Color.DarkOrchid);classBrush.Color = Color.FromArgb(255, 255, 245);SolidBrush overlapBrush = new SolidBrush(Color.DarkOrchid);overlapBrush.Color = Color.FromArgb(255, 0, 0);HatchBrush myHatchBrush = new HatchBrush(HatchStyle.BackwardDiagonal, Color.Red,Color.Transparent);int nr = Configuration.GetInstance().GetNumberOfRooms();for (int k = 0; k < nr; k++){for (int i = 0; i < ROOM_COLUMN_NUMBER; i++){for (int j = 0; j < ROOM_ROW_NUMBER; j++){int l = k % 2;int m = k / 2;if (i == 0 && j == 0){Rectangle rect2 = new Rectangle(sx+ROOM_MARGIN_WIDTH + ROOM_TABLE_WIDTH * l, sy+ROOM_MARGIN_HEIGHT,ROOM_CELL_WIDTH, ROOM_CELL_HEIGHT);gac.DrawRectangle(Pens.Black, rect2);Rectangle rect3 = new Rectangle(rect2.X, rect2.Y + 8, rect2.Width, rect2.Height - 16);string str;str = string.Format("教室:{0}", Configuration.GetInstance().GetRoomById(k).GetName());TextRenderer.DrawText(gac, str, roomDescFont, rect3, Color.FromArgb(0, 0, 0), TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter);}if (i == 0 && j > 0){string str = string.Format("{0} - {1}", 8 + j - 1, 8 + j);Rectangle rect3 = new Rectangle(sx + ROOM_MARGIN_WIDTH + ROOM_TABLE_WIDTH * l ,sy + ROOM_MARGIN_HEIGHT + ROOM_CELL_HEIGHT * (j), ROOM_CELL_WIDTH, ROOM_CELL_HEIGHT);TextRenderer.DrawText(gac, str, tableHeadersFont, rect3, Color.FromArgb(0, 0, 0), TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter);gac.DrawRectangle(Pens.Black, rect3);}if (j == 0 && i > 0){string[] days = { "周一", "周二", "周三", "周四", "周五" };Rectangle rect3 = new Rectangle(sx + ROOM_MARGIN_WIDTH + ROOM_TABLE_WIDTH * l + ROOM_CELL_WIDTH * (i),sy + ROOM_MARGIN_HEIGHT, ROOM_CELL_WIDTH, ROOM_CELL_HEIGHT);TextRenderer.DrawText(gac, days[i - 1], tableHeadersFont, rect3, Color.FromArgb(0, 0, 0), TextFormatFlags.VerticalCenter | TextFormatFlags.HorizontalCenter);gac.DrawRectangle(Pens.Black, rect3);}}}}if (_schedule != null){Dictionary<CourseClass, int> classes = _schedule.GetClasses();int ci = 0;for (Dictionary<CourseClass, int>.Enumerator it = classes.GetEnumerator(); it.MoveNext(); ci += 5){CourseClass c = it.Current.Key;int p = it.Current.Value;int t = p % (nr * DAY_HOURS);int d = p / (nr * DAY_HOURS) + 1;int r = t / DAY_HOURS;t = t % DAY_HOURS + 1;int l = r % 2;int m = r / 2;Rectangle rect = new Rectangle(sx + ROOM_TABLE_WIDTH * l + ROOM_MARGIN_WIDTH + d * ROOM_CELL_WIDTH ,sy + ROOM_TABLE_HEIGHT * m + ROOM_MARGIN_HEIGHT + t * ROOM_CELL_HEIGHT ,ROOM_CELL_WIDTH ,c.GetDuration() * ROOM_CELL_HEIGHT);string str = string.Format("{0}\n({1})\n", c.GetCourse().GetName(), c.GetProfessor().GetName());for (LinkedList<StudentsGroup>.Enumerator it2 = c.GetGroups().GetEnumerator(); it2.MoveNext(); ){str += (it2.Current).GetName();str += "/";}str=str.TrimEnd('/');if (c.IsLabRequired())str += "[多媒体]";gac.FillRectangle(classBrush, rect);gac.DrawRectangle(Pens.Black, rect);TextRenderer.DrawText(gac, str, tableTextFont, rect, Color.FromArgb(0, 0, 0), TextFormatFlags.WordBreak);if (!_schedule.GetCriteria()[ci + 0]){bzColor = errorColor;TextRenderer.DrawText(gac, "R", tableTextFont, new Point(rect.Left, rect.Bottom - 20), bzColor);gac.FillRectangle(myHatchBrush, rect);}else{TextRenderer.DrawText(gac, "R", tableTextFont, new Point(rect.Left, rect.Bottom - 20), bzColor);}bzColor = newColor;if (!_schedule.GetCriteria()[ci + 1]){bzColor = errorColor;TextRenderer.DrawText(gac, "S", tableTextFont, new Point(rect.Left + 10, rect.Bottom - 20), bzColor);}else{TextRenderer.DrawText(gac, "S", tableTextFont, new Point(rect.Left + 10, rect.Bottom - 20), bzColor);}bzColor = newColor;if (!_schedule.GetCriteria()[ci + 2]){bzColor = errorColor;TextRenderer.DrawText(gac, "L", tableTextFont, new Point(rect.Left + 20, rect.Bottom -20), bzColor);}else{TextRenderer.DrawText(gac, "L", tableTextFont, new Point(rect.Left + 20, rect.Bottom - 20), bzColor);}bzColor = newColor;if (!_schedule.GetCriteria()[ci + 3]){bzColor = errorColor;TextRenderer.DrawText(gac, "P", tableTextFont, new Point(rect.Left + 30, rect.Bottom -20), bzColor);}else{TextRenderer.DrawText(gac, "P", tableTextFont, new Point(rect.Left + 30, rect.Bottom -20), bzColor);}bzColor = newColor;if (!_schedule.GetCriteria()[ci + 4]){bzColor = errorColor;TextRenderer.DrawText(gac, "G", tableTextFont, new Point(rect.Left + 40, rect.Bottom - 20), bzColor);}else{TextRenderer.DrawText(gac, "G", tableTextFont, new Point(rect.Left + 40, rect.Bottom - 20), bzColor);}}}
}

执行后,遗传算法多次迭代后,显示的UI界面如下:

中间环节,还不能得到可行解的迭代过程,可能显示如下的界面:

由于周一的【8-10】和【9-11】有两个课程同时占用了同一个教室,因此,UI上会显示红色的斜纹,同时R(Room)为红色。至此,我们基本实现了一个用C#语言实现的遗传算法,来进行简单的课程排程操作。最后,本博客参考 https://www.codeproject.com/articles/23111/making-a-class-schedule-using-a-genetic-algorithm 一文。

点击关注,第一时间了解华为云新鲜技术~

用遗传算法进行智能排课,相信老师会很喜欢相关推荐

  1. matlab 排课,Matlab 遗传算法解决智能排课算法 一天四节课,上午两节,下午两

    Matlab 遗传算法解决智能排课算法 一天四节课,上午两节,下午两 Matlab 遗传算法解决智能排课算法 一天四节课,上午两节,下午两节,同一门课不能相邻,特殊课程不能相邻(语文和英语,数学和科学 ...

  2. Matlab 遗传算法解决智能排课算法 一天四节课,上午两节,下午两节,同一门课不能相邻,特殊课程不能相邻(语文和英语,数学和科学),求可行方案?

    1.要排课的课程有9门,分别给与编码1,2,3,4,5,6,7,8,9.对应的一周上课次数如下所示: 课程名 编码 一周上几次 Chinese 1 3 English 2 3 Math 3 3 Sci ...

  3. 广州大学软件方向综合课程设计报告(专业课程数据库系统,模拟一个学期选课退课)带智能排课算法(遗传算法)

    广州大学软件方向综合课程设计目录 序章 第一章 系统需求简介 1.1 需求分析 1.2 数据结构需求分析 1.3系统功能设计 第二章 需求描述 2.1 数据流图 2.2 数据字典 第三章 概念设计 3 ...

  4. 基于Python的课程管理智能排课系统

    实现一个具体的课程管理系统.按照软件工程思路设计简化的专业课数据库,尽量模拟现有专业课程一个学期的选课排课原型实际情况.(注:本系统由本人单独设计.开发完成) 1.2 数据结构需求分析 课程管理系统需 ...

  5. mysql自动排课_jsp1934高校智能排课系统 mysql

    jsp1934高校智能排课系统 mysql 该设计有演示视频 100%能运行 买重包换 保密发送 一校一份 编号: jsp1934 语言+数据库: jsp+mysql 论文字数: 12159字 内容摘 ...

  6. 基于jsp(java)高校智能排课系统设计

    随着我国科学技术的进步和综合国力的增强,计算机在我们学习生活中有着越来越多的应用,我们对计算机的依赖也越来越强烈.可以说,离开了计算机我们的日常生活都不能得到保证.然而,在计算机如此普及的今天,有一些 ...

  7. java毕业设计——基于JSP+sqlserver的高校智能排课系统设计与实现(毕业论文+程序源码)——高校智能排课系统

    基于JSP+sqlserver的高校智能排课系统设计与实现(毕业论文+程序源码) 大家好,今天给大家介绍基于JSP+sqlserver的高校智能排课系统设计与实现,文章末尾附有本毕业设计的论文和源码下 ...

  8. 基于遗传算法的高校排课系统研究

    基于遗传算法的高校排课系统研究 沈丽容  陈明磊 (南京林业大学信息学院计算机科学与工程系  南京 210037)     摘  要   提出并实现了一种高校自动排课算法,利用遗传算法建立数据模型,定 ...

  9. 计算机毕业设计springboot的学校智能排课信息系统(源码+系统+mysql数据库+Lw文档)

    运行环境: 开发工具:IDEA /Eclipse 数据库:MYSQL5.7 应用服务:Tomcat7/Tomcat8 使用框架springboot 项目介绍 本系统采用了jsp技术,将所有业务模块采用 ...

最新文章

  1. Exception loading sessions from persistent storage
  2. linux c 各头文件作用总结
  3. Swift中的模式分类
  4. 如何理解clone对象
  5. 游戏的社交与延伸:怎样把玩家连结起来?
  6. 业务模块化打造单体和分布式部署同步支持方案
  7. dataset.filter
  8. 新年春节海报素材精品,再也不怕老板催稿!
  9. 电脑怎么彻底删除软件_电脑强力卸载工具,删除电脑无用软件,让电脑更加快捷顺畅...
  10. mysql删除员工_MySQL误删数据救命指南:开发人员必收藏
  11. 彩超中ri是什么意思_胎儿b超ri是什么意思
  12. linux汇编预处理,Linux程序在预处理、编译、汇编、链接、运行步骤的作用
  13. 报表系统软件有哪些_报表系统软件功能
  14. 失败产品手册:一款影音娱乐平台的败局
  15. layui上传文件限制选择文件类型 upload.render
  16. Timer 和TimerTask分析
  17. 维斯易联网络打印机配置教程
  18. Unity LineRenderer 画运动轨迹
  19. MOOC网课爬虫逆向(一)
  20. linux监听耳机按键,Android 中如何监听耳机键消息

热门文章

  1. JavaScript 通过字符串获取function
  2. c#向MFC窗体发送消息
  3. 面试 | #面试面试面试 做#Java 就是要这种不要脸的…
  4. JavaScript中语句与函数的执行辨析
  5. Bootstrap3 响应式表格
  6. 学习笔记 vs19 报错:E1696 C++ 无法打开 源 文件
  7. java中常见的编译错误的是_编译时JAVA最常见的错误有哪些
  8. mui + php,GitHub - alphaphp/mui-kidApp: 基于 MUI 构建一个具有 90 +页面的APP应用
  9. mysql存储过程 简书_MySQL存储过程
  10. php label,HTML的label标签