Составить расписание всегда былом делом непростым. Доверить эту задачу компьютеру решались не все, потому что задача NP-полная и алгоритмического решения «в лоб» за обозримое время не имеет (объяснение).

Картинка про 4х мерный куб. Но как она связана с расписанием – внутри статьи
Картинка про 4х мерный куб. Но как она связана с расписанием – внутри статьи

Недавно ко мне в руки попал пакет математического решателя IBM CPLEX Solver и я попробовала сделать помощника для составления школьного расписания.

Модель решила сделать целочисленную, еще её называют MIP.

Исходные данные

Действующих лиц у нас четыре. Поэтому пусть будет четыре оси системы координат. Т.е. наше пространство будет четырехмерным. Зададим их в виде одномерных массивов данных:

Название оси

Пример данных

Пример в dat файле

классы учеников

5а или 9б

classes = {"5a", "5b", "6a", "6b", "7a", "7b", "8a", "8b", "9a", "9b","10a", "10b", "11a", "11b"};

учителя

Иванова Мария Ивановна, Петрова Татьяна Сергеевна и т.д.

teachers = {"ivanova", "petrova_en", "sidorova_ge", "shumova", "trudovik_m", "trudovik_w", "stoprtteacher"};

кабинеты

это про физические комнаты: 101 кабинет, спорт. зал, кабинет труда № 105, кабинет физики № 205 и т.д.

rooms = {"18", "sport1", "trudy_m", "trudy_w" , "21","22","23","24", "25", "26", "27", "31","32","33","34"};

уроки

это вакантные места в расписании. Выглядит это как 3х-значное число, в котором

первая цифра – это номер дня от 1 до 6, если учатся по шестидневке и до 5, если по пятидневке

вторя цифра – номер смены 1 или 2

третья цифра – номер урока в смене. Обычно от 1 до 6 или до 7

// day, shift, lesson number
lessons = {
111, 112, 113, 114, 115, 116, 117, 121, 122, 123, 124, 125, 126, 127//,
211, 212, 213, 214, 215, 216, 217, 221, 222, 223, 224, 225, 226, 227,
311, 312, 313, 314, 315, 316, 317, 321, 322, 323, 324, 325, 326, 327,
411, 412, 413, 414, 415, 416, 417, 421, 422, 423, 424, 425, 426, 427,
511, 512, 513, 514, 515, 516, 517, 521, 522, 523, 524, 525, 526, 527,
611, 612, 613, 614, 615, 616, 617, 621, 622, 623, 624, 625, 626, 627
};

Между осями координат у нас получаются плоскости. И тут можно наложить ограничения на эти плоскости. Они представляют собой двумерные матрицы. В этих матрицах хранятся определенные целочисленные значения. Далее подробнее про каждое соотношение:

Название плоскости

Пример

Как выглядит в dat файле

Отношения между кабинетами и учителями

Хранится целое число >= 0. Если учитель НЕ может работать в этом кабинете – то 0. Например, нельзя проводить физкультуру в кабинете математики. Но можно провести математику в кабинете физики. Тут будем играть приоритетами. 1 – самый лучший кабинет для этого учителя. 2 – не такой хороший, 3 – приемлемый, но нежелательный. Далее мы еще используем это в целевой функции.

roomTeachersRelation = [
[2,1,1,1,0,0,0],
[0,0,0,0,0,0,1],
[0,0,0,0,1,0,0],
[0,0,0,0,0,1,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[2,1,1,1,0,0,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[2,1,1,1,0,0,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[2,1,1,1,0,0,0],
[2,1,1,1,0,0,0]
];

Отношение между учителями и классами учеников

Тут пишем сколько уроков в неделю должен провести конкретный учитель у конкретного класса. Например, Иванова М.И. должна провести 4 русских языка и 2 литературы в неделю у 6 Б класса. Значит в пересечении пишем цифру 4 + 2  = 6

teacherClassRelation = [
[2,1,0,0,1,1,0,0,0,0,0,0,0,0],
[0,0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,0,1,0,1,0,0,1,1,0,0,0,1,0],
[0,0,0,0,0,0,1,0,0,0,0,2,0,0],
[0,0,0,1,0,0,1,0,0,1,1,0,1,1],
[0,0,0,1,0,0,1,0,0,1,1,0,1,1],
[1,0,1,0,1,0,0,1,1,0,0,1,0,0]
];

Отношение между классами учеников и уроками

Значения этой матрицы 0 или 1. А точнее тут мы задаем в какой смене учится класс. Если 0 – то класс не может иметь уроки в это время.

classLessonRelation = [
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1]
];

Отношение между учителями и номером урока

Значение этой матрицы 0 или 1. Если стоит 0 – значит учитель не может вести уроки в это время. Это на тот случай, если какой-то учитель может работать только в определённые дни недели. Например, учителя с маленькими детьми не работают по субботам.

teacherLessonRelation = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,0,0,0]
];

