65.9K
CodeProject 正在变化。 阅读更多。
Home

使用遗传算法制作课程表

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (101投票s)

2008年1月22日

CPOL

9分钟阅读

viewsIcon

677564

downloadIcon

59472

如何使用遗传算法制作课程表。

目录

引言

制作课程表是 NP 难题之一。这个问题可以通过启发式搜索算法来解决,以找到最优解,但这只适用于简单的情况。对于更复杂的输入和要求,找到一个相当好的解决方案可能需要一段时间,或者根本不可能。这时遗传算法就派上用场了。在本文中,我假设您熟悉遗传算法的基本概念,并且不会详细描述它们,因为这已经说过很多次了。

背景

当您制作课程表时,必须考虑许多要求(教授、学生、课程和教室的数量、教室的大小、教室里的实验室设备等等)。这些要求可以根据其重要性分为几类。硬性要求(如果违反其中任何一条,则课程表将不可行)

  • 课程只能安排在空闲的教室里。
  • 任何教授或学生组在同一时间不能有超过一门课程。
  • 教室必须有足够的座位容纳所有学生。
  • 要将课程安排在教室中,如果课程需要实验室设备(在本例中为计算机),则该教室必须拥有实验室设备。

一些软性要求(可以打破,但课程表仍然可行)

  • 教授偏好的上课时间。
  • 教授偏好的教室。
  • 学生组或教授的课程(按时间或空间)分布。

当然,硬性和软性要求取决于具体情况。在本例中,只实现了硬性要求。让我们开始解释构成课程表的对象。

课程表对象

教授

Professor 类具有教授的 ID 和姓名。它还包含教授所教课程的列表。

学生组

StudentsGroup 类具有学生组的 ID 和姓名,以及学生数量(组的大小)。它还包含学生组参加的课程列表。

教室

Room 类具有教室的 ID 和名称,以及座位数和设备信息(计算机)。如果教室有计算机,则预期每个座位都有计算机。ID 是内部自动生成的。

课程

Course 类具有课程的 ID 和名称。

CourseClass 包含一个对课程的引用,该课程属于该课程;一个对教授的引用,该教授讲授该课程;以及一个学生组列表,这些学生组参加该课程。它还存储了教室所需的座位数(学生组大小的总和),如果课程需要教室中的计算机,以及课程的持续时间(以小时为单位)。

染色体

当我们处理遗传算法时,首先要考虑的是如何表示我们的解决方案,使其能够进行交叉和变异等遗传操作。此外,我们应该知道如何指定我们解决方案的好坏程度。换句话说,我们应该能够计算我们解决方案的适应度值。

表示

我们如何表示课程表的染色体?嗯,我们需要为每个房间、每天的每个小时(我们假设时间是以小时为单位的)分配一个时间-空间槽。此外,我们假设课程不能早于上午 9 点开始,并且应在下午 9 点或之前结束(共 12 小时),工作日为周一至周五(共 5 天)。我们可以使用大小为 12*5*number_of_roomsstd::vector。该槽应该是一个 std::list,因为在算法执行过程中,我们允许在同一时间-空间槽内有多个课程。还有一个附加的哈希映射,用于从类对象的地址获取类开始的第一个时间-空间槽(它在向量中的位置)。课程的每个小时在向量中都有一个单独的条目,但在哈希映射中每个类只有一个条目。例如,如果一门课程从下午 1 点开始,持续三个小时,那么它在下午 1 点、下午 2 点和下午 3 点的槽中都有条目。

Class Schedule Chromosome Representation

图 1 - 染色体表示

染色体由 Schedule 类表示,它在以下两个属性中存储课程表的表示

// Time-space slots, one entry represent one hour in one classroom
vector<list<CourseClass*>> _slots;

// Class table for chromosome
// Used to determine first time-space slot used by class
hash_map<CourseClass*, int> _classes;

此外,染色体还应存储其适应度值以及遗传操作使用的参数。

适应度值存储在此处

// Fitness value of chromosome
float _fitness;

// Flags of class requiroments satisfaction
vector<bool> _criteria;

染色体参数

// Number of crossover points of parent's class tables
int _numberOfCrossoverPoints;

// Number of classes that is moved randomly by single mutation operation
int _mutationSize;

// Probability that crossover will occure
int _crossoverProbability;

// Probability that mutation will occure
int _mutationProbability;

适应度

