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

Revenire asupra șabloanelor orare cu ferestre

graf | limbajul R | orar şcolar
2025 jan

Prin programele R din [1] și [2], lecțiile prof|cls sunt repartizate pe zilele de lucru, apoi lecțiile prof|cls|zl sunt alocate pe orele 1:7 ale zilei curente (fără a lua prea mult în seamă ferestrele apărute); subliniem că repartizând întâi pe zile — în loc de a aloca direct pe sloturile "zl|ora" — am urmărit să obținem orare echilibrate: fiecare profesor (și fiecare clasă) are în fiecare zi cam câte un același număr de ore și de regulă, fiecare disciplină apare la clasă cel mult câte o singură dată pe zi.
În final, după obținerea unui orar echilibrat, urmează o etapă mai lungă (totuși, numai circa 6 minute/zi), constând în reducerea pe cât se poate a numărului de ferestre.

Pentru reducerea ferestrelor din orarul zilei curente, întâi se transformă forma normală prof|cls|ora a orarului, în formatul matriceal: fiecărui profesor i se asociază o linie care înregistrează clasele la care trebuie să intre în fiecare oră, sau "-" pentru cazul când în ora curentă este liber; de exemplu:

         1   2   3   4   5   6   7  
    Ch4  7B  -   10D 9F  -   8F  -    # 4 lecții cu două ferestre, cu șablonul 
                                      #    "*-**-*-" sau binar 0x2D = 45

În "matricea-orară" fiecare clasă apare câte o singură dată pe fiecare coloană 1:n, unde n este numărul de ore la clasa respectivă, în ziua curentă. Pentru a elimina o fereastră, se mută o clasă dintr-o coloană orară în alta — ceea ce implică (pentru a păstra unicitatea pe coloană a fiecărei clase) schimburi bilaterale între cele două coloane (aceste schimburi urmează ciclurile Kempe ale grafului valorilor din cele două coloane, adiacente dacă se află pe o aceeași linie; v. [2]).

În prealabil înființasem SBC.Rda, un "dicționar" care asociază fiecărui șablon de orar individual cu ferestre, o listă de cupluri (h1, h2) — vizând această semnificație: mutând clasa din coloana h1 în coloana h2, se asigură eliminarea ferestrei existente inițial în coloana h2. De exemplu, pentru șablonul redat mai sus avem:

SBC[["*-**-*-"]] <- list(h1 = c(1,6), h2 = c(5,2))

Mop fiind matricea-orar inițială, după mutarea move_cls(Mop, 1, 5, "7B") rezultă o nouă matrice-orar, în care Ch4 a scăpat de fereastra existentă inițial în ora a 5-a; în mod implicit, prin mutarea respectivă se vor fi eliminat și alte ferestre sau dimpotrivă, se vor fi produs noi ferestre, la alți profesori — iar problema revine la a căuta asemenea mutări (v. search_better() din [2]) care diminuează numărul total de ferestre.

Dar formulasem SBC în mod manual și expeditiv — suficient inițial, pentru a demara punerea la punct a unui mecanism prin care să înlănțuim cumva mutările indicate de SBC, până ce rezultă un orar cu număr mai mic de ferestre; anume, am înscris într-un fișier liniile SBC[[<șablon>]] = list(h1=c(), h2=c()), apoi am deschis fișierul respectiv într-un editor de text și am completat pe rând câmpurile h1 și h2 (rezumându-ne la cele mai directe mutări pentru eliminarea ferestrelor din șablonul curent; v. [3]).

Ne ocupăm acum de generarea automată a unui dicționar SBC, considerând și alte mutări decât cele înregistrate manual prin [3]:

# cover.R
library(tidyverse)

h2bin <- as.integer(c(1, 2, 4, 8, 16, 32, 64))  # măştile orelor zilei (7 ore)

cnt_hours_gaps <- function(sb) {  # sb: şablon binar 1:127, pe orele 1:7
    bits <- which(bitwAnd(sb, h2bin) > 0)  # rangurile biţilor '1'
    n <- length(bits)  # numărul de ore (lecții la clase)
    g <- bits[n] - bits[1] - n + 1  # numărul de ferestre
    c(n, g)
}

HG <- map_int(1:127, function(S) {
    ng <- cnt_hours_gaps(S) 
    ifelse(ng[2] > 0, S, 0)
})
HG <- HG[HG > 0]  # cele 99 șabloane binare cu ferestre

