本文将用C#语言来实现遗传算法对排课系统的优化,算法代码参考了洛荷大佬的Python实现基于遗传算法的排课优化,用C#实现后做了一个界面方便操作。
编写软件,实现界面友好的系统设计,完成整个排课系统优化的过程。
要求:每个步骤中,要把所有功能均编写成模块调用形式,如:导入数据,表间建立联
系、约束条件选择,排课课表导出与显示。课表按不同班级用户导出,用户可以根据提示进行选择,进入相应算法调用实现计算。
1.导入数据并对数据进行初步处理;
2.设置基础参数:种群规模,突变的可能性,精英数目,执行次数。
3. 构造函数,初始化不同的种群(课程表的人数和教室数等);
4.通过随机方式产生多个求解问题的二进制染色体编码,选择适应度高的染色体。
5. 交叉操作:随机对两个对象交换不同位置的属性,返回列表。交叉操作采用分块小基因交叉算法,即每个班的课程单元只能在相同的班级进行交叉操作,而不能跨班(行)进行交叉。例如两个个体进行交叉时,只能个体1的1班行基因与个体2的1班行基因进行交换。这样的操作不会破坏班级对课程和课时的要求。
6.变异操作:随机对Schedule象中对的某个可改变属性在允许范围内进行随机加减,返回列表,变异的过程是针对每个染色体个体内部的,为保证每个班级的既定的课程课时所以同交叉相同,变异操作限制在每个班级的基因段中。
7.GA优化:进化,启动GA算法进行优化,返回最佳结果的索引和最佳冲突的分数。计算课表种群的冲突,返回最佳结果的索引,最佳冲突的分数,当被测试课表冲突为0的时候,这个课表就是个符合规定的课表当冲突为零时,按班级输出当前排课表。
遗传算法(Genetic Algorithm, GA)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。
其主要特点是直接对结构对象进行操作,不存在求导和函数连续性的限定;具有内在的隐并行性和更好的全局寻优能力;采用概率化的寻优方法,不需要确定的规则就能自动获取和指导优化的搜索空间,自适应地调整搜索方向。
遗传算法以一种群体中的所有个体为对象,并利用随机化技术指导对一个被编码的参数空间进行高效搜索。其中,选择、交叉和变异构成了遗传算法的遗传操作;参数编码、初始群体的设定、适应度函数的设计、遗传操作设计、控制参数设定五个要素组成了遗传算法的核心内容。
遗传算法是从代表问题可能潜在的解集的一个种群(population)开始的,而一个种群则由经过基因(gene)编码的一定数目的个体(individual)组成。每个个体实际上是染色体(chromosome)带有特征的实体。
染色体作为遗传物质的主要载体,即多个基因的集合,其内部表现(即基因型)是某种基因组合,它决定了个体的形状的外部表现,如黑头发的特征是由染色体中控制这一特征的某种基因组合决定的。因此,在一开始需要实现从表现型到基因型的映射即编码工作。由于仿照基因编码的工作很复杂,我们往往进行简化,如二进制编码。
初代种群产生之后,按照适者生存和优胜劣汰的原理,逐代(generation)演化产生出越来越好的近似解,在每一代,根据问题域中个体的适应度(fitness)大小选择(selection)个体,并借助于自然遗传学的遗传算子(genetic operators)进行组合交叉(crossover)和变异(mutation),产生出代表新的解集的种群。这个过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。
courseId代表课程号,classID代表班级号,teacherID代表教师号,这三个数据是从外部导入的,roomId代表教室号,weekDay代表星期,slot代表时间,这三个数据是随机生成的,通过遗传算法得到不冲突的结果。DeepClone()函数实现深度拷贝,RandomInit(int roomRange)生成随机的roomId,weekDay和slot。
[Serializable]
class Schedule : ICloneable
{
private int courseId; //课程号
private int classId; //班级号
private int teacherId; //教师号
private int roomId = 0; //教室
private int weekDay = 0; //星期
private int slot = 0; //时间
public int CourseId { get => courseId; set => courseId = value; }
public int ClassId { get => classId; set => classId = value; }
public int TeacherId { get => teacherId; set => teacherId = value; }
public int RoomId { get => roomId; set => roomId = value; }
public int WeekDay { get => weekDay; set => weekDay = value; }
public int Slot { get => slot; set => slot = value; }
//构造函数
public Schedule(int courseId, int classId, int teacherId) //课程表类包含的内容,包括课程号,班级号,教师ID
{
this.courseId = courseId;
this.classId = classId;
this.teacherId = teacherId;
}
#region 拷贝主体
///
/// 深度拷贝
///
public Schedule DeepClone()
{
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(0, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as Schedule;
}
}
public object Clone()
{
return this.MemberwiseClone();
}
#endregion
//随机匹配教室号和时间
public void RandomInit(int roomRange)
{
Random random = new Random();
this.RoomId = random.Next(1, roomRange + 1);
this.WeekDay = random.Next(1, 6);
this.Slot = random.Next(1, 6); //随机生成时间
}
public override string ToString()
{
return String.Format("课程号:{0}\n班级号:{1}\n教师号:{2}\n房间号:{3}",CourseId,ClassId,TeacherId,RoomId);
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace Course_scheduling_optimization
{
class GeneticOptimize
{
private int popsize; //种群规模
private double mutprob; //突变的可能性
private int elite; //精英数目
private int maxiter; //执行次数
private Random random = new Random(); //随机数,方便调用函数
//封装字段
public int Popsize { get => popsize; set => popsize = value; }
public double Mutprob { get => mutprob; set => mutprob = value; }
public int Elite { get => elite; set => elite = value; }
public int Maxiter { get => maxiter; set => maxiter = value; }
//默认构造函数
public GeneticOptimize()
{
this.popsize = 30;
this.mutprob = 0.3;
this.elite = 5;
this.maxiter = 100;
}
//构造函数
public GeneticOptimize(int popsize, double mutprob, int elite, int maxiter)
{
this.popsize = popsize;
this.mutprob = mutprob;
this.elite = elite;
this.maxiter = maxiter;
}
//随机初始化不同的种群
//schedules:List,课程表的人数
//roomRange:int,教室数
private List<List<Schedule>> InitPopulation(List<Schedule> schedules,int roomRange)
{
List<List<Schedule>> population = new List<List<Schedule>>();
for (int i = 0; i < popsize; i++)
{
List<Schedule> entity = new List<Schedule>();
foreach (Schedule s in schedules)
{
s.RandomInit(roomRange);
entity.Add(s.DeepClone()); //深层拷贝,备份
}
population.Add(entity);
}
return population;
}
//变异操作,随机对Schedule对象中的某个可改变属性在允许范围内进行随机加减,返回列表,变异后的种群
//eiltePopulation:List,精英时间表的种群
//roomRange: int,教室数
private List<Schedule> Mutate(List<List<Schedule>> eiltePopulation, int roomRange)
{
int e = random.Next(0, elite); //elite-精英数目
int pos = random.Next(0, 2);
List<Schedule> ep = new List<Schedule>(); //再次生成Schedule对象
foreach (var epe in eiltePopulation[e])
{
ep.Add(epe.DeepClone());
}
foreach (var p in ep)
{
pos = random.Next(0, 3);
double operation = random.NextDouble();
if (pos == 0) p.RoomId = AddSub(p.RoomId, operation, roomRange);
if (pos == 1) p.WeekDay = AddSub(p.WeekDay, operation, 5);
if (pos == 2) p.Slot = AddSub(p.Slot, operation, 5);
}
return ep;
}
private int AddSub(int value,double op,int valueRange)
{
if(op > 0.5)
{
if (value < valueRange) value += 1;
else value -= 1;
}
else
{
if (value - 1 > 0) value -= 1;
else value += 1;
}
return value;
}
//交叉操作,随机对两个对象交换不同位置的属性,返回列表,交叉后的种群
//eiltePopulation:List,精英时间表的种群
private List<Schedule> Crossover(List<List<Schedule>> eiltePopulation)
{
int e1 = random.Next(0, elite);
int e2 = random.Next(0, elite);
int pos = random.Next(0, 2);
List<Schedule> ep1 = new List<Schedule>();
List<Schedule> ep2 = new List<Schedule>();
foreach (var epe1 in eiltePopulation[e1])
{
ep1.Add(epe1.DeepClone());
}
ep2 = eiltePopulation[e2];
for (int i = 0; i < ep1.Count; i++)
{
if(pos == 0)
{
ep1[i].WeekDay = ep2[i].WeekDay;
ep1[i].Slot = ep2[i].Slot;
}
if(pos == 1)
{
ep1[i].RoomId = ep2[i].RoomId;
}
}
return ep1;
}
//进化,启动GA算法进行优化,返回最佳结果的索引和最佳冲突的分数
//schedules:优化课程表
//elite:int,最佳结果的数目
public List<Schedule> evolution(List<Schedule> schedules, int roomRange, RichTextBox richTextBox)
{
//主循环
int bestScore = 0;
List<int> eliteIndex = new List<int>();
List<Schedule> newp = new List<Schedule>();
List<Schedule> bestSchedule = new List<Schedule>();
List<List<Schedule>> population = new List<List<Schedule>>(); //种群
population = InitPopulation(schedules, roomRange); //初始化种群
for (int i = 0; i < maxiter; i++) //maxiter-执行次数
{
List<List<Schedule>> newPopulation = new List<List<Schedule>>();
Tuple<List<int>, int> scheduleCostRes = ScheduleCost(population, elite);//新的人口
eliteIndex = scheduleCostRes.Item1; //精英指数
bestScore = scheduleCostRes.Item2; //冲突最少的冲突数
richTextBox.Text += String.Format("Iter: {0} | conflict: {1}\n", i + 1, bestScore);
richTextBox.SelectionStart = richTextBox.TextLength;
richTextBox.ScrollToCaret();
//输出冲突
if (bestScore == 0)
{
richTextBox.Text += "排课完成";
bestSchedule = population[eliteIndex[0]];
break;
}
//从精英开始
foreach (var ei in eliteIndex)
{
newPopulation.Add(population[ei]);
}
//添加精英的变异和繁殖形式
while(newPopulation.Count < popsize)
{
if (random.NextDouble() < mutprob) //小于突变可能性
{
//突变
newp = Mutate(newPopulation, roomRange); //变化
}
else
{
//交叉操作
newp = Crossover(newPopulation);
}
newPopulation.Add(newp);
}
population = newPopulation;
if(i == maxiter - 1)
{
MessageBox.Show("未找到最佳排课结果!");
}
}
return bestSchedule;
}
// 计算课表种群的冲突,返回最佳结果的索引,最佳冲突的分数
// 当被测试课表冲突为0的时候,这个课表就是个符合规定的课表
// population:课程表的种群
// elite:最佳结果的数目
public Tuple<List<int>, int> ScheduleCost(List<List<Schedule>> population, int elite)
{
List<int> conflicts = new List<int>();
List<int> index = new List<int>();
List<int> bestResultIndex = new List<int>();
int n = population[0].Count;
foreach (List<Schedule> p in population)
{
int conflict = 0;
for (int i = 0; i < n - 1; i++)
{
for (int j = i + 1; j < n; j++)
{
//同一个教室在同一个时间只能有一门课
if (p[i].RoomId == p[j].RoomId & p[i].WeekDay == p[j].WeekDay & p[i].Slot == p[j].Slot)
conflict += 1;
//同一个班级在同一个时间只能有一门课
if (p[i].ClassId == p[j].ClassId & p[i].WeekDay == p[j].WeekDay & p[i].Slot == p[j].Slot)
conflict += 1;
//同一个教师在同一个时间只能有一门课
if (p[i].TeacherId == p[j].TeacherId & p[i].WeekDay == p[j].WeekDay & p[i].Slot == p[j].Slot)
conflict += 1;
//同一个班级在同一天不能有相同的课
if (p[i].ClassId == p[j].ClassId & p[i].CourseId == p[j].CourseId & p[i].WeekDay == p[j].WeekDay)
conflict += 1;
}
}
conflicts.Add(conflict);
}
index = argsort(conflicts); //返回列表值从小到大的索引值
for (int i = 0; i < elite; i++)
{
bestResultIndex.Add(index[i]);
}
return Tuple.Create(bestResultIndex, conflicts[index[0]]);
}
//argsort为手动实现Python中的numpy.argsort,返回列表值从小到大的索引值
public List<int> argsort(List<int> list)
{
Dictionary<int, int> indexDict = new Dictionary<int, int>();
List<int> indexList = new List<int>();
for (int i = 0; i < list.Count; i++)
{
indexDict.Add(i, list[i]);
}
var orderedDict = indexDict.OrderBy(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
foreach (var key in orderedDict.Keys)
{
indexList.Add(key);
}
return indexList;
}
}
}
using System;
using System.Windows.Forms;
using System.Collections.Generic;
using System.IO;
using System.Data;
using System.Linq;
using System.Drawing;
namespace Course_scheduling_optimization
{
public partial class Main : Form
{
private List<Schedule> res; //全局变量
public Main()
{
InitializeComponent();
}
//限制textBox1只能输入正数
private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
//如果输入的不是数字键,也不是回车键、Backspace键,则取消该输入
if (!(Char.IsNumber(e.KeyChar)) && (e.KeyChar != (char)13) && (e.KeyChar != (char)8))
{
e.Handled = true;
}
}
private void textBox3_KeyPress(object sender, KeyPressEventArgs e)
{
//如果输入的不是数字键,也不是回车键、Backspace键,则取消该输入
if (!(Char.IsNumber(e.KeyChar)) && (e.KeyChar != (char)13) && (e.KeyChar != (char)8))
{
e.Handled = true;
}
}
private void textBox5_KeyPress(object sender, KeyPressEventArgs e)
{
//如果输入的不是数字键,也不是回车键、Backspace键,则取消该输入
if (!(Char.IsNumber(e.KeyChar)) && (e.KeyChar != (char)13) && (e.KeyChar != (char)8))
{
e.Handled = true;
}
}
private void textBox6_KeyPress(object sender, KeyPressEventArgs e)
{
//如果输入的不是数字键,也不是回车键、Backspace键,则取消该输入
if (!(Char.IsNumber(e.KeyChar)) && (e.KeyChar != (char)13) && (e.KeyChar != (char)8))
{
e.Handled = true;
}
}
private void textBox4_KeyPress(object sender, KeyPressEventArgs e)
{
//如果输入的不是数字键,也不是回车键、Backspace键,也不是小数点,则取消该输入
if (((int)e.KeyChar < 48 || (int)e.KeyChar > 57) && (int)e.KeyChar != 8 && (int)e.KeyChar != 46)
e.Handled = true;
//小数点的处理。
if ((int)e.KeyChar == 46) //小数点
{
if (textBox4.Text.Length <= 0)
e.Handled = true; //小数点不能在第一位
else
{
float f;
float oldf;
bool b1 = false, b2 = false;
b1 = float.TryParse(textBox4.Text, out oldf);
b2 = float.TryParse(textBox4.Text + e.KeyChar.ToString(), out f);
if (b2 == false)
{
if (b1 == true)
e.Handled = true;
else
e.Handled = false;
}
}
}
}
private void Main_Load(object sender, EventArgs e)
{
comboBox1.Items.Insert(0, "----请选择----");
comboBox1.SelectedIndex = 0;
//设置默认值
//textBox1.Text = "3";
textBox3.Text = "50";
textBox4.Text = "0.3";
textBox5.Text = "10";
textBox6.Text = "1000";
toolStripStatusLabel1.Text = "当前系统时间:" + DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
this.timer1.Interval = 1000;
this.timer1.Start();
//IrisSkin4
skinEngine1.SkinFile = Application.StartupPath + @"/Skins/mp10.ssk";
}
//输出课程表
private void PrintSchedule(List<Schedule> res)
{
DataTable dt = new DataTable();
List<List<String>> Arr = new List<List<string>>();
for (int i = 1; i < 6; i++)
{
Arr.Add(new List<string>() { i.ToString(), "", "", "", "", "" });
}
//表头
dt.Columns.Add("week/slot");
dt.Columns.Add("星期一");
dt.Columns.Add("星期二");
dt.Columns.Add("星期三");
dt.Columns.Add("星期四");
dt.Columns.Add("星期五");
foreach (var r in res)
{
int weekDay = r.WeekDay;
int slot = r.Slot;
Arr[slot-1][weekDay] = r.ToString();
}
foreach (var arr in Arr)
{
dt.Rows.Add(arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]);
}
dataGridView1.DataSource = dt;
//实现自动换行
dataGridView1.DefaultCellStyle.WrapMode = DataGridViewTriState.True;
dataGridView1.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCells;
dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
}
private void toolStripStatusLabel2_Click(object sender, EventArgs e)
{
toolStripStatusLabel2.Alignment = System.Windows.Forms.ToolStripItemAlignment.Right;
}
private void timer1_Tick(object sender, EventArgs e)
{
toolStripStatusLabel1.Text = "当前系统时间:" + DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
}
private void button1_Click_1(object sender, EventArgs e)
{
//清空数据
richTextBox1.Text = "";
dataGridView1.DataSource = null;
if (textBox1.Text == "")
{
MessageBox.Show("请输入教室数目!");
}
else if (textBox2.Text == "")
{
MessageBox.Show("请选择文件路径!");
}
else
{
string filePath = textBox2.Text;
int roomRange = Convert.ToInt32(textBox1.Text);
int popsize = Convert.ToInt32(textBox3.Text);
double mutprob = Convert.ToDouble(textBox4.Text);
int elite = Convert.ToInt32(textBox5.Text);
int maxiter = Convert.ToInt32(textBox6.Text);
List<Schedule> schedules = new List<Schedule>();
List<int> classIds = new List<int>();
//清空数据
res = new List<Schedule>(); //全局变量初始化
comboBox1.Items.Clear(); //清空comboBox1中的数据
comboBox1.Items.Insert(0, "----请选择----");
comboBox1.SelectedIndex = 0;
//读入excel/csv文件
StreamReader reader = new StreamReader(filePath);
string line = "";
List<string[]> listStrArr = new List<string[]>();
line = reader.ReadLine();//读取一行数据
while (line != null)
{
listStrArr.Add(line.Split(','));//将文件内容分割成数组
line = reader.ReadLine();
}
foreach (var s in listStrArr)
{
//放入schedule中
int courseId = Convert.ToInt32(s[0]);
int classId = Convert.ToInt32(s[1]);
int teacherId = Convert.ToInt32(s[2]);
schedules.Add(new Schedule(courseId, classId, teacherId));
classIds.Add(classId);
}
//优化
GeneticOptimize ga = new GeneticOptimize(popsize, mutprob, elite, maxiter);
res = ga.evolution(schedules, roomRange, richTextBox1);
//comboBox1绑定数据
foreach (var c in classIds.Distinct().ToList())
{
comboBox1.Items.Add(c);
}
}
}
private void button2_Click(object sender, EventArgs e)
{
OpenFileDialog openFileDialog1 = new OpenFileDialog(); //显示选择文件对话框
openFileDialog1.InitialDirectory = "c:\\";
// openFileDialog1.Filter = "xlsx files (*.xlsx)|*.xlsx";
openFileDialog1.Filter = "csv files (*.csv)|*.csv";
openFileDialog1.FilterIndex = 2;
openFileDialog1.RestoreDirectory = true;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
this.textBox2.Text = openFileDialog1.FileName; //显示文件路径
//去textBox2.Text的路径下读取当前excel/csv文件
}
}
private void comboBox1_SelectedIndexChanged_2(object sender, EventArgs e)
{
if (comboBox1.SelectedIndex != 0)
{
int classId = Convert.ToInt32(comboBox1.Text);
List<Schedule> vis_res = new List<Schedule>();
foreach (var r in res)
{
if (r.ClassId == classId)
{
vis_res.Add(r);
}
}
PrintSchedule(vis_res);
}
else
{
dataGridView1.DataSource = null;
}
}
}
}