现在我们需要为染色体分配一个适应度值。如前所述,只有硬性要求用于计算课程表的适应度。我们这样做的方式如下:

  • 每门课程可获得 0 到 5 分。
  • 如果一门课程使用了空闲教室,我们就增加它的分数。
  • 如果一门课程需要计算机并且它位于带有计算机的教室中,或者它不需要计算机,我们就增加该课程的分数。
  • 如果一门课程位于有足够可用座位的教室中,你猜怎么着,我们就增加它的分数。
  • 如果教授在同一时间没有其他课程,我们就再次增加该课程的分数。
  • 我们检查的最后一件事是,参加该课程的任何学生组是否在同一时间有其他课程,如果没有,我们就增加该课程的分数。
  • 如果一门课程在它占用的任何时间-空间槽中违反了规则,那么它在该规则下的分数就不会增加。
  • 一门课程表总分数是所有课程分数的总和。
  • 适应度值计算为 schedule_score/maximum_score,而 maximum_scorenumber_of_classes*5

适应度值表示为单精度浮点数 (float),范围为 0 到 1。

交叉

交叉操作会合并两个父代哈希映射中的数据,然后根据新哈希映射的内容创建一个槽向量。交叉会“分割”两个父代的哈希映射,分成大小随机的部分。部分数量由染色体参数中的交叉点数量(加一)决定。然后,它会交替地从父代复制部分到新染色体,并形成一个新的槽向量。

Class Schedule Crossover Operation

图 2 - 交叉操作
// Performes crossover operation using to chromosomes
// and returns pointer to offspring
Schedule* Crossover(const Schedule& parent2) const;

变异

变异操作非常简单。它只是随机选择一门课程,并将其移动到另一个随机选择的槽。在单次操作中将要移动的课程数量由染色体参数中的变异大小决定。

// Performs mutation on chromosome
void Mutation();

算法

遗传算法相当简单。每一代,它执行两个基本操作:

  1. 从当前种群中随机选择 N 对父代,并通过对父代对进行交叉操作产生 N 个新染色体。
  2. 从当前种群中随机选择 N 个染色体,并用新染色体替换它们。如果某个染色体是种群中最好的染色体之一,算法就不会选择它进行替换。

这两个操作会重复进行,直到最好的染色体达到适应度值 1(表示课程表中的所有课程都满足要求)。如前所述,遗传算法会跟踪种群中 M 个最好的染色体,并确保它们在属于最好的染色体时不会被替换。

// Genetic algorithm
class Algorithm
{

private:

    // Population of chromosomes
    vector<Schedule*> _chromosomes;

    // Inidicates wheahter chromosome belongs to
    // best chromosome group
    vector<bool> _bestFlags;

    // Indices of best chromosomes
    vector<int> _bestChromosomes;

    // Number of best chromosomes currently saved in
    // best chromosome group
    int _currentBestSize;

    // Number of chromosomes which are replaced in
    // each generation by offspring
    int _replaceByGeneration;

    // Pointer to algorithm observer
    ScheduleObserver* _observer;

    // Prototype of chromosomes in population
    Schedule* _prototype;

    // Current generation
    int _currentGeneration;

    // State of execution of algorithm
    AlgorithmState _state;

    // Synchronization of algorithm's state
    CCriticalSection _stateSect;

    // Pointer to global instance of algorithm
    static Algorithm* _instance;

    // Synchronization of creation and destruction
    // of global instance
    static CCriticalSection _instanceSect;

public:

    // Returns reference to global instance of algorithm
    static Algorithm& GetInstance();

    // Frees memory used by gloval instance
    static void FreeInstance();

    // Initializes genetic algorithm
    Algorithm(int numberOfChromosomes,
        int replaceByGeneration,
        int trackBest,
        Schedule* prototype,
        ScheduleObserver* observer);

    // Frees used resources
    ~Algorithm();

    // Starts and executes algorithm
    void Start();

    // Stops execution of algoruthm
    void Stop();

    // Returns pointer to best chromosomes in population
    Schedule* GetBestChromosome() const;

    // Returns current generation
    inline int GetCurrentGeneration() const { return _currentGeneration; }

    // Returns pointe to algorithm's observer
    inline ScheduleObserver* GetObserver() const { return _observer; }

private:

    // Tries to add chromosomes in best chromosome group
    void AddToBest(int chromosomeIndex);

    // Returns TRUE if chromosome belongs to best chromosome group
    bool IsInBest(int chromosomeIndex);

    // Clears best chromosome group
    void ClearBest();

};

观察

ScheduleObserver 类处理由遗传算法触发的事件。该类向应用程序的视图窗口发送消息。此外,您可以通过调用 WaitEvent() 方法来阻止调用线程,直到算法执行完成或停止。

// Handles event that is raised
// when algorithm finds new best chromosome
void NewBestChromosome(const Schedule& newChromosome);