SBC <- map(HG, function(S) {
    L <-  bitwAnd(S, h2bin) 
    ik <- range(which(L > 0))  # indecșii lecțiilor
    H1 <- H2 <- vector("integer")  # pentru move_cls() din H1 în H2
    for(i in ik[1]:(ik[2]-1)) {
        if(L[i] > 0) {
            for(j in (i+1):ik[2]) {
                if(L[j] == 0) {  # va muta L[i] în fereastră
                    H1 <- c(H1, i)
                    H2 <- c(H2, j)
                }
            }
        } else {
            for(j in (i+1):ik[2]) {
                if(L[j] > 0) {  # va muta L[j] în fereastră
                    H1 <- c(H1, j)
                    H2 <- c(H2, i)
                }
            }
        }
    }
    list(h1 = H1, h2 = H2)
}) %>% setNames(HG) 

În pofida neîncrederii cu care pornisem lucrurile acum vreo trei ani, generarea automată a listei SBC se dovedește a fi mult mai simplu de instituit, față de formularea manuală evocată în [3]… Iar gama de "mutări corectoare" este acum cam de trei ori mai largă; de exemplu, pentru șablonul exemplificat mai sus acum avem:

> SBC[["45"]]  # byte_line(45) este "*-**-*-"
$h1
[1] 1 1 3 4 6 3 4 6  # anterior, h1=(1,6) și h2=(5,2)
$h2
[1] 2 5 2 2 2 5 5 5

În cadrul mecanismului iterativ pe care l-am instituit pentru reducerea treptată a ferestrelor, alegem la întâmplare (aleatoriu), câte una dintre mutările corectoare. Fiind acum, mai multe mutări pe șablon, decât cele "directe" din [3] — ar trebui probabil să le prioritizăm; de exemplu, pentru șablonul "45" de mai sus, par preferabile mutările (1,5) și (6,2) care "repară" direct cele două ferestre, față de o mutare ca (1,2) care ar elimina o singură fereastră…

Deocamdată observăm doar că avem de peste trei ori mai multe mutări decât în [3]:

> SBC %>% unlist() %>% length()
[1] 1404  # mutări corectoare
> load("~/24oct/SBC.Rda")  # SBC din [3] (folosit ultima dată în ~/24oct/)
> SBC %>% unlist() %>% length()
[1] 420

Precizăm și funcția byte_line(), prin care transformăm un șablon, din forma binară în forma literală (mai simplu și elegant, decât prin funcția byte_as_line() din [3]):

> byte_line
function(sb) 
    bitwAnd(sb, h2bin) %>% 
    sapply(., function(b) if(b) "*" else "-") %>%
    paste0(., collapse = "")

Folosind byte_line() putem vizualiza orarele individuale cu ferestre, existente pe matricele-orar ale zilelor:

G <- map_dfr(Zile, function(zi) {
    Oz <- W[[zi]] %>% as.data.frame()  # matricea-orar curentă
    map_dfr(rownames(Oz), function(P) {
        patt <- sum(h2bin[which(! Oz[P, ] %in% "-")])
        if(cnt_holes(patt) > 0)
            Oz[P, ] %>% mutate(prof = P, SB = byte_line(patt))
    }) %>% compact() %>% mutate(zl = zi)
}) %>% remove_rownames() %>% arrange(prof, zl)

De exemplu, pe "Biologie":

> G %>% filter(grepl("Bi", prof))
        1   2   3  4  5  6 7 prof      SB zl
    1 10G 12B  8A  - 6B 6G -  Bi2 ***-**- Jo
    2  9C  8F  6G  - 7D  - -  Bi2 ***-*-- Vi
    3  8B  5D   - 8G 9D 5F -  Bi3 **-***- Mi
    4 10D  5B  6D  - 8C  - -  Bi4 ***-*-- Lu
    5 12D  6D 11A  - 9E  - -  Bi4 ***-*-- Vi

vedem că Bi2 are două zile cu câte o fereastră, iar ceilalți trei au câte o zi cu câte o fereastră; precizăm că datele respective fac parte din orarul rezultat anterior în "Cazul cel mai simplu al problemei orarului" — în care am probat noul set SBC, dar (de data aceasta)… cam fără succes: numărul de ferestre n-a putut fi redus mai mult (poate doar, mai repede) decât atunci când foloseam setul SBC inițial, din [3].
Probabil că avem de modificat cumva și mecanismul search_better(), pentru a beneficia într-adevăr, de această extindere a gamei SBC de "mutări corectoare"… Trebuie văzut întâi (ceea ce nu este mai simplu), dacă n-ar fi suficient să prioritizăm mutările, pentru fiecare șablon — alegând câte o mutare corectoare nu chiar la întâmplare, ci în funcție și de anumite probabilități de corectare asociate acestora.
(… „ Ce e rău și ce e bine. Tu te-ntreabă și socoate; ”)

vezi Cărţile mele (de programare)

docerpro | Prev |