momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Încă un experiment, pe orarul unei școli (X)

limbajul R | orar şcolar
2023 nov

[1] Problema orarului școlar echilibrat și limbajul R

[2] V. Bazon - Orare școlare echilibrate și limbajul R https://books.google.ro/books?id=aWrDEAAAQBAJ

[3] Transformarea fișierului PGN în obiect de date (I)

Condiționarea față de tuplaje, a montării orelor pe lecții

Un tuplaj implică doi sau trei profesori și două sau trei clase (sau „grupe” ori „noi clase”, care partiționează ansamblul elevilor claselor inițiale), astfel încât lecțiile prof|cls respective decurg în paralel, într-o aceeași oră 1:7 a zilei curente.
Înainte ziceam „pereche sau triplet de clase cuplate” – subînțelegând doar, profesorii implicați; astfel, 10EF este o pereche de clase cuplate, chiar pe două discipline ("Desen/Muzică" și "Germană") – dar de fapt, avem de-a face cu două tuplaje distincte.

Amintim că prin cuplaj înțelegem două lecții în care doi profesori intră într-o aceeași oră a zilei, la o aceeași clasă – lucrând independent pe câte o grupă de elevi ai acelei clase.

Anterior, pentru a repartiza pe zile cele 928 de lecții prof|cls existente aici, am specificat separat tuplajele, sub forma unui „orar prealabil” – alegând manual ziua care să-l conțină pe fiecare (de exemplu tuplajului "Ds1Mz1 /10B 10D/" i-am repartizat ziua Mi, iar tuplajului "LG3LG1LG4 /11A 11B 11D/" i-am repartizat zilele Lu și Vi); prin programul "by_days.R" am obținut o repartizare pe zile a lecțiilor rămase în afara tuplajelor, pe care apoi am întregit-o cu „orarul” pe zile prealabil constituit, al tuplajelor.

În schimb, pentru repartizarea pe ore 1:7 a lecțiilor zilei curente (prin funcția mount_hours()), am ignorat complet tuplajele (încât de exemplu, în ziua Mi clasele 10B și 10D au apărut în ore diferite la profesorii Ds1 și Mz1); apoi, pentru a aranja tuplajele în câte o aceeași oră a zilei au fost necesare unele intervenții interactive (prin funcția move_cls() din programul "reduce_gaps.R", ușor modificată). Dar nu-i cazul de a amâna pe cine știe când, ideea firească de a modifica mount_hours() pentru a ține seama din capul locului, și de tuplajele zilei respective…

Să ne amintim (iarăși) cum procedează mount_hours()
Pentru fiecare clasă – într-o ordine aleatorie „preferențială” (urmărind cumva ordinea crescătoare a coeficienților „betweenness”) – se apelează funcția internă mountHtoCls(); aceasta etichetează lecțiile clasei cu o permutare de 1:N (N≥4 fiind numărul de ore/zi la clasa respectivă) cu proprietatea principală că nu are valori comune cu etichetări făcute anterior altor clase, pe profesorii comuni cu cei din clasa curentă (altfel, un profesor ar trebui să intre în aceeași oră la două clase); se verifică deasemenea ca prin etichetarea cu ore aplicată pe clasa curentă, niciunul dintre profesori să nu capete (coroborând cu alocările anterioare) mai mult de două ferestre (dar fără a socoti pe cele induse eventual de cuplaje) – facilitând astfel, reducerea ulterioară a totalului de ferestre din orarul rezultat în final.

Acum, separăm tuplajele pe zile și adăugăm câte un câmp "ora", ale cărui valori urmează să fie determinate – rămâne de văzut cum – în mount_hours():

P23 <- readRDS("cls23_days.RDS") %>% rename(zl = zi) %>%
       mutate(prof = as.character(prof), ora = 0L) %>% 
       split(.$zl) %>% 
       map(function(S) S %>% select(prof, cls, ora)) %>%
       setNames(Zile)
saveRDS(P23, "P23i.RDS")  # Pr1Pr2 | Cl1 Cl2 [Cl3] | ora=0

De exemplu, pentru o zi mai complicată – cu aceleași clase în două tuplaje:

> P23[["Vi"]]
            prof         cls ora
    5     LG2LG1     10A 10B   0  # LG2/10A și LG1/10B, într-o aceeași oră
    10    LG2LG1     12E 12F   0
    15    LG2LG3     11E 11F   0
    20 LG3LG1LG4 11A 11B 11D   0
    24    LG3LG4     10E 10F   0
    27    Ds1Mz1     10E 10F   0