// Handles event that is raised when state
// of execution of algorithm is changed
void EvolutionStateChanged(AlgorithmState newState);

// Block caller's thread until algorithm finishes execution
inline void WaitEvent() //...

如果您打算修改 NewBestChromosome 方法,请记住,如果您想保留最好的染色体以供显示,则必须制作硬拷贝(通过使用 Schedule 类的 MakeCopy 方法),因为算法可以在下一代中删除该染色体。

配置

配置文件

对象类型

  1. professor (#prof 标签) - 描述一位教授。
  2. course (#course 标签) - 描述一门课程。
  3. room (#room 标签) - 描述一个房间。
  4. group (#group 标签) - 描述一个学生组。
  5. course class (#class 标签) - 描述一门课,并将教授、课程和学生组关联起来。

每个对象都以其标签开头,以 #end 标签结束,所有标签必须单独一行。在对象体中,每行只包含一个键值对(属性),用 = 字符分隔。每个属性应仅指定一次,除了 #group 对象中的 group 属性,它可以有多个 group 属性。标签和键名区分大小写。以下是对象的属性列表:

  1. #prof
    • id (数字,必需) - 教授的 ID。
    • name (字符串,必需) - 教授的姓名。
  2. #course
    • id (数字,必需) - 课程的 ID。
    • name (字符串,必需) - 课程的名称。
  3. #room
    • name (字符串,必需) - 教室的名称。
    • size (数字,必需) - 教室的座位数。
    • lab (布尔值,可选) - 指示房间是否为实验室(有计算机);如果未指定,则默认值为 false。
  4. #group
    • id (数字,必需) - 学生组的 ID。
    • name (字符串,必需) - 学生组的名称。
    • size (数字,必需) - 学生组中的学生人数。
  5. #class
    • professor (数字,必需) - 教授的 ID;将教授与课程关联。
    • course (数字,必需) - 课程的 ID;将课程与课程关联。
    • group (数字,必需) - 学生组的 ID;将学生组与课程关联;每门课程可以关联多个学生组。
    • duration (数字,可选) - 课程持续时间(以小时为单位);如果未指定,则默认值为 1。
    • lab (布尔值,可选) - 如果课程需要房间中的计算机;如果未指定,则默认值为 false。

请注意,教授、学生组和课程对象必须在与课程类对象关联之前定义。

配置文件示例

#prof
    id = 1
    name = John Smith
#end

#course
    id = 1
    name = Introduction to Programming
#end

#room
    name = R1
    lab = true
    size = 24
#end

#group
    id = 1
    name = 1O1
    size = 19
#end

#class
    professor = 1
    course = 1
    duration = 2
    group = 1
    group = 2
#end

#class
    professor = 1
    course = 1
    duration = 3
    group = 1
    lab = true
#end

#class
    professor = 1
    course = 1
    duration = 3
    group = 2
    lab = true
#end

解析配置

配置文件解析由 Configuration 类完成。ParseFile 方法打开并解析配置文件。它查找对象标签并调用适当的对象解析方法。ParseFile 方法还会清除先前解析的对象。

public:
    void ParseFile(char* fileName);

private:

    Professor* ParseProfessor(ifstream& file);
    StudentsGroup* ParseStudentsGroup(ifstream& file);
    Course* ParseCourse(ifstream& file);
    Room* ParseRoom(ifstream& file);
    CourseClass* ParseCourseClass(ifstream& file);

要解析文件

Configuration::GetInstance().ParseFile( "GaSchedule.cfg" );

解析的对象存储在哈希映射中,课程类除外,因此可以轻松快速地访问它们。

private:

    hash_map<int, Professor*> _professors;
    hash_map<int, StudentsGroup*> _studentGroups;
    hash_map<int, Course*> _courses;
    hash_map<int, Room*> _rooms;

    list<CourseClass*> _courseClasses;

Configuration 类还包含用于检索已解析信息和对象的函数。

public:
    inline Professor* GetProfessorById(int id) //...
    inline int GetNumberOfProfessors() const //...

    inline StudentsGroup* GetStudentsGroupById(int id) //...
    inline int GetNumberOfStudentGroups() const //...

    inline Course* GetCourseById(int id) //...
    inline int GetNumberOfCourses() const //...

    inline Room* GetRoomById(int id) //...
    inline int GetNumberOfRooms() const //...

    inline const list<CourseClass*>& GetCourseClasses() const //...
    inline int GetNumberOfCourseClasses() const //...

附加信息

本文是基于 此文本,但采用不同的许可。

© . All rights reserved.