Visual Basic Simple
Schedulare task secondo pesi e cammini minimi
Sincronizza Indice
Sincronizza Indice
Scarica il progetto
Scarica il progetto
Scarica il testo dell'articolo
Testo dell'articolo
Stampa l'articolo
Stampa l'articolo

I metodi di scheduling, in congiunzione con le tipiche tecniche di ottimizzazione, possono risolvere efficientemente problemi complessi, ove l'attuazione di un solo metodo non sarebbe sufficiente.

e tecniche di scheduling possono essere combinate con altre tecniche di ottimizzazione, al fine di risolvere complessi problemi che necessitano di una riformulazione e quindi di un'applicazione di più metodi per pervenire alla soluzione.
In generale, la rappresentazione logica della teoria dei grafi può essere ottimamente combinata con quella dello scheduling, ottenendo una semplificazione della trattazione del problema.
Ad esempio, la tipica rappresentazione di un problema di schedulazione è quella della rappresentazione dei carichi con delle barre, in cui viene rappresentato il tempo processore a disposizione, o orizzonte di processamento, mentre le singole barre rappresentano di fatto i task da schedulare. Tali informazioni possono essere riportate all'interno di un apposito grafo nel quale i rami rappresentano i task, i pesi definiti sui singoli rami rappresentano il tempo di processamento di ogni task ed ogni singolo nodo rappresenta il cambio di sequenza da un task ad un altro. Quindi, la numerazione dei singoli nodi è pari alla numerazione dei singoli task. Si consideri allora la Fig.1, in cui devono essere processati 6 task, con il vincolo di assenza di preemption; per tale vincolo i possibili cambi di sequenza possono essere 5. Le quantità intere riportate sui singoli rami sono i singoli tempi di processamento dei task. In base a tale rappresentazione è possibile formulare una vasta tipologia di problemi.

Figura 1
Fig.1: La tipica struttura a grafo orientato
pesato: essa può rappresentare una sequenza di
task ed i relativi cambi.


MINIMIZZARE IL MAKESPAN

I diversi algoritmi di schedulazione sono preposti alla determinazione di una o più misura di performance, cioé minimizzare la massima lateness, o minimizzare i costi/tempi di set-up, o ancora minimizzare il makespan. Con il termine makespan si indica la massima lunghezza della schedula; in questo caso si desidera minimizzare la lunghezza totale della sequenza ammissibile di task, cioè si desidera ottenere una sequenza la cui lunghezza sia minima.
Per ottenerer una simile misura di prestazione, si può utilizzare un algoritmo tipico della teoria dello scheduling oppure si può tentare di adattare alcune tipiche tecniche di scansione dei grafi per pervenire a tale misura. L'algoritmo che maggiormente può adattarsi al problema è quello del minimo albero ricoprente o minimo spanning tree.
Sia dato un grafo orientato pesato, la cui definizione è la seguente:

"Si definisce grafo pesato orientato una struttura G(N, A), con N insieme dei nodi e cardinalità | N |, ed A insieme degli archi e cardinalità | A |. L'insieme degli archi, A, è caratterizzato da un orientamento a nodo origine ad uno destinazione i,j".

Quindi il problema del minimo spanning tree può essere così enunciato:

"Sia dato un grafo orientato pesato G(N, A), con un insieme di interi senza segno definito sugli archi, allora l'obiettivo è quello di determinare una struttura che connetta tutti i nodi del grafo con archi di peso minimo".

Un albero ricoprente di costo minimo è una struttura che un unico nodo origine, detto radice, ed una serie di ramificazioni; il cammino in esso individuabile è composto da tutti i nodi del grafo origine, ma in una sequenza tale per cui la somma dei pesi sui rami sia minima.
Risulta chiaro, allora, che la determinazione di un minimo spanning tree induce alla determinazione del makespan minimo.
Un tipico algoritmo per la determinazione dell'albero minimo ricoprente è quello di Kruskal; di seguito ne sono riportate le parti più salienti:

while (archi_trovati < n-1 && prossimo_arco <= numero_archi)
//si consideri il prossimo arco
from = archi[prossimo_arco].end1;
to = archi[prossimo_arco].end2;

//determina l'assenza di cicli
group_from = group[from];
group_to = group[to];

if(group_from != group_to)
               // non esistono cicli
  lunghezza_totale = lunghezza_totale + archi[prossimo_arco].lunghezza;
  archi_trovati = archi_trovati+1;
  //aggiornamento del numero degli archi che entrano
  //nella soluzione, e degli elementi che compongono l'albero
  for(j = 1; j <= n; j++) {
    if(group[j] = = group_to)
      group[j] = group_from;
    prossimo_arco = prossimo_arco + 1; }
  esiste_albero = archi_trovati + 1;