Să presupunem că, pentru Vi, s-a ajuns la alocarea lecțiilor clasei 10E și nu s-a întâlnit anterior clasa 10F – atunci, pentru cele două tuplaje /10E 10F/ câmpul $ora din P23[["Vi"]] are valoarea 0; în acest caz, odată cu etichetarea în curs a lecțiilor clasei 10E, se înscriu celor două tuplaje, în câmpul $ora, valorile cu care au fost etichetate lecțiile la profesorii LG3 și respectiv, Ds1 – să zicem pentru exemplu, 2 și 5.
Când se va ajunge ulterior la alocarea lecțiilor celeilalte clase din tuplaj, 10F – va trebui ca lui LG4 să i se aloce aceeași oră (=2) ca anterior lui LG3 la 10E, citind-o din câmpul $ora al tuplajului respectiv; la fel, lui Mz1 va trebui să i se aloce ora 5, înregistrată anterior în tuplajul cu Ds1 pe clasele /10E 10F/.

Dacă etichetarea lecțiilor clasei curente nu reușește, pentru nicio permutare de ore, atunci se anulează toate alocările făcute (inclusiv câmpul $ora din tuplaje), se reordonează aleatoriu clasele și se relansează procesul, în noua ordine a claselor.

Pentru a instrumenta procedeul descris mai sus, constituim întâi o listă în care specificăm pe fiecare zi, clasele care fac parte din tuplaje în acea zi:

zTC <- map(Zile, function(z) {
    P23[[z]] %>% pull(cls) %>%
    lapply(function(tk) strsplit(tk, " ")[[1]]) %>%
    unlist() %>% as.vector() %>% unique()
}) %>% setNames(Zile) 
saveRDS(zTC, "zTC.rds")  # clasele din tuplaje, pe fiecare zi
>  zTC[["Vi"]]
 [1] "10A" "10B" "12E" "12F" "11E" "11F" "11A" "11B" "11D" "10E" "10F"

Mai avem nevoie de două funcții, pentru lucrul intern; în vederea modificării, copiem definiția funcției mount_hours() într-un alt fișier, "mount_h1.R" și înainte de funcția internă mountHtoCls() specificăm:

    P23 <- P23i[[zi]]
    ztc <- zTC[[zi]]
    ph_in_tuplaj <- function(Q) {  # Q fiind o clasă din tuplaje
        p23 <- z23 %>% filter(grepl(Q, cls))  # unde z23 = P23[[zi]] curent
        map_dfr(1:nrow(p23), function(n) {
            i <- match(Q, strsplit(p23[n, 2], " ")[[1]])
            vpr <- strsplit(p23[n, 1], "(?<=.{3})", perl=TRUE)[[1]]
            data.frame(prof = vpr[i], ora = p23[n, 3])
        })
    }
    set_h_tuplaj <- function(Q, P, h) {
        if(h %in% z23[which(grepl(Q, z23[,2])), 3]) 
            return(FALSE)
        z23[which(grepl(Q, z23[,2]) & grepl(P, z23[,1])), 3] <<- h
        TRUE
    }    

Prin ph_in_tuplaj() (…nu avem încă, denumiri mai potrivite) se va obține un „tabel” conținând profesorii de la clasa indicată și valoarea curentă a câmpului $ora, din tuplajele în care apare acea clasă; astfel, reluând exemplul de mai sus – după ce la clasa 10E am fixat valorile 2 și 5 pentru LG3 și Ds1, când se va ajunge la clasa 10F vom avea:

> ph_in_tuplaj("10F")
      prof ora
    1  LG4   2  # la 10F, LG4 trebuie alocat în aceeași oră cu LG3 de la 10E
    2  Mz1   5  # iar Mz1 în aceeași oră cu Ds1 de la 10E