Графически все вместе (4 оси координат и соотношения между ними) можно представить, как четырёхмерный куб. У нас есть 4 измерения и 4 заданные поверхности. Можно ограничить и больше поверхностей, например, соотношение классов и кабинетов, если каким-то классам нельзя ставить уроки в этих кабинетах. Или между номером урока и кабинетом, если какой-то кабинет недоступен, например, в среду. Но пока ограничимся малым.

Переменная для результата

В такой постановке задачи переменной для хранения результата являться 4х мерный массив, в котором каждое значение может быть 0 или 1.

1 - означает, что условия соотношений выполняется и урок может быть у данного класса, учителя в данном кабинете и в данное время.

0 - означает, что четыре условия не выполняются.

dvar int schedule [rooms][teachers][classes][lessons] in 0..1;

Задание ограничений в модели

Имея все данные и переменные следует задать ограничения.

  • Самое строгое ограничение, что если в наших двумерных массивах встречается ноль, то и в результирующей переменной должен быть ноль

forall(
	 	r in rooms, 
	 	t in teachers, 
	 	c in classes, 
	 	l in lessons 
	 	)
	 	if(roomTeachersRelation[r][t] == 0 || teacherClassRelation[t][c] == 0 || classLessonRelation[c][l] == 0 || teacherLessonRelation[t][l] == 0)
	 	{
	   		schedule[r][t][c][l] == 0;
	   	}   
  • Используем ограничение по поверхности между учителями и классами учеников

Мы знаем количество уроков в неделю, которое должен провести учитель у класса. Поэтому получаем такое ограничение: для каждого учителя и каждого класса сумма уроков в кабинетах должна быть равна цифре из нашего ограничения.

// teacher - class relations
	forall( t in teachers, c in classes) 
		sum(r in rooms, l in lessons)
			schedule[r][t][c][l] == teacherClassRelation[t][c]; 
  • Используем ограничение по поверхности между кабинетами и номерами уроков

В одном кабинете может проходить только один урок, что естественно

// room - lesson relations
	 forall(r in rooms, l in lessons)
		sum(t in teachers, c in classes)
			schedule[r][t][c][l] <= 1;
  • Используем ограничение по поверхности между учителями и номерами уроков.

Потому что один учитель может присутствовать только на одном уроке

// teacher - lesson relations
	  forall( t in teachers, l in lessons)   
	  	sum(r in rooms, c in classes) 
	  		schedule[r][t][c][l] <= 1;
  • И последнее ограничение по поверхности между классами учеников и номерами уроков. Это ограничение самое сложное, потому что для таких предметов как иностранный язык, физкультура, труды, информатика характерно разделение на подгруппы.

Подрассказ как решена проблема разделения на подгруппы

В реальной школе классы часто делятся на подгруппы: физкультура у юношей и девушек, труды у мальчиков и девочек, подгруппы по языку и информатике.

У этой проблемы тоже есть решение: введем набор данных для хранения подмножества «особенных» учителей. Например, учителя иностранного:

{string} foreignLangTeachers = ...;

Тогда в ограничениях к модели напишем, что класс в одно и тоже время может быть у двух разных учителей и в разных кабинетах.

	  	// class - lesson relations
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in foreignLangTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in foreignLangTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in trudyTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in trudyTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in informaticsTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in informaticsTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in sportTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in sportTeachers) 
	  		schedule[r][t][c][l] <= 2;

Целевая функция

При написании мат. модели требуется еще задать целевую функцию. Т.е. обозначить к чему мы стремимся. Тут нужно обратиться к пользователю и узнать, что для них важно. Первое, это чтобы уроки для учеников проводились как можно раньше в сутках. Т.е. желательно, чтобы все начинали учиться с первого урока своей смены. Второе требование, это чтобы учителя начинали работать пораньше. Третье условие, чтобы учителя вели уроки в приемлемых кабинетах. Но если это невозможно, то выбираем из того, что есть. Выглядит целевая функция следующим образом:

minimize 
staticLex(	
 sum(c in classes, l in lessons) (  ClassLessonResult [c][l] * l  )
   ,
sum(t in teachers, l in lessons) (  TeacherLessonResult [t][l] * l  )
,
sum(r in rooms, t in teachers, c in classes, l in lessons) schedule[r][t][c][l] * roomTeachersRelation[r][t]
   )
;

Удобство считывания результата

Для удобства обработки результатов в более наглядном виде в OPL Studio я ввела дополнительные переменные

dvar int TeacherLessonResult[teachers][lessons] ;
dvar int ClassLessonResult[classes][lessons] ;
dvar int RoomLessonResult[rooms][lessons];
dvar int TeacherClassResult [teachers][classes];
dvar int RoomClassResult [rooms][classes];
dvar int RoomTeacherResult [rooms][teachers];

Эти переменные копируют состояние соответствующих поверхностей в переменной schedule.

Поэтому можно видеть такие полезные наглядные результаты

Модель

Вот так выглядит полная модель
{string} rooms = ...;
{string} teachers = ...;
{string} foreignLangTeachers = ...;
{string} trudyTeachers = ...;
{string} informaticsTeachers = ...;
{string} sportTeachers = ...;
{string} classes = ...;
{int} lessons = ...;

int roomTeachersRelation [rooms][teachers]= ...;
int teacherClassRelation [teachers][classes] = ...;
int classLessonRelation [classes][lessons] = ...;
int teacherLessonRelation [teachers][lessons] = ...;

dvar int TeacherLessonResult[teachers][lessons] ;
dvar int ClassLessonResult[classes][lessons] ;
dvar int RoomLessonResult[rooms][lessons];
dvar int TeacherClassResult [teachers][classes];
dvar int RoomClassResult [rooms][classes];
dvar int RoomTeacherResult [rooms][teachers];

dvar int schedule [rooms][teachers][classes][lessons] in 0..1;
 		
minimize 
staticLex(	
 sum(c in classes, l in lessons) (  ClassLessonResult [c][l] * l  )
   ,
sum(t in teachers, l in lessons) (  TeacherLessonResult [t][l] * l  )
,
sum(r in rooms, t in teachers, c in classes, l in lessons) schedule[r][t][c][l] * roomTeachersRelation[r][t]
   )
;

subject to
{
	 forall(
	 	r in rooms, 
	 	t in teachers, 
	 	c in classes, 
	 	l in lessons 
	 	)
	 	if(roomTeachersRelation[r][t] == 0 || teacherClassRelation[t][c] == 0 || classLessonRelation[c][l] == 0 || teacherLessonRelation[t][l] == 0)
	 	{
	   		schedule[r][t][c][l] == 0;
	   	}   		
   
	// teacher - class relations
	forall( t in teachers, c in classes) 
		sum(r in rooms, l in lessons)
			schedule[r][t][c][l] == teacherClassRelation[t][c];   
			
	forall( t in teachers, c in classes) 		
		TeacherClassResult[t][c] == sum(r in rooms, l in lessons) schedule[r][t][c][l];
	     
	 // room - lesson relations
	 forall(r in rooms, l in lessons)
		sum(t in teachers, c in classes)
			schedule[r][t][c][l] <= 1;
	       
	 forall(r in rooms, l in lessons)
		RoomLessonResult[r][l] == sum(t in teachers, c in classes) schedule[r][t][c][l]  ;
	            
	// teacher - lesson relations
	  forall( t in teachers, l in lessons)   
	  	sum(r in rooms, c in classes) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( t in teachers, l in lessons)   
		TeacherLessonResult[t][l] == ( sum(r in rooms, c in classes)  schedule[r][t][c][l] ) ;
	  			
	  	// class - lesson relations
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in foreignLangTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in foreignLangTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in trudyTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in trudyTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in informaticsTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in informaticsTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t not in sportTeachers) 
	  		schedule[r][t][c][l] <= 1;
	  		
	forall( c in classes, l in lessons )   
	  	sum(r in rooms, t in teachers : t in sportTeachers) 
	  		schedule[r][t][c][l] <= 2;
	  		
	forall( c in classes, l in lessons )  
	{
		ClassLessonResult [c][l] == ( sum(r in rooms, t in teachers)  schedule[r][t][c][l] ) ;	
	}
	
		
	//
	forall( r in rooms, c in classes) 
		RoomClassResult[r][c]  == ( sum(l in lessons, t in teachers)  schedule[r][t][c][l] ) ;
		
	//
	forall( r in rooms, t in teachers) 
	RoomTeacherResult[r][t] == ( sum(l in lessons, c in classes)  schedule[r][t][c][l] ) ;
}  
Вот так выглядит файл с подготовленными данными

