ROC-00/01 Capitolo 1 1 . 4 Il problema dell'albero di copertura di costo minimo: algoritmo di Kruskal, operazioni di Find-Union. Nel corso di Programmazione Matematica è stato già introdotto il problema dell'albero di copertura di costo minimo ed introdotto l'algoritmo di Kruskal, come un particolare algoritmo Greedy. Nel seguito richiamiamo i principali concetti teorici legati agli algoritmi Greedy e mostreremo le strutture di dati più adatte per un'implementazione efficiente dell'algoritmo di Kruskal. 1.4.1 Sistemi di insiemi indipendenti Sia E un insieme finito e F una famiglia di suoi sottoinsiemi (F ⊆ 2 E ), tale che valga la seguente proprietà: (1.4.1) (A ⊆ B) & (B ∈ F) → A ∈ F; Allora, chiameremo F una famiglia di insiemi indipendenti, e (E, F) un sistema di insiemi indipendenti. Un insieme I ∈ F è detto un insieme massimale se e ∈ E \ I → I ∪ {e }∉ F. Data una funzione w: 2E → R, chiameremo problema associato a (E, F) il seguente problema di ottimizzazione: Determinare un insieme I ∈ F massimale, tale che w(I) ≤ w(Y), ∀ Y ∈ F e massimale. Analogamente possiamo far corrispondere a (E, F) un problema di massimo. Il problema dell'albero di copertura di peso minimo Dato il grafo simmetrico pesato (N, A, w) connesso, con: N : insieme dei nodi [ |N| = n] A : insieme degli archi [ |A| = m] w : A → R+ [ pesi] determinare un albero di copertura di peso minimo. L'insieme delle soluzioni ammissibili è: (1.4.2) T = {I ⊆ A: (N, I) è un albero di copertura per (N, A, w)}. F = {I ⊆ A: (N, I) è un grafo parziale privo di cicli di (N, A, w)}. Il peso di un insieme di archi I ⊆ A è dato da: (1.4.3) w(I) = Σ e∈I w(e) Il problema dell'albero di copertura di costo minimo può anche essere descritto come il problema di minimo associato al sistema di insiemi indipendenti (A, F). 1.4.2 Algoritmo greedy Si dice algoritmo "greedy" (vorace) un algoritmo che determina la soluzione attraverso una sequenza di decisioni parziali (localmente ottime), senza mai tornare, modificandole, sulle decisioni prese. Questi algoritmi, in generale, non garantiscono l'ottimalità né l'ammissibilità. È conveniente fare riferimento a problemi rappresentati sotto forma di sistemi di insiemi indipendenti; sia (E, F) un sistema di insiemi indipendenti, e sia data la funzione w : 2E → R. Un algoritmo di tipo Greedy costruisce l'insieme S ∈ F, insieme degli elementi “inseriti” nella soluzione, partendo dall'insieme vuoto ed inserendovi ad ogni passo l'elemento di E più “promettente” fra quelli che non violano l'indipendenza dell'insieme. Nella sua forma più generale un algoritmo di tipo Greedy è descritto dalla procedure GREEDY in figura 1.4.1. X 41 ROC-00/01 Capitolo 1 rappresenta l'insieme degli elementi ancora da valutare, R l'insieme degli elementi “cancellati” dalla soluzione. Procedure GREEDY(E,F,w) begin S := Ø; R := Ø; X := E; repeat e := BEST(X,w); X := X \ {e}; if IND(S,e) then S := S ∪ {e} else R := R ∪ {e} until X = Ø or S is a solution end. greedy Fig. 1.4.1 - La procedura GREEDY L'algoritmo fa uso delle due sottoprocedure BEST e IND: BEST fornisce il migliore elemento di X sulla base di un prefissato criterio. IND è una funzione logica così definita: IND(S, e) = vero, se S ∪ {e }∈ F , falso, altrimenti. Analizziamo la complessità della procedura GREEDY. Siano |E| = n, h(n) la complessità di BEST e k(n) la complessità di IND. La complessità della procedura GREEDY è allora O(n(h(n)+k(n))). Se supponiamo che gli elementi di E vengano ordinati all'inizio, e che quindi ad ogni passo BEST fornisca il primo fra i rimanenti, la complessità diventa O(nlogn + nk(n)). 1.4.3 Matroidi Si consideri il sistema di insiemi indipendenti (E, F) ed il problema ad esso associato con la funzione obiettivo, da minimizzare, definita da (1.4.3). Definizione - Il sistema (E, F) è un matroide se l'algoritmo greedy fornisce la soluzione ottima per il problema associato nel caso in cui si definisca BEST(X,w) = argmin{w(e): e ∈ X} [argmax{w(e): e ∈ X} per problemi di massimizzazione]. Il seguente teorema caratterizza i matroidi e consente in molti casi un loro agevole riconoscimento. Teorema 1.4.1 - Sia (E, F) un sistema di insiemi indipendenti. Le seguenti proposizioni sono equivalenti: 1) (E, F) è un matroide; 2) Se Ip e Ip+1 appartengono ad F, con |Ip| = p e |Ip+1| = p+1, allora esiste un elemento e ∈ Ip+1 \ Ip tale che Ip ∪ {e }∈ F; 3) Se E' ⊆ E e I e J sono sottoinsiemi indipendenti massimali di E', allora è |I| = |J|. Dimostrazione (1) ⇒ (2): Supponiamo che sia vera la (1) e falsa la (2); siano Ip e Ip+1 due insiemi che rendono falsa la (2). Consideriamo il problema di massimo associato a (E, F) con pesi: p+2, p+1, w(e) = 0, se e ∈ I p , se e ∈ I p + 1 \ I p , se e ∉ I p+1 ∪ I p . Osserviamo che Ip non è ottima, infatti: W(Ip+1) ≥ (p+1)2 > p(p+2) = W(Ip). 42 ROC-00/01 Capitolo 1 Tuttavia l'algoritmo greedy fornisce una soluzione che contiene tutti gli elementi di Ip oltre ad eventuali elementi a peso nullo, e ciò contraddice l'ipotesi che (E, F) sia un matroide. (2) ⇒ (3): Sia vera la (2) e siano I e J due insiemi massimali di E', con |I| < |J|. Per la (2) esiste un elemento e appartenente a J, e quindi a E', tale che I ∪ {e }∈ F, ma ciò contraddice la massimalità di I. (3) ⇒ (1): Sia vera la (3) e falsa la (1). Sia w una funzione peso per cui il problema di massimo associato a (E, F) non sia risolto dall'algoritmo greedy. Sia I = {e1,…, ei } la soluzione fornita dall'algoritmo greedy e J = {e'1,…, e'j } la soluzione ottima, con W(I)<W(J). Si assume senza perdita di generalità che sia w(e1)≥…≥w(ei), w(e'1)≥…≥w(e'j). Osserviamo che essendo I e J massimali ed essendo vera la (3) è i = j. Dimostriamo ora per induzione che è w(eh) ≥ w(e'h), h = 1, …, i. Ciò è vero per h = 1 (dalla definizione di algoritmo greedy). Sia h > 1 e w(eh) < w(e'h), con w(ek) ≥ w(e'k), per k < h. Definiamo l'insieme A = {e : w(e) ≥ w(e'h)}; l'insieme {e1, …, eh-1} è massimale per A, altrimenti l'algoritmo greedy non avrebbe scelto eh; ma e'1, …, e'h è un sottoinsieme indipendente di A e ciò contraddice l'ipotesi che la (3) sia vera.◊ Si consideri il grafo non orientato (N, A, w) del problema dell'albero di copertura di costo minimo, e il sistema di insiemi indipendenti (A, F) associato, dove F è definita in (1.4.2). È facile verificare che si tratta di un matroide, infatti gli insiemi massimali hanno tutti la stessa cardinalità: corrispondono agli alberi di copertura del grafo (foreste di copertura, nel caso che il grafo non sia connesso). Un matroide di questo tipo viene detto matroide grafico. Ciò fornisce implicitamente la prova di correttezza dell'algoritmo greedy per il problema dell'albero di copertura di costo minimo. 1.4.4 Algoritmo di Kruskal Riportiamo in figura 1.4.2 l'algoritmo di Kruskal introdotto nel corso di Programmazione Matematica: Procedure Kruskal(G,Fw,S) begin S := Ø; R := Ø; X := Sort(A); repeat Estrai da X il primo arco (u,v); if Component(S,(u,v)) then R := R ∪ {(u,v)} else S := S ∪ {(u,v)} until X = Ø or |S| = n-1 end. Fig. 1.4.2 - La procedura Kruskal La procedura cardine è la Component(S, (u,v)), che deve verificare se i nodi estremi dell'arco (u,v) appartengono alla stessa componente connessa (true) o meno (false). A tal fine introdurremo particolari strutture di dati che permettono la gestione di insiemi disgiunti (gli insiemi di nodi appartenenti alla stessa componente connessa). 1.4.5 Gestione di insiemi disgiunti: le operazioni di Find e Union Sia N = {1, 2, ..., n} un insieme di elementi (per il nostro problema, sono i nodi del grafo, ma verranno indicati come “elementi” per non creare confusione con i “nodi” della struttura di dati), e S1, S2, ..., Sm sottoinsiemi disgiunti di N. Gli insiemi Sj, j = 1,…, m, possono essere rappresentati per mezzo di alberi. 43 ROC-00/01 Capitolo 1 Ogni insieme è rappresentato per mezzo di un albero i cui nodi sono gli elementi dell'insieme stesso. Ogni elemento, x, punta al suo predecessore p(x). L'elemento radice dell'albero punta a se stesso; tale elemento viene anche detto elemento canonico dell'insieme. Un esempio è illustrato in figura 1.4.3. x a bc y t sv d r g h us ef Fig. 1.4.3 - Due insiemi disgiunti di elementi canonici x e y Definiamo le seguenti operazioni: Makeset(x): costruisce un nuovo insieme {x}, con x non appartenente a nessuno degli insiemi già esistenti; viene effettuata ponendo p(x) := x e costa O(1). Find(x): restituisce l'elemento canonico dell'insieme contenente x; comporta il percorrere il cammino da x fino alla radice, e costa O(n). Union(x,y): costruisce un nuovo insieme unione degli insiemi aventi x ed y come elementi canonici (x≠y) in sostituzione dei due vecchi insiemi (che si assumono disgiunti); infine restituisce l'elemento canonico del nuovo insieme; può essere realizzata ponendo p(x) := y con costo O(1). L'operazione è descritta in figura 1.4.4. y x y x Fig. 1.4.4 - L'operazione Union Sia T un albero con radice x, e v un suo nodo. Definiamo: 0, se v è una foglia, altezza(v) = 1+max{altezza(w): p(w)=v}, altrimenti; si ha che altezza(x) ≡ altezza dell'albero T. Un esempio è dato in figura 1.4.5. x e b a g f h altezza (a)= 0 altezza(e)= 2 altezza(x )=3 Fig. 1.4.5 - L'altezza dei nodi di un albero Per rendere basso il costo dell'operazione Find, bisogna operare in modo da ridurre l'altezza degli alberi. Un primo risultato in questa direzione può essere ottenuto effettuando l'unione tra due insiemi in modo che sia l'albero meno alto ad essere collegato a quello più alto e non viceversa. In tal caso, se i due alberi sono di altezza diversa, l'albero ottenuto attraverso l'operazione di Union avrà l'altezza dell'albero più alto. Se i due alberi sono di uguale altezza, l'altezza dell'albero unione sarà più grande di un'unità. 44 ROC-00/01 Capitolo 1 Naturalmente l'uso dell'altezza come criterio per l'operazione di unione non evita la creazione di alberi molto alti se è grande il numero di operazioni da effettuarsi. Una tecnica che consente di limitare consistentemente l'altezza degli alberi è quella del compattamento lungo cammini (path compression), che viene effettuata nella Find. L'idea è quella di ristrutturare l'albero ad ogni chiamata di Find in modo da ridurne l'altezza. Quest'idea è illustrata nella figura 1.4.6, dove è considerato il caso di una chiamata di Find applicata all'elemento a. x x c b a b c a Fig. 1.4.6 - L'operazione di path compression La tecnica di compattamento lungo i cammini rende costoso l'uso dell'altezza per le operazioni di unione. Bisognerebbe dopo ogni chiamata di Find effettuare un aggiornamento dell'altezza, operazione costosa perché implica la visita dell'albero. Si preferisce allora usare invece dell'altezza la funzione rango. Si dice rango(x) una valutazione per eccesso dell'altezza di x, ottenuta ponendo rango(x) = altezza(x) = 0 all'inizio quando gli insiemi sono formati da elementi singoli; quindi successivamente ad ogni operazione di unione tra due alberi di elemento canonico x e y, il rango viene aggiornato, come per l'altezza: se rango(x) < rango(y) si pone x come figlio di y e il rango di y non cambia; se invece rango(x) = rango(y), oltre a porre x come figlio di y, il rango di y cresce di un'unità. Praticamente l'effetto è quello di trascurare il compattamento delle operazioni Find. Esaminiamo ora gli effetti in termini di complessità computazionale di questo modo di realizzare l'unione tra insiemi. Utilizzando il rango, è possibile valutare la complessità nel caso peggiore di ciascuna operazione FIND, come si ricava dalle seguenti proposizioni. Proposizione 1.4.1 - Il numero di nodi nell'albero di radice x è almeno 2rango(x). Dimostrazione: La prova è per induzione sul numero delle operazioni di unione. La tesi è certamente vera dopo la prima operazione di unione. Assumiamo che sia ancora vera dopo la k-esima, e si effettui la (k+1)-esima unione sugli alberi di radice x ed y. Se rango(x) ≠ rango(y), la proprietà continua ad essere vera dopo l'operazione. Se rango(x) = rango(y), il nuovo albero avrà rango pari a rango(x)+1, e conterrà almeno 2rango(x) + 2rango(y) = 2rango(x)+1 nodi, e quindi la proprietà continua ad essere vera. La tesi è così dimostrata.◊ Proposizione 1.4.2 - La complessità di una operazione Find è O(logn). Dimostrazione: Il costo computazionale di una operazione Find(x) è dato dalla lunghezza del cammino dal nodo x alla radice r dell'albero a cui x appartiene, e dunque è limitato da altezza(r). Dalla proposizione 1.4.1 risulta evidente che ogni nodo ha rango al più logn, e quindi per ogni radice r si ha altezza(r) ≤ rango(r) ≤ logn ; dunque nel caso pessimo Find avrà complessità O(logn).◊ 45 ROC-00/01 Capitolo 1 Le procedure Makeset, Find e Union sono descritte in figura 1.4.7. Procedure Makeset(i) begin p[i] := i; rango[i] := 0 end. Procedure Find(i) begin j := i; while j ≠ p[j] do j := p[j]; while i ≠ j do begin w := p[i]; p[i] := j; i := w end; return j end. Procedure Union(x,y) begin if rango[x] > rango[y] then begin w := y; y := x; x := w end; else if rango[x] = rango[y] then rango[y] := rango[y]+1; p[x] := y end. Fig. 1.4.7 - Le procedure Makeset, Find e Union Analizziamo ora una sequenza di n operazioni Union, n operazioni Makeset ed m operazioni Find. Ricordando che la complessità di Union e Makeset è O(1), si ha una complessità (n + mlogn). Si può tuttavia dimostrare la seguente: Osservazione - Una sequenza di n Makeset, m≥n Find, e n-1 Union, ha una complessità O(mα(m,n)) [Θ(m α(m,n))], con α(m,n) inversa della funzione di Ackerman [ per n<216 è α(m,n) ≤ 3; agli effetti pratici possiamo assumere α(m,n) una costante ≤ 4]. 1.4.6 La procedura Component e la complessità di Kruskal Una descrizione formale della procedura Component è data in figura 1.4.8. Procedure Component(S,(u,v)) begin x := Find(u); y := Find(v); if x = y then return true else begin Union(x,y); return false end end. Fig. 1.4.8 - La procedura Component La procedura Kruskal è ora completamente descritta. Si ricorda che l'inizializzazione prevede una sequenza di n chiamate a Makeset, una per ogni nodo, per inizializzare la foresta non contenente archi. Analizziamo la complessità di Kruskal. L'inizializzazione prevede il preordinamento degli archi, secondo i loro costi. Tale operazione ha complessità O(mlogn), che è la complessità di Kruskal. Seguendo l'osservazione, se gli archi sono già ordinati secondo i loro costi e se si adotta l'operazione di path compression, la complessità è O(mα(m,n)). Comunque, nel caso peggiore si faranno 2m chiamate alla procedura Find; anche senza la fase di path compression, Find ha complessità O(logn). Pertanto, Kruskal ha complessità O(mlogn), anche nel caso non si effettuino le path compression. 46 ROC-00/01 Capitolo 1 1 . 5 . Il problema di flusso di costo minimo “multicommodity” Quando in una rete sono assegnati flussi relativi a beni (commodities) diversi, che condividono l'utilizzo degli archi, si parla di problemi di flusso di costo minimo di tipo “multicommodity”. Sia k il numero di beni che si spostano lungo una rete G = (N, A). Le offerte e le domande del generico bene (o commodity) h sono date dal vettore b(h), h = 1,…, k. Anche i costi e le capacità associate agli archi del grafo sono definite per ciascuna commodity; indicheremo (h) (h) pertanto con c(h) = [c(h) ij ] e u = [u ij ] rispettivamente il vettore dei costi e quello delle capacità relative alla commodity h, h = 1,…, k. Inoltre, ad ogni arco (i,j) ∈ A è associata una capacità globale uij dell'arco che limita la quantità globale di beni che possono attraversare l'arco stesso. (h) Il vettore dei flussi x(h) = [x(h) ij ], h = 1,…, k, fornisce, per ogni arco (i,j) ∈ A, il flusso x ij della commodity h che attraversa tale arco. Ricordando che E = [eik] indica la matrice di incidenza del grafo G, il problema di flusso di costo minimo di tipo multicommodity può essere formulato nel seguente modo: k (P) Min ∑ c(h)x(h) h=1 Ex(h) = b(h) (1.5.1) k ∑ x(h) h = 1,…, k ≤u h=1 0 ≤ x(h) ≤ u(h) h = 1,…, k In forma estesa: k (P) Min ∑ ∑ c (h)ij x(h)ij (i,j)∈A h=1 x (h) ij (j,i)∈BS(i) ∑ (1.5.2) 0≤ - ∑ x (h)ij = b(h)i i ∈ N, ∑ (i,j) ∈ A (i,j)∈FS(i) k x (h) ij h=1 (h) x(h) ij ≤ x ij ≤ uij h = 1,…, k (i,j) ∈ A, h = 1,…, k Si noti che il doppio tipo di capacità sugli archi, sia quelle per singola commodity che quella globale, permettono di escludere che particolari archi siano utilizzati per una specifica commodity; basta infatti porre a zero le capacità di questi archi relative alla commodity in questione. Ciò permette di non dover trattare grafi diversi, uno per commodity. Inoltre, un caso particolare, sovente presente in problemi concreti, è che il costo unitario di attraversamento degli archi possa essere uguale per tutte le commodities. Studiamo il duale di (1.5.1), detto problema di potenziale multicommodity. Le variabili (h) (h) sono un vettore di potenziale π(h) = [π(h) i ] e un vettore µ = [µ ij ] per ogni commodity h, h = 1,…, k, oltre al vettore di variabili duali λ = [λij] associate ai vincoli globali di capacità: k (D) Max ∑ h=1 (1.5.3) π(h)b(h) π(h)E k - ∑ µ(h)u(h) - λu h=1 - µ(h) - λ ≤ c(h) µ(h) ≥0, λ ≥ 0 47 h = 1,…, k h = 1,…, k ROC-00/01 Capitolo 1 o, in forma estesa: k (D) ∑ ∑ Max (h) π (h) i b i - h=1 i∈N k ∑ ∑ µ (h)ij u(h) ij - ∑ λ ij u ij h=1 (i,j)∈A (h) π(h) j -π i (1.5.4) - µ(h) ij (i,j)∈A - λij ≤ c(h) ij µ(h) ij ≥ 0 λij ≥ 0 (i,j) ∈ A, h = 1,…, k (i,j) ∈ A , h = 1,…, k (i,j) ∈ A La intera matrice A dei coefficienti in cui si considerano esplicitamente i vincoli globali di capacità, ha una forma a blocchi diagonali, ciascuno formato dalla matrice E e da una sequenza di matrici identità che legano i vari flussi di commodity relativi allo stesso arco. Pertanto A è formata da nk+m righe e mk colonne: 0 E … 0 A= … … … … 0 0 … E E 0 … 0 I I … I La matrice A non garantisce la proprietà di integralità; è cioè possibile, a causa dei vincoli globali di capacità, che il flusso ottimo trovato mediante algoritmi del Simplesso per la Programmazione Lineare (o loro adattamento alla particolare struttura della matrice A) forniscano soluzioni ottime frazionarie. Se non si hanno vincoli sull'integralità dei flussi, il problema (1.5.1) è risolto. Altrimenti, il costo globale della soluzione frazionaria fornisce solo un'approssimazione per difetto (lower bound) del valore ottimo della funzione obiettivo di (1.5.1) quando si impone che i flussi siano valori interi. È possibile trasformare una soluzione ottima frazionaria in una soluzione ammissibile intera, se esiste. Il costo di tale soluzione ammissibile fornisce una valutazione per eccesso (upper bound) del costo della soluzione ottima intera. Per analizzare alcune proprietà del problema (1.5.1) quando aggiungiamo il vincolo di interezza, studiamo il suo Rilassamento Lagrangiano ottenuto rilassando i vincoli globali di capacità, i cui moltiplicatori di Lagrange sono le variabili duali λ = [λij]: (P(λ)) φ(λ) = Min k ∑ k c(h)x(h) h=1 + λ ( ∑ x (h) - u ) h=1 Ex(h) = bh) (1.5.5) 0≤ x(h) ≤ u(h) h = 1,…, k h = 1,…, k Il problema da risolvere, detto duale generalizzato o duale Lagrangiano, è: (1.5.6) (DL) Max φ(λ) λ≥0 La funzione obiettivo di (1.5.5), in forma estesa, è: k ∑ ∑ (i,j)∈A h=1 k (h) c (h) ij x ij + ∑ λ ij ( ∑ x (h)ij - uij); (i,j)∈A h=1 48 ROC-00/01 Capitolo 1 raccogliendo assieme le parti, si ha k ∑ ∑ (c (h)ij + λij)x(h)ij ∑ λ iju ij. - (i,j)∈A h=1 (i,j)∈A _ Una volta assegnati dei valori λ ≥ 0 ai moltiplicatori di Lagrange, il _problema (1.5.5), _ parametrico in λ , può essere separato _in k problemi indipendenti (P h (λ )), per ciascuna commodity h, h = 1,…, k, in cui la parte λu è omessa in quanto un valore costante: _ (Ph(λ)) _ φh(λ) = Min _ (h) ∑ c (h) ij x ij (i,j)∈A Ex(h) = bh) 0 ≤ x(h) ≤ u(h) (1.5.7) _ _ dove c (h) = c + λ ij ij, per ogni (i,j) ∈ A. Il problema (1.5.7) è un problema di flusso di costo ij minimo e può essere risolto applicando uno degli algoritmi studiati (ad esempio il Simplesso per flussi o il Simplesso per potenziali) fornendo peraltro una soluzione ottima intera se le capacità _ degli archi e le domande/offerte dei nodi sono valori interi. Indichiamo comunque con x (h) la soluzione ottima di (1.5.7). _ Una volta risolti i k problemi (Ph(λ)), h = 1,…, k, si ha il seguente valore della funzione obiettivo del problema (1.5.6): (1.5.8) k _ _ φ(λ) = ∑ φ h ( λ) = h=1 k ∑ ∑ _ _ (h) c (h) ij x ij − (i,j)∈A h=1 ∑ _ λ iju ij (i,j)∈A _ Il valore φ(λ) è un lower bound del valore ottimo della funzione obiettivo di (DL) che, a sua volta è un lower bound del valore ottimo della funzione obiettivo di (1.5.1). Dalla (1.5.8) si hanno anche preziose informazioni (in termini di subgradiente della funzione poliedrale φ(λ)) _ che permettono di attivare un processo iterativo per costruire un nuovo vettore di valori_λ' per cui trovare, risolvendo i problemi di flusso (1.5.7), dei nuovi _ _ valori ottimi di flusso x '(h), h = 1,…, k, che forniscono un valore φ(λ') > φ(λ ), e quindi un migliore lower bound. La ricerca del migliore (più alto) lower bound e del migliore (più basso) upper bound permette di restringere il margine di incertezza sul valore ottimo della funzione obiettivo e di valutare l'errore relativo che si causa se si adotta la soluzione associata al miglior upper bound come soluzione approssimata del problema. I metodi di ottimizzazione basati sui rilassamenti, sull'utilizzo di lower e upper bounds sia per la ricerca della soluzione ottima che per la valutazione della bontà di una soluzione ammissibile sono l'oggetto dei corsi di Ottimizzazione Combinatoria e Ottimizzazione Combinatoria: Laboratorio. 49