Nell'algoritmo vengono considerate alcune strutture dati (vettori) per la conservazione dei nodi e dei pesi intermedi. L'utilizzazione congiunta di queste metodologie può essere di valido ausilio ad esempio per uscire da un labirinto.


LABIRINTI E PROCESSORI

Si consideri un tipico labirinto, all'interno del quale vengono predisposti degli oggetti, che possono essere dei job da assemblare al fine di ottenere un oggetto finito. Si è certi che l'insieme degli oggetti sia disposto lungo il percorso che unisce l'ingresso e l'uscita del labirinto, paragonabilmente ad una traccia per l'orientamento nel labirinto. Tali oggetti godono, inoltre, delle seguenti proprietà:

  • sono di peso diverso;
  • sono dotati di una diversa fase di lavorazione e di un differente tempo di processamento;
  • non è assegnata una rigida sequenza di assemblaggio finale.

Allora il problema può essere così formulato:
un operatore deve assemblare un prodotto, le cui parti componenti sono i task, la cui struttura è stata precedentemente descritta, impiegando un tempo minimo. Si tratta cioè di determinare una sequenza di task che minimizzi il tempo di completamento.
I task però sono disposti all'interno di un labirinto, con coordinate che congiungono entrata ed uscita. Inoltre, l'operatore è dotato di un contenitore per la raccolta dei task la cui capienza non è sufficiente a trasportare tutti i task con un unico prelievo.
Risulta quindi chiaro che la risoluzione ottima del problema si ottiene per combinazione di una serie di metodologie ed algoritmi noti, quali ad esempio i grafi, il Knapsack e gli algoritmi di scheduling.

Figura 2
Fig.2: Il labirinto è la struttura che meglio si
presta alla modellizzazione a grafi. Il cammino in
esso può essere evidenziato da una serie di
marcature/task.


LA RISOLUZIONE

Dalla formulazione del problema, riportata nella precedente sezione, è chiaro che è indispensabile richiamare alcuni concetti fondamentali.
Si consideri il labirinto riportato in Fig.2. Tale schema può essere rappresentato secondo una struttura a Grafo orientato. Passo fondamentale è quello dell'individuazione degli elementi all'interno di un labirinto. L'individuazione di tali elementi si può ottenere seguendo almeno due tecniche:

  1. determinazione del percorso;
  2. individuazione degli elementi secondo una tecnica di backtacking.

Se si sceglie il primo approccio, si dovrà utilizzare un metodo di ricerca del percorso: una tecnica simile è di seguito riportata:

BOOL Cammino(const Grafo &G, int v, int w, Lista &Cammino)
{
    // utilizzare una struttura a pila per conservare i cammini intermedi
    Pila P;
    int u, x;
    array prec (G.n())
    for (u = 1; u <= prec.n(); u++)
        // inizializzazione dell'array di precedenti
        prec[u] = 0;
    P.InserisciPila(v);
    prec[v] = v;
    BOOL Trovato = False;
    G.PrimoAdiac(v);
    while (!P.PilaVuota() && !Trovato)
    {
        x = P.CimaPila();
        u = G.CorrAdiac(x);
        while (G.Nodo(u) && (prec[u] != 0))
            u = G.succAdiac(u);
        if (!G.Nodo(u))
            P.FuoriPila();
        else
        {
            P.InserisciPila(u);
            G.SuccAdiac(x);
            G.PrimoAdiac(u);
            if (u = = w)
                Trovato = True;
        }
        if (Trovato)
            do
            {
                cammino.InserisciInLista(w);
                u = w;
                w = prec[w];
            }
            return Trovato;
    }
}

Come è possibile notare, la strategia è quella di determinare le coordinate di ogni singolo nodo del cammino all'interno del labirinto/grafo. Le singole coordinate vengono poi conservate all'interno di una lista che sarà disponibile per il prosieguo della procedura.
All'interno della lista, non sono soltanto riportate le coordinate dei singoli pezzi ma anche i relativi pesi e le informazioni inerenti alla fase di lavorazione. Individuato il cammino all'interno del labirinto, si dovranno operare a questo punto, una serie di scelte inerenti le estrazioni da effettuare, cioè: da quale pezzo incominciare la successiva fase di lavorazione? In questo caso sarà opportuno operare una semplificazione del problema di partenza, ciò in quanto una sua trattazione rigida imporrebbe la costruzione di una serie di vincoli aggiuntivi, elevando ulteriormente il costo computazionale della soluzione. Tale necessità è determinata dalle seguenti ragioni:

  • La strategia di scelta dei pezzi dovrebbe essere effettuata in relazione alla sequenza ottima che minimizza il makespan, ai pesi degli oggetti, ai tempi di processamento.

La soluzione più logica sembrerebbe quella di applicare subito e direttamente un tipico algoritmo della bisaccia o knapsack per poi impiegare, banalmente l'algoritmo della schedulazione. Questo modo di procedere sarebbe però fuorviante, ciò in quanto l'applicazione a priori dell'algoritmo di Knapsack restituirebbe solo una sequenza di relazione ad alcuni parametri; probabilmente non sarebbe la sequenza che ottimizza la schedula.
Allora la logica più efficace è quella di utilizzare prima un algoritmo in modo fittizio che restituisca la sequenza di maespan. Utilizzando questo vincolo si potrà allora costruire il sacco, che non sarà quello ottimo, ma che sarà ammissibile per ottenere il risultato finale, ovvero la schedula ottima. Si immagini di voler processare gli n task su m macchine parallele, con vincolo di preemption, al fine di minimizzare il makespan. Quindi sia:



il massimo tempo di completamento (in questo caso può anche essere interpretato come l'istante in cui tutte le macchine hanno completato il proprio lavoro).
Mediante il meccanismo dell'interruzione, ogni job può essere processato su più macchine, purché non simultaneamente, in altri termini, il job i potrebbe iniziare il suo processamento sulla macchina i per continuare, successivamente, sulla stessa macchina o su qualunque altra macchina, eventualmente con ulteriori interruzioni. Si consideri allora:

= frazione di job j processata sulla macchina i

ed il seguente vincolo:



Con tale definizione delle variabili, il tempo totale di lavoro della i-esima macchina si può esprimere nel seguente modo:



il problema quindi può essere formulato come:



Si noti che non è consentita la preemption, il problema potrà essere formulato in maniera analoga, ma bisognerà cambiare il vincolo sulla variabile decisionale: sarà quindi = {0,1}, con variabili binarie ed il vincolo esprimerà il fatto che ogni job può essere processato da una sola macchina. È possibile dimostrare la seguente affermazione per il problema precedentemente formulato:

  • Se esiste una soluzione ammissibile in corrispondenza della quale tutti i vincoli di diseguaglianza siano soddisfatti come uguaglianze; sia x^, allora tale soluzione sarà ottima (x^=x*).

Infatti, rispetto a x^, tutte le macchine hanno lo stesso carico di lavoro e terminano nel medesimo istante ; se per assurdo la soluzione ottima fosse x*<>x^, con carico di lavoro sbilanciato sulle macchine, sarebbe sempre possibile ridurre il valore del makespan, ridistribuendo opportunamente tra le macchine parte dei jobs assegnati a quella più carica. Ciò sarebbe però in contraddizione con l'ottimalità di x*.

La soluzione ottima del problema si ottiene sommando tra loro i vincoli sulle macchine e tenendo conto dei vincoli sui jobs, ottenendo così la seguente posizione:

Il vettore delle variabili decisionali x^ fornirà quindi la sequenza ottima per minimizzare il .
Trovato tale valore, sarà possibile passare alla determinazione del Knapsack. Il problema, però, è che l'operatore non ha a disposizione un sacco di capienza sufficiente al trasporto di tutti gli oggetti in un unico viaggio, quindi l'azione sarà quella di entrare ogni volta nel labirinto con l'aiuto della lista delle coordinate. Il vettore delle sequenze di processamento, all'interno, non potrà prelevare gli oggetti così come gli vengono proposti, ma dovrà estrarli secondo una procedura Knapsack, tenendo conto che non potranno essere trasportati tutti gli oggetti in un unico viaggio. Il problema, come già più volte evidenziato, può essere formulato nel seguente modo. Si hanno a disposizione n oggetti, ciascuno con valore e costo ; nella fattispecie, tali parametri possono essere rivisti come tempo di processamento e peso. In questo caso il problema può essere espresso come massimizzazione del valore, nel rispetto della sequenza makespan e minimizzazione del peso, visto che dovranno essere effettuati diversi viaggi. Se C è la disponibilità massima del trasporto, si avrà:

quindi in corrispondenza di tutte le variabili che assumono valore 1; allora, il corrispondente oggetto entrerà nella soluzione e quindi nel sacco.

Figura 3
Fig.3: L'intervallo O - T rappresenta l'orizzonte
temporale in cui avviene il processamento
dell'intera sequenza. Tale risorsa di tempo viene
solitamente suddivisa tra i vari task.

Per una fattiva implementazione del problema di Knapsack, si consideri una struttura di oggetti, con i campi che riportano le informazioni fondamentali quali l'indice, indispensabile per la sequenza, il peso, le coordinate, il tempo.
Si avrà:

Soluzione Knapsack (array oggetti, int C)
{
    int n = oggetti.n();
    array Fk(C + 1);
    for (int j = 1; j <= C + 1; j++)
        Fk[j] = 0;
    matrix xk(C + 1, n);        // allocare una matrice di booleani
    for (j = 1; j <= C + 1; j++)
        for (int i = 1; i <= n; i++)
            xk[j][i] = FALSO;
    for (int k = 1; k <= n - 1; k++)
        for (int j = C + 1; j > oggetti[i].c; j--)
            if (Fk[j - oggetti[k].c] + oggetti[k].v > Fk[j])
            {
                Fk[j] = Fk[j - oggetti[k].c + oggetti[k].v;
                for (int i = 1; i <= k - 1; i++)
                    xk[j][i] = xk[j - oggetti[k].c][i];
                xk[j][k] = TRUE;
            }
    // calcola la soluzione
    Soluzione S;
    if (oggetti[n].c <= c && Fk[C + 1 - oggetti[n].c + oggetti[n].v > Fk[C + 1])
    // scegli gli elementi soluzione che non violano
    // il vincolo di capacità
}

Perciò mediante l'applicazione di tale algoritmo, sarà possibile determinare, di volta in volta, gli oggetti che devono essere prelevati dal labirinto e posti in fase di processamento. L'azione dinamica si può espletare nel seguente modo:

  • determinare il cammino e la lista di oggetti;
  • implementare il makespan e determinare la sequenza di schedulazione;
  • entrare nel labirinto e con la procedura di knapsack riempire il sacco, uscire ed avviare il processamento; mentre ciò avviene, rientrare nel labirinto e prelevare un altro sottoinsieme di oggetti fintantochè tutti gli oggetti non sono stati rimossi dal labirinto stesso.

Un'ulteriore complicazione può essere data dall'inserimento nel vincolo di rispetto della sequenza, e che negli intervalli di tempo in cui l'operatore sta asportando gli oggetti dal labirinto, almeno un processamento sia in atto; cioè l'insieme delle macchine non deve mai essere inoperoso. Chiaramente tale imposizione complica notevolmente la soluzione del problema.


CONCLUSIONI

Le tecniche di ottimizzazione congiunte spesso vengono utilizzate per risolvere problemi complessi che non ricadono in una branca specifica dell'ottimizzazione.
L'esempio riportato nelle precedenti sezioni può fattivamente prestarsi come implementazione di un gioco. La logica fondamentale è quella di riuscire a customizzare le diverse tecniche e a fonderle per ottenere la soluzione ottima di un particolare problema.

NOTE:

Cammino minimo
La procedura cammino ha complessità nell'ordine . Tale complessità è sostanzialmente determinata dalla scansione delle strrutture aggiuntive al fine di ottenere la migliore sequenza di visita degli elementi. Nel caso invece di determinazione di un cammino ottimale, la computazione della complessità, sia essa spaziale che temporale, deve ovviamente variare, tenendo conto cioè dei vincoli del problema e della dimensione dell'input.

Complessità di knapsack
La complessità spaziale dell'algoritmo di knapsack è in generale, dell'ordine di , mentre quella temporale è nell'ordine di , nei casi peggiore e medio, mentre sarà nell'ordine nel caso migliore; ciò in quanto si dovrà fare riferimento ai tre cicli di for innestati, con quello esterno che deve essere eseguito per n volte. Le complessità espresse sono però falsamente polinomiali. Infatti C fa parte dell'input e lo spazio richiesto per la sua memorizzazione è: .

Assegnamento generalizzato
Eliminando il vincolo di preemption dalla determinazione del makespan, si ottiene un tipico problema di assegnamento generalizzato, così detto perché più jobs possono essere assegnati ad una stessa macchina, come risulta dal primo gruppo di vincoli caratteristici della formulazione del problema, mancando la corrispondenza 1-1 come nel problema di assegnamento classico. Il problema di assegnamento generalizzato è un tipico problema NP-HARD.

L'albero ricoprente
La complessità dell'algoritmo per la determinazione del minimo albero ricoprente è dell'ordine di , ciò in quanto per ogni arco sono richieste n operazioni per aggiornare il gruppo di connessioni. Soo questa parte dell'algoritmo ha complessità dell'ordine di . Il ciclo innescato per tutti gli archi richiede O(E) operazioni. Pertanto la complessità effettiva dipende sostanzialmente dalla densità degli archi; quindi se esistono pochi archi , l'aggiornamento del gruppo delle connessioni è il fattore predominante. Se invece il grafo è denso sarà il primo termine a dominare.

Tratto da: IoProgrammo - Marzo 1999
16 Dicembre 2000

Scarica il progetto
Scarica il progetto
Scarica il testo dell'articolo
Scarica il testo dell'articolo
Stampa l'articolo
Stampa l'articolo
Torna all'indice degli articoli