[1] SCHAERF, A. A survey of automated timetabling. Artificial intelligence review 13, 2 (1999), 87–127.
[2] V. Bazon - Problema orarului școlar, pe tărâmul limbajului R (Google Play/Books; 50 lei)
Problema orarului școlar—School Timetable Problem, STP—constă în a repartiza anumite resurse date (tupluri de profesori,
grupuri de elevi și săli de clasă), pe anumite sloturi de timp (cupluri (Zi, Oră)), astfel încât să se evite suprapunerile și să fie satisfăcute într-o proporție cât mai mare, anumite preferințe inițiale.
STP este tratată de obicei (v. [1]) ca „problemă combinatorială de căutare și optimizare”, concretizând și dezvoltând teorii, tehnici și euristici (simulated annealing, evolutionary algorithms, integer programming, constraint programming, etc.) care vor fi adus (în ultimii vreo 50 de ani) câștiguri importante pentru informatică și programare.
[2] conturează totuși altă direcție pentru STP, observând în mod direct realitatea: încadrarea profesorilor dintr-o școală dată este un „set de date”, care trebuie completat cumva, pentru a deveni un orar (care este un alt set de date).
Setul inițial conține toate lecțiile prof | obj | cls care trebuie desfășurate în săptămâna curentă și avem de adăugat coloana Zi și apoi coloana Ora, astfel încât:
(1) – repartizarea pe zile a lecțiilor să fie echilibrată (pe zile, clase, profesori și obiecte);
(2) – să nu existe suprapuneri de lecții într-o aceeași oră a zilei (exceptând cuplajele);
(3) – pe o zi sau alta, fiecare profesor să aibă cel mult două ferestre.
Iar după obținerea unui astfel de orar, avem de coborât pe cât se poate numărul total de ferestre, pe fiecare zi.
În principiu, montarea variabilei Zi pe tabelul lecțiilor decurge prin generare aleatorie a valorilor acesteia, până când sunt satisfăcute condițiile (1) (fiind foarte multe repartizări posibile, avem suficiente șanse de a nimeri una cât mai potrivită). Cam la fel, adăugăm și câte o coloană Ora, încât să fie îndeplinite condițiile (2) și (3): permutăm aleatoriu lecțiile repartizate în câte o aceeași zi și ținem seama de anumite proprietăți ale unor grafuri asociate acestora. Prevedem și posibilități de retușare interactivă (prin consola R).
Pentru aceste seturi de date folosim consecvent „forma normală”: datele propriu-zise sunt valorile posibile ale câte uneia dintre niște variabile independente fixate (nu amestecăm profesorii și obiectele; nu operăm cu „sloturi”, ci vizăm separat variabilele Zi și Ora); orice investigație dorim, va decurge folosind operații de filtrare (pentru linii de valori) și de selectare (de variabile, sau coloane) – iar pentru facilitare, transformăm o coloană sau alta în factor (ceea ce asigură și operații de grupare și „splitare”).
R împreună cu „dialectul” tidyverse și cu pachetul igraph ne oferă tipurile corelate de date ('data.frame', 'matrix', 'list', 'graph' etc.) și metodele de lucru (filtrare, selectare, adăugare, grupare, etc.) necesare implementării ideii evidențiate mai sus.
Bineînțeles că în mod implicit, [2] ajunge să fie și un soi de „manual”, sau compendiu de folosire a limbajului R și a pachetului tidyverse (într-un context de programare diferit totuși, de orientarea statistică specifică mediului R).
Programele R de repartizare pe zile și pe orele zilei a lecțiilor, au fost dezvoltate plecând de la seturi de date inițiale deduse de pe orare generate prin aplicația asctimetables, orare postate (în format PDF, HTML sau Excel) pe site-urile unor diverse școli din țară.
Numele de profesori care apar în [2] au fost generate aleatoriu și nu corespund unor persoane reale; declarăm că numele de profesori care apar uneori mai jos, sunt și ele, fictive (ni s-a părut inutil, efortul de a le înlocui din start cu nume generate aleatoriu).
De pe site-ul unei școli am obținut un fișier PDF conținând orarele claselor; redăm aici pagina corespunzătoare uneia dintre clase:
Folosind gs
(v. Ghostscript) cu -sDEVICE=txtwrite
, obținem orarele claselor într-un format textual care păstrează cu aproximație (spațiind între coloane), formatul original.
Redăm aici (de sub editorul de text mousepad) primele trei coloane din primele linii ale fișierului-text rezultat pentru pagina PDF expusă mai sus:
Este necesară vreo oră de muncă de editare atentă, pentru a aduce fișierele-text respective la un format unitar, evidențiind datele de care avem nevoie:
Anume, am eliminat antetele de pagină (numele și adresa școlii, respectiv "Orar generat... aSc Orare"), precum și linia de intervale orare "7:45 – 8:35 ..."; am comasat nume de obiecte care inițial erau despărțite pe două sau mai multe rânduri (de exemplu, "Optional Romana"). Pentru lecțiile care se desfășoară „pe grupe”, am eliminat "GR1" și "GR2" și am „unificat” numele celor doi profesori, alipindu-le printr-un caracter "/
" (la fel, pentru numele celor două obiecte implicate în cuplajul respectiv).
În urma acestei editări, fișierele-text respective conțin câte 12 linii de date (și eventual, o linie vidă la sfârșit); prima linie conține doar numele clasei; pe a doua linie avem orele 1..6 sau 1..7; pe liniile 3 și 4 avem orarul clasei pe ziua Lu (obiectele pe linia 3 și profesorii corespunzători pe linia 4); pe liniile 5 și 6 – orarul pe ziua Ma, ș.a.m.d.
Fiecăruia dintre fișierele-text obținute îi asociem prin readLines()
un vector conținând (ca șiruri de caractere) cele 12 linii din fișier; de exemplu:
> Lines[3] [1] " Lu Lb. Romana Geografie Informatica si TIC Matematica"
Eliminăm spațiile inițiale și finale și apoi ținem seama de faptul că datele din coloane vecine sunt separate între ele printr-o secvență de cel puțin două spații; prin stringr::str_split()
transformăm șirurile de caractere în vectori, cu câte un număr de componente egal cu numărul de ore din ziua respectivă (eventual plus 1, pentru numele zilei):
> Q[[3]] [1] "Lu" "Lb. Romana" "Geografie" "Informatica si TIC" "Matematica"
Se poate ca numărul de ore să difere de la o zi la alta; în acest caz, completăm vectorul respectiv cu valori NA
(pentru a aplica list2DF()
, vectorii trebuie să aibă aceeași dimensiune).
Apoi, prin map_dfr
și list2DF
transformăm listele de vectori într-un „tabel” data.frame, conținând toate datele orarului pe coloanele prof, obj, zi, ora și cls:
library(tidyverse) path <- "EM_cls/" # Subdirectorul fişierelor (editate) "cls-*.txt" txt2DF <- function(cls_txt) { con <- file(paste0(path, cls_txt)) Lines <- readLines(con, warn=FALSE) # Vector cu cele 12/13 linii din 'cls-*.txt' close(con) Lines <- Lines[1:12] # linia 13, dacă există este vidă Q <- Lines %>% str_trim(., side="both") %>% str_split(., "\\s{2,}") no <- length(Q[[2]]) # numărul maxim de ore pe zi, ale clasei for(i in 3:12) { Li <- Q[[i]] n <- if(i %% 2) no+1 else no # liniile impare încep cu numele zilei if(length(Li) == n) next while(length(Li) < n) Li <- c(Li, NA) # completează când este cazul, cu valori NA Q[[i]] <- Li } map_dfr(seq(3, 11, by=2), function(i) list2DF(list( prof = Q[[i+1]], obj = Q[[i]][-1], zi = rep(Q[[i]][1], no), ora = 1:no)) ) %>% mutate(cls = Q[[1]]) } EM <- map_dfr(list.files(path=path, pattern=".txt"), txt2DF) # 1115 obs. EM <- EM %>% filter(!is.na(prof)) #'data.frame': 1018 obs. of 5 variables: saveRDS(EM, file="EMorig.RDS")
După eliminarea în final, a liniilor cu valori NA
, ne-au rămas 1018 lecții prof|obj|zi|ora|cls
și le-am salvat în fișierul EMorig.RDS
. Pentru programele din [2], de repartizare echilibrată a lecțiilor pe zile și pe orele zilei, ne interesează numai datele de încadrare prof|obj|cls
.
Folosind editorul de text gedit, constituim un program prin care investigăm datele (cel mai adesea, interactiv – din consola R), urmând să le adaptăm treptat specificațiilor din [2]:
# sicoas.R library(tidyverse) EM <- readRDS("EMorig.RDS") > str(EM) # inspectăm interactiv structura datelor 'data.frame': 1018 obs. of 5 variables: $ prof: chr "Istudor Ramona" "Fosalau Manuel" ... $ obj : chr "Lb. Romana" "Geografie" "Informatica si TIC" "Matematica" ... $ zi : chr "Lu" "Lu" "Lu" "Lu" ... $ ora : int 1 2 3 4 1 2 3 4 5 6 ... $ cls : chr "05A" "05A" "05A" "05A" ...
Să vedem și câteva eșantioane aleatorii de linii (repetând slice_sample()
):
> slice_sample(EM, n=3) prof obj zi ora cls 1 Ivascu Vica Sport Ma 6 10E 2 Frost Alina/Nenov Simona LGer/LFr Lu 3 09A 3 Popa Simona Muzica Ma 1 08C
Deși nu este neapărat necesar, vom simplifica—și corecta, eventual—denumirile obiectelor (înlocuind "Lb. Romana" cu "Română", etc.), precum și numele claselor (preferând "9A" în loc de "09A"). Vom încerca să codificăm profesorii după disciplinele principale și numărul de ore ale fiecăruia; pentru încadrările „pe grupe” (cum vedem mai sus pe obiectul "LGer/LFr") vom introduce „profesori fictivi” și vom asocia anumite dicționare de legături.
În final, vom elimina coloanele zi și ora (iar câmpul obj va fi necesar numai când va fi de afișat orarul pe care-l vom obține prin programele din [2]).
Să „corectăm” întâi numele claselor:
EM <- EM %>% mutate(cls = sub("^0", "", cls)) # elimină prefixul "0" (09A -> 9A)
Pentru a modifica numele obiectelor (pe toate liniile), întâi transformăm în factor coloana respectivă:
EM <- EM %>% mutate(obj = factor(obj, ordered=TRUE))
levels(EM$obj)
ne afișează în consolă disciplinele respective, indexând liniile sub forma "[1]" – adică alipind '[', o secvență de cifre și ']', corespunzător expresiei regulate \[\d*\]
; copiem liniile în gedit și întâi eliminăm indecșii de linie, folosindu-ne de meniul Find and Replace (care are și opțiunea de căutare după expresii regulate); prin același meniu, putem apoi înlocui spațiile dintre denumiri (redate fiecare, între ghilimele) cu o virgulă – obținând după ambalarea în c(
...)
un vector conținând numele respective.
Modificăm (în gedit) elementele acestui vector și apoi îl copiem în program într-o variabilă OBJ
. Vom avea de folosit denumirile din OBJ
numai când va fi să afișăm orare, nu și în programele de generare a orarelor; dar în loc să le eliminăm pur și simplu, le abreviem la câte două caractere și le folosim astfel pentru a codifica profesorii: de exemplu, codul "Bi3
" ar reprezenta un profesor de "Biologie" – anume pe al treilea încadrat pe acest obiect, în ordinea descrescătoare a numărului de ore.
Abreviem deci numele obiectelor și transformăm OBJ
într-un „dicționar” (prin funcția names()
) care precizează asocierea dintre abrevieri și denumiri:
> OBJ # dicționarul disciplinelor AB Bi Ch CP De "AB" "Biologie" "Chimie" "CoProgramare" "Desen" Di Ec EA ES FL "Dirigenție" "Economie" "EdAntrep" "EdSocială" "Filosofie" Fi Ge IA IP IC "Fizică" "Geografie" "InfApl" "InfoAplicații" "InfoCurs" IT IRC IRO Is Ju "InfoTIC" "IR/OpCh" "IR/OS" "Istorie" "Jurnalism" La En Fr Ge Ro "Latină" "Engleză" "Franceză" "Germană" "Română" Sp GF Li Lo Ma "Spaniolă" "Ger/Fra" "LitUniv" "Logică" "Matematică" MUD Mu OIB OCB OB "Muz/Des" "Muzică" "OI/OpBi" "OpCh/OpBi" "OptBiologie" OC OI OM OR OS "OptChimie" "OptIstorie" "OptMatematică" "OptRomână" "OptSport" Ps Re So Sp Te TI "Psihologie" "Religie" "Sociologie" "Sport" "Tehnologie" "TIC"
Salvăm dicționarul disciplinelor și înlocuim prin abrevieri, denumirile din EM$obj
:
saveRDS(OBJ, "dctObj.RDS") levels(EM$obj) <- names(OBJ)
Când va fi să afișăm orare, vom încărca dctObj.RDS și vom ști atunci, că lecțiile profesorului "Ro4" de exemplu, trebuie afișate ca "Română".
De observat funcționalitatea bilaterală a funcției levels()
: furnizează nivelele factorului, dar permite și modificarea ulterioară a nivelelor (de altfel și alte funcții, de exemplu names()
, asigură atât operații "Get" cât și operații "Set").
Să despărțim lecțiile obișnuite („cu întreaga clasă”), de cele care decurg „pe grupe” (pentru care în câmpul prof
apare ’/
’) și să evidențiem profesorii în fiecare caz:
L12 <- EM %>% split(grepl("/", .$prof)) P1 <- L12[[1]] %>% pull(prof) %>% unique() %>% sort() # cei care au și ore proprii ("cu clasa întreagă") P2 <- L12[[2]] %>% pull(prof) %>% unique() %>% strsplit(., "/") %>% unlist() %>% unique() %>% sort() # profesorii angajați în cuplaje ("pe grupe") print(setdiff(P2, P1)) # character(0) (Nu există profesori "externi")
Constatăm prin setdiff()
că nu există profesori „externi” (care nu au ore proprii, ci doar în cuplaje); aceasta simplifică lucrurile, față de cazurile tratate în [2].
Următoarea funcție produce disciplinele pe care este încadrat un profesor (din setul P1
, sau din P2
), în ordinea descrescătoare a numărului de ore:
prof_objs <- function(P, lss) lss %>% filter(grepl(P, prof)) %>% count(obj, sort=TRUE)
Subliniem că prof_objs()
păstrează calitatea de factor a câmpului $obj
și implicit, toate nivelele acestuia; dacă un profesor este încadrat pe mai multe discipline, ne va interesa acum numai cea „principală” (pe care are cel mai multe ore) și va trebui să folosim droplevels()
, pentru a păstra numai nivelele corespunzătoare disciplinelor principale.
Putem obține acum o listă care asociază fiecărei discipline principale, setul profesorilor încadrați pe acea disciplină, ordonați descrescător după numărul de ore:
Pob <- map_dfr(P1, function(P) { OB <- prof_objs(P, L12[[1]])[1, ] if(P %in% P2) { # decide asupra disciplinei principale OB2 <- prof_objs(P, L12[[2]])[1, ] if(OB2$n > OB$n) OB <- OB2 } data.frame(prof = P, obj = OB$obj, no = OB$n) }) %>% droplevels() %>% # ignoră disciplinele secundare arrange(desc(no)) %>% split(.$obj)
În sfârșit – putem formula un „tabel” care asociază numelor din P1
coduri de câte 3 caractere, formate din codul disciplinei principale și un sufix care indică numărul de ordine după numărul descrescător al orelor fiecăruia:
Pcd <- map_dfr(seq_along(Pob), function(i) { N <- nrow(Pob[[i]]) # numărul de profesori pe disciplina curentă (< 10) ob <- Pob[[i]]$obj[1] obn <- paste0(ob, 1:N) data.frame(prof = Pob[[i]]$prof, cod = obn) })
Dar bineînțeles că în loc de „tabel”, va fi mai convenabil să lucrăm cu dicționare:
prof_cod <- Pcd$cod names(prof_cod) <- Pcd$prof CPR <- setNames(names(prof_cod), prof_cod) # dicționar nume_profesor -> COD
Folosind dicționarul prof_cod
, putem codifica acum și cuplajele – alipind
codurile celor doi membri (iar în final, reunim toate codurile):
tws <- L12[[2]] %>% pull(prof) %>% unique() %>% sort() # cuplajele de profesori, de forma Prof1/Prof2 cup_cod <- vector("character", length(tws)) for(i in seq_along(tws)) { tw <- strsplit(tws[i], "/")[[1]] # legasem prin "/" cele două nume dintr-un cuplaj kod <- as.vector(prof_cod[tw]) # codurile membrilor cup_cod[i] <- paste0(kod[1], kod[2]) # alipește codurile membrilor } cup_cod <- setNames(cup_cod, tws) prof_cod <- c(prof_cod, cup_cod) # dicționar profesor sau cuplaj -> COD saveRDS(prof_cod, "dctProf.RDS") # util când va fi de afișat orarul final
Dicționarul prof_cod
va fi util—dacă-l vom inversa— când va fi să afișăm orarul final.
Înregistrăm codurile din vectorul prof_cod
, pe toate liniile din setul EM
:
EM <- EM %>% mutate(prof = as.vector(prof_cod[prof]))
Să observăm pe un eșantion, că unii profesori au și discipline secundare:
> slice_sample(EM, n=5) prof obj zi ora cls 1 TI2 IP Mi 2 11B # "IP" este disciplină secundară a lui "TI2" 2 TI2 TI Ma 1 9C # "TI" este disciplina principală a lui "TI2" 3 Ma2 Ma Mi 5 6A 4 De1 De Lu 7 8C 5 GF2GF3 GF Lu 4 12D
Fiecare lecție este reprezentată pe câte o linie din setul EM
; pe linia respectivă, codul din câmpul prof
induce pe cel din câmpul obj
numai în cazul disciplinelor „principale” – deci comparând pe fiecare linie, valorile prof
și obj
, vom putea depista liniile corespunzătoare disciplinelor secundare.
În mod implicit, operațiile pe un obiect data.frame decurg „pe coloane”; dar funcția dplyr::rowwise()
asigură și posibilitatea de a opera „pe linii”, cum am avea nevoie acum:
scd <- EM %>% rowwise() %>% filter(! grepl(obj, prof)) # liniile cu discipline "secundare" lSC <- scd %>% split(.$prof) OSE <- map_dfr(seq_along(lSC), function(i) { sec <- lSC[[i]] # pe profesorul curent map_dfr(seq_along(unique(sec$obj)), function(j) { Obj <- sec$obj[j] # pe fiecare disciplină secundară Cls <- sec %>% filter(obj == Obj) %>% pull(cls) data.frame( prof = sec$prof[1], obj = Obj, cls = paste(Cls, collapse = " ")) }) })
Pentru a ne convinge că lucrurile sunt în regulă, să listăm liniile din OSE
și frecvența pe discipline a lecțiilor din EM
, corespunzătoare unuia dintre profesori:
> OSE %>% filter(prof == "TI1") prof obj cls 1 TI1 Di 9B 2 TI1 IP 9B 10D 12B 12B 12B 12B 3 TI1 IC 9D > ti1 <- EM %>% filter(prof == "TI1") > frq <- table(ti1$obj); print(frq[frq != 0]) Di IP IC TI 1 6 1 11
Pe exemplul redat aici, vedem că profesorul "TI1
" are disciplina principală (pe 11 ore) "TI
" (abrevierea adoptată pentru TIC) și are mai multe discipline secundare: o oră "Di
" (Dirigenție) la clasa 9B
, 6 ore "IP
" (InfoAplicații) și o oră "IC
" (InfoCurs) la clasa 9D
.
De notat că aici am făcut un mic pas înainte față de [2], unde consideram doar cazul (cel mai obișnuit) când un profesor are cel mult o singură disciplină secundară (caz în care era suficient un singur map_dfr()
, nu două imbricate, ca mai sus).
Când va fi de afișat orarul vreunei clase, va trebui să consultăm și OSE
:
saveRDS(OSE, "dfObjSec.RDS")
pentru a stabili ce disciplină afișăm pe lecția curentă a unuia sau altuia dintre profesori.
Alocarea pe zile și ore a lecțiilor cuplajelor și profesorilor care sunt angajați în cuplaje, va depinde mereu de alocările făcute celor cu care sunt astfel conexați; vom constitui niște „dicționare” care să evidențieze aceste dependențe.
Împărțim lecțiile în două părți, după lungimea codului (3 sau 6) din $prof
și degajăm profesorii din fiecare parte:
S36 <- EM %>% split(nchar(.$prof)) K3 <- S36[[1]] %>% pull(prof) %>% unique() %>% sort() K6 <- S36[[2]] %>% pull(prof) %>% unique() %>% sort()
Următorul dicționar are drept chei profesorii care intră măcar într-un cuplaj și care au și ore proprii, iar drept valori – vectorii care conțin profesorii de care depind aceștia, la alocarea pe zile și ore:
Tw1 <- map(K3, function(P) K6[grepl(P, K6)]) %>% setNames(K3) %>% compact()
Dacă P
este una dintre cheile lui Tw1
, ora din zi pe care o vom aloca uneia dintre lecțiile lui P
trebuie să nu coincidă cu vreuna dintre orele alocate deja cuplajelor din vectorul Tw1[[P]]
:
P Tw1[[P]] $Bi1 "Bi1Fi1" "Ch3Bi1" "IP1Bi1" $Ch1 "Re1Ch1" $Ch3 "Ch3Bi1" $De1 "Mu1De1" $ES1 "La1ES1" $Fi1 "Bi1Fi1" $Fr1 "GF1Fr1" "GF2Fr1" $GF1 "GF1Fr1" "GF1GF3" "GF1GF4" $GF2 "GF2Fr1" "GF2GF3" "GF2GF4" $GF3 "GF1GF3" "GF2GF3" $GF4 "GF1GF4" "GF2GF4" $IP1 "IP1Bi1" $La1 "La1ES1" $Mu1 "Mu1De1" $Re1 "Re1Ch1"
Constituim (mai simplu ca în [2], fiindcă acum nu avem profesori „externi”) și un dicționar cumva invers, în care cheile sunt cuplajele existente, iar valorile sunt vectori care conțin profesorii—fictivi sau propriu-ziși—de care depinde alocarea orelor cheii respective:
Tw2 <- map(K6, function(PP) { P1 <- substr(PP, 1, 3) P2 <- substr(PP, 4, 6) setdiff(c(P1, P2, union(Tw1[[P1]], Tw1[[P2]])), PP) %>% unique() }) %>% setNames(K6) %>% compact()
La alocarea lecțiilor unui cuplaj P
(cheie din Tw2
), va trebui să ținem seama de alocările făcute deja profesorilor și cuplajelor din vectorul Tw2[[P]]
:
P Tw2[[P]] $Bi1Fi1 "Bi1" "Fi1" "Ch3Bi1" "IP1Bi1" $Ch3Bi1 "Ch3" "Bi1" "Bi1Fi1" "IP1Bi1" $GF1Fr1 "GF1" "Fr1" "GF1GF3" "GF1GF4" "GF2Fr1" $GF1GF3 "GF1" "GF3" "GF1Fr1" "GF1GF4" "GF2GF3" $GF1GF4 "GF1" "GF4" "GF1Fr1" "GF1GF3" "GF2GF4" $GF2Fr1 "GF2" "Fr1" "GF2GF3" "GF2GF4" "GF1Fr1" $GF2GF3 "GF2" "GF3" "GF2Fr1" "GF2GF4" "GF1GF3" $GF2GF4 "GF2" "GF4" "GF2Fr1" "GF2GF3" "GF1GF4" $IP1Bi1 "IP1" "Bi1" "Bi1Fi1" "Ch3Bi1" $La1ES1 "La1" "ES1" $Mu1De1 "Mu1" "De1" $Re1Ch1 "Re1" "Ch1"
De exemplu, GF1Fr1
nu se va putea afla într-o aceeași oră a zilei ca și vreunul dintre cei 5 profesori (fictivi sau nu) înregistrați în Tw2$"GF1Fr1"
.
Repartizarea pe zile și pe orele zilei a lecțiilor, precum și reducerea ferestrelor din orarele obținute, se bazează pe dicționarele de dependențe; le salvăm împreună:
save(Tw1, Tw2, file="dctTw12.Rda")
Trebuie să verificăm dacă nu cumva, există și clase care depind una de alta (clase cuplate); condiţia ca un cuplaj de profesori să lucreze nu pe câte o grupă a clasei, ci pe clase întregi, rezultate prin combinarea ad_hoc a grupelor a două clase – este apariţia de două ori într-o aceeaşi zi şi oră, a acelui cuplaj:
> EM %>% filter(nchar(prof) > 3) %>% group_by(prof, zi, ora) %>% count() %>% filter(n > 1) # reţinem numai apariţiile duble, dacă există
În cazul de față nu avem clase cuplate (de observat că doar consultând orarul PDF original, nu prea aveam cum să ne dăm seama dacă există sau nu, clase cuplate).
Putem încă folosi variabilele originale Zi și Ora, dacă vrem să investigăm unele caracteristici ale orarului respectiv (distribuția orelor pe zile, pe clase, pe profesor; statistici privitoare la ferestre, etc.). Atfel, nu mai avem nevoie de aceste coloane:
LSS <- EM %>% select(prof, cls) saveRDS(LSS, "lessons.RDS")
Acum în lessons.RDS
avem toate lecțiile prof|cls
care trebuie desfășurate săptămânal în școala respectivă (am exclus și câmpul obj
, devenit inutil în urma codificării pe discipline a profesorilor și a constituirii dicționarului dctObj.RDS
și a tabelului dfObjSec.RDS
).
Existența unui orar decurge din faptul că încadrarea este normată: pe săptămână, clasele au câte 25-33 ore, profesorii au câte cel mult 18-27 ore, disciplinele au cel mult 7 ore (iar valorile extreme sunt foarte rare); sunt posibile foarte multe orare corecte, astfel că dacă știm să generăm unul oarecare, atunci sunt mari șanse de a nimeri și un orar acceptabil.
Programul by_days.R
din [2] definește (pe mai puțin de 70 de linii) o funcție pentru alocarea echilibrată pe zile a lecțiilor profesorilor neangajați în cuplaje și o funcție pentru alocarea (echilibrată) pe zile a lecțiilor celor angajați în cuplaje – de folosit cam așa:
# EMbyDays.R library(tidyverse) LSS <- readRDS("lessons.RDS") Zile <- c("Lu", "Ma", "Mi", "Jo", "Vi") perm_zile <- readRDS("lstPerm47.RDS")[[2]] # cele 120 permutări de 1..5 load("dctTw12.Rda") # [1] listele de dependență "Tw1", "Tw2" source("by_days.R") # funcțiile mount_days_necup() și mount_days_cup() LS1 <- LSS %>% filter(! prof %in% union(names(Tw1), names(Tw2))) # neangajați LS2 <- anti_join(LSS, LS1, by=c('prof', 'cls')) # angajați în cuplaje R1 <- mount_days_necup(LS1) # alocare pe zile a lecțiilor neangajate în cuplaje R2 <- mount_days_cup(LS2) # alocare pe zile a lecțiilor celor angajați în cuplaje R12 <- R1 %>% rbind(R2) # alocarea pe zile a tuturor lecțiilor săptămânii saveRDS(R12, "R12.RDS")
În cazul de față (cu numai 34 de clase, 48 de profesori neangajați în cuplaje și 27 de profesori sau „profesori fictivi”, angajați în cuplaje) execuția programului redat mai sus ia cam la fiecare repetare a lui, mai puțin de 10 secunde; însă nu așa („am obținut rezultatul, îl afișăm și gata”) trebuie procedat…
Rezultatele R1
și R2
variază de la o execuție la alta, fiindcă funcțiile de alocare menționate mizează pe aspecte aleatorii (de exemplu, permutări aleatorii de Zile); trebuie repetată execuția programului (de vreo trei ori, în acest caz), până obținem repartiții promițătoare:
> addmargins(table(R1[c('cls','zl')])) zl cls Lu Ma Mi Jo Vi Sum 10A 5 4 4 4 5 22 10B 4 4 4 4 5 21 # ... 9D 4 5 5 4 5 23 9E 5 4 5 5 4 23 Sum 152 152 154 152 153 763 # distribuție aproape uniformă > addmargins(table(R2[c('cls','zl')])) zl cls Lu Ma Mi Jo Vi Sum 10A 1 3 1 2 1 8 10B 3 2 2 3 0 10 10C 1 2 2 1 2 8 # ... 9D 0 2 2 2 2 8 9E 2 1 0 2 2 7 Sum 50 50 52 52 51 255 # distribuție aproape uniformă
În R1
(nu neapărat și în R2
) obținem totdeauna distribuții omogene pentru orele claselor (diferența pe clasă între o zi și alta este de cel mult o oră), însă trebuie să repetăm eventual execuția programului, pentru a obține o distribuție cât mai apropiată de uniform, a totalului de ore pe zi (precum mai sus, 152 152 154 152 153). Doar având R1
și R2
suficient de omogene, putem anticipa că și pe profesori, să rezulte câte o distribuție echilibrată a orelor…
Reunind R1
și R2
(în R12
), pot apărea excepții: una-două clase cu 8 ore într-o zi și 4 în altele; sau, unii profesori—dintre cei angajați în cuplaje—cumulează distribuții dezechilibrate pe zile. Folosind funcțiile de ajustare interactivă (de la consola R) prezentate în [2], putem transforma cu ușurință R12
, astfel încât distribuția lecțiilor să fie omogenă pe zile (anume, 204 203 203 204 203) și pe clase și cu câteva excepții, să fie omogenă și pe profesori (pentru majoritatea distribuțiilor individuale, diferența de ore între zile este de cel mult o oră).
În plus, putem folosi o aplicație interactivă javaScript + HTML precum recast/ (v. codul-sursă pe github) – cu avantajul că vedem și putem controla împreună toate datele și pe de altă parte, interschimbăm mai comod clase, dintr-o zi în alta (și cu dezavantajul că avem de transformat între seturile normalizate de date din R și formatul CSV necesar tabelării HTML).
După ce vom fi definitivat repartizarea pe zile R12
, constituim o listă care asociază fiecărei zile, lecțiile prof|cls
ale acelei zile:
R12 <- readRDS("R12rec.RDS") # repartizare echilibrată prof|cls|zl ldz <- map(Zile, function(z) R12 %>% filter(zl == z) %>% select(prof, cls) ) %>% setNames(Zile) # lista distribuțiilor prof|cls pe fiecare zi saveRDS(ldz, "R12_ldz.RDS")
Funcția mount_hours()
din [2] (pe mai puțin de 80 de linii) etichetează lecțiile unei aceleiași zile cu orele 1..7 ale zilei, astfel încât să se evite suprapunerea a două lecții într-o aceeași oră, să se respecte programul normal (care începe de la prima oră a zilei, pentru toate clasele) și să se creeze cumva premise pentru existența unui număr cât mai mic de ferestre (impunând condiția ca orarele individuale să nu conțină mai mult de două ferestre).
Controlul suprapunerilor necesită informațiile de dependență din dctTw12.Rda
– dar acestea trebuie actualizate pe fiecare zi; ca în [2], constituim lPrTz.Rda
, conținând listele lTz1
și lTz2
care indică pe fiecare zi, dependențele între profesorii angajați în cuplaje pe acea zi (precum și lista lPrz
a tuturor celor care au lecții în ziua respectivă).
Obs. Plecând de la lista map(lTz1, length)
, putem verifica interactiv în ce măsură lecțiile cuplate sunt repartizate echilibrat pe zile – și eventual, putem decide să mutăm în alte zile unele cuplaje (urmând apoi să reluăm cele de mai sus). Dar subliniem că orice modificare am face acum sau mai departe, asupra repartiției pe zile R12
, ea trebuie urmată de actualizarea listei lPrTz.Rda
(și deasemenea, de recalcularea coeficienților "betweenness").
mount_hours()
alocă lecțiile fiecărei clase în parte, pe orele 1..7 ale zilei; experimentele evocate în [2] privitoare la ordinea lecțiilor respective, au condus la poziționarea fiecărui profesor în raport cu ceilalți în funcție de prezența unor clase comune și la o ordonare aleatorie ponderată de numărul de profesori comuni, a claselor.
Ca în [2], constituim graful profesorilor și graful claselor, în care adiacența reflectă existența unor clase comune, respectiv a unor profesori comuni; reținem în BTW.rda
coeficienții "betweenness" ai nodurilor celor două grafuri. Redăm graful claselor pentru ziua Lu
(mărimea nodurilor este proporțională cu valorile "betweenness"):
Fixarea orarului unuia dintre cei care au coeficient ”betweenness” mare, ar diminua posibilitățile de alocare ulterioară pentru mult mai mulți profesori (cei aflați pe câte o aceeași geodezică a grafului, cu cel căruia i se fixează orarul), decât fixarea orarului unuia de coeficient ”betweenness” mult mai mic. De aceea, am vizat profesorii și clasele în ordinea crescătoare a coeficienților ”betweenness” (sau una apropiată cumva, de aceasta).
De exemplu, după graful pe Lu
redat mai sus, am începe repartizarea pe ore cu lecțiile claselor 7C
, 6A
, 6C
, 10C
și am lăsa mai la urmă, pe cele ale claselor mai dense în privința legăturilor comune, 8A
, 6C
, 11C
, 12D
; procedând astfel, mărim șansele de reușită pentru alocarea lecțiilor clasei curente (altfel se poate ajunge repede la blocare: oricum am permuta pe ore lecțiile clasei curente, dăm peste lecții cu același profesor, alocate anterior altor clase).
mount_hours()
produce foarte repede orarele zilelor, dacă păstrăm plafonul implicit pentru numărul de „ferestre” – 20% din totalul lecțiilor pe ziua respectivă; reducând în cazul de față la 16%, avem totuși timpi buni (sub 5 minute, în total):
06:09:18 30 Lu 06:09:46 # cu 30 de ferestre, "pe orizontale" individuale 31 Ma 06:10:21 30 Mi 06:12:07 33 Jo 06:14:09 29 Vi 06:14:10 > saveRDS(ORR, file="orar1.RDS")
Dar precizăm că s-au avut în vedere numai ferestrele „pe orizontală”, pentru fiecare profesor sau profesor-fictiv în parte… Listăm de exemplu orarele unora dintre lecțiile pe limbi străine, pentru ziua Lu
:
> lu <- orarByProf(ORR[["Lu"]]) > lu %>% filter(grepl("GF", prof)) prof 1 2 3 4 5 6 7 1 GF4 - - - 10E - - - 2 GF3 6A 8A - - - - - 3 GF2Fr1 - - - 10C - - - 4 GF1GF4 12A - - - - - - 5 GF2 5D 6D - - - - - 6 GF2GF3 - - - - - 10E - 7 GF2GF4 - - 11E - 12E - - 8 GF1GF3 - - 9C 10A - - - 9 GF1 - - - - 7C 5C -
GF4
intră în ora 1 împreună cu GF1
la clasa 12A
, apoi intră împreună cu GF2
, în ora 3 la 11E
și în ora 5 la 12E
, iar ora a 4-a o face singur la clasa 10E
; prin urmare, GF4
are o fereastră ascunsă în ora a 2-a – neluată în seamă de mount_hours()
, care în schimb consideră că GF2GF4
are o fereastră în ora a 4-a (falsă de fapt, atât pentru GF4
cât și pentru GF2
).
Încă în mount_hours()
—pentru a supraveghea pe parcurs alocarea pe ore a lecțiilor—am folosit un vector de „octeți de alocare”, având drept chei numele profesorilor și drept valori, câte un un octet (de fapt nu chiar 8 biți, ci 32 sau 64 de biți – cât măsoară tipul integer) în care biții erau setați odată cu alocarea de ore, profesorului respectiv: bitul de rang 0..6 corespunde orei 1..7 a zilei.
Șablonând astfel orarele individuale, putem calcula corect și numărul de ferestre; de exemplu, în orarul pe Lu
din care am redat mai sus, pentru GF4
avem șablonul binar 0001000
, pentru GF1GF4
avem 0000001
, iar pentru GF2GF4
, 0010100
– care prin „reunire” dau 0011101
(pentru lecțiile în care este implicat GF4
); fiindcă avem un singur bit '0
' între biți '1
', deducem că GF4
are o fereastră (în a doua oră a zilei).
Constituim întâi lista SBC.Rda
, conținând drept chei toate șabloanelor binare posibile, de orare individuale cu ferestre (avem exact 99 de asemenea orare, pentru 7 ore; v. problema L445 din Recreații matematice); pe fiecare dintre aceste chei, SBC
asociază o listă de „mutări corectoare” – după acest model:
SBC[["5"]] <- list(h1 = c(1,1,3), h2 = c(2,4,2)) # "5" este octetul '00000101'
cu semnificația următoare: fereastra din ora a 2-a a unui profesor care are prima și a treia oră se poate „repara” fie mutându-i clasa din ora h1[1]
(=1) în ora h2[1]
(=2), fie mutând clasa din ora h1[2]
(=1) în ora h2[2]
(=4), fie mutând clasa h1[3]
(=3) în ora h2[3]
(=2). După mutare, profesorul va avea fie primele două ore, fie a 2-a și a 3-a, fie a 3-a și a 4-a oră (fără fereastră).
Desigur, mutând o clasă dintr-o coloană orară în alta, dăm eventual peste lecția la clasa respectivă a unui alt profesor…; pentru a evita suprapunerile, trebuie făcute succesiv mai multe interschimbări de clase, între cele două coloane. În [2] am stabilit că interschimbările necesare sunt date de lanțurile Kempe din graful asociat celor două coloane (conținând ca noduri clasele respective, adiacente dacă aparțin aceluiași profesor) și am formulat o funcție move_cls()
care urmărind un asemenea lanț, produce schimbările necesare pe orarul zilei (conform celor două coloane și clasei care i-au fost specificate).
În programul reduce_gaps.R
din [2] se identifică ferestrele din orarul zilei curente și folosind SBC
, se constituie o „listă de mutări corectoare” pentru aceste ferestre; prin move_cls()
se efectuează câte o mutare din lista mutărilor corectoare și în principiu, se înlocuiește orarul inițial cu cel rezultat după mutare, dacă acesta are mai puține ferestre.
Iterând și repetând aceste operații se poate ajunge la o listă de orare zilnice pe care numărul de ferestre este cel mult 5% din totalul lecțiilor pe ziua respectivă. De exemplu, pentru cazul orarului de față (pentru care din mount_hours()
rămăsesem cu 29-33 de ferestre pe zi), ne-au rezultat orare zilnice cu 7, 11, 10, 7 și respectiv 6 ferestre (în total, 41 de ferestre; de observat că pe orarul original de la care am plecat existau în total 55 de ferestre); este drept că timpul total de execuție a programului respectiv a fost de aproape 2 ore (pare satisfăcător, dar pe reduce_gaps.R
sunt posibile anumite îmbunătățiri semnificative).
Redăm aici orarul pe ziua Lu
(cu 204 lecții); coloana suplimentară SB
indică șablonul orelor la profesorii propriu-ziși care au ore proprii și sunt angajați și în cuplaje (am adăugat și o coloană pe care am înregistrat disciplinele principale asociate profesorilor):
prof 1 2 3 4 5 6 7 SB Obiect 1 Is3 5A - - - - - - Istorie 2 En6 7C - - - - - - Engleză 3 Ch3Bi1 - 12D - - - - - Chimie (/Biologie) 4 Mu1De1 - - 10B - - - - Muzică (/Desen) 5 TI2 11B 9C - - - - - TIC 6 La1 11E 9E - - - - - Latină 7 Ge3 12B 9D - - - - - Geografie 8 GF4 10E - - - - - - ***-*-- Ger/Fra (Franceză) 9 Bi4 - - - - 12C 11D - Biologie 10 Fi5 12A 9B - - - - - Fizică 11 Fr1 - - - - 7A 6B - ---***- Franceză 12 De1 8B 8C - - - - - ***---- Desen 13 GF3 6A 8A - - - - - **-***- Ger/Fra (Franceză) 14 EA1 10C 10D - - - - - EdAntrep 15 IT1 - - - 7A 8A 6A - InfoTIC 16 GF2Fr1 - - - 10C - - - Ger/Fra 17 Fi2 8C 6D 11B - - - - Fizică 18 Fi4 - - - - 7B 7C - Fizică 19 GF1GF4 - 12A - - - - - Ger/Fra 20 Re2 5D 10C 12D - - - - Religie 21 ES1 10B 6A 6B - - - - EdSocială 22 IP1 - - 12A 9A 12A 11A - InfoAplicații 23 Ge2 - - - - 10E 8A - Geografie 24 Ro8 - - - - 7C 9B - Română 25 Te1 6B 5A 6A - - - - Tehnologie 26 GF2 6D 5D - - - - - ******- Ger/Fra (Germană) 27 GF2GF3 - - - - - 10E - Ger/Fra 28 Bi3 9A 10B 8A - - - - Biologie 29 Bi2 5C 7B 10A - - - - Biologie 30 Sp3 - - - 6C 8C 7A - Spaniolă 31 ES2 10D 7C - - - - - EdSocială 32 Ro4 - - - 7B 6D 9E - Română 33 Ch3 - - - 11B 12B 8B - -*-***- Chimie 34 Sp2 - - 11C 5C 5B 7B - Spaniolă 35 Is2 9E 11E 7B 5B - - - Istorie 36 Ch2 12C 10A 8C - - - - Chimie 37 Ge1 11D 6B 10C - - - - Geografie 38 TI1 - - - 9B 9E 10D - TIC 39 Lo1 - - 9E 12E 11A 12A - Logică 40 En5 5B 11D 12C 5D - - - Engleză 41 En4 - - 9A 8B 6C 12E - Engleză 42 Ma2 - - 6C 9C 6A 10B - Matematică 43 GF2GF4 - - 12E - 11E - - Ger/Fra 44 GF1GF3 - - - 10A 9C - - Ger/Fra 45 Ro7 - - - 12C 8B 11E - Română 46 Re1 - 6C 11D - 5A 9A - Religie 47 Is1 12E 11A 8B 8A - - - Istorie 48 Ro5 - - - 10D 6B 6C - Română 49 Ro6 11C 9A 5B 5A - - - Română 50 GF1 - - 7C - - 5C - -*****- Ger/Fra (Germană) 51 Ro3 - 12E 12B 6A 12E 9C - Română 52 Ma3 - - 9B 6D 11D 9D - Matematică 53 Ma5 - - 5A 6B 12D 10C - Matematică 54 Ma4 8A 11B 5C 12B - - - Matematică 55 Ma1 10A 5B 7A 11A 9A - - Matematică 56 Ma7 - - - 7C 11C 12C - Matematică 57 Ma6 - - - 12A 5D 8C 8B Matematică 58 En3 7B 12B 10E 11E - - - Engleză 59 Ch1 9B 11C 9D 10E - - - Chimie 60 En2 - - 6D 9D 10D 11B 8C Engleză 61 Fi3 - - - 12D 10A 11C 10E Fizică 62 Fi1 11A 12C 9C - 10B - - Fizică 63 En1 - - 11A 9E 9B 12D - Engleză 64 Sp1 6C 10E 11E 11D - - - Spaniolă 65 Ro1 9D 5C 5D 8C 10C - - Română 66 Ro2 12D 7A - 10B 11B 10A - Română 67 Bi1 9C - 10D 11C 9D 6D - ******- Biologie 68 Mu1 7A 8B - - 5C 5A - ***-**- Muzică
Să observăm de exemplu, că Mu1
are nu două ferestre, cum vedem pe linia 68, ci are doar una – cum arată șablonul care îi este asociat în coloana SB
; ora a 3-a o face împreună cu De1
(v. linia 4); iar De1
are nu două ore cum vedem pe linia 12, ci trei (conform șablonului existent pentru el în coloana SB
). Se vede că apar 4 ferestre la profesori angajați în cuplaje și încă 3 ferestre la ceilalți (pe liniile 46, 62 și 66), toate 7 fiind de câte o singură oră.
Cam la fel stau lucrurile și pe celelalte zile…
reduce_gaps.R
angajează și anumite elemente aleatorii (de exemplu, mutări de clasă la întâmplare), încât repetând execuția obținem orare cu altă dispoziție a orelor (și pe unele zile, chiar cu mai puține ferestre decât cele de pe care am redat mai sus o mostră).
vezi Cărţile mele (de programare)