Înainte de a apela mountHtoCls() pentru clasa următoare, mount_hours() va verifica dacă aceasta este implicată în setul claselor din tuplaje ztc – caz în care va stoca în pth rezultatul returnat de ph_in_tuplaj():

        z23 <- P23  # (re)inițializează setul tuplajelor ($ora devine 0)
        for(K in lstCls) { 
            if(K %in% ztc)
                pth <- ph_in_tuplaj(K)  # pentru clasa curentă (dintr-un tuplaj)
            else {pth <- NULL}
            W <- mountHtoCls(Z[[K]])  # încearcă un orar pentru clasa curentă

Cealaltă funcție, set_h_tuplaj() actualizează câmpul $ora din tuplajul care conține clasa curentă și profesorul căruia tocmai i s-a alocat o anumită oră; dar dacă sunt mai multe tuplaje care conțin clasa curentă și la unul dintre acestea câmpul $ora conține chiar valoarea cu care se intenționa actualizarea acestuia – atunci doar se returnează FALSE (evitând ca două tuplaje distincte să se suprapună într-o aceeași oră).
De observat că a trebuit să folosim "<<-" (operatorul de „atribuire globală”), dat fiind că setul tuplajelor z23 este extern funcției.

În funcția mountHtoCls(), înainte de a căuta permutarea de ore cu care să eticheteze lecțiile clasei curente, se extrage din pth vectorul profesorilor clasei implicați în tuplaje:

        dP <- ""
        if(!is.null(pth)) 
            dP <- pth$prof  # profesorii din tuplaje, ai clasei curente

O permutare de ore este de fapt un octet în care poziția fiecărui bit 1 indică a câta oră se alocă lecției, în ordinea dată de câmpul $prof din setul lecțiilor clasei pe ziua respectivă. Când mountHtoCls() găsește o permutare bis convenabilă (nu se suprapune cu alocările făcute anterior altor clase, pe profesorii clasei curente), se verifică valorile curente din câmpul $ora al tuplajelor în care apar eventual, profesori ai clasei curente (procedând cum am ilustrat mai sus pentru tuplajele pe /10E 10F/ din ziua de Vi):

                if(P %in% dP) {
                   h <- pth[pth[, 1] == P, 2]  # $ora este câmpul 2
                   if(h > 0) {  # de la o clasă întâlnită anterioar
                        bs <- h2bin[h]
                        if(bitwAnd(bis[k], bs) == 0L) {
                            knd <- FALSE  # suprapunere cu alocarea anterioară
                            break  # trebuie căutată o altă permutare
                        }
                   } else {  # înscrie $ora, dacă nu coincide cu 
                             #    a altui tuplaj pe clasa curentă
                        if(! set_h_tuplaj(Q$cls[1], P, 
                                          which(h2bin == bis[k]))) {
                            knd <- FALSE 
                            break
                        }
                   }    
                }
                if(! knd) next  # trece la o altă permutare de ore

Anterior, după ce reușea alocarea lecțiilor pentru toate clasele, mount_hours() verifica în final dacă totalul ferestrelor „pe orizontale” este mai mic decât un anumit procent din totalul lecțiilor zilei curente (căutând un alt orar, în caz contrar).
Dar acum, în "mount_h1.R", ne interesează mai mult ca participanții la tuplaje să nu aibă ferestre (fiindcă în procedura de reducere a numărului de ferestre pe care o intenționăm, orarul stabilit pentru tuplaje nu va suferi modificări); însă verificăm numai pentru tuplajele cu profesori LG* (fiindcă Ds1 și Mz1 sunt în tuplaj doar câte o singură dată pe zi):

        if(succ) {  # dacă s-au alocat pe ore toate lecțiile
            if(sum(unlist(lapply(bith[grepl("LG", names(bith))], 
                                 cnt_holes))) < 1)
                break  # încheie dacă cei din tuplaje nu au ferestre
        }

Formulăm acum un program care să folosescă funcția mount_hours() (modificată cum am evidențiat mai sus):

# to_hours.R
rm(list = ls())  # elimină obiectele din sesiunea precedentă
library(tidyverse)

Zile <- c("Lu", "Ma", "Mi", "Jo", "Vi")
load("Tw12.Rda")  # [1] "Tw1"  "Tw2"
load("BTW.rda") # "BTW_prof", "BTW_cls" ("betweenness", pe zile)
load("lPrTz.Rda") # "lTz1", "lTz2" (dependențele cuplajelor, pe zile)
PRMh <- readRDS("lstPerm47.RDS") # matricele de permutări cu 4:7 elemente
P23i <- readRDS("P23i.RDS")  # tuplaje, pe fiecare zi
zTC <- readRDS("zTC.rds")  # clasele angajate în tuplaje, pe fiecare zi
h2bin <- as.integer(c(1, 2, 4, 8, 16, 32, 64)) # măștile binare ale orelor 1:7

# funcții pe care le-am formulat anterior pe aici:
#         cnt_holes(),  hourly_matrix(),  prnTime()

source("mount_h1.R") # încarcă funcția mount_hours() (modificată)
LSS <- readRDS("R12_ldz.RDS") # dicționar zi ==> lecțiile prof|cls ale zilei
ORR <- vector("list", 5) # vom salva aici orarul constituit zilei curente
names(ORR) <- Zile

prnTime('\n') # afișează timpul inițial
for(zi in Zile) {
    ORR[[zi]] <- mount_hours(LSS[[zi]], zi) # orar prof|cls|ora al zilei
    cat(zi, " ");  prnTime('\n') # timpul curent
}
saveRDS(ORR, file = "ORR_3n.RDS")

Pentru a verifica eventual lucrurile, putem adăuga următoarea secvență, prin care constituim un fișier-text cu matricele orare (ordonate după profesori) și tuplajele zilelor:

sink(file="ORR_3n.txt", append=TRUE)
for(zi in Zile) {
    orz <- ORR[[zi]] %>% hourly_matrix()  # matricea orară a zilei
    orz <- orz[order(rownames(orz)), ]  # ordonăm liniile după profesori
    print(P23i[[zi]])  # tuplajele zilei curente
    print.table(orz)
}
sink()

Subliniem că duratele, orarele furnizate, numărul de ferestre și distribuția acestora – depind de momentul lansării programului, de calitatea coeficienților "betweenness" (…n-am mai considerat și conenexiunile care apar între profesorii și între clasele din câte un același tuplaj) și desigur (dar mai puțin important) de calculatorul și sistemul de operare folosit.
Redăm un caz fericit (execuția a luat nu 30-60 minute, ci doar câteva minute):

vb@Home:~/23nov$ R -q
> source("to_hours.R")
    06:03:45 
    33  Lu  06:04:13 
    34  Ma  06:04:54 
    33  Mi  06:05:08 
    35  Jo  06:08:02 
    36  Vi  06:11:06   # saveRDS(ORR, "ORR_3n.RDS")

Redăm (reorganizând și marcând după caz) datele rezultate în "ORR_3n.txt" pentru ziua Vi:

        prof         cls ora
5     LG2LG1     10A 10B   0  # tuplaj alocat în ora 3
10    LG2LG1     12E 12F   0  # 6
15    LG2LG3     11E 11F   0  # 5
20 LG3LG1LG4 11A 11B 11D   0  # 4
24    LG3LG4     10E 10F   0  # 6
27    Ds1Mz1     10E 10F   0  # 4

              1   2   3   4   5   6              1   2   3   4   5   6
        Bl1   -   - 11F 12F 11C 10A     LI2LI3   -  9A   -   -   -   -
        Bl2   -   -  9B 11E  7B  8B        LI3 11B   - 11A   -   -   -
        Bl3 11D  6A 10D   -   -   -        LR1  9A 12B   -   - 11B 11A
        CC1 11E 11E   -  5B   - 11F        LR2   -  9B  5A   -  5B  6B
        Ch1  9C 10C   - 10D 10B   -        LR3   - 11F   -  9F 10C 10D
        Ch2  9D   -  7A   -  8B  9B        LR4  7A  8B   -  8A   -  7B
        Ch4   -   -   -   - 12C   -        LR5   -   - 12C  9D 10F  6A
        Ds1   -  9F  6B 10E   -   -        LR6 12F   -   - 12E  9E 11E
        ET1  5B  5A   -   -   -   -        LR7 11C 12D   -  9C   -   -
        Fl1   -  9D   - 12B 12E  9E        LR8 10F 12E   -   -   -   -
        Fz1 10D  9E 10F 11C  9D 10B        Lt1  7B   -   - 11F   -   -
        Fz2   -   - 12D 12C  9A   -        Mt1   -   -  5B  7A  6A  5A
        Fz3  9F 12A   -   -   -   -        Mt2 11A   -  9A 12A 10A  9D
        Fz4  8B  7B   -   -   -   -        Mt3 10B 11B 12B  9B   -   -
        Gg1   -   -  9E 12D 10E  7A        Mt4 12D   - 12A   - 11D 12C
        Gg2   -   - 11C  8B  9F 11D        Mt5 10E 10D 11E 10C   -   -
        Is1 10C 10F   -   -  5A  8A        Mt6  6B  8A  9C   -   -   -
        Is2  9E   - 11B   -  9B   -        Mz1   -   -  8B 10F 12F   -
        Is3   -  5B 11D   - 12D   -        Ps1 10A 10E 10C   -   -   -
        LE1  6A 11D  8A   -  9C   -        Ps2 12E 12F   -   -   -   -
        LE2 12B   -  9F 10B 11A   -        Rl1   -  9C  9D   -  8A  9A
        LE3   -  6B 12E 10A   -   -        Rl2   - 11C  6A  6B   -   -
        LE4 12A 12C   -   -   -   -        Sp1   - 11A   -  7B 12B  9C
        LG1   -   - 10B 11B  6B 12F        Sp2  8A  7A   -  6A   - 11B
        LG2   -   - 10A  5A 11E 12E        Sp3 11F   -   -   -   -   -
        LG3   -   -  7B 11A 11F 10E        Tc1 12C   -   -  9E 10D 11C
        LG4   -   - 10E 11D  7A 10F        Tc2   - 10A 12F  9A   -   -
        LI1  9B 10B   -   -   -   -     Tc2LI1   -   -   -   - 12A   -
        LI2  5A   -   -   -   -   -                                   

Am marcat aici, tuplajele și se verifică ușor că ele au fost corect repartizate (10E și 10F cad în aceeași oră la Ds1 și Mz1; apoi 11ABD sunt într-o aceeași oră la LG3, LG1 și LG4 ș.a.m.d.) – și încă, fără ferestre. Dar au rezultat în total, peste 30 de ferestre…

Reducerea numărului de ferestre

Pentru a reduce numărul de ferestre folosim programul anterior reduce_gaps_1.R, dar fără funcțiile (un)taboo_hours() – acestea trebuiau formulate „manual” pentru fiecare matrice orară în parte, notând coloanele orare corespunzătoare tuplajelor.

Generalitatea necesară se obține totuși ușor.
Mai întâi (cam la fel ca în funcția anterioară decompose23()) separăm profesorii și clasele aferente lor din setul tuplajelor pe fiecare zi P23i.RDS:

PCt <- map(P23i, function(TP) {
    map_dfr(1:nrow(TP), function(i) {
        vpr <- strsplit(TP[i, 1], "(?<=.{3})", perl=TRUE)[[1]]
        vcl <- strsplit(TP[i, 2], " ")[[1]]
        data.frame(prof = vpr, cls = vcl) 
    })
}) %>% setNames(Zile)
> PCt[["Lu"]]
       prof cls
    1   Ds1  9A
    2   Mz1  9B
    3   LG2 10A
    4   LG1 10B
    5   LG2 12E
    6   LG1 12F
    7   LG2 11E
    8   LG3 11F
    9   LG3 11A
    10  LG1 11B
    11  LG4 11D

Valorile unice din coloanele tabelului PCt[[zi]] reprezintă profesorii și respectiv clasele, care intră în tuplaje (clasele din tuplaje, pe fiecare zi, le obținusem pe altă cale mai sus, în zTC.rds – de care acum ne putem lipsi).

Să întregim PCt[[zi]], adăugând pe fiecare linie ora alocată în orarul ORR[[zi]] lecției respective:

ORR <- readRDS("ORR_3n.RDS") 
PCth <- map(Zile, function(zi) {
    pt <- PCt[[zi]]$prof %>% unique()  # profesorii din tuplajele zilei
    ct <- PCt[[zi]]$cls %>% unique()  # clasele din tuplajele zilei
    ORR[[zi]] %>% filter(prof %in% pt & cls %in% ct) %>%
        arrange(ora, cls) %>%
        inner_join(PCt[[zi]])  # exclude liniile "false"
}) %>% setNames(Zile)
saveRDS(PCth, "PCH_tup.RDS")  # zi --> prof|cls|ora (la tuplaje)
> PCth[["Lu"]]
       prof cls ora
    1   LG2 11E   3
    2   LG3 11F   3
    3   Ds1  9A   3
    4   Mz1  9B   3
    5   LG2 10A   4
    6   LG1 10B   4
    7   LG2 12E   5
    8   LG1 12F   5
    9   LG3 11A   6
    10  LG1 11B   6
    11  LG4 11D   6

Am reținut din orarul zilei ORR[[zi]], numai liniile aferente profesorilor și claselor din tuplajele acelei zile; dar între acestea există eventual și linii „false” – de exemplu, linia Mz1 12F 5 trebuie exclusă, fiindcă ea nu corespunde vreunui tuplaj (chit că și Mz1 și 12F intră fiecare în câte un tuplaj); inner_join() ne-a asigurat comod eliminarea acelor linii care nu se află (pe primele două câmpuri) în setul tuplajelor zilei.

Acum, orarele cu număr redus de ferestre se obțin adăugând în reduce_gaps_1.R cu modificările notate mai sus, următorul „program principal”:

ORR <- readRDS("ORR_3n.RDS")
PCH <- readRDS("PCH_tup.RDS")  # orarele tuplajelor, pe fiecare zi 
W <- list()  # pentru orarele cu număr redus de ferestre
for(zi in Zile) {
    set_globals(zi)
    OZ <- ORR[[zi]] %>% hourly_matrix()  # matricea orară a zilei
    NG1 <- count_gaps(OZ) # numărul inițial de ferestre
    Good <- 6  # numărul sperat de ferestre
    PCh <- PCH[[zi]]  # orele tuplajelor, pe ziua curentă
    for(i in 1:nrow(PCh))  # maschează tuplajele ('X'), în matricea orară
        OZ[PCh$prof[i], PCh$ora[i]] <- "X"
    Cmb <- combn(ncol(OZ), 2)  # combinările de 7 (6 sau 5) luate câte două
    prnTime(paste0(" (", NG1, " ferestre)\n")) 
    orr <- search_better(OZ)  # declanșează procesul de reducere a ferestrelor
    for(i in 1:nrow(PCh))  # demaschează tuplajele
        orr[PCh$prof[i], PCh$ora[i]] <- PCh$cls[i]  
    W[[zi]] <- orr[order(rownames(orr)), ]  
    prnTime("\n") 
}

Redăm cursul uneia dintre execuțiile programului:

11:54:39  (31 ferestre)
28  26  24  22  21  19  18  17  16  15  14  13  12  11  10  9  8  * 8  * 8  7  * 7  * 7  12:08:00 
12:08:00  (43 ferestre)
41  39  37  36  35  33  32  31  30  29  28  27  26  25  24  23  22  21  20  19  18  17  16  15  14  13  12  11  10  9  * 9  * 9  * 9  * 9  12:22:58 
12:22:58  (35 ferestre)
34  33  32  31  30  29  28  27  26  25  24  23  22  21  19  18  17  16  15  14  13  12  11  10  * 10  * 10  * 10  * 10  12:36:30 
12:36:31  (39 ferestre)
38  37  36  35  34  33  32  31  30  29  28  27  26  25  24  23  22  21  20  19  18  17  16  15  14  13  12  11  10  * 10  * 10  * 10  * 10  12:50:45 
12:50:46  (35 ferestre)
34  33  32  31  30  29  28  27  26  25  24  23  22  21  20  19  18  17  16  15  13  12  * 12  * 12  * 12  * 12  13:10:42 
> saveRDS(W, "W3.RDS")

Extrăgând din mai multe (trei) execuții, orarul câte unei zile – am obținut un orar care are în total 47 de ferestre (5.06% față de totalul 928 al lecțiilor):

W <- list(Lu = W3[["Lu"]], Ma = W1[["Ma"]], Mi = W1[["Mi"]], 
          Jo = W2[["Jo"]], Vi = W3[["Vi"]])
sapply(Zile, function(zi) {set_globals(zi); count_gaps(W[[zi]])})
    Lu Ma Mi Jo Vi 
     7  9  9  9 12  # total, 47 ferestre

Redăm orarul pe Vi, marcând iarăși tuplajele (dar și ferestrele rămase, față de orarul inițial pe care l-am redat mai sus):

              1   2   3   4   5   6              1   2   3   4   5   6
        Bl1 11F 12F   -   - 11C 10A     LI2LI3   -  9A   -   -   -   -
        Bl2 11E  9B   -  8B  7B   -        LI3 11A   - 11B   -   -   -
        Bl3 11D  6A 10D   -   -   -        LR1   - 11B 11A  9A 12B   -
        CC1  5B 11E 11F 11E   -   -        LR2  6B  5B   -   -  5A  9B
        Ch1   -   -  9C 10C 10D 10B        LR3 10D  9F   - 11F 10C   -
        Ch2  7A  8B  9D  9B   -   -        LR4  8B  7A  8A  7B   -   -
        Ch4 12C   -   -   -   -   -        LR5 10F  9D   -  6A 12C   -
        Ds1   -   -   - 10E  9F  6B        LR6   -   - 12F  9E 12E 11E
        ET1   -   -   -   -  5B  5A        LR7 11C  9C 12D   -   -   -
        Fl1  9D 12B  9E 12E   -   -        LR8 12E 10F   -   -   -   -
        Fz1 10B 10D 10F 11C  9D  9E        Lt1  7B 11F   -   -   -   -
        Fz2   -   -   - 12D  9A 12C        Mt1   -   -  5B  5A  6A  7A
        Fz3  9F 12A   -   -   -   -        Mt2  9A 11A   - 10A 12A  9D
        Fz4   -   -   -   -  8B  7B        Mt3   -   - 12B 10B  9B 11B
        Gg1  9E 12D  7A   - 10E   -        Mt4 12D 11D 12C 12A   -   -
        Gg2   -   -  8B  9F 11D 11C        Mt5 10C 10E 11E 10D   -   -
        Is1  8A 10C  5A   - 10F   -        Mt6   -   -   -  6B  8A  9C
        Is2 11B  9E  9B   -   -   -        Mz1   -   -   - 10F 12F  8B
        Is3   -   -   -  5B 12D 11D        Ps1 10E 10A 10C   -   -   -
        LE1   -   - 11D  8A  9C  6A        Ps2 12F 12E   -   -   -   -
        LE2   -   -  9F 12B 10B 11A        Rl1  9C  8A  9A  9D   -   -
        LE3 10A  6B 12E   -   -   -        Rl2  6A 11C  6B   -   -   -
        LE4 12A 12C   -   -   -   -        Sp1 12B  7B   -  9C 11A   -
        LG1   -   - 10B 11B  6B 12F        Sp2   -   -  6A  7A 11B  8A
        LG2   -  5A 10A   - 11E 12E        Sp3   -   -   -   -   - 11F
        LG3   -   -  7B 11A 11F 10E        Tc1   -   - 11C 12C  9E 10D
        LG4   -   - 10E 11D  7A 10F        Tc2   -   -   - 12F 10A  9A
        LI1  9B 10B   -   -   -   -     Tc2LI1   -   - 12A   -   -   -
        LI2  5A   -   -   -   -   -                                   

Bineînțeles că tuplajele au rămas în pozițiile inițiale, fiindcă move_cls() le-a găsit marcate cu "X" și a refuzat mutarea claselor respective într-o altă coloană orară; inițial, pe tuplaje nu existau ferestre – dar clasa 5A de la LG2 nu intră în tuplaje, deci a putut fi mutată (pentru a mai reduce din ferestre), astfel că LG2 a căpătat totuși o fereastră.
Cele 12 ferestre n-au mai putut fi reduse; din păcate, la doi profesori (Bl1 și LR2) au rezultat câte două ferestre (legate; la alți 8 profesori avem câte o singură fereastră).
Să mai observăm că profesorii care intră în cuplaje obișnuite (LI* și Tc2) nu au ferestre: ora 2 la LI3 este acoperită de LI2LI3 (și continuă ora 1 de la LI2), iar cuplajul Tc2LI1 are oră la 12A între orele lui LI1 și cele ale lui Tc2.

Repartiția ferestrelor

Probabil că este avantajos ca orarul final să conțină multe ferestre (ne amintim că orarul original de la care am plecat, conținea 121 de ferestre): s-ar evita reproșurile obișnuite (păi… mai toți au ferestre!). Dar dacă orarul conține un număr rezonabil (mic) de ferestre, atunci devine importantă repartiția acestora – fiind de dorit ca un același profesor să nu aibă una sau două ferestre pe zi, de prea multe ori.

Având lista W a matricelor orare pe fiecare zi, putem formula într-o manieră generală tabelarea liniilor corespunzătoare profesorilor neangajați în cuplaje, care au una sau două ferestre într-o zi sau alta (pentru a investiga apoi, tabelul respectiv):

ntp <- union(names(Tw1), names(Tw2))
lgp <- map(Zile, function(zi) {
    moz <- W[[zi]] %>% as.data.frame() %>% 
        mutate(prof = rownames(.)) %>% 
        relocate(prof, .before = 1) %>% `rownames<-`(NULL)
    if(ncol(moz) < 8)  # rbind() cere un același număr de coloane
        moz <- moz %>% mutate('7' = '-')
    map_dfr(1:nrow(moz), function(i) {
        np <- rownames(moz)[i]
        if(! moz[i, 1] %in% ntp) {
            rc <- paste0(moz[i, 2:ncol(moz)], collapse="")
            if(grepl("\\w+-{1,2}\\w+", rc))  # liniile cu ferestre
                return(moz[i, ])
        }
    })
}) %>% setNames(Zile)
GPS <- Reduce(rbind, lgp)

Repartiția pe zile a profesorilor (neangajați în cuplaje) cu una sau două ferestre pe zi:

> (tg <- table(GPS$prof))
Bl1 Bl2 Bl3 CC1 Ch1 Ch2 Ds1 Fz1 Gg1 Is1 Is3 LE3 LG1 LG2 LG4 LR1 LR2 LR3 LR5 Mt1 
  3   1   1   1   1   1   2   2   3   1   1   2   1   3   1   1   2   3   1   1 
Mt2 Mt4 Rl1 Sp1 Sp2 Tc1 
  2   1   1   2   1   1 

Să verificăm ferestrele pentru profesorii care au 3 zile cu ferestre:

> GPS %>% filter(prof %in% names(tg[tg == 3])) %>% arrange(prof)
       prof   1   2   3   4   5   6 7
    1   Bl1 12A 12C 11F   - 12B   - -
    2   Bl1  9A 12C 11C   - 10A   - -
    3   Bl1 11F 12F   -   - 11C 10A -
    4   Gg1 11E  5B   -  5A  9D   - -
    5   Gg1 10A  9B   -   -  9A  9E -
    6   Gg1  9E 12D  7A   - 10E   - -
    7   LG2  8B  9E   - 10C  8A  9A -
    8   LG2  5B  9E   -  9A 10C   - -
    9   LG2   -  5A 10A   - 11E 12E -
    10  LR3  9F 11F   - 10E 10C   - -
    11  LR3 10E  9F   -   - 10D 10C -
    12  LR3 10D  9F   - 11F 10C   - -

Constatăm că trei profesori (Bl1, Gg1 și LR3) au câte 4 ferestre (o zi cu două ferestre și două zile cu câte una); reproșurile din partea acestora sunt iminente…

Dar pentru fiecare, numărul de ferestre trebuie raportat cumva, la numărul de ore:

> LSS <- readRDS("R12i.RDS")  # cele 928 lecții prof|cls|zl
> table(LSS$prof)[names(tg)]
Bl1 Bl2 Bl3 CC1 Ch1 Ch2 Ds1 Fz1 Gg1 Is1 Is3 LE3 LG1 LG2 LG4 LR1 LR2 LR3 LR5 Mt1 
 19  20  18  20  18  18  19  28  20  20  16  19  21  23  21  20  19  18  18  23 
Mt2 Mt4 Rl1 Sp1 Sp2 Tc1 
 21  18  21  21  21  21

Cum era de așteptat (având în vedere procedurile pe care le-am folosit pentru repartizarea lecțiilor pe zile și pe orele zilei), profesorii care au puține ore nu au ferestre; media numărului de ore mean(table(LSS$prof)) este de 14 ore și cum vedem mai sus, au ferestre numai unii cu măcar 4 ore mai mult ca media (exceptând un caz: Is3 are numai 2 ore peste medie).

Este de așteptat ca pe majoritatea orarelor care pot rezulta prin procedurile noastre (cu sub 50 de ferestre pe orarul final), să avem cam aceleași proprietăți ale distribuției ferestrelor, evidențiate mai sus (dar eventual, cu alți doi-trei profesori îndreptățiți să fie nemulțumiți).
În orice caz – este improbabil să putem obține un orar fără ferestre, care să fie echilibrat pe zile, pe clase și pe profesori și în care toate clasele să aibă program normal (de la prima oră a zilei) și să nu aibă într-o aceeași zi două ore cu un același profesor (exceptând situația când pe disciplina respectivă, clasa are mai mult de 5 ore pe săptămână).
Este drept pe de altă parte, că procedura folosită pentru reducerea numărului de ferestre este susceptibilă de îmbunătățiri și poate fi încă optimizată…

vezi Cărţile mele (de programare)

docerpro | Prev | Next