rooms = {"18", "sport1", "trudy_m", "trudy_w" , "21","22","23","24", "25", "26", "27", "31","32","33","34"};

teachers = {"ivanova", "petrova_en", "sidorova_ge", "shumova", "trudovik_m", "trudovik_w", "stoprtteacher"};

foreignLangTeachers = {"petrova_en", "sidorova_ge"};

trudyTeachers = {"trudovik_m", "trudovik_w"};

informaticsTeachers = {"trudovik_m", "trudovik_w"};

sportTeachers = {"trudovik_m", "trudovik_w"};

classes = {"5a", "5b", "6a", "6b", "7a", "7b", "8a", "8b", "9a", "9b","10a", "10b", "11a", "11b"};

// day, shift, lesson number
lessons = {
111, 112, 113, 114, 115, 116, 117, 121, 122, 123, 124, 125, 126, 127//,
/* 211, 212, 213, 214, 215, 216, 217, 221, 222, 223, 224, 225, 226, 227,
311, 312, 313, 314, 315, 316, 317, 321, 322, 323, 324, 325, 326, 327,
411, 412, 413, 414, 415, 416, 417, 421, 422, 423, 424, 425, 426, 427,
511, 512, 513, 514, 515, 516, 517, 521, 522, 523, 524, 525, 526, 527,
611, 612, 613, 614, 615, 616, 617, 621, 622, 623, 624, 625, 626, 627*/
};

roomTeachersRelation = [
[2,1,1,1,0,0,0],
[0,0,0,0,0,0,1],
[0,0,0,0,1,0,0],
[0,0,0,0,0,1,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[2,1,1,1,0,0,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[2,1,1,1,0,0,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[1,2,1,1,0,0,0],
[2,1,1,1,0,0,0],
[2,1,1,1,0,0,0]
];

teacherClassRelation = [
[2, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 2, 0, 0],
[0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1],
[0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1],
[1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0]
];

classLessonRelation = [
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,1,1,1,1,1,1,1]
];

teacherLessonRelation = [
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[1,1,1,1,1,1,1,1,1,1,1,0,0,0]
];

Это была тонировочная попытка автоматизировать составление школьного расписания. Следующим шагом должно быть написание программной «оболочки» для введение реальных данных.

Комментарии (4)


  1. JSmitty
    03.02.2022 15:30

    как вы собираетесь санпин учитывать (например 5 математик во начальном звене в день нельзя)? и учительский критерий "поменьше окон"?


    1. MrsTroyan Автор
      04.02.2022 08:52

      Спасибо за Ваш комментарий.

      Критерий "поменьше окон" стараемся удовлетворить в целевой функции, чтобы уроки были ближе к началу дня. Но даже в жизни это иногда почти невозможно, хотя и зависит от конкретной жизненной ситуации (т.е. от датасета).

      А чтобы не более 5 математик в день, то нужно ограничение добавить, чтобы сумма по учителю и классу на уроках от 111 до 117, например, не была более 5.


  1. Yerin
    04.02.2022 08:13

    Для подобного класса задач есть еще OR-tools от гугл. В бесплатной версии Cplex есть ограничение на количество данных и условий, поэтому в моем случае (планирование производства) выдавало ошибку.


    1. MrsTroyan Автор
      04.02.2022 08:54

      Согласна, OR-Tools тоже хороший инструмент, но у меня пока и датасет небольшой, поэтому Cplex работал.