Quaderni di Informatica Strutture ed Algoritmi Luigino Calvi I.I.S. Negrelli-Forcellini - Feltre 2014 ii L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Indice 1 Sequenze 1.1 LE COMPOSIZIONI . . . . . . . . . . . . . . . . . 1.2 LE SEQUENZE . . . . . . . . . . . . . . . . . . . . 1.3 RAPPRESENTAZIONI DELLE SEQUENZE . . . 1.4 OPERAZIONI SULLE SEQUENZE . . . . . . . . 1.5 CLASSIFICAZIONE DELLE SEQUENZE . . . . 1.6 LE STRUTTURE . . . . . . . . . . . . . . . . . . . 1.7 LE SEQUENZE COMPOSTE . . . . . . . . . . . . 1.8 OPERAZIONI FUNZIONALI E PROCEDURALI 1.9 COSTRUZIONE DI SEQUENZE . . . . . . . . . . 1.10 ELABORAZIONE DI SEQUENZE . . . . . . . . . 1.11 ESERCIZI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 3 5 6 8 9 9 10 11 14 16 2 Liste 2.1 LE LISTE . . . . . . . . . . . . . . . . . . . . 2.2 ELABORAZIONE DI LISTE . . . . . . . . 2.3 ALGORITMI SULLE LISTE SEMPLICI . 2.4 ALGORITMI SULLE LISTE COMPOSTE 2.5 ESERCIZI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 22 23 25 28 30 . . . . . . . 35 36 36 37 39 40 42 44 3 Array 3.1 GLI ARRAY . . . . . . . . . . . . . . . . . . 3.2 ELABORAZIONE DI ARRAY . . . . . . . 3.3 ARRAY E LISTE . . . . . . . . . . . . . . . 3.4 LE MATRICI . . . . . . . . . . . . . . . . . . 3.5 ELABORAZIONE DI MATRICI . . . . . . 3.6 SOLUZIONE DI UN SISTEMA LINEARE 3.7 ESERCIZI . . . . . . . . . . . . . . . . . . . . iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . iv 4 Complessità Computazionale 4.1 LA COMPLESSITÀ COMPUTAZIONALE 4.2 OPERAZIONE DOMINANTE . . . . . . . 4.3 DIMENSIONE DI UN PROBLEMA . . . . 4.4 COMPLESSITÀ DI UN ALGORITMO . . 4.5 COMPLESSITÀ DI UN PROBLEMA . . . 4.6 COMPLESSITÀ ASINTOTICA . . . . . . . 4.7 ESEMPI VARI . . . . . . . . . . . . . . . . . 4.8 PROBLEMI INTRATTABILI . . . . . . . . 4.9 ESERCIZI . . . . . . . . . . . . . . . . . . . . INDICE . . . . . . . . . 53 54 55 56 56 57 57 58 60 63 5 Ricerca 5.1 ALGORITMI DI RICERCA . . . . . . . . . . . . . . . . . . . . . 5.2 ESERCIZI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67 67 70 6 Ordinamento 6.1 ALGORITMI DI ORDINAMENTO . . . . . . . . . . . . . . . . 6.2 ESERCIZI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 71 72 7 Alberi 7.1 ALBERI . . . . . . . . . . . . . . . . . . . . . . . 7.2 RAPPRESENTAZIONI DEGLI ALBERI . . . 7.3 METODI DI VISITA DEGLI ALBERI . . . . 7.4 ALBERI BINARI . . . . . . . . . . . . . . . . . 7.5 OPERAZIONI SUGLI ALBERI BINARI . . . 7.6 VISITA DEGLI ALBERI BINARI . . . . . . . 7.7 RAPPRESENTAZIONE DI ALBERI BINARI 7.8 ALBERI BINARI DI RICERCA . . . . . . . . 7.9 ESERCIZI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 76 77 79 80 80 81 82 83 85 . . . . . . . . . 89 90 91 92 92 94 94 94 95 95 8 Grafi 8.1 DEFINIZIONI . . . . . . . . . . . . . . . . . 8.2 TIPOLOGIE DI GRAFI . . . . . . . . . . . 8.3 OPERAZIONI SUI GRAFI . . . . . . . . . 8.4 RAPPRRESENTAZIONI DEI GRAFI . . . 8.5 PROBLEMI SUI GRAFI . . . . . . . . . . . 8.5.1 Problema del cammino minimo . . . 8.5.2 Problema del collegamento minimo . 8.5.3 Problema del commesso viaggiatore 8.5.4 Problema dell’isomorfismo fra grafi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 v INDICE 8.6 8.5.5 Problema della planarità di un grafo . . . . . . . . . . . . ESERCIZI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95 96 L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 vi INDICE L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 1 Sequenze I programmi per computer usualmente operano su tabelle d’informazioni. Nella maggioranza dei casi queste tabelle non sono semplicemente masse amorfe di valori numerici; esse coinvolgono importanti relazioni strutturali fra i dati elementi. D. Knuth, The Art of Computer Programming Uno dei concetti fondamentali e trasversali a tutta l’informatica, e non solo all’informatica, si fonda sul meccanismo della composizione di elementi di base, raggruppandoli in un’unica entità che può essere considerata, denominata e manipolata in modo unitario. Questo artificio sottende il concetto di costruire: in fondo, questa non è una scoperta dell’informatica in quanto è la stessa strategia che consente all’artigiano di costruire un cesto intrecciando un fascio di vimini. Nella composizione, in taluni casi è sufficiente considerare gli elementi che vengono composti, mantenendosi al livello di astrazione degli insiemi; in altri casi è importante considerare anche la struttura con cui gli elementi vengono composti. Riprendendo l’esempio sopra, il peso del cesto vuoto può essere determinato calcolando il peso del fascio di vimini con i quali è composto, ma se si vuole utilizzare il cesto come contenitore, è necessario che i vimini siano intrecciati opportunamente. 1 2 1.1 CAPITOLO 1. SEQUENZE LE COMPOSIZIONI Nel mondo fisico ogni cosa è composta da elementi fondamentali primitivi non scomponibili (atomi) o, almeno, considerati tali nel contesto in cui ci si pone; similmente in informatica esistono componenti elementari che sono i più piccoli frammenti dotati di significato e non ulteriormente scomponibili. Gli elementi fondamentali possono essere composti per formare oggetti più complessi. Le composizioni possono essere formate da elementi che sono a loro volta composti da elementi più elementari. Questi elementi possono essere di diversa natura. Al livello di astrazione predisposto dai tradizionali linguaggi di programmazione, gli atomi sono costituiti dai numeri, dalle stringhe, dalle istruzioni elementari del linguaggio, dalle funzioni ed altro ancora; ad un livello più basso, gli atomi sono i byte e le istruzioni in linguaggio macchina; ad un livello più alto, in un sistema complesso, gli atomi del sistema sono le componenti del sistema stesso e a loro volta possono essere costituite da altre componenti più elementari. La potenza dei linguaggi di programmazione viene esplicata proprio mediante la possibilità di comporre fra loro delle entità di base. Il meccanismo di composizione può essere pensato come un’operazione che raggruppa un insieme di elementi, secondo lo schema descritto nella figura 1.1. ● ● ● ● ● ● ● ● ● composizione - ● ● ● ● ● ● ● ● ● Figura 1.1: Operazione di composizione di un insieme di elementi. Ogni meccanismo di composizione comporta la definizione di meccanismi di accesso e di manipolazione delle componenti. Per questo sulle composizioni sono predisposti degli appositi operatori che sono classificati come segue: costruttori : sono delle operazioni che generano composizioni selettori : sono delle operazioni che permettono di accedere ad uno specifico elemento di una composizione; in taluni casi un selettore può fornire, come risultato, una sottocomposizione. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 3 1.2. LE SEQUENZE manipolatori : sono delle operazioni che hanno l’effetto di modificare la struttura della composizione, mediante operazioni di inserimento, eliminazione e modifica di elementi. Queste categorie di operatori (costruttori, selettori, manipolatori) sono realizzate con diverse modalità, dando origine a diverse tipologie di composizioni. 1.2 LE SEQUENZE Gli elementi che costituiscono una composizione possono essere organizzati e strutturati in diversi modi; una delle forme più elementari di organizzazione è costituita dalla sequenzializzazione che consiste nel disporre (fisicamente) o considerare (virtualmente) gli elementi in fila, uno dopo l’altro, secondo lo schema descritto nella figura 1.2. ● ● ● ● ● ● ● ● ● sequenzializzazione 1 2 3 4 5 6 7 8 9 - ● ● ● ● ● ● ● ● ● Figura 1.2: Operazione di sequenzializzazione di un insieme di elementi. ne. Il formalismo per denotare le sequenze è descritto nella seguente definizio- DEFINIZIONE 1. Con il termine sequenza si indica una composizione di elementi, detti atomi, disposti in fila, uno dopo l’altro; in generale, un atomo o una sequenza viene chiamato elemento. Se a1 , . . . , an sono degli elementi, la scrittura 1 [a1 , . . . , an ] denota la sequenza costituita dagli elementi a1 , . . . , an , mentre la scrittura [] 1 La scrittura [. . . ] usata per denotare una sequenza (oltre che essere sintatticamente compatibile con la maggior parte dei linguaggi di programmazione) ha una valenza figurativa che richiama la gestualità delle mani per raggruppare degli oggetti. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 4 CAPITOLO 1. SEQUENZE denota la sequenza vuota, ossia la sequenza composta da nessun elemento. Una sequenza composta da n elementi viene detta n-sequenza. Data una sequenza α = [a1 , . . . , an ] e degli indici i, j tali che 1 ≤ i ≤ j ≤ n, la sequenza [ai , . . . , aj ] dicesi sottosequenza di α. Se i = 1 la sottosequenza è detta prefisso (di α); se j = n la sottosequenza è detta suffisso (di α). Se h e k sono due numeri naturali con h ≤ k, con [h..k] si denota la sequenza composta dai numeri naturali compresi fra h e k, estremi compresi. Esempio 1. Dalla definizione di sequenza, si può immediatamente riconoscere che i seguenti sono degli esempi di sequenze: [] [7] [3, 2, 5] Le sequenze sono delle composizioni costruite mediante l’operazione di prodotto cartesiano di insiemi precedentemente definiti. A seguire sono descritti i principali metodi per costruire insiemi strutturati mediante il ricorso all’operazione di prodotto cartesiano. Se A è un insieme, An = A × A × ⋯ × A è un nuovo insieme i cui elementi sono le sequenze [a1 , a2 , . . . , an ] di lunghezza n di elementi di A. Si assume, per convenzione, A0 = [ ]. Se A è un insieme, ∞ A∗ = A0 ∪ A1 ∪ A2 ∪ ⋯ = ⋃ Ak k=0 è un nuovo insieme i cui elementi sono tutte le sequenze (di qualsiasi lunghezza) di elementi di A. Se A1 , A2 , . . . , An sono insiemi, A1 × A2 × ⋯ × An è un insieme i cui elementi sono le sequenze [a1 , a2 , . . . , an ] non omogenee di elementi con ai ∈ Ai . L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 5 1.3. RAPPRESENTAZIONI DELLE SEQUENZE Osservazione. Il concetto di sequenza è più forte e pregnante del concetto di composizione, in quanto una sequenza stabilisce anche una posizione d’ordine degli elementi componenti. Non si tratta di una semplice composizione insiemistica degli elementi, ma di una composizione in sequenza in quanto ogni elemento della composizione è caratterizzato da una sua specifica posizione. Il meccanismo di sequenzializzazione fornisce il supporto per lo sviluppo di moltissimi ed importantissimi algoritmi, come gli algoritmi di ricerca e di ordinamento. Inoltre, le sequenze costituiscono un meccanismo spesso utilizzato per realizzare delle particolari strutture di dati astratte. 1.3 RAPPRESENTAZIONI DELLE SEQUENZE Una sequenza [a1 , a2 , . . . , an ] viene spesso descritta graficamente mediante uno schema simile a quello riportato nella figura 1.3. a1 a2 ... an Figura 1.3: Rappresentazione grafica della sequenza [a1 , a2 , . . . , an ]. In talune considerazioni, specialmente nel caso di sequenze composte, risulta utile descrivere una sequenza mediante uno schema ad albero: una generica sequenza [a1 , . . . , an ] viene descritta come illustrato nella figura 1.4. 2 @ @ @ @ @ a1 a2 ... @ @ R @ an Figura 1.4: Rappresentazione grafica ad albero della sequenza [a1 , a2 , . . . , an ]. Esempio 2. La sequenza [a, [b], [ [ ], c, d] ] è descritta dall’albero riportato in figura 1.5. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 6 CAPITOLO 1. SEQUENZE 2 @ @ @ a ? 2 ? b @ R @ 2 B B B ?BBN ⊡ c d Figura 1.5: Rappresentazione grafica ad albero della sequenza [a, [b], [ [ ], c, d] ]. Il simbolo ⊡ denota l’operatore di sequenzializzazione se si trova in un nodo interno, mentre denota la sequenza vuota se si trova su un nodo foglia. 1.4 OPERAZIONI SULLE SEQUENZE La costruzione di una sequenza [a1 , . . . , an ] può essere vista come l’applicazione dell’operatore n-ario [ ] in notazione distribuita applicato agli operandi a1 , . . . , an . Tale operazione viene detta sequenzializzazione. Data una generica sequenza α, con size α si denota la lunghezza di α, ossia il numero di elementi che la compongono: size[a1 , . . . , an ] = n La sequenza vuota [ ] ha lunghezza 0. L’operazione di concatenazione fra due sequenze produce come risultato una sequenza costituita dagli elementi della prima sequenza seguiti dagli elementi della seconda; l’operazione di concatenazione viene solitamente denotata mediante l’operatore binario infisso ∣ come segue: [a1 , . . . , an ] ∣ [b1 , . . . , bm ] = [a1 , . . . , an , b1 , . . . , bm ] Spesso, qualora non risulti ambiguo, il segno ∣ dell’operatore di concatenazione viene omesso e le due sequenze da concatenare vengono semplicemente scritte affiancate. La sequenza vuota [ ] costituisce l’elemento neutro (sinistro e destro) per l’operazione di concatenazione. L’operazione di concatenazione gode della proprietà associativa, ossia, per generiche sequenze α, β, γ, vale l’identità (αβ)γ = α(βγ); l’operazione di concatenazione non gode incece della proprietà commutativa, ossia, in generale, αβ ≠ βα. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 1.4. OPERAZIONI SULLE SEQUENZE 7 Le operazioni di inserimento in testa ed aggiunta in coda di un elemento in una sequenza possono essere espresse tramite l’operatore [ ] di sequenzializzazione e l’operatore ∣ di concatenazione, come illustrato dai seguenti esempi: [x] ∣ [a, b, c] = [x, a, b, c] [a, b, c] ∣ [x] = [a, b, c, x] Sulle sequenze si può applicare l’operazione di ripetizione: il risultato della ripetizione di una sequenza α = [a1 , . . . , an ] per una costante naturale k è costituito da una sequenza ottenuta concatenando k sequenze α, ossia [a1 , . . . , an ]k = [a1 , . . . , an , a1 , . . . , an , . . . ] L’espressione naturale k è detta ripetitore. Il valore k = 1 costituisce l’elemento neutro per l’operazione di ripetizione di sequenze, ossia α1 = α. Per convenzione si pone α0 = [ ]. L’operazione di ripetizione di sequenze realizza anche il meccanismo di costruzione e dimensionamento iniziale: l’espressione [a]n produce una sequenza di n elementi; ciascun elemento della sequenza viene inizializzato con il valore a. L’operazione di ripetizione di una sequenza per un numero gode di alcune proprietà; in particolare, se α è una sequenza e h e k sono due numeri naturali, vale la proprietà αh ∣ αk = αh+k Esempio 3. A seguire sono riportati alcuni esempi di operazioni e corrispondenti calcoli sulle sequenze: [a, b, c] ∣ [d, e] = [a, b, c, d, e] [0]8 = [0, 0, 0, 0, 0, 0, 0, 0] [1, 2]4 = [1, 2, 1, 2, 1, 2, 1, 2] [ [4]3 ]2 = [ [4, 4, 4], [4, 4, 4] ] [2 ∶ 4]3 = [2, 3, 4, 2, 3, 4, 2, 3, 4] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 8 CAPITOLO 1. SEQUENZE Esempio 4. Le operazioni di aggregazione e ripetizione di elementi possono combinarsi in tutti i modi possibili in modo da costituire un’effettiva algebra con proprie specifiche proprietà; segue qui un esempio di calcolo che illustra come si opera in quest’algebra: [a, b] ∣ ([a]2 ∣ [b])2 ∣ [a] → [a, b] ∣ ([a, a] ∣ [b])2 ∣ [a] → → [a, b] ∣ [a, a, b]2 ∣ [a] → → [a, b] ∣ [a, a, b] ∣ [a, a, b] ∣ [a] → → [a, b] ∣ [a] ∣ [a, b] ∣ [a] ∣ [a, b] ∣ [a] → → [a, b, a] ∣ [a, b, a] ∣ [a, b, a] → → [a, b, a]3 Esempio 5. Nel contesto dei linguaggi di programmazione le sequenze possono essere denotate mediante degli identificatori e questi identificatori possono essere usati come elementi costituenti altre sequenze; un esempio di questa possibilità è riportato nella sequenza di assegnazioni che seguono. a ← [3, 5] b ← [4, 2] c ← a∣b d ← [a, b] 1.5 CLASSIFICAZIONE DELLE SEQUENZE Le sequenze possono essere classificate in base a diversi criteri; i più frequentemente usati sono di seguito elencati: in base al tipo degli elementi componenti : se tutti gli elementi sono dello stesso tipo le sequenze vengono dette sequenze omogenee, altrimenti vengono dette sequenze eterogenee o strutture in base alla struttura degli elementi componenti : se tutti gli elementi sono elementari le sequenze vengono dette sequenze semplici o piatte, altrimenti vengono dette sequenze composte o strutturate L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 1.6. LE STRUTTURE 9 in base alle modalità di accesso agli elementi componenti : le operazioni di accesso agli elementi componenti una sequenza sono realizzate con diverse modalità e danno luogo a diverse tipologie di sequenze; fra queste le più importanti sono gli array e le liste in base alle operazioni di modifica : le operazioni di inserimento, eliminazione e modifica degli elementi di una sequenza possono essere realizzate con diverse modalità; a seconda di quali operazioni (e con quali limitazioni) sono permesse, le sequenze assumono diversi nomi (pile, code, insiemi, . . . ). Queste diverse tipologie di sequenze saranno trattate nei prossimi capitoli. 1.6 LE STRUTTURE Gli elementi che costituiscono una sequenza possono essere di tipi diversi. Per evidenziare questa caratteristica, una sequenza viene detta sequenza eterogenea o struttura. Esempio 6. A seguire sono riportate alcune espressioni di tipo struttura: [7, F ALSE] [′ +′ , 8, 5, 3, 6, 2] [′′ M ario′′ ,′′ Rossi′′ , 2,′′ maggio′′ , 2012] 1.7 LE SEQUENZE COMPOSTE Un’ulteriore generalizzazione delle sequenze eterogenee è rappresentata dalle sequenze costituite da elementi che sono a loro volta delle sequenze; in questi casi si parla di sequenze composte. Di contrappunto, le sequenze formate da elementi atomici (numeri, valori logici, caratteri, stringhe, . . . ) sono dette sequenze piatte o sequenze elementari. Esempio 7. A seguire sono riportati alcuni esempi di sequenze composte: [ [ [ ] ], [ ] ] [3, [4, 5], 2, [ [6], [ ] ] ] [ [′′ M ario′′ ,′′ Rossi′′ ], [2,′′ maggio′′ , 2012] ] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 10 CAPITOLO 1. SEQUENZE [ [′′ rosso′′ , [255, 0, 0] ], [′′ verde′′ , [0, 255, 0] ] ] Esempio 8. Una sequenza composta si presta a descrivere un’espressione, rappresentando in modo uniforme operatori di diverse arietà e di diverse notazioni, mediante la generica notazione [⊗, x1 , . . . , xn ] dove ⊗ denota un generico operatore e gli argomenti xi sono delle generiche espressioni. La seguente tabella descrive alcuni esempi di espressioni descritte in notazione algebrica e nella corrispondente notazione mediante sequenze composte. notazione algebrica 2+3 a/(b + c) x cos(1/x) x+1 ax+by notazione mediante sequenza [+, 2, 3] [/, a, [+, b, c] ] [∗, x, [cos, [/, 1, x] ] ] [/, [+, x, 1], [+, [∗, a, x], [∗, b, y] ] Esempio 9. Una sequenza può essere costituita da comandi; ad esempio, nel contesto della grafica della tartaruga, una figura composta da 36 quadrati ruotati l’uno rispetto all’altro di 10 gradi può essere definita mediante la seguente porzione di codice: a ← [f orward 100, lef t 90] q ← a4 f ← [q, lef t 10]36 1.8 OPERAZIONI FUNZIONALI E PROCEDURALI Le sequenze, essendo degli oggetti composti, vengono elaborate mediante delle operazioni che possono essere impostate secondo due diverse modalità: • modalità funzionale: l’operazione genera una nuova sequenza che costituisce il risultato dell’operazione L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 11 1.9. COSTRUZIONE DI SEQUENZE • modalità procedurale: l’operazione modifica la sequenza alla quale viene applicata; tale modalità è applicabile solo se l’argomento dell’operazione è una variabile che identifica una sequenza Solitamente i due approcci sono entrambi disponibili nella maggior parte dei linguaggi di programmazione e possono essere utilizzati interscambievolmente. Essi tuttavia sono profondamente diversi e rientrano in due paradigmi di programmazione distinti; il primo (paradigma funzionale) suggerisce spesso delle soluzioni ricorsive e non richiede l’uso di variabili ed assegnazioni (in questo contesto le sequenze vengono manipolate mediante uno specifico insieme di operazioni e vengono dette liste); il secondo approccio (paradigma procedurale) si fonda sul fatto che un’operazione modifica direttamente la sequenza alla quale viene applicata; in questi casi solitamente si fa ricorso ad algoritmi basati su cicli e si fa uso di variabili e di assegnazioni che permettono di modificare selettivamente (in base ad un indice di posizione) un elemento della sequenza; in questi casi le sequenze vengono dette array. Queste due tipologie di sequenze saranno trattate nei prossimi capitoli. 1.9 COSTRUZIONE DI SEQUENZE Molti problemi sono caratterizzati dall’avere delle sequenze come risultato. In tutti questi casi l’algoritmo risolvente il problema costruisce, generalmente mediante un adeguato controllo ciclico la cui forma dipende dallo specifico problema, una sequenza che costituirà alla fine il risultato del problema. Tale sequenza risultato viene spesso costruita accodando man mano degli elementi, a partire dalla sequenza iniziale vuota. Il seguente esempio illustra la tecnica appena descritta. Esempio 10. Sviluppiamo un algoritmo per scomporre un numero naturale in fattori primi. Facciamo riferimento all’usuale metodo di scomposizione descritto dallo schema riportato nella figura 1.6 dal quale si ricava 126 = 2⋅32 ⋅7. In questo caso il risultato potrà essere rappresentato dalla sequenza [2, 3, 3, 7]. 126 63 21 7 1 2 3 3 7 Figura 1.6: Schema della scomposizione in fattori primi del numero 126. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 12 CAPITOLO 1. SEQUENZE Seguendo la traccia sopra, si arriva all’algoritmo 1. Algoritmo 1 - Sequenza dei divisori primi di un numero naturale Input: numero naturale n Output: sequenza dei divisori primi di n 1: inizializza la sequenza vuota a dei divisori 2: considera d = 2 come primo potenziale divisore 3: while n > 1 do 4: while n è divisibile per d do 5: accoda d alla sequenza dei divisori 6: dividi n per d 7: end while 8: considera il numero primo successivo di d 9: end while 10: return sequenza a dei divisori primi Nell’algoritmo 1 rimangono ancora da esplicitare le soluzioni di alcuni sottoproblemi; fra questi, l’unico di una qualche complessità è il seguente: considera il numero primo successivo di d In prima approssimazione (anche se non ottimale) questo problema può essere risolto con l’istruzione incrementa d di 1 La soluzione completa è riportata nell’algoritmo 2. Algoritmo 2 - Sequenza dei divisori primi di un numero naturale Input: numero naturale n Output: sequenza dei divisori primi di n 1: a ← [ ] 2: d ← 2 3: while n > 1 do 4: while (n%d) = 0 do 5: a ← a ∣ [d] 6: n ← n/d 7: end while 8: d ++ 9: end while 10: return a L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 1.9. COSTRUZIONE DI SEQUENZE 13 Osservando che nessun numero pari, escluso 2, è primo, l’algoritmo di scomposizione 2 risulta più efficiente se il divisore d viene incrementato di due unità ad ogni ciclo, ossia se l’istruzione di incremento di d viene sostituita con incrementa d di 2. Esempio 11. L’algoritmo sviluppato nell’esempio precedente può essere adattato in modo che vengano determinati i fattori del numero n con le rispettive molteplicità (vedi tabella 1.7). Una tabella di questo formato può essere rappresentata mediante la sequenza di coppie [ [2, 1], [3, 2], [7, 1] ]. fattore 2 3 7 molteplicità 1 2 1 Figura 1.7: Tabella della scomposizione del numero 126. A seguire è riportato l’algoritmo che generalizza la tavola di traccia riportata nella figura 1.7, in due versioni a diverso livello di dettaglio (algoritmi 3 e 4). Algoritmo 3 - Sequenza dei divisori primi di un numero naturale Input: numero naturale n Output: sequenza dei divisori primi di n 1: inizializza la sequenza vuota dei divisori 2: considera d = 2 come primo potenziale divisore 3: while n > 1 do 4: determina la molteplicità m del divisore d 5: if m > 0 then 6: accoda la coppia (d, m) alla sequenza dei divisori 7: end if 8: considera il numero primo successivo di d 9: end while 10: return sequenza dei divisori primi L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 14 CAPITOLO 1. SEQUENZE Algoritmo 4 - Sequenza dei divisori primi di un numero naturale Input: numero naturale n Output: sequenza dei divisori primi di n 1: a ← [ ] 2: d ← 2 3: while n > 1 do 4: m←0 5: while (n%d) = 0 do 6: m ++ 7: n ← n/d 8: end while 9: if m > 0 then 10: a ← a ∣ [ [d, m] ] 11: end if 12: d ++ 13: end while 14: return a 1.10 ELABORAZIONE DI SEQUENZE Una volta costruite, le sequenze devono essere elaborate. La forma più elementare di elaborazione consiste nel passare in esame tutti gli elementi della sequenza ed elaborarli sequenzialmente mediante un controllo della forma riportata nell’algoritmo 5. Algoritmo 5 - Elaborazione sequenziale di una sequenza Input: sequenza a 1: for each x of a do 2: elabora elemento x 3: end for Nell’elaborazione di una sequenza risultano utili i seguenti due predicati: • operazione di controllo di atomicità: isatom x il risultato è T RU E se x è un atomo, F ALSE altrimenti L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 1.10. ELABORAZIONE DI SEQUENZE 15 • operazione di controllo di lista nulla: isempty a che è equivalente all’espressione a = [ ]. Esempio 12. Calcolo della somma degli atomi di una sequenza numerica piatta. Algoritmo 6 - Somma degli elementi di una sequenza semplice numerica Input: sequenza a Output: somma degli elementi della sequenza a 1: s ← 0 2: for each x of a do 3: s←s+x 4: end for 5: return s Con riferimento all’algoritmo 5, se la sequenza a da elaborare è composta, l’elemento x dell’elaborazione superficiale può essere a sua volta una sequenza. Nel caso in cui sia necessaria una elaborazione in profondità fino ad arrivare agli atomi componenti, è sufficiente adattare l’algoritmo 5 ricorrerendo al predicato isatom. Il questo caso l’algoritmo, per gestire in modo generico i livelli di annidamento della sequenza, assume una forma ricorsiva, come evidenziato nell’algoritmo 7. Algoritmo 7 - elabora(a) : elaborazione in profondità di una sequenza Input: sequenza a 1: for each x of a do 2: if isatom(x) then 3: elabora l’atomo x 4: else 5: elabora(x) 6: end if 7: end for L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 16 CAPITOLO 1. SEQUENZE Esempio 13. Calcolo della somma degli atomi di una sequenza numerica composta. Algoritmo 8 - somma(a) : somma elementi di una sequenza composta Input: sequenza a Output: somma degli elementi della sequenza composta a 1: s ← 0 2: for each x of a do 3: if isatom(x) then 4: s←s+x 5: else 6: s ← s + somma(x) 7: end if 8: end for 9: return s Gli schemi di elaborazione riportati negli algoritmi 5 e algoritmi 7 non sono adatti nel caso in cui debbano essere elaborati solo alcuni elementi della sequenza (ad esempio nel caso in cui si debba ricercare se un dato valore è presente in una sequenza). 1.11 ESERCIZI 1. Determinare il numero di sottosequenze che si possono estrarre, mantenendo l’ordine di posizione degli elementi, da una sequenza di lunghezza n composta da elementi distinti. 2. Determinare il numero di sottosequenze di lunghezza k che si possono estrarre, mantenendo l’ordine di posizione degli elementi, da una sequenza di lunghezza n composta da elementi distinti, con k ≤ n. 3. Determinare il numero di sottosequenze, composte da elementi adiacenti, che si possono estrarre da una sequenza di lunghezza n composta da elementi distinti. 4. Determinare il numero di sottosequenze, di lunghezza k, composte da elementi adiacenti, che si possono estrarre da una sequenza di lunghezza n composta da elementi distinti, con k ≤ n. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 17 1.11. ESERCIZI 5. Sia x un atomo e m, n due numeri naturali. espressioni sono equivalenti: Discutere se le seguenti (a) [x]mn (b) ([x]m )n (c) ([x]n )m 6. Dato l’atomo x ed il numero naturale k, scrivere un’espressione che rappresenti la sequenza [x, x, . . . , x] costituita da k atomi x. 7. Date le sequenze a = [x], b = [y], scrivere un’espressione che rappresenti la sequenza [x, x, . . . , x, y, y, . . . , b] costituita da k atomi x e da k atomi y. 8. Date le sequenze a = [x], b = [y], scrivere un’espressione che rappresenti la sequenza [x, y, x, y, . . . , x, y] costituita da k atomi x e da k atomi y alternati. 9. Data la sequenza a = [x, y, z] calcolare le seguenti espressioni: (a) a2 (b) [a]2 (c) [a2 ] (d) [a ∶ a] (e) a∣a (f) [a]∣[a] 10. Scrivere delle espressioni che generino le seguenti sequenze: (a) [1, 1, 1, 1, 1, 1, 1, 1, 1, 1] (b) [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] (c) [1, 2, 1, 2, 1, 2, 1, 2, 1, 2] (d) [1, 1, 1, 1, 1, 2, 2, 2, 2, 2] (e) [1, 2, 3, 4, 5, 1, 2, 3, 4, 5] 11. Dato il numero naturale n, generare le seguenti sequenze: (a) [1, 2, 3, . . . , n] (b) [n, n − 1, . . . , 3, 2, 1] (c) [1, 1, 2, 1, 2, 3, 1, 2, 3, 4, . . . , 1, 2, . . . , n] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 18 CAPITOLO 1. SEQUENZE (d) [1, 2, 2, 3, 3, 3, . . . , n, n, . . . , n] 12. Determinare le eventuali soluzioni naturali di un’equazione di secondo grado. 13. Risolvere un’equazione di secondo grado, gestendo anche i casi in cui le radici siano complesse non reali. 14. Determinare la sequenza delle cifre di un dato numero naturale. 15. Determinare la sequenza dei divisori di un dato numero naturale. 16. Determinare la sequenza dei primi n numeri primi. 17. Determinare tutte le coppie di numeri naturali che danno per prodotto un dato numero naturale. 18. Dato un numero naturale rappresentante una quantità di denaro espressa in centesimi di Euro, determinare il più piccolo insieme di monete (da 1, 2, 5, 10, 20, 50 centesimi di Euro) equivalente alla data cifra. Esprimere il risultato mediante una sequenza di coppie. 19. Risolvere il problema della torre di Hanoi, determinando la sequenza delle mosse da eseguire. 20. Il triangolo di Pascal è costituito da numeri naturali disposti in righe; ogni riga è formata da un numero di numeri uguale al numero di riga e, disponendo le righe in modo centrato, ogni numero è la somma dei due numeri adiacenti della riga superiore. A seguire è riportata una porzione del triangolo. 1 1 1 1 1 1 2 3 4 1 3 6 1 4 1 (a) Determinare il k-esimo numero dell’n-esima riga del triangolo di Pascal (tale numero viene detto binomiale di n su k). (b) Determinare l’n-esima riga del triangolo di Pascal. (c) Determinare la sequenza delle prime n righe del triangolo di Pascal ([ [1], [1, 1], [1, 2, 1], . . . ]) L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 19 1.11. ESERCIZI 21. Determinare tutte le possibili scomposizioni in addendi non nulli di un dato numero naturale (indipendentemente dall’ordine); ad esempio, il numero n = 4 può essere scomposto in addendi come segue: 4 = 4 4 = 1+3 4 = 2+2 4 = 1+1+2 4 = 1+1+1+1 e la soluzione può essere espressa mediante la sequenza [ [4], [1, 3], [2, 2], [1, 1, 2], [1, 1, 1, 1] ] 22. Data una sequenza semplice di numeri naturali, determinare la sottosequenza dei numeri pari e quella dei numeri dispari. Analogamente per una sequenza composta. 23. Data una sequenza semplice determinare la sottosequenza degli elementi di posizione pari e quella degli elementi di posizione dispari. 24. Determinare il numero di atomi presenti in una sequenza composta. 25. Determinare la sequenza degli atomi di una sequenza composta; ad esempio, data la sequenza composta [ [1, 2], 3, [4, [ ], [ [5], 6] ] ] si deve determinare la sequenza semplice [1, 2, 3, 4, 5, 6]. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 20 CAPITOLO 1. SEQUENZE L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 2 Liste Nella sua forma più semplice, una tabella può essere una lista lineare di elementi, quando le sue proprietà strutturali rilevanti includono la risposta a questioni come: quale elemento è il primo della lista? qual è l’ultimo? quali elementi precedono e seguono un dato elemento? quanti elementi ci sono nella lista? C’è molto da dire sulla struttura perfino su questo caso apparentemente semplice. D. Knuth, The Art of Computer Programming Le liste sono delle particolari sequenze caratterizzate da specifici operatori di accesso agli elementi che le compongono. Questi operatori sono costruiti secondo l’impostazione del paradigma funzionale: il risultato di un’operazione è sempre un atomo o una lista e non esistono operatori per modificare una lista. Coerentemente con questa impostazione, le liste vengono generalmente elaborate mediante degli algoritmi ricorsivi. Le liste costituiscono una importante struttura dati, così potente e duttile che si presta a supplire a tutte le altre strutture organizzative. Ne è conferma il fatto che alcuni linguaggi di programmazione (Lisp, Prolog, Logo, Askell ed altri) hanno le liste come unica struttura dati disponibile. 21 22 CAPITOLO 2. LISTE 2.1 LE LISTE Dal punto di vista strutturale le liste sono delle sequenze caratterizzate da specifici operatori per la creazione, la composizione e l’accesso agli elementi. Nella trattazione delle liste risulta spesso più utile riferirsi alla seguente definizione alternativa: DEFINIZIONE 2. Una lista è una sequenza vuota di elementi, denotata con [ ], oppure è una coppia (x, a), dove x è un atomo ed a è una lista. Una tale coppia viene denotata 1 con [x ∶ a]. L’elemento x viene detto f irst e la lista a è detta rest. Prendendo spunto da questa definizione ricorsiva, per gestire le liste si fa uso alle seguenti operazioni di base (predisposte in alcuni linguaggi di programmazione): • operazione di costruzione prefissa: dato un elemento x (atomo o lista) ed una lista a, l’operazione [x ∶ a] genera una lista avente x come primo elemento con a seguire gli elementi di a. L’operazione di costruzione prefissa [x ∶ a] può essere espressa in modo equivalente mediante l’operatore di concatenazione con [x]∣a • operazione di primo: il risultato è costituito dal primo elemento della lista: f irst[a1 , . . . , an ] = a1 Se a = [ ] o a =⊥, allora f irst α =⊥. • operazione di resto: il risultato è costituito dalla lista ottenuta escludendo il primo elemento: rest[a1 , . . . , an ] = [a2 , . . . , an ] Anche in questo caso, se a = [ ] o a =⊥, allora rest a =⊥. Indicando con a una generica lista, le tre operazioni di costruzione prefissa [◻ ∶ ◻], f irst e rest sono fra loro legate dalla proprietà invariante [f irst a ∶ rest a] = a 1 Nel linguaggio Prolog viene utilizzata la notazione [x ∣ a]. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 2.2. ELABORAZIONE DI LISTE 23 Osservazione. L’operatore [◻ ∶ ◻] costituisce il meccanismo fondamentale per generare una lista a partire dalla lista vuota [ ]; gli operatori f irst e rest permettono di accedere ai vari elementi di una lista, consentendone l’elaborazione; il predicato isatom costituisce l’operatore per gestire la struttura composta di una lista, ai vari livelli di annidamento; l’elaborazione degli elementi di una lista viene svolta in modo ricorsivo, dove il predicato isnull costituisce il test per chiudere la ricorsione. Queste operazioni costituiscono un insieme completo di operazione per elaborare le liste. Esempio 14. Gli esempi che seguono illustrano le operazioni sopra descritte. Si noti l’uso delle parentesi per forzare l’associatività a destra. [a ∶ [b ∶ [c] ] ] → [a, b, c, d] f irst(rest[a, b, c]) → b rest[a, b, c, d] → [b, c, d] size(rest[a, b, c]) → 2 isatom[ [a], b] → F ALSE isempty(rest[a]) → T RU E isatom(f irst[ [a], b]) → F ALSE isatom(f irst(rest[ [a], b])) → T RU E 2.2 ELABORAZIONE DI LISTE Le liste possono essere eleborate con due meccanismi formalmente distinti ma computazionalmente equivalenti. Una prima modalità consiste nell’elaborazione degli elementi della lista in modalità iterativa, mediante un ciclo, come descritto nell’algoritmo 9. Algoritmo 9 - Elaborazione di una lista a (modalità iterativa) Input: sequenza a 1: b ← a 2: while ¬(isempty b) do 3: elabora l’elemento f irst b 4: b ← rest b 5: end while L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 24 CAPITOLO 2. LISTE Una modalità di elaborazione alternativa è descritta nell’algoritmo 10. Tale algoritmo si fonda sulla definizione ricorsiva di lista. Algoritmo 10 - Elaborazione di una lista a (modalità ricorsiva) Input: sequenza a 1: if ¬(isempty a) then 2: elabora l’elemento f irst a 3: elabora la lista rest a 4: end if Esempio 15. Applichiamo gli algoritmi 9 e 10 alla determinazione del k-esimo elemento di una lista α. Nella modalità iterativa e ricorsiva i due algoritmi si scrivono come descritto negli algoritmi 11 e 12. Come si può dedurre dal confronto dei due algoritmi, la versione ricorsiva risulta più concisa e leggibile della corrispondente versione iterativa. Algoritmo 11 - item[a, k] : k-esimo elemento della lista a (versione iterativa) Input: lista a, numero naturale k Output: k-esimo elemento della lista a 1: t ← a 2: i ← 1 3: while i < k do 4: t ← rest t 5: i++ 6: end while 7: return f irst t Algoritmo 12 - item[a, k] : k-esimo elemento della lista a (versione ricorsiva) Input: lista a, numero naturale k Output: k-esimo elemento della lista a 1: if k = 1 then 2: return f irst a 3: else 4: return item[rest a, k − 1] 5: end if L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 2.3. ALGORITMI SULLE LISTE SEMPLICI 2.3 25 ALGORITMI SULLE LISTE SEMPLICI Le liste, essendo delle strutture che si prestano ad essere definite in modo ricorsivo, suggeriscono, in modo naturale, dei procedimenti ricorsivi per il loro trattamento. A seguire ne sono riportati alcuni. Esempio 16. Lista costituita dai primi n numeri naturali: Algoritmo 13 - list n : lista [1, 2, . . . , n] Input: numero naturale n Output: lista [1, 2, . . . , n] 1: if n = 0 then 2: return [ ] 3: else 4: return list(n − 1) ∣ [n] 5: end if Esempio 17. Somma degli elementi di una lista semplice di numeri naturali: Algoritmo 14 - sum a : somma degli elementi della lista a Input: lista a = [a1 , . . . , an ] Output: a1 + ⋅ ⋅ ⋅ + an 1: if isempty a then 2: return 0 3: else 4: return f irst a + sum(rest a) 5: end if Esempio 18. Lunghezza di una lista semplice: Algoritmo 15 - len a : lunghezza della lista a Input: lista a = [a1 , . . . , an ] Output: n 1: if isempty a then 2: return 0 3: else 4: return 1 + len(rest a) 5: end if L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 26 CAPITOLO 2. LISTE Esempio 19. Inversione di una lista semplice: Algoritmo 16 - inv a : inverso della lista a Input: lista a = [a1 , . . . , an ] Output: lista [an , . . . , a1 ] 1: if isempty a then 2: return [ ] 3: else 4: return inv(rest a) ∣ [f irst a] 5: end if Esempio 20. Fusione di due liste ordinate: Algoritmo 17 - merge[a, b] : fusione di due liste ordinate a e b Input: liste a e b ordinate (crescentemente) Output: lista ottenuta dalla fusione di a e b 1: if isempty a then 2: return b 3: else if isempty b then 4: return a 5: else if f irst a < f irst b then 6: return [(f irst a) ∶ merge[rest a, b] ] 7: else 8: return [(f irst b) ∶ merge[a, rest b] ] 9: end if Esempio 21. Date due n-liste a = [a, ..., an ] e b = [b1 , ..., bn ], determiniamo la lista composta dalle coppie di elementi aventi la stessa posizione nelle due liste: [ [a1 , b1 ], [a2 , b2 ], ..., [an , bn ] ] La soluzione di questo problema è riportata nell’algoritmo 18. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 2.3. ALGORITMI SULLE LISTE SEMPLICI 27 Algoritmo 18 - coppie[a, b] : lista delle coppie delle due liste a e b Input: liste a e b Output: lista delle coppie formate combinando gli elementi di uguale posizione delle due liste a e b 1: if (isempty a) ∨ (isempty b) then 2: return [ ] 3: else 4: return [ [f irst a, f irst b] ∶ coppie[rest a, rest b] ] 5: end if Esempio 22. Il prodotto cartesiano fra due liste è costituito dalla lista di tutte le possibili coppie che si possono formare prendendo il primo elemento dalla prima lista ed il secondo elemento dalla seconda; ad esempio il prodotto cartesiano fra le due liste [a, b, c] e [x, y] produce la lista di coppie [ [a, x], [a, y], [b, x], [b, y], [c, x], [c, y] ] . Costruiamo dapprima l’algoritmo che esegue il prodotto cartesiano fra un atomo ed una lista (algoritmo 19). Algoritmo 19 - prod[x, a] : prodotto dell’atomo x per la lista a Input: atomo x, lista a = [a1 , . . . , an ] Output: lista delle coppie [ [x, a1 ], [x, a2 ], . . . , [x, an ] ] 1: if isempty a then 2: return [ ] 3: else 4: return [ [x, f irst a] ∶ prod[x, rest a] ] 5: end if A questo punto il prodotto cartesiano fra due liste si esprime mediante il seguente algoritmo 20. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 28 CAPITOLO 2. LISTE Algoritmo 20 - prodcart[a, b] : prodotto cartesiano fra le due liste a e b Input: liste a e b Output: lista delle coppie del prodotto cartesiano a × b 1: if isempty a then 2: return [ ] 3: else 4: return prod[f irst a, b] ∣ prodcart[rest a, b] 5: end if 2.4 ALGORITMI SULLE LISTE COMPOSTE In modo analogo alle forme di elaborazione viste nel paragrafo precedente per l’elaborazione di liste semplici, vengono presentati a seguire degli esempi di elaborazione di liste composte. Esempio 23. Determiniamo il massimo di una lista composta . A questo scopo consideriamo dapprima l’algoritmo 21 che determina il massimo di una lista semplice (in questo algoritmo max[x, y] denota il massimo fra i due elementi della coppia [x, y]). Algoritmo 21 - maxl a : massimo della lista semplice a Input: lista semplice a Output: massimo della lista semplice a 1: if isempty a then 2: return N U LL 3: else if (size a) = 1 then 4: return f irst a 5: else 6: return max[f irst a, maxl(rest a)] 7: end if L’algoritmo 21 può essere adattato al caso di una lista composta: è sufficiente applicare ricorsivamente l’algoritmo agli elementi che non sono atomici, come descritto nell’algoritmo 22. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 2.4. ALGORITMI SULLE LISTE COMPOSTE 29 Algoritmo 22 - maxlc a : massimo della lista composta a Input: lista semplice a Output: massimo della lista composta a 1: if isempty a then 2: return N U LL 3: else if (size a) = 1 then 4: if isatom(f irst a) then 5: return f irst a 6: else 7: return maxlc(f irst a) 8: end if 9: else 10: return max[maxlc[f irst a], maxlc(rest a) ] 11: end if Esempio 24. Una interessante operazione su una lista composta consiste nel determinare la corrispondente lista semplice formata dagli atomi della lista (operazione di flat di una lista); ad esempio la lista [ [ [x], 1, [y, z, t], 2, [3] ], appiattita, produce la lista [x, 1, y, z, t, 2, 3]. Questa operazione è descritta nell’algoritmo 23. Algoritmo 23 - f lat a : lista degli atomi della lista a Input: lista a Output: lista degli atomi di a 1: if isempty a then 2: return [ ] 3: else if isatom(f irst a) then 4: return [f irst a ∶ f lat(rest a)] 5: else 6: return f lat(f irst a) ∣ f lat(rest a) 7: end if L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 30 CAPITOLO 2. LISTE 2.5 ESERCIZI 1. Valutare le seguenti espressioni: (a) f irst[1, 2, 3] (b) rest[1, 2, 3] (c) f irst(rest[1, 2 < 3, 4]) (d) rest(f irst[ [ [1] ], [2], 3]) (e) f irst[x] = [x] (f) if [3 < 4, x, y] (g) f irst(rest[1, 2, 3]) + if [2 = 3, 4, 5] (h) [size[0] ∶ rest[1..4] ] (i) if [isatom(rest[1]), f irst[2, 3], f irst(rest[4, 5])] (j) ([ ] ∣ [x] = [x]) < (isatom[x]) 2. Risolvere la seguente equazione (nell’incognita x): [size x] = x 3. Decidere se una lista è formata da un solo elemento (eventualmente composto). 4. Analizzare le operazioni relative alle liste, discutendo quali sono totali e quali sono parziali, indicando per queste ultime la precondizione che deve essere verificata affinché l’operazione sia applicabile. Discutere quali strategie si possono adottare per il controllo che l’invocazione di tali operazioni non generi errore. 5. Sia a = [a1 , . . . , an ] una generica lista di n elementi. Stabilire per quali valori di n e per quale struttura della lista a risulta valutabile l’espressione f irst(rest(f irst(rest a))) Determinarne il valore. 6. Sia a = [a1 , . . . , an ] una generica lista di n elementi. Stabilire il valore delle seguenti espressioni: (a) size(rest a) (b) f irst(rest(rest a)) L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 2.5. ESERCIZI 31 7. Data la lista a = [x, y, z], scrivere un’espressione che produca la lista [z, y, x]. 8. Data la lista [ [a, b], c, d, [e, [f ] ], g, [h] ], scrivere delle espressioni che forniscano, rispettivamente, i valori c, f e g. 9. Data la lista a = [x], scrivere, in funzione di a, un’espressione che rappresenti la lista [ [x], x, [ ] ]. 10. Date le due liste a = [p, q, r] e b = [x, y], scrivere un’espressione coinvolgente solo a e b che, valutata, costituisca la lista [p, [x], [q, r] ]. 11. Data la lista a = [a1 , . . . , an ], determinare la lista costituita dagli elementi di a maggiori di un dato elemento x. Analogamente per una lista composta. 12. Determinare la somma degli elementi di una lista semplice numerica. Analogamente per una lista composta. 13. Data una lista semplice di numeri naturali, calcolare la sottolista dei numeri pari. Analogamente per una lista composta. 14. Data una lista semplice determinare la sottolista degli elementi di posizione pari. 15. Date due liste semplici, determinare la lista degli elementi comuni delle due liste. 16. Inserire in una lista semplice un dato elemento in una data posizione. Ad esempio, inserendo nella lista [5, 2, 6, 1, 8] l’elemento 7 alla posizione 4 si ottiene la lista [5, 2, 6, 1, 4, 8]. 17. Data una lista a = [a1 , . . . , an ], determinare la lista ottenuta da a mediante una rotazione a destra di una posizione in modo ciclico, ottenendo una lista a′ = [an , a1 , . . . , an−1 ]. 18. Data una lista a = [a1 , . . . , an ], determinare la lista ottenuta da a mediante una rotazione a sinistra di una posizione in modo ciclico, ottenendo una lista a′ = [a2 , . . . , an , a1 ]. 19. Descrivere, in modo iterativo ed in modo ricorsivo, un algoritmo per decidere se in una lista semplice è presente un dato elemento. Analogamente per una lista composta. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 32 CAPITOLO 2. LISTE 20. Decidere se una lista semplice è composta da elementi tutti uguali. 21. Decidere se due liste semplici sono uguali (basandosi sull’operatore di uguaglianza fra atomi). Le due liste devono avere la stessa lunghezza ed avere gli elementi uguali nelle stesse posizioni. 22. Decidere se una lista semplice è ordinata. 23. Eliminare i valori ripetuti in una lista semplice, mantenendo la prima occorrenza degli elementi che sono presenti più di una volta; ad esempio, data la lista [4, 1, 3, 4, 1, 1, 2, 3, 7, , 3], si deve ottenere la lista [4, 1, 3, 2, 7]. 24. Eliminare i valori ripetuti in una lista semplice ordinata; ad esempio, data la lista [3, 4, 4, 4, 6, 8, 9, 9], si deve ottenere la lista [3, 4, 6, 8, 9]. 25. Determinare il rovescio di una lista composta; ad esempio, il rovescio della lista [1, [2, 3], 4, [5] ] è la lista [ [5], 4, [2, 3], 1]. 26. Determinare il numero di atomi di una lista; ad esempio, il numero di atomi della lista [1, [2, 3], 4, [5] ] è 5. 27. Una lista semplice di numeri naturali contiene le cifre che rappresentano un numero in notazione decimale. Determinare il numero corrispondente. Ad esempio la lista [3, 7, 4] rappresenta il numero 374. 28. Una lista semplice di numeri naturali contiene le cifre che rappresentano un numero in notazione decimale. Determinare la lista corrispondente al successivo. Ad esempio, data la lista [3, 5, 9] si dovrà determinare la lista [3, 6, 0]. 29. Determinare la distanza fra due n-liste a = [a1 , ...., an ] e b = [b1 , ..., bn ] di numeri, sommando le distanze delle coppie di elementi corrispondenti: dist(a, b) = Σ∣ai − bi ] 30. Comprimere e decomprimere una lista, secondo i seguenti schemi di trasformazione di evidente interpretazione. (a) [a, b, b, c, d, d, d, d] ↔ [ [a, 1], [b, 2], [c, 1], [d, 4] ] (b) [a, b, b, c, d, d, d, d] ↔ [a, 1, b, 2, c, 1, d, 4] (c) [a, b, b, c, d, d, d, d] ↔ [a, [b, 2], c, [d, 4] ] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 33 2.5. ESERCIZI 31. Comprimere e decomprimere una lista semplice numerica, secondo lo schema di trasformazione descritto nel seguente esempio: [2, 3, 4, 5, 7, 9, 10] ↔ [ [2, 5], 7, [9, 10] ] 32. Determinare il livello (di annidamento) di una lista (ossia l’altezza dell’albero che la rappresenta); ad esempio: l’atomo 3 ha livello 0; la lista semplice [2, 4, 6] ha livello 1; la lista [1, 3, [4] ] ha livello 2; la lista [1, [3, 2, 5], [7, [6, 7] ], 8] ha livello 3. 33. Con struttura di una lista si intende la lista ottenuta sostituendo ciascun atomo con un simbolo convenzionale (∗); ad esempio la struttura della lista [a, [b, c], d, [ [e], f ] ] è la lista [∗, [∗, ∗], ∗, [ [∗], ∗] ]. Data una lista, determinarne la struttura. Due liste aventi la stessa struttura sono dette isomorfe; ad esempio sono isomorfe le seguenti due liste: [ [a], b, [ ] ], [ [1], 2, [ ] ]. Decidere se due date liste sono isomorfe. 34. Con scheletro di una lista si intende la lista in cui vengono eliminati tutti gli atomi e vengono mantenute solo le parentesi di annidamento; ad esempio lo scheletro della lista [a, [b, c], d, [ [e], f ] ] è la lista [ [ ], [ [ ] ] ]. Data una lista, determinarne lo scheletro. 35. Una lista è formata dalle coppie di elementi che sono in una relazione di equivalenza. Determinare le liste formate dalle liste di elementi della partizione indotta dalla relazione di equivalenza. 36. Decidere se una lista semplice è contenuta all’interno di un’altra lista semplice. Ad esempio la lista [3, 1, 4] è contenuta nella lista [3, 2, 3, 1, 4] ma non è contenuta nella lista [5, 3, 2, 1, 4]. Estendere al caso un cui le due liste possano essere composte. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 34 CAPITOLO 2. LISTE L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 3 Array In situazioni più complicate, la tabella potrebbe essere un array bi-dimensionale, o potrebbe essere un array n-dimensionale per valori più elevati di n; potrebbe essere una struttura ad albero, rappresentante relazioni gerarchiche o suddivisioni; o potrebbe essere una complessa struttura con molti legami con un grande numero di interconnessioni come possiamo trovare in un cervello umano. D. Knuth, The Art of Computer Programming Gli array costituiscono le forme più frequenti ed utilizzate di sequenze. Sono caratterizzati dal metodo di accesso ad indice nella forma a[i]. Costituiscono una forma di composizione molto duttile e molto utilizzata essendo di supporto a molti importanti algoritmi (ricerca, ordinamento, . . . ). 35 36 3.1 CAPITOLO 3. ARRAY GLI ARRAY Qualora una sequenza venga elaborata mediante un particolare operatore di accesso, come descritto nella seguente definizione, viene detta array. DEFINIZIONE 3. Gli array sono delle sequenze caratterizzate dall’operatore di selezione ad indice: se a = [a1 , . . . , an ] è una sequenza di elementi ed e è un’espressione numerica naturale che, valutata, vale il valore naturale i, allora a[e] denota l’i-esimo elemento della sequenza a, ossia ai . Per indicare un array il cui generico elemento è ai si usa spesso la notazione [ai ]. L’operatore di accesso mediante indice viene usato anche per modificare una sequenza, secondo la seguente sintassi: se a è una sequenza, e un’espressione che, valutata, vale il numero naturale i ed esp è una generica espressione, con la sintassi a[e] ← esp si ottiene l’effetto di modificare l’i-esimo elemento della sequenza a, assegnandogli il valore ottenuto dalla valutazione dell’espressione esp. Esempio 25. Esempi di assegnazioni sugli array: a ← [0]8 a[1] ← 4 a[a[1] ] ← 6 − a[1] Per esercizio si chiede di descrivere il contenuto dell’array a alla fine della sequenza di istruzioni sopra elencate. 3.2 ELABORAZIONE DI ARRAY Lo schema generale di elaborazione di un array a di dimensione n è descritto dall’algoritmo 24. Algoritmo 24 - Elaborazione di un array a di dimensione n Input: array a = [a1 , . . . , an ] 1: for i from 1 to n do 2: elabora l’elemento ai 3: end for L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 3.3. ARRAY E LISTE 37 Gli esempi che seguono illustrano l’applicazione dell’algoritmo 24. Esempio 26. Il seguente algoritmo calcola la somma degli elementi di un array. Algoritmo 25 - somma(a) : somma degli elementi dell’array a Input: array a = [a1 , . . . , an ] Output: a1 + ⋅ ⋅ ⋅ + an 1: s ← 0 2: for i from 1 to size(a) do 3: s ← s + ai 4: end for 5: return s Esempio 27. A seguire è riportato un algoritmo per la determinazione del massimo e del minimo di un array. Algoritmo 26 - minmax(a) : minimo e massimo dell’array a Input: array a = [a1 , . . . , an ] Output: [min(a), max(a)] 1: min ← a[1] 2: max ← min 3: for i from 2 to size(a) do 4: if a[i] < min then 5: min ← a[i] 6: else if a[i] > max then 7: max ← a[i] 8: end if 9: end for 10: return [min, max] 3.3 ARRAY E LISTE Array e liste sono delle strutture aggregative equivalenti dal punto di vista strutturale (sono entrambe delle sequenze), ma differenziate dal diverso meccanismo di accesso e manipolazione degli elementi componenti: gli array vengono elaborati modificandone direttamente gli elementi, secondo un’impostazione tipicamente procedurale-imperativa; le liste vengono elaborate in modo L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 38 CAPITOLO 3. ARRAY funzionale (e spesso ricorsivo) mediante delle funzioni che ritornano liste come risultato. Tale differenza fra array e liste non è solo formale e superficiale ma induce una profonda differenza sui meccanismi algoritmici per la loro elaborazione. Benché gli operatori di accesso agli elementi di un array e di una lista siano utilizzabili contemporaneamente, si preferisce utilizzarli in modo esclusivo. Il tipico metodo di elaborazione di un array avviene come segue: si definisce inizialmente l’array di una data dimensione; successivamente l’array viene elaborato mediante l’uso combinato dell’operatore di accesso e dell’operatore di assegnazione. Invece, il tipico metodo di elaborazione di una lista può essere descritto come segue: a partire dalla lista vuota [ ], mediante l’uso combinato degli operatori di lista, si ottiene una lista risultato. Gli operatori di accesso degli array e delle liste sono fra loro legati; ad esempio: a[1] equivale a f irst a a[2] equivale a f irst(rest a) a[2 .. size a] equivale a rest a a[1] ← equivale a a ← [x] ∣ rest a) In alcuni casi le operazioni su array e liste possono essere usate contemporaneamente, come evidenziato nel seguente esempio. Esempio 28. Lista delle dimensioni di un array : 0 per elemento non array, [n] per un array di dimensione n, [2, 3] per un array 2 × 3. Algoritmo 27 - dimensioni(a) : lista delle dimensioni della lista a Input: array a Output: lista delle dimensioni di a 1: if isatom a then 2: return [ ] 3: else 4: return [size a] ∣ dimensioni(a1 ) 5: end if L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 39 3.4. LE MATRICI 3.4 LE MATRICI Gli array possono essere composti da elementi che sono a loro volta array (solitamente tutti della stessa lunghezza). Tali array vengono comunemente detti matrici (bidimensionali). Esempio 29. L’array [ [2, 3, 7], [1, 1, 4] ] costituisce una matrice bidimensionale. Una tale matrice viene usualmente descritta in forma tabellare come segue: 2 1 3 1 7 4 Se a denota una matrice bidimensionale, coerentemente con la notazione usata per l’operatore di selezione ad indice, con la notazione a[i] si denota l’array costituito dall’i-esima riga della matrice a, mentre con a[i][j] si denota il j-esimo elemento dell’i-esima riga della matrice a. Per semplicità di scrittura, l’elemento a[i][j] viene spesso indicato con la notazione aij . Per indicare una matrice il cui generico elemento è aij si usa spesso la notazione [aij ]. Con la scrittura a[i][j] ← esp si ottiene l’effetto di assegnare all’elemento a[i][j] il valore ottenuto dalla valutazione dell’espressione esp. Esempio 30. Esempi di operazioni sulle matrici: a ← [ [0]4 ]3 a[1][2] ← 5 Per esercizio si chiede di descrivere il contenuto della matrice a alla fine delle istruzioni sopra elencate. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 40 3.5 CAPITOLO 3. ARRAY ELABORAZIONE DI MATRICI Lo schema generale di elaborazione di una matrice bidimensionale a di m righe ed n colonne è descritto nell’algoritmo 28. Algoritmo 28 - Elaborazione di una matrice a = [aij ] di dimensioni m × n Input: matrice a di dimensione m × n 1: for i from 1 to m do 2: for j from 1 to n do 3: elabora l’elemento aij 4: end for 5: end for Consideriamo ora delle operazione fra matrici numeriche, operazioni che si presentano in moltissime applicazioni e per questo frequentemente eseguite sugli elaboratori. DEFINIZIONE 4. Siano a = [aij ], b = [bij ] due matrici numeriche m × n. Si definisce come somma a + b fra le due matrici a e b la matrice c = [cij ] di dimensioni m × n il cui generico elemento cij è definito da cij = aij + bij L’operazione di somma fra matrici richiede che le due matrici abbiano le stesse dimensioni. Quando due matrici soddisfano a questo vincolo sulle dimensioni si dice che le due matrici sono somma-compatibili. Adattando a questa definizione lo schema generale dell’algoritmo 28, il calcolo della somma fra due matrici numeriche può essere descritto mediante l’algoritmo 29. Algoritmo 29 - Somma fra matrici Input: matrici a e b di dimensione m × n Output: matrice c = a + b 1: for i from 1 to m do 2: for j from 1 to n do 3: cij ← aij + bij 4: end for 5: end for 6: return c = [cij ] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 41 3.5. ELABORAZIONE DI MATRICI L’operazione di moltiplicazione fra matrici si basa sulla seguente DEFINIZIONE 5. Sia a = [aij ] una matrice numerica m × n e b = [bij ] una matrice numerica n × p. Si definisce come prodotto a ∗ b fra le due matrici a e b la matrice c = [cij ] di dimensioni m × p il cui generico elemento cij è definito da n cij = ∑ aik bkj k=1 L’operazione di prodotto richiede che il numero di colonne della prima matrice sia uguale al numero di righe della seconda. Due matrici che soddisfino a questo vincolo sulle dimensioni vengono dette prodotto-compatibili. Spesso l’operazione di moltiplicazione fra due matrici a e b viene indicata semplicemente giustapponendo le due matrici, con la notazione ab. Il calcolo del prodotto fra due matrici numeriche, basato direttamente sulla definizione data sopra, è descritto dall’algoritmo 30. Algoritmo 30 - Prodotto fra matrici Input: matrici a di dimensione m × n e b di dimensione n × p Output: matrice c = a ∗ b (di dimensione m × p) 1: for i from 1 to m do 2: for j from 1 to n do 3: cij ← 0 4: for k from 1 to n do 5: cij ← cij + aik bkj 6: end for 7: end for 8: end for 9: return c = [cij ] Esempio 31. Applicando l’algoritmo 30 si ricava il seguente prodotto: 3 1 0 2 2 4 ∗ 2 0 1 5 1 3 2 3 = 7 3 5 18 2 6 4 6 8 12 10 22 L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 42 3.6 CAPITOLO 3. ARRAY SOLUZIONE DI UN SISTEMA LINEARE Sia A una matrice quadrata di dimensioni n × n e b un vettore n-dimensionale. A seguire è riportato l’algoritmo per la soluzione di un sistema lineare A x = b mediante il metodo di riduzione di Gauss con pivoting parziale. Il procedimento si basa sulla trasformazione del sistema Ax = b in un sistema equivalente (avente la stessa soluzione) riducendo la matrice A ad una matrice avente tutti 0 nel triangolo inferiore sinistro. La seconda fase del procedimento consiste nel calcolo a ritroso (xn , xn−1 , . . . , x1 ) delle incognite. Questo procedimento è descritto nell’algoritmo 31. Algoritmo 31 - Soluzione del sistema lineare Ax = b Input: matrice A di dimensioni n × n, vettore b di dimensione n Output: vettore x = [xi ] (di dimensione n) delle incognite 1: // riduzione a 0 degli elementi del triangolo inferiore sinistro 2: for j from 1 to n − 1 do 3: ricerca della riga p del pivot 4: scambio delle righe j ↔ p 5: riduzione a 0 degli elementi della j-esima colonna 6: end for 7: // soluzione del sistema con valutazione a ritroso delle incognite xi 8: for i from n downto 1 do 9: calcola xi a partire dalla conoscenza di xi+1 , . . . , xn 10: end for 11: return vettore x = [xi ] L’algoritmo 31 può essere ulteriormente raffinato come descritto nell’algoritmo 32. In entrambi questi algoritmi si assume l’ipotesi che il sistema Ax = b ammetta un’unica soluzione. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 3.6. SOLUZIONE DI UN SISTEMA LINEARE 43 Algoritmo 32 - Soluzione del sistema lineare Ax = b Input: matrice A di dimensione n × n, vettore b di dimensione n Output: vettore x = [xi ] (di dimensione n) delle incognite 1: // riduzione a 0 degli elementi del triangolo inferiore sinistro 2: for j from 1 to n − 1 do 3: // ricerca della riga p del pivot 4: p←j 5: for k from j + 1 to n do 6: if ∣akj ∣ > ∣apj ∣ then 7: p←k 8: end if 9: end for 10: // scambio delle righe j ↔ p 11: if j ≠ p then 12: for k from 1 to n do 13: scambia ajk ↔ apk 14: end for 15: scambia bj ↔ bp 16: end if 17: // riduzione a 0 degli elementi della j-esima colonna 18: for i from j + 1 to n do 19: f ← aij /ajj 20: for k from 1 to n do 21: aik ← aik − ajk ∗ f 22: end for 23: bi ← bi − bj ∗ f 24: end for 25: end for 26: // soluzione del sistema con valutazione a ritroso delle incognite 27: for i from n downto 1 do 28: s←0 29: for k from i + 1 to n do 30: s ← s + aik ∗ xk 31: end for 32: xi ← (bi − s)/aii 33: end for 34: return vettore x = [xi ] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 44 CAPITOLO 3. ARRAY 3.7 ESERCIZI 1. Valutare le seguenti espressioni coinvolgenti gli array: (a) [a, b, c, d] [3] (b) [ [a, b, c], [x, y] ] [2] [1] (c) [ [a, b, c]2 ][1] [5] (d) [ [1, 2, 3]4 [5] ]6 2. Scrivere delle espressioni che producano i seguenti array di dimensione 16 (si consideri che viene assunta l’associatività a sinistra degli operatori sugli array): (a) [a, a, a, a, a, a, a, a, a, a, a, a, a, a, a, a] (b) [a, b, a, b, a, b, a, b, a, b, a, b, a, b, a, b] (c) [a, a, b, b, a, a, b, b, a, a, b, b, a, a, b, b] (d) [a, a, a, a, a, a, a, a, b, b, b, b, b, b, b, b] 3. Determinare il massimo di un array di numeri. 4. Determinare le posizioni del massimo di un array di numeri. 5. Determinare, mediante un’unica scansione, il minimo ed il massimo elemento presenti in un array di numeri. 6. Determinare il massimo e la sua frequenza, in un array di numeri. 7. Determinare il massimo e le sue posizioni, in un array di numeri. 8. Determinare il prodotto degli elementi di un array di numeri. 9. Determinare i numeri primi presenti in un array di numeri naturali. 10. Determinare il massimo comune divisore ed il minimo comune multiplo di un array di numeri naturali. 11. Decidere se all’interno di un dato array è presente un dato valore. 12. Determinare la frequenza con la quale compare un dato valore in un array. 13. Determinare il risultato dell’operazione and eseguita su tutti gli elementi di un array di valori booleani. Analogamente per l’operatore or. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 45 3.7. ESERCIZI 14. Determinare la media degli elementi di un array di numeri. Si assuma l’ipotesi che l’array contenga almeno un elemento. 15. Determinare la media degli elementi di un array numerico, escludendo dalla media i valori uguali a zero. 16. Determinare il valore dell’elemento più vicino alla media degli elementi di un array di numeri. Ad esempio, l’elemento più vicino alla media 3.2 dell’array [2, 7, 1, 4, 2] è 4. 17. Valutare la media k-filtrata, dei valori di un array numerico di dimensione n ottenuta facendo la media aritmetica degli n − k elementi dell’array scartando i k elementi più lontani dalla media aritmetica dell’array originale. 18. Dato un array numerico a di dimensione n, valutare lo scarto quadratico medio di a, definito dalla seguente espressione: ¿ Á n Á À( ∑ (ak − m)2 ) /n k=1 essendo m la media dei valori dell’array. 19. Sia a un array di n di numeri reali positivi e w un array di n numeri reali non negativi (detti pesi) tali che ∑nk=1 wk = 1. Calcolare i valori delle seguenti medie pesate: n A[a, w] = ∑ ak wk (media aritmetica) k=1 H[a, w] = 1 w ∑nk=1 akk n G[a, w] = ∏ (ak )wk (media armonica) (media geometrica) k=1 Q[a, w] = ¿ Án Á À ∑ a k wk (media quadratica) k=1 Dimostrare che valgono le disuguaglianze H ≤ G ≤ A ≤ Q. 20. Ridurre una frazione ai minimi termini; ad esempio: 18/24 → 3/4. Suggerimento: rappresentare la frazione n/d mediante un array [n, d]. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 46 CAPITOLO 3. ARRAY 21. Scomporre una frazione in una frazione mista; ad esempio: 17/3 → 5+2/3. 22. Date due frazioni, determinare la frazione somma, riducendo il risultato ai minimi termini. 23. Dato un numero decimale (anche periodico), determinare la sua frazione generatrice. 24. Data una frazione, determinare se essa rappresenta un numero decimale finito, un numero decimale periodico semplice oppure un numero decimale periodico misto. 25. Un array a contiene i distacchi parziali dei vari concorrenti in una gara, ossia a[i] rappresenta il distacco del concorrente i-esimo dal concorrente (i − 1)-esimo (a[1] = 0 per il primo concorrente). Determinare l’array contenente i distacchi complessivi dal primo concorrente. 26. Rovesciare un array in modo che il primo elemento venga scambiato con l’ultimo, il secondo con il penultimo e così via. 27. Definire, in modalità funzionale ed in modalità procedurale, l’interfaccia di un blocco equivalente all’assegnazione s[k] ← c essendo s una stringa, k un numero naturale, s[k] il k-esimo carattere della stringa s e c un generico carattere. È consentita l’assegnazione fra stringhe. 28. Dire qual è l’effetto della seguente porzione di algoritmo agente su una matrice quadrata a di dimensione n: 1: 2: 3: 4: 5: for i from 1 to n do for j from 1 to n do aij ← aji end for end for 29. Completare la seguente porzione di algoritmo, mettendo delle opportune espressioni naturali (in funzione di i) al posto degli indici h e k, in modo da elaborare tutti gli elementi di una matrice a di dimensioni m × n: L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 47 3.7. ESERCIZI for i from 1 to m ∗ n do 2: elabora l’elemento ahk 3: end for 1: 30. Costruire delle matrici rettangolari secondo i seguenti schemi: 0 0 0 0 0 0 1 1 1 0 0 1 1 1 0 0 1 1 1 0 0 1 1 1 0 0 0 0 0 0 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 31. Costruire una matrice quadrata di ordine n con gli n2 numeri naturali 1, 2, 3, . . . , n2 , secondo gli schemi sotto riportati: 1 3 4 10 11 2 5 9 12 19 6 8 13 18 20 7 14 17 21 24 15 16 22 23 25 21 20 19 18 17 22 7 6 5 16 23 8 1 4 15 24 9 2 3 14 25 10 11 12 13 32. Costruire delle matrici di numeri secondo i seguenti formati, riferiti a matrici di dimensioni m = 5, n = 6: 11 21 31 41 51 12 22 32 42 52 13 23 33 43 53 14 24 34 44 54 15 25 35 45 55 16 26 36 46 56 1 7 13 19 25 2 8 14 20 26 3 9 15 21 27 4 10 16 22 28 5 11 17 23 29 6 12 18 24 30 33. Costruire una matrice di numeri come illustrato dallo schema che segue, riferito ad una matrice di dimensioni m = 5, n = 14: 0 1 2 3 4 1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 4 5 6 7 8 5 6 7 8 9 6 7 8 9 0 7 8 9 0 1 8 9 0 1 2 9 0 1 2 3 0 1 2 3 4 1 2 3 4 5 2 3 4 5 6 3 4 5 6 7 L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 48 CAPITOLO 3. ARRAY 34. Costruire una matrice 5 × 10 con numeri casuali secondo le regole delle estrazioni del lotto. 35. Data una matrice quadrata, scrivere delle porzioni di algoritmo per riempire la matrice con i valori 0 e 1, mettendo il valore 1 (e 0 altrove) secondo i seguenti schemi: (a) sulla diagonale principale (b) sulla diagonale secondaria (c) su entrambe le diagonali (d) sui bordi della matrice (e) sul triangolo superiore destro (f) sul triangolo superiore sinistro (g) sul triangolo inferiore destro (h) sul triangolo inferiore sinistro (i) sulle righe di indice pari (j) sulle colonne di indice pari 36. Costruire una matrice quadrata n × n con i numeri naturali costituenti la tavola pitagorica della moltiplicazione. 37. Costruire una matrice quadrata n × n che costituisca la tabella di addizione modulo n. 38. Costruire una matrice quadrata di ordine n dispari con i numeri con i numeri naturali 1, 2, ..., n2 posizionando il numero 1 al centro e poi proseguendo a spirale in senso antiorario. 39. Costruire una matrice quadrata di numeri naturali con i numeri 1, 2, ..., n2 in modo da formare un quadrato magico. 40. Realizzare le seguenti operazioni matematiche sulle matrici: addizione, moltiplicazione, opposta, inversa, trasposta, determinante, discutendo nei vari casi quali devono essere le dimensioni delle matrici affinché l’operazione sia possibile. 41. Data una tabella bidimensionale 24 × 365 contenente le temperature di ogni ora dei giorni di un dato anno, determinare il giorno dell’anno avente la temperatura media più bassa; determinare il giorno avente la massima escursione termica. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 49 3.7. ESERCIZI 42. Sia m una matrice numerica di dimensioni 12 × 31 in cui nel generico elemento m[i][j] è registrato il fatturato di una azienda relativo al mese i-esimo ed al giorno j-esimo (ad esempio, m[3][27] contiene il fatturato relativo al giorno 27 marzo). Determinare il mese in cui si è avuto il maggior fatturato complessivo. 43. Determinare l’indice di riga di una matrice per il quale la somma degli elementi è massima. Questo problema costituisce una generalizazzione del problema precedente. 44. Su un quotidiano economico si trova la seguente tabella, dove la prima colonna indica il nome della provincia, la seconda il miglioramento/declassamento in classifica rispetto all’anno precedente. L’indice di posizione denota la posizione di classifica attuale. città Belluno Aosta Bolzano posizione +1 +1 −2 ... Figura 3.1: Tabella della classifica. Definire un tipo di dato per rappresentare la tabella. Decidere se una tabella è compatibile. Nell’ipotesi di tabella compatibile, ricostruire in-loco la posizione di classifica dell’anno precedente. 45. Determinare in una matrice bidimensionale binaria il rettangolo di maggior superficie composto da tutti 1. 46. Determinare il valore minimo ed il valore massimo fra quelli presenti in una matrice numerica. 47. Determinare il minimo ed il massimo e la loro frequenza, in una matrice numerica. 48. Decidere se all’interno di una matrice è presente un dato elemento. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 50 CAPITOLO 3. ARRAY 49. Determinare quante volte un dato elemento è presente all’interno di una matrice. 50. Decidere se una matrice è composta da elementi tutti uguali. 51. Determinare l’indice di riga di una matrice numerica, la cui somma degli elementi è massima. 52. Decidere se in una data matrice esistono due righe uguali. 53. Determinare il prodotto degli elementi di una matrice di numeri. Ottimizzare il procedimento, terminando il calcolo quando si incontra un elemento di valore zero. 54. Contare il numero di numeri primi presenti in una matrice. 55. Costruire una matrice quadrata ad anelli concentrici, mettendo 1 sull’anello esterno, 2 nel secondo anello e così via. 56. Ricercare se e dove è presente un dato valore in una matrice. 57. Determinare l’elemento che compare con maggiore frequenza in una matrice. 58. Determinare la frequenza dell’elemento che compare con maggiore frequenza in una matrice. 59. Una matrice è ordinata per righe se ogni riga, indipendentemente dalle altre, è ordinata. Decidere se una data matrice è ordinata per righe. Ordinare per righe una matrice numerica. 60. Una matrice rettangolare contiene numeri naturali estratti da un’urna; il valore 0 rappresenta la non presenza di un valore non estratto. Decidere se due dati numeri formano un ambo, ossia se sono entrambi presenti in una stessa riga della matrice. 61. Ricercare se in una matrice di caratteri è presente (orizzontalmente o verticalmente) una data parola. 62. Decidere se una data matrice quadrata costituisce un quadrato magico. 63. Decidere se una data matrice numerica 9 × 9 è corretta in base alle regole del gioco del sudoku. 64. Contare il numero di elementi distinti presenti in una matrice. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 3.7. ESERCIZI 51 65. Ruotare una matrice quadrata di 90o a destra rispetto al suo centro. 66. Data una matrice di caratteri composta dai soli caratteri ’ ’ (spazio) e ’*’ (muro), decidere se esiste un percorso che attraversa la matrice da una casella del bordo ad un’altra casella del bordo (eventualmente anche sullo stesso lato). 67. Data una matrice di caratteri ’ ’ (blanc)’ e ’*’, determinare il numero di isole presenti, interpretando il carattere ’*’ come terra ed il carattere ’ ’ come acqua. 68. Realizzare un risolutore del gioco del sudoku. 69. Data una matrice 3 × N contenenti le dimensioni (larghezza,altezza, profondità di N scatole a forma di parallelepipedo, determinare le dimensioni della più piccola scatola in grado di contenerle tutte (una alla volta). 70. Risolvere con un metodo a piacere un sistema lineare della forma A x = b, essendo A la matrice quadrata dei coefficienti e b il vettore dei termini noti. 71. In una matrice a un elemento aij dicesi punto di sella se esso è maggiore di tutti gli elementi dell’i-esima riga e minore di tutti gli elementi della j-esima colonna, oppure, simmetricamente, minore di tutti gli elementi dell’i-esima riga e maggiore di tutti gli elementi della j-esima colonna. Determinare i punti di sella di una matrice numerica. 72. In un’aula rettangolare si devono predisporre in file allineate delle postazioni per svolgere un esame. Per evitare possibili copiature vengono predisposte 4 prove distinte, individuate mediante i numeri 1, 2, 3, 4. Per rappresentare la disposizione dell’aula ed il numero della prova corrispondente a ciascuna postazione viene utilizzata una matrice. Riempire una matrice rettangolare con i numeri 1, 2, 3, 4 in modo che caselle contigue non contengano valori uguali; preferibilmente fare in modo tale che, se due elementi sono uguali, le due corrispondenti caselle abbiano una distanza, secondo la metrica Manhattan, la maggiore possibile. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 52 CAPITOLO 3. ARRAY L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 4 Complessità Computazionale Come la teoria della ricorsività, tracciando una linea di separazione tra ciò che una macchina può fare e ciò che non può fare, costituisce un primo livello di approssimazione allo studio delle proprietà del calcolo automatico, così la teoria della complessità costituisce un secondo livello di approssimazione poiché consente di caratterizzare ciò che una macchina può fare in una determinata quantità di tempo e di memoria. G.Ausiello, Complessità di calcolo delle funzioni Il tempo di esecuzione di un dato programma dipende da diversi fattori: velocità di elaborazione del calcolatore, quantità dei dati da elaborare, grandezza della memoria a disposizione, linguaggio di programmazione utilizzato. La complessità computazionale si pone l’obiettivo di analizzare in generale la qualità degli algoritmi, a prescindere da tutti i fattori tecnici sopra elencati che risultano marginali; in particolare lo studio è finalizzato a ripartire gli algoritmi in algoritmi efficienti ed algoritmi non efficienti. Questa classificazione degli algoritmi induce un’equivalente ripartizione dei problemi: problemi trattabili, ossia risolvibili in tempi ragionevoli e problemi intrattabili, ossia problemi che, qualsiasi algoritmo si adotti, superano la potenza di calcolo di qualsiasi calcolatore esistente e futuro. La vaghezza di questa linea di ripartizione è solo apparente in quanto esiste, in realtà, un abisso di separazione fra le due classi. 53 54 4.1 CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE LA COMPLESSITÀ COMPUTAZIONALE I criteri in base ai quali stabilire la qualità di un algoritmo possono basarsi sulle caratteristiche intrinseche dell’algoritmo (lunghezza, leggibilità) oppure sulle risorse richieste dall’algoritmo (tempo di CPU impiegato, memoria RAM occupata). La complessità computazionale studia gli algoritmi in relazione alla quantità di risorse impiegate. Il parametro più caratterizzante e significativo nella valutazione della complessità di un algoritmo è costituito dal tempo di elaborazione impiegato per l’esecuzione dell’algoritmo. Il parametro tempo risulta ancora poco espressivo in quanto fa riferimento ad una particolare esecuzione di una istanza del problema su una data macchina. È chiaramente poco significativa, infatti, una frase del tipo: L’algoritmo di ordinamento A impiega 6.2 secondi ad essere eseguito in quanto non è precisato né il numero di elementi da ordinare, né la loro disposizione iniziale, né la macchina sulla quale viene eseguito l’algoritmo. Inoltre, un tale approccio al problema richiederebbe sempre delle prove per poter confrontare due algoritmi. Quando il tempo di esecuzione è elevato (un’ora o addirittura un secolo!) tale approccio pragmatico non è più chiaramente attuabile. Ed è proprio in questi casi estremi che è utile sapere (senza provare!) la complessità di un algoritmo per stimare il tempo di esecuzione, senza eseguire l’algoritmo. Ad esempio, se si dispone di un algoritmo per il calcolo del determinante di una matrice si vorrebbe sapere in anticipo quant’è approssimativamente il tempo che sarà necessario per eseguire l’algoritmo. Dalle osservazioni precedenti si deduce che il problema della valutazione della complessità degli algoritmi non ammette una soluzione di tipo pragmatico della forma ”prova e vedi” ma deve essere affrontato sulla carta. È necessario pertanto astrarre le considerazioni senza fare riferimento ad alcuna macchina particolare. Questo passaggio è facilmente attuabile se si assume l’ipotesi (plausibile) che il tempo di esecuzione di un imprecisato numero k di operazioni elementari è proporzionale a k e che il numero k sia una funzione f crescente della misura n dei dati da elaborare. Pertanto, tale funzione f (n) può essere assunta come misura del tempo impiegato, per le varie istanze del problema aventi valori diversi di n. Per quanto riguarda la misura della complessità dell’input su cui deve lavorare l’algoritmo è sufficiente definire un valore che esprime la dimensione del problema. Per quanto riguarda i valori particolari che può assumere un input di data lunghezza, si considerano le seguenti situazioni: caso ottimo, caso medio, caso pessimo. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 4.2. OPERAZIONE DOMINANTE 4.2 55 OPERAZIONE DOMINANTE La complessità computazionale di un algoritmo rappresenta una stima del tempo di elaborazione impiegato per l’esecuzione dell’algoritmo; tale stima del tempo viene indirettamente ottenuta contanto il numero di passi elementari svolti nell’esecuzione dell’algoritmo. Questa valutazione indiretta permette di svincolare la misura del tempo impiegato senza riferirsi ad una particolare macchina. Questa trasposizione dalla misura del tempo al conteggio dei passi elementari non costituisce comunque un concetto assoluto in quanto dipende da cosa si intende per passo elementare; ed inoltre dipende anche dal modo con cui si rappresentano i dati sui quali opera l’algoritmo. Il concetto di passo elementare risulta perfettamente definito qualora si consideri l’esecutore costituito da una macchina di Turing: in questo caso il passo elementare è rappresentato da una transizione di stato dell’automa ossia dall’esecuzione di una quintupla. Poiché gli algoritmi vengono descritti con formalismi diversi dalle quintuple di una macchina di Turing, risulta necessario estendere il concetto di passo elementare ad altri formalismi. Con algoritmi ad un più alto livello si incontrano dei passi elementari di diversa natura e computazionalmente non uniformabili; ci possono essere delle assegnazioni, delle addizioni, delle moltiplicazioni, delle valutazioni di condizioni. Per semplificare il calcolo della complessità è stato introdotto il concetto di operazione dominante. DEFINIZIONE 6. Si chiama operazione dominante o caratteristica di un algoritmo l’operazione che viene eseguita con maggiore frequenza; nel caso di operazioni eseguite con la stessa frequenza si considera quella più onerosa in termini di tempo impiegato per la sua esecuzione. Esempio 32. Nella tabella che segue sono riportati alcuni problemi e la corrispondente operazione dominante. Problema calcolo del fattoriale scomposizione di un numero ricerca in un array ordinamento di un array moltiplicazione fra due matrici torre di Hanoi commesso viaggiatore Operazione dominante moltiplicazione fra due numeri calcolo del resto della divisione confronto fra due elementi confronto fra due elementi moltiplicazione fra due numeri spostamento di un disco calcolo della lunghezza di un percorso Tabella 4.1: Problemi e corrispondenti operazioni dominanti. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 56 CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE 4.3 DIMENSIONE DI UN PROBLEMA Un problema, o più propriamente una classe di problemi è costituita da molte istanze di problemi il cui tempo di risoluzione dipende dalla grandezza dell’input, come precisato nella definizione che segue. DEFINIZIONE 7. La dimensione di un problema è un numero naturale che esprime un’opportuna (rispetto all’operazione dominante) misura dei dati di input del problema. Nonostante l’apparente vaghezza del termine opportuna che compare nella definizione precedente, la dimensione di un problema è facilmente identificabile, come emerge dall’esempio che segue. Esempio 33. La seguente tabella descrive la dimensione di alcuni importanti problemi. Problema calcolo del fattoriale scomposizione di un numero ricerca in un array ordinamento di un array moltiplicazione fra due matrici torre di Hanoi commesso viaggiatore Dimensione del problema numero di cui si calcola il fattoriale numero di cifre del numero numero di elementi dell’array numero di elementi dell’array dimensione delle matrici numero di dischi da spostare numero di città da visitare Tabella 4.2: Problemi e corrispondenti dimensioni. 4.4 COMPLESSITÀ DI UN ALGORITMO In funzione dell’operazione dominante e della dimensione di un problema è possibile definire una misura del tempo impiegato da un algoritmo. Il concetto è precisato dalla seguente definizione. DEFINIZIONE 8. La complessità di un algoritmo è una funzione che esprime il numero di operazioni dominanti in funzione della dimensione del problema. Esempio 34. Il problema della moltiplicazione fra due matrici quadrate di lato n ha dimensione n. L’operazione dominante è la moltiplicazione. Il tradizionale algoritmo di moltiplicazione righe-per-colonne ha complessità n3 in quanto richiede l’esecuzione di n moltiplicazioni per il calcolo di ciascuno degli n2 elementi della matrice risultante. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 4.5. COMPLESSITÀ DI UN PROBLEMA 4.5 57 COMPLESSITÀ DI UN PROBLEMA In funzione del concetto di complessità di un algoritmo è possibile stabilire la complessità di un problema, come precisato nella seguente definizione. DEFINIZIONE 9. La complessità di un problema è la complessità del miglior algoritmo che risolve il dato problema. Esempio 35. Il problema dell’ordinamento di un array di dimensione n ha complessità, nel caso medio, pari a n log n, in quanto esistono algoritmi di ordinamento che hanno una tale complessità; d’altra parte si può dimostrare che, in generale, non si può ordinare un array con meno di n log n confronti. 4.6 COMPLESSITÀ ASINTOTICA La complessità di un algoritmo e di un problema risulta importante e decisiva per valori grandi della dimensione del problema. Si danno le seguenti definizioni. DEFINIZIONE 10. Date due funzioni f e g, reali a valori reali, si dice che la funzione f è O grande di g, e si scrive f è O(g), se esiste una costante reale c > 0 ed un numero reale x0 tale che per x ≥ x0 si abbia f (x) ≤ c g(x). DEFINIZIONE 11. La complessità asintotica di un algoritmo o di un problema è la complessità per valori grandi della dimensione del problema; viene denotata con la notazione O (o grande). Esempio 36. L’affermazione: Il problema della Torre di Hanoi ha una complessità computazionale asintotica pari a O(2n ). significa che per spostare n dischi da un piolo ad un altro, per n grande, si esegue un numero di spostamenti proporzionale a 2n . Esempio 37. Il problema della moltiplicazione di due matrici quadrate di ordine n ha una complessità non nota. Il tradizionale algoritmo di moltiplicazione righe-per-colonne ha una complessità asintotica pari a n3 ; il migliore algoritmo attualmente conosciuto ha una complessità asintotica pari a n2,78 ma attualmente non si conosce una limitazione inferiore per la complessità asintotica degli algoritmi di moltiplicazione di matrici. Esempio 38. La seguente tabella riporta una lista di problemi e le loro corrispondenti complessità asintotica nel caso medio. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 58 CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE Problema ricerca in un array ordinato ricerca in un array non ordinato ordinamento di un array moltiplicazione fra due matrici commesso viaggiatore 4.7 Complessità asintotica del problema O(log n) O(n) O(n log n) non è nota O(n!) ESEMPI VARI Esempio 39. Consideriamo il problema di decidere se un array è composto da elementi tutti distinti. Una soluzione di questo problema può basarsi sulla seguente idea: Confrontare ciascun elemento con tutti gli elementi che lo precedono. L’algoritmo basato su questa idea è il seguente: Algoritmo 33 - Decisione se un array è composto da elementi distinti Input: array a Output: T RU E se e solo se (a[i] ≠ a[j] per ogni i, j (i ≠ j)) 1: distinti ← T RU E 2: i ← 2 3: while distinti AN D i ≤ size a do 4: ricerca se a[i] è presente nella porzione di array a[1...i − 1] 5: if è presente then 6: distinti ← F ALSE 7: else 8: i++ 9: end if 10: end while 11: return distinti Il problema di ricerca della linea 4 può essere risolto efficientemente adottando una ricerca con sentinella sulla porzione di array a[1..i]. Si lascia per esercizio il calcolo della complessità. Esempio 40. Analizziamo la complessità del seguente schema di algoritmo, dove n denota la dimensione del problema ed op l’operazione dominante. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 59 4.7. ESEMPI VARI 1: 2: 3: 4: 5: 6: 7: 8: for i from 1 to n do for j from i + 1 to n do op end for end for for i from 1 to 2 ∗ n do op end for La complessità consiste nel numero di operazioni op che vengono eseguite dall’algoritmo ed è pari a ((n − 1) + (n − 2) + ⋯ + 1) + 2n = n(n − 1)/2 + 2n = (n2 + 3n)/2 Di conseguenza la complessità asintotica è pari a O(n2 ) Esempio 41. Consideriamo il problema di determinare l’ampiezza del più piccolo intervallo contenente tutti i numeri di un array. Per risolvere il problema è sufficiente determinare la differenza fra il valore massimo ed il valore minimo; a seguire è riportato l’algoritmo basato su questa semplice idea. Algoritmo 34 - Calcolo dell’ ”ampiezza” di un array Input: array a Output: ampiezza dell’intervallo dei valori di a 1: min ← a[1] 2: max ← a[1] 3: for i from 2 to size a do 4: if a[i] < min then 5: min ← a[i] 6: else if a[i] > max then 7: max ← a[i] 8: end if 9: end for 10: return max − min In questo caso n costituisce la dimensione del problema ed il confronto costituisce l’operazione dominante. Nel caso pessimo la complessità vale 2(n− 1) e quindi la complessità asintotica vale O(n). L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 60 CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE Esempio 42. Scriviamo un algoritmo per determinare la frequenza degli elementi di un array aventi la massima frequenza. Algoritmo 35 - Calcolo della frequenza massima Input: array a di dimensione n Output: massima frequenza degli elementi di a 1: f max ← 0 2: for i from 1 to n do 3: f ← frequenza di a[i] 4: if f > f max then 5: f max ← f 6: end if 7: end for 8: return f max La complessità della funzione frequenza è n; essendo che questa funzione viene richiamata n volte all’interno del ciclo for, si conclude che l’algoritmo ha complessità n2 . Si lascia per esercizio la valutazione della complessità del problema. 4.8 PROBLEMI INTRATTABILI Il più potente calcolatore che si possa immaginare di costruire non potrà essere più grande dell’universo, non potrà essere fatto di componenti di elaborazione più piccoli dei protoni, non potrà trasmettere le informazoni fra le sue componenti ad una velocità superiore alla velocità della luce. Un semplice calcolo porta alla conclusione che un tale ipotetico calcolatore non potrebbe essere composto da più di 10126 componenti. Anche ammettendo che le informazioni di questo calcolatore venissero trasmesse fra le sue componenti ad una velocità uguale a quella della luce, esistono numerosi problemi di grande interesse pratico che, anche per moderati valori della dimensione del problema, tengono in scacco questo calcolatore estremo per 20 miliardi di anni, ossia per un tempo paragonabile a quello dell’universo. Esistono problemi che, pur risolvibili sul piano teorico in quanto si conoscono degli algoritmi risolutivi, non sono effettivamente risolvibili in quanto richiederebbero tempi esagerati anche per il più potente calcolatore immaginabile. Questi problemi vengono detti problemi intrattabili. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 61 4.8. PROBLEMI INTRATTABILI L’esistenza di problemi intrattabili può sembrare, a prima vista, un aspetto negativo, un’evidenziazione di un limite della scienza e della tecnica. Sembra paradossale, ma, invece, questi problemi costituiscono l’elemento fondante di alcune tecnologie: l’intrattabilità del problema della fattorizzazione in primi di un numero naturale è alla base di tutti i sistemi basati sulla crittografia; l’esplosione dell’albero delle mosse del gioco degli scacchi comporta l’intrattabilità del problema di determinare una strategia vincente nella conduzione del gioco in una partita a scacchi a causa dell’intrattabilità del problema di un’analisi esaustiva dell’albero delle mosse e questo, nonostante l’eccezionale velocità e complessità dei calcolatori attuali, lascia margini di battaglia alla pari all’uomo che gioca a scacchi contro il computer, almeno ai grandi maestri degli scacchi; ed anche se un computer batte agevolmente anche un buon scacchista, le mosse brillanti, ingegnose, artistiche rimangono prerogativa dell’uomo. La linea di demarcazione fra problemi trattabili e problemi intrattabili dipende dalla tipologia della funzione di complessità: i problemi trattabili sono caratterizzati da funzioni di complessità polinomiali, mentre i problemi intrattabili hanno funzioni di complessità di categoria esponenziale. La figura 4.1 dispone in linea di complessità crescenti un insieme di funzioni che si incontrano spesso nell’analisi degli algoritmi. complessità polinomiali log n √ n n n log n n2 n3 complessità esponenziali n10 2n n! nn Figura 4.1: Complessità polinomiali ed esponenziali di alcune funzioni che si incontrano frequentemente nell’analisi degli algoritmi. Nella tabella 4.3 è riportato il tempo in secondi che impiega un calcolatore che esegue 100 milioni di operazioni al secondo a risolvere un problema di dimensione n. Nella tabella 4.4 è riportata la dimensione del problema risolvibile in una data quantità di tempo, sempre nell’ipotesi di disporre di un calcolatore che esegue 100 milioni di operazioni al secondo. Un’analisi, anche superficiale, delle due tabelle 4.3 e 4.4 evidenzia che l’evoluzione tecnologica dei calcolatori non potrà intaccare il carattere di intrattabilità dei problemi. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 62 f (n) log n √ n n n log n n2 n3 2n n! nn CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE n = 10 3.3 µs 3.1 µs 10 µs 33 µs 100 µs 1 ms 17 min 42 g 317 a n = 20 4.3 µs 4.5 µs 20 µs 86 µs 400 µs 8 ms 12 g 8 ⋅ 1010 a 3 ⋅ 1018 a n = 30 4.9 µs 5.5 µs 30 µs 147 µs 900 µs 27 ms 34 a 8 ⋅ 1024 a 6 ⋅ 1036 a n = 40 5.3 µs 6.3 µs 40 µs 213 µs 1600 µs 64 ms 34865 a 3 ⋅ 1040 a 4 ⋅ 1056 a 50 5.6 µs 7.0 µs 50 µs 282 µs 2500 µs 125 ms 3.5 ⋅ 107 a 9 ⋅ 1056 a 4 ⋅ 1077 a n = 100 6.6 µs 10 µs 100 µs 664 µs 10000 µs 1 sec 4 ⋅ 1022 a 3 ⋅ 10150 a 3 ⋅ 10192 a Tabella 4.3: Tempi delle funzioni di complessità di alcune funzioni. Le unità di misura dei tempi sono: µs = microsecondi, ms = millisecondi, sec = secondi, min = minuti, g = giorni, a = anni. f (n) 1 sec 1 min log n √ n n n log n n2 n3 2n n! nn 10400000 1012 106 9 ⋅ 104 103 100 20 10 7 1010 4 ⋅ 1015 6 ⋅ 107 106 8 ⋅ 103 392 26 11 8 7 1 ora 9 1010 1019 4 ⋅ 109 2 ⋅ 108 6 ⋅ 104 1533 32 13 9 1 giorno 10 1010 7 ⋅ 1021 9 ⋅ 1010 4 ⋅ 109 3 ⋅ 105 4414 36 14 10 1 mese 12 1010 7 ⋅ 1024 3 ⋅ 1012 1011 2 ⋅ 106 13715 41 15 11 1 anno 13 1010 1027 3 ⋅ 1013 1012 6 ⋅ 106 31551 45 16 12 1 secolo 15 1010 1031 3 ⋅ 1015 1014 6 ⋅ 107 146452 51 18 13 Tabella 4.4: Dimensioni dei problemi risolvibili in una data quantità di tempo in funzione della complessità f (n) dei problemi. Osservazione. Da tutte le precedenti considerazioni risulta coerente designare come polinomiali i problemi intrattabili e come esponenziali i problemi intrattabili. La macchina di Turing costituisce lo strumento per tracciare la linea di demarcazione fra problemi risolvibili e problemi irrisolvibili. La distinzione fra problemi trattabili e problemi intrattabili viene invece stabilita in base alla funzione di complessità del problema. La figura 4.2 descrive la L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 63 4.9. ESERCIZI classificazione dei problemi, evidenziando, all’interno dei problemi risolvibili, i problemi trattabili e quelli intrattabili. Ci sono dei problemi per i quali non è attualmente noto a quale di queste due categorie appartengono. ' problemi risolvibili problemi intrattabili problemi trattabili & $ problemi non risolvibili % Figura 4.2: Problemi non risolvibili, risolvibili, trattabili ed intrattabili. 4.9 ESERCIZI 1. Ordinare le seguenti funzioni di complessità: √ n log n10 n! n log n nn 10n 10n log n n10 10100 log2 n 2. Siano A1 ed A2 due algoritmi funzionalmente√equivalenti aventi rispettivamente complessità c1 (n) = n log n e c2 = n 2 . Stabilire quale dei due algoritmi è preferibile, per n sufficientemente grande. 3. Determinare per quale valore n0 (punto di taglio) della dimensione n del problema un algoritmo A1 di complessità polinomiale f (n) = n10 risulta preferibile rispetto ad un equivalente algoritmo A2 di complessità esponenziale g(n) = (1.1)n . Fare delle considerazioni in merito alla conclusione. 4. Descrivere mediante dei diagrammi di Venn le seguenti classi di problemi: problemi risolvibili, problemi non risolvibili, problemi polinomiali, problemi esponenziali. 5. Valutare la complessità e la complessità asintotica delle seguenti porzioni di algoritmi, essendo op l’operazione dominante ed n la dimensione del problema. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 64 1: 2: 3: 4: 5: CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE for i from 1 to n2 do for j from 1 to 2 n do op end for end for for i from 1 to n do for j from i to n do 3: op 4: end for 5: end for 1: 2: 1: 2: 3: 4: 5: 6: 7: 1: 2: 3: 4: 5: 6: 7: k←n while k > 0 do for i from 1 to n do op end for k ← k/2 end while k ← n2 while k > 0 do for i from 1 to 2 n do op end for k ← k/2 end while L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 4.9. ESERCIZI 65 6. Scrivere e valutare la complessità degli algoritmi che risolvono i seguenti problemi: (a) calcolo del fattoriale di un numero naturale (b) ricerca di un elemento in un array (c) moltiplicazione fra due matrici (d) problema della torre di Hanoi 7. Si consideri il problema P : Decidere se un array contiene elementi tutti uguali fra loro. Spiegare perchè il problema P ′ : Decidere se un array contiene elementi tutti diversi fra loro. è sostanzialmente diverso dal problema P . 8. Dimostrare che il problema Decidere se due array hanno elementi in comune ha una complessità asintotica nel caso pessimo non superiore a O(n log n). Nota. Per ciascuno dei seguenti problemi svolgere i seguenti punti: descrivere un algoritmo risolutivo; stabilire in cosa consistono i casi ottimo, medio, pessimo; valutare la complessità e la complessità asintotica dell’algoritmo nel caso pessimo; stabilire la complessità del problema. 9. Determinare quante volte è presente un dato elemento in un array. 10. Determinare quante volte è presente un dato elemento in un array ordinato. 11. Determinare il più piccolo numero non appartenente ad un dato array di numeri naturali. 12. Decidere se un array di numeri interi contiene tutti i valori compresi fra il minimo ed il massimo degli elementi contenuti nell’array. 13. Decidere se un dato array di numeri naturali è ordinato crescentemente ed è composto da valori contigui; ad esempio l’array di numeri naturali a = [5, 6, 7, 8, 9] è ordinato crescentemente ed ha gli elementi contigui, mentre non lo sono gli array b = [5, 7, 8, 6, 9], c = [4, 6, 7, 8, 9]. 14. Decidere se due array (di uguali dimensioni) sono una permutazione uno dell’altro, ossia se contengono gli stessi valori, con la stessa frequenza, indipendentemente dalla posizione. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 66 CAPITOLO 4. COMPLESSITÀ COMPUTAZIONALE 15. Decidere se un array contiene elementi tutti uguali fra loro. 16. Decidere se un dato array è composto da elementi tutti distinti. 17. Determinare il numero di elementi distinti presenti in un array. 18. Determinare la massima distanza fra tutte le possibili coppie di numeri di un array. 19. Determinare la coppia di elementi di un array aventi la massima distanza reciproca. 20. Determinare la distanza della coppia di punti più distanti presenti in un array di punti. 21. Determinare il valore dell’elemento mediano in un array numerico di dimensione dispari. L’elemento mediano è il valore che in un ipotetico ordinamento dell’array verrebbe a trovarsi nella posizione mediana. 22. Decidere se due date stringhe soddisfano alla relazione d’ordine lessicografico sulle stringhe, ossia decidere se la prima precede la seconda, secondo l’usuale ordine del vocabolario. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 5 Ricerca 5.1 ALGORITMI DI RICERCA La ricerca di informazioni costituisce la forma più frequente di elaborazione in diversi ambiti; di più, questa forma di elaborazione costituisce elemento strategico per diverse attività: si pensi, ad esempio, ai motori di ricerca nel web ed alla ricerca nelle basi di dati. Ad un livello molto generale i problemi di ricerca si inquadrano nelle seguenti due forme: R1 : Ricercare in un insieme A se esistono elementi soddisfacenti ad una data proprietà P . R2 : Ricercare in un insieme A tutti gli elementi soddisfacenti ad una data proprietà P . La proprietà P alla quale devono soddisfare gli elementi cercati spesso coincide con l’essere uguale ad un elemento fissato. Nonostante l’apparente somiglianza, queste due forme di ricerca vengono risolte con strategie sostanzialmente diverse in quanto nel primo caso la ricerca termina non appena viene trovato un elemento soddisfacente, mentre nel secondo caso bisogna comunque analizzare tutti gli elementi dello spazio di ricerca. La forma di ricerca R1 è quella più interessante e quella che viene usata più frequentemente. Per questa forma di ricerca, lo schema generale è descritto nell’algoritmo 36. 67 68 CAPITOLO 5. RICERCA Algoritmo 36 - Ricerca di un elemento Input: insieme A in cui cercare, proprietà P da soddisfare Output: T RU E se e solo se esiste un elemento x ∈ A tale che P (x) 1: while ¬ (trovato alcun elemento) ∧ (lo si può ancora trovare) do 2: ricerca l’elemento 3: end while 4: return esito della ricerca Questo è un algoritmo così generale che può essere considerato uno schema di algoritmo che può essere adattato in tanti modi, a seconda dello specifico problema di ricerca che si sta considerando, a seconda della struttura dell’insieme A ed a seconda della particolare strategia di ricerca attuata; in particolare sono frequentemente adottate le seguenti due strategie: a) passare in rassegna sequenzialmente tutti gli elementi di A b) restringere ripetutamente l’insieme A ad un suo sottoinsieme La prima strategia di ricerca (a)) si traduce nell’algoritmo 37. Nel caso in cui l’insieme A sia enumerabile, l’algoritmo 37 assume la forma riportata nell’algoritmo 38. Questo algoritmo di ricerca viene utilizzato nel caso di array, matrici, catene ed altre strutture dati che ammettono un meccanismo di enumerazione degli elementi. Nel caso in cui l’insieme A soddisfi a dei requisiti più forti (ad esempio, sia ordinato) si possono usare degli algoritmi di ricerca più specifici e più efficienti. Algoritmo 37 - Ricerca di un elemento (caso a) Input: insieme A in cui cercare, proprietà P da soddisfare Output: T RU E se e solo se esiste un elemento x ∈ A tale che P (x) 1: trovato ← F ALSE 2: trovabile ← ∣A∣ > 0 3: while (¬ trovato) ∧ trovabile do 4: considera un elemento x ∈ A non ancora esaminato 5: if P (x) then 6: trovato ← T RU E 7: else if non ci sono più elementi da esaminare then 8: trovabile ← F ALSE 9: end if 10: end while 11: return trovato L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 5.1. ALGORITMI DI RICERCA 69 Algoritmo 38 - Ricerca di un elemento in un insieme enumerabile Input: insieme A in cui cercare, proprietà P da soddisfare Output: T RU E se e solo se esiste un elemento x ∈ A tale che P (x) 1: trovato ← F ALSE 2: i ← 1 3: while (¬ trovato) ∧ (i ≤ ∣A∣) do 4: if P (ai ) then 5: trovato ← T RU E 6: else 7: i←i+1 8: end if 9: end while 10: return trovato La seconda strategia di ricerca (b)) si traduce nell’algoritmo 39. La restrizione dell’insieme T ad un suo sottoinsieme proprio (istruzione 8 dell’algoritmo 39) può essere realizzata, ad esempio, eliminando l’elemento x dall’insieme T stesso, oppure delimitando lo spazio di ricerca. Algoritmo 39 - Algoritmo di ricerca (caso b) Input: insieme A in cui cercare, proprietà P da soddisfare Output: T RU E se e solo se esiste un elemento x ∈ A tale che P (x) 1: trovato ← F ALSE 2: T ← A 3: while (¬ trovato) ∧ (T ≠ ∅) do 4: considera un elemento x ∈ T non ancora esaminato 5: if P (x) then 6: trovato ← T RU E 7: else 8: restringi T ad un suo sottoinsieme proprio 9: end if 10: end while 11: return trovato La forma di ricerca R2 può essere svolta passando in rassegna tutti gli elementi e registrando quelli soddisfacenti, che costituiranno, alla fine, il risultato della ricerca. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 70 CAPITOLO 5. RICERCA 5.2 ESERCIZI 1. Adattare l’algoritmo 38 al caso in cui A sia un array. 2. Adattare l’algoritmo 39 al caso in cui A sia un array ordinato e la proprietà P sia costituita da un controllo di uguaglianza con un elemento prefissato k. Suggerimento: controllare se l’elemento di posizione centrale nell’array è uguale a k; in caso contrario restringere lo spazio di ricerca a una delle due metà dell’array (algoritmo di ricerca binaria o ricerca dicotomica). 3. Analizzare la complessità della ricerca su un array, distinguendo il caso in cui l’array sia ordinato e nel caso in cui non sia ordinato. 4. Spiegare cosa si intende con la frase L’algoritmo di ricerca binaria in un array ordinato ha complessità asintotica logaritmica. Motivare perchè l’algoritmo di ricerca binaria su un array ordinato ha complessità asintotica O(log n). 5. Determinare quante volte un dato valore è presente in un array. Valutare la complessità dell’algoritmo. Spiegare perché in questo caso ha poco senso parlare di complessità nel caso pessimo, medio, ottimo. 6. Decidere se in un dato array a è presente un dato elemento x nelle posizioni comprese fra due dati indici naturali p e q (p ≤ q). L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 6 Ordinamento 6.1 ALGORITMI DI ORDINAMENTO Il problema dell’ordinamento di un array può essere enunciato come segue: Dato un array a di dimensione n, permutarne gli elementi in modo tale che a[1] ≤ a[2] ≤ ⋯ ≤ a[n] Per ordinare un array si possono adottare diverse strategie. Alcune sono descritte a seguire. Ordinamento a bolle (bubble-sort): Far salire successivamente verso la fine dell’array gli elementi maggiori, attraverso degli scambi fra elementi successivi, seguendo la seguente strategia: si confrontano i primi due: se sono fuori ordine vengono scambiati; si ripete il procedimento con il secondo ed il terzo e così via, fino alla fine dell’array. Alla fine del primo ciclo l’elemento maggiore è sicuramente posizionato alla fine dell’array. Si inizia allora un altro ciclo per far salire al penultimo posto l’elemento immediatamente minore e così via fino ad aver ordinato tutto l’array. Ordinamento per selezione (selection-sort): Determina il minimo fra gli elementi dell’array e scambialo con il primo. Determina il minimo fra gli elementi dell’array, escluso il primo, e scambialo con il secondo. Determina il 71 72 CAPITOLO 6. ORDINAMENTO minimo dell’array, esclusi i primi due, e scambialo con il terzo e così via, fino a quando la porzione di array da ordinare si è ridotta ad un unico elemento. Ordinamento per inserzione (del giocatore di carte) (insertion-sort): A partire dal secondo elemento fino all’ultimo si inserisce l’elemento al posto giusto fra quelli compresi fra il primo e quello dello stesso elemento che si inserisce. Ordinamento veloce (quick-sort): Scegliere un valore a caso (perno); ripartire l’array in due porzioni: nella prima mettere tutti gli elementi minori o uguali al perno, nella seconda elementi maggiori o uguali al perno. Ripetere poi ricorsivamente l’ordinamento su entrambe le porzioni dell’array. La chiusura della ricorsione è costituita dalla condizione di avere un array di un solo elemento. La partizione rispetto al perno avviene ponendo inizialmente due indici alle estremità della porzione di array da ordinare, avvicinandoli verso il centro, scambiando gli elementi quando si arriva ad elementi da scambiare, in quanto fuori ordine rispetto al perno. 6.2 ESERCIZI 1. Scrivere un algoritmo per ciascuna delle strategie di ordinamento sopra descritte. 2. Implementare in un linguaggio di programmazione ciascuno degli algoritmi descritti nell’esercizio precedente e verificarne la correttezza. 3. Descrivere la situazione dell’array [5, 6, 8, 1, 2, 7, 5, 3, 9, 6, 8, 4] dopo aver eseguito un ciclo dell’algoritmo quicksort prendendo come perno l’elemento di posizione 1. 4. Spiegare cosa significa la frase: L’algoritmo di ordinamento quicksort ha una complessità asintotica nel caso medio pari a O(n log n). 5. Fare una tabella che riporti la complessità degli algoritmi di ordinamento bubblesort, insertionsort, selectionsort, quicksort, mergesort, heapsort nei diversi casi ottimo, medio, pessimo. 6. Su una rivista si trova la seguente frase: Gli algoritmi di ordinamento della classe A hanno, nel caso pessimo, una complessità asintotica pari a n2 . Spiegare cosa significa la frase. Dire quali, fra gli algoritmi di ordinamento studiati, appartengono alla classe A di cui si parla nella rivista citata. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 6.2. ESERCIZI 73 7. Su una rivista si legge: Il prof. Dalla Balla ha scoperto un algoritmo di ordinamentodi array avente, nel caso medio, complessità uguale a n log (logn). Dire se il prof. Dalla Balla ha fatto una scoperta degna di nota e se è credibile. 8. Decidere se un array è ordinato (crescentemente o decrescentemente). 9. Descrivere un algoritmo di complessità lineare per ordinare un array di valori booleani. Motivare perché tale algoritmo non contraddice la teoria che afferma che il problema dell’ordinamento di un array ha complessità asintotica pari a O(n log n). 10. Decidere se due array sono uno una permutazione dell’altro. 11. Decidere se due array contengono gli stessi elementi. Valutarne la complessità. Dimostrare che la complessità asintotica del problema è non superiore a O(n log n). 12. Decidere se un array è composto da elementi tutti distinti. Valutare, nel caso ottimo e nel caso pessimo, la complessità, dell’algoritmo descritto. Motivare perché la complessità asintotica del problema in questione è non superiore a O(n log n). 13. Determinare la frequenza massima con la quale compare l’elemento avente la più alta frequenza all’interno di un array. Determinare la complessità dell’algoritmo. Motivare perché il problema in questione ha una complessità asintotica non superiore a O(n log n). L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 74 CAPITOLO 6. ORDINAMENTO L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 7 Alberi Ogni genere posto ad un nodo alto dell’albero comprende le specie che ne dipendono, ogni specie subordinata a un genere è un genere per la specie che le è subordinata, sino all’estremità inferiore dell’albero, dove sono collocate le specie specialissime o sostanze seconde, come ad esempio uomo. U. Eco, Dall’albero al labirinto Gli alberi costituiscono una tipologia di contenitori particolarmente importante, per il loro largo uso che ne viene fatto in informatica, per la ricchezza e profondità dei problemi che permettono di descrivere e trattare e per l’eleganza delle soluzioni che suggeriscono. 75 76 7.1 CAPITOLO 7. ALBERI ALBERI DEFINIZIONE 12. Un albero (radicato) è un insieme vuoto oppure un insieme non vuoto di elementi detti nodi in cui esiste un nodo speciale detto radice e gli altri nodi sono ripartiti in una sequenza ordinata di n ≥ 0 sottoinsiemi T1 , . . . , Tn , detti sottoalberi o figli della radice, ciascuno dei quali è un albero. Una sequenza di alberi viene detta foresta. Si dicono foglie di un albero gli elementi dell’albero che non hanno sottoalberi. Diconsi discendenti di un nodo tutti gli elementi che appartengono ai sottoalberi del nodo stesso. Un nodo dicesi padre dei nodi (figli) che costituiscono la radice dei suoi sottoalberi. Tali nomenclature derivate per analogia con i gradi di parentela possono essere estese. Ad esempio, due nodi figli di uno stesso padre vengono detti fratelli. Dicesi cammino da un nodo p ad un nodo discendente q una successione di nodi (n1 , . . . , nk ) tali che n1 = p, nk = q, ni è padre di ni+1 , per ogni i = 1, k − 1. Un arco è il cammino che unisce due nodi adiacenti (padre-figlio). La lunghezza di un cammino (n1 , . . . , nk ) è rappresentata dal numero di archi che uniscono il primo nodo all’ultimo nodo, ossia k − 1. La profondità di un nodo è uguale alla lunghezza del cammino dalla radice al nodo stesso. L’altezza di un albero è pari alla massima propondità dei suoi nodi foglia. Un livello di un albero è costituito dai nodi aventi la stessa profondità. Il numero massimo dei figli di uno stesso nodo viene detto grado (dell’albero). Un albero di grado k dicesi completo se ogni nodo, esclusi i nodi foglia ed al più i nodi padri delle foglie, ha esattamente k figli. Il termine albero è mutuato direttamente dalla botanica, anche se usualmente, per questioni di praticità grafica, si usa rappresentare gli alberi con la radice in alto. La figura 7.1 illustra un esempio di albero. el bl @ @ @ l f al H H cl J J lJ l l m i l HH H H H l d @ @ @ l gl h nl Figura 7.1: Albero di grado 3, altezza 3, costituito da 12 nodi (di cui 5 sono nodi interni e 7 sono foglie), strutturati in 4 livelli. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 7.2. RAPPRESENTAZIONI DEGLI ALBERI 77 Osservazione. Gli alberi vengono solitamente definiti in modo ricorsivo, come descritto nella precedente definizione; ciò suggerisce in molti casi la struttura dati (ricorsiva) per la loro rappresentazione e la forma degli algoritmi (ricorsivi) che si adottano per trattare queste strutture. 7.2 RAPPRESENTAZIONI DEGLI ALBERI Un metodo per rappresentare un albero T , vuoto oppure costituito da una radice r e dalla sequenza di sottoalberi [T1 , . . . , Tn ], consiste nella rappresentazione a struttura, ossia: rap(T ) = { [] se T è vuoto [r, rap(T1 ), . . . , rap(Tn )] altrimenti Graficamente un albero non vuoto composto dalla radice r e dalla sequenza di sottoalberi [T1 , . . . , Tn ] può essere rappresentato come descritto nella figura 7.2. rjP PP P PP PP PP PP P B B B B B B B B B ... B B B B B B T1 BB T2 BB Tn BB Figura 7.2: Rappresentazione ricorsiva di un albero composto dalla radice r e dalla sequenza di sottoalberi [T1 , . . . , Tn ]. Esempio 43. L’albero descritto nella figura 7.1 viene rappresentato mediante la struttura [a, [b, [e], [f, [i], [l], [m] ] ] , [c], [d, [g], [h, [n] ] ] ] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 78 CAPITOLO 7. ALBERI Una rappresentazione alternativa consiste nell’utilizzo dei diagrammi di Venn, come descritto nella figura 7.3. ' a ' b ' e $ ' $ $ f m i c $ d g h n l & % & % & % & % Figura 7.3: Rappresentazione di un albero mediante diagrammi di Venn. Un’altra rappresentazione mediante parentesi adotta la sintassi del linguaggio Lisp, come descritto nella figura 7.4. (a(b(e)(f (i)(l)(m)))(c)(d(g)(h(n)))) Figura 7.4: Rappresentazione di un albero mediante parentesi. Un albero può essere descritto anche mediante una scrittura indentata dei nodi, come illustrato nella figura 7.5. Questa notazione risulta utile quando si voglia rappresentare un albero in modo testuale ed in modo che risulti facilmente decifrabile la relazione di gerarchia fra i nodi stessi. a b e f i l m c d g h n Figura 7.5: Rappresentazione di un albero mediante indentazione. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 7.3. METODI DI VISITA DEGLI ALBERI 79 La rappresentazione indentata suggerisce un’altra possibilità di rappresentazione: un albero viene rappresentato mediante una sequenza di coppie [x, n] dove x denota l’elemento ed n il grado di indentazione. Questa modalità di rappresentazione è descritta nella figura 7.6. [ [a, 0], [b, 1], [e, 2], [f, 2], [i, 3], [l, 3], [m, 3], [c, 1], [d, 1], [g, 2], [h, 2], [n, 3] ] Figura 7.6: Rappresentazione di un albero mediante sequenza di coppie. 7.3 METODI DI VISITA DEGLI ALBERI Visitare un albero significa attraversarlo e produrre una sequenza piatta degli elementi memorizzati nei nodi dell’albero. Si possono adottare diverse strategie di visita: • visita in ordine anticipato: 1. visita la radice 2. visita i sottoalberi in ordine anticipato • visita in ordine differito: 1. visita i sottoalberi in ordine differito 2. visita la radice Esempio 44. Le visite in ordine anticipato e differito dell’albero descritto figura 7.1 producono rispettivamente le seguenti sequenze: [a, b, c, f, i, l, m, c, d, g, h, n] [e, i, l, m, f, b, c, g, n, h, d, a] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 80 CAPITOLO 7. ALBERI 7.4 ALBERI BINARI DEFINIZIONE 13. Dicesi albero binario un un albero nel quale ogni nodo ha al più due figli. Un’equivalente definizione ricorsiva, più utile, è la seguente: DEFINIZIONE 14. Dicesi albero binario un insieme vuoto oppure una terna [r, TL , TR ] in cui TL e TR sono alberi binari detti, rispettivamente, figlio sinistro e figlio destro. La figura 7.7 illustra un esempio di albero binario. am m b Z Z Z ZZ cm @ @ dm gm @ @ em J J J hm fm J J J im Figura 7.7: Rappresentazione di un albero binario composto da 9 nodi strutturati in 4 livelli. 7.5 OPERAZIONI SUGLI ALBERI BINARI Denotando con BE l’insieme degli alberi binari composti da elementi di tipo E, gli operatori sugli alberi binari possono essere descritti, in notazione funzionale, dal seguente elenco: L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 81 7.6. VISITA DEGLI ALBERI BINARI BE BE BE lef t right root isEmpty 7.6 : : : : : : : ∅ E 2 E × BE BE BE BE BE → → → → → → → BE BE BE BE BE E Bool // // // // // // // costruttore (albero vuoto) costruttore (albero radice) costruttore (albero con figli) selettore del figlio sinistro selettore del figlio destro selettore della radice test se l’albero è vuoto VISITA DEGLI ALBERI BINARI Un albero binario può essere visitato secondo le strategie di seguito descritte: • visita in ordine preordine: 1. visita la radice 2. visita il figlio sinistro in preordine 3. visita il figlio destro in preordine • visita in ordine inordine o in ordine simmetrico: 1. visita il figlio sinistro in inordine 2. visita la radice 3. visita il figlio destro in inordine • visita in postordine: 1. visita il figlio sinistro in postordine 2. visita il figlio destro in postordine 3. visita la radice Esempio 45. Visitando l’albero riportato nella figura 7.7 mediante una visita in preordine, inordine e postordine si ottengono, rispettivamente, le seguenti sequenze: [a, b, d, g, h, c, e, i, f ] [g, d, h, b, a, e, i, c, f ] [g, h, d, b, i, e, f, c, a] L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 82 7.7 CAPITOLO 7. ALBERI RAPPRESENTAZIONE DI ALBERI BINARI Gli alberi binari, essendo dei casi particolari di liste non lineari, possono essere rappresentati derivando il metodo di rappresentazione delle liste non lineari. Poiché questa rappresentazione è poco efficiente, viene usualmente scelta una rappresentazione specifica (in tale modo si perde però, in un linguaggio orientato agli oggetti, la possibilità di derivare la classe degli alberi binari dalla classe delle liste lineari). Un albero binario vuoto può essere rappresentato graficamente da un simbolo convenzionale, ad esempio ○. Un albero binario non vuoto T = (r, TL , TR ) può essere rappresentato graficamente come descritto nella figura 7.8. rj @ @ B B B B TL B BB @ @ B B B B B TR B B Figura 7.8: Rappresentazione ricorsiva di un albero binario. Questo schema di rappresentazione può essere immediatamente ricondotto ad una rappresentazione mediante una struttura ricorsiva: [r, rap(TL ), rap(TR )] chiudendo la ricorsione rappresentando l’albero vuoto con [ ]. Esempio 46. L’albero binario riportato nella figura 7.7 viene rappresentato mediante la seguente struttura: [a, [b, [d, [g, [ ], [ ] ], [h, [ ], [ ] ], [ ] ], [c, [e, [ ], [i, [ ], [ ] ] ], [f, [ ], [ ] ] ] ] Un albero binario completo o quasi completo può essere efficientemente rappresentato mediante una strutturazione sequenziale dei suoi elementi. In altri termini, un albero binario è rappresentabile mediante una sequenza. L’albero viene completato con dei nodi contenenti il valore nullo (indicato con ♢); L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 83 7.8. ALBERI BINARI DI RICERCA successivamente si memorizzano per livelli tutti i nodi dell’albero completo così costruito. La rappresentazione sequenziale di un albero di altezza h richiede una sequenza di al più 2h+1 − 1 elementi. Serve inoltre un particolare valore, non appartenente al tipo di base E, per rappresentare l’elemento nullo, ossia non esistente. Esempio 47. La figura 7.9 descrive la rappresentazione sequenziale per livelli dell’albero binario riportato nella figura 7.7. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 a b c d ♢ e f g h ♢ ♢ ♢ i ♢ ♢ Figura 7.9: Rappresentazione sequenziale dell’albero binario riportato nella figura 7.7 Si ricava facilmente che, dato un elemento di posizione k nella sequenza a che rappresenta l’albero, gli elementi che costituiscono la radice del figlio sinistro, del figlio destro e del padre (se k > 1) sono individuati mediante le seguenti relazioni: root(lef t(a[k])) = a[2k] root(right(a[k])) = a[2k + 1] root(f ather(a[k])) = a[⌈k/2⌉] 7.8 ALBERI BINARI DI RICERCA L’obiettivo degli alberi binari di ricerca è quello di permettere degli algoritmi di ricerca con complessità logaritmica, non degradando al contempo la complessità dell’inserimento ordinato di nuovi elementi, mantenendo questa operazione ad una complessità logaritmica (gli array e le liste concatenate non permettono di mantenere entrambe queste performances sia per la ricerca che per l’inserimento). DEFINIZIONE 15. Un albero binario di ricerca (ABR) è un albero binario in cui ogni nodo è maggiore o uguale dei nodi del sottoalbero sinistro e minore o uguale dei nodi del sottoalbero destro. Risulta più utile la seguente definizione: L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 84 CAPITOLO 7. ALBERI DEFINIZIONE 16. Un albero binario di ricerca (ABR) è un albero binario vuoto oppure è un albero binario [r, TL , TR ] dove TL è un albero binario vuoto oppure è un albero binario di ricerca avente la radice minore o uguale di r e TR è un albero vuoto oppure è un albero binario di ricerca avente la radice maggiore o uguale di r. 5l Z Z Z Z Z 4l 8l @ 2l 1l @ @ 6l J J J 3l 9l J J J 7l Figura 7.10: Rappresentazione di un albero binario di ricerca. Gli ABR vengono solitamente impiegati come struttura dati sulla quale eseguire efficientemente delle ricerche. L’algoritmo 40 costituisce la tipica forma della ricerca in un ABR. Algoritmo 40 - ricercaABR(a, x) - Algoritmo di ricerca in un ABR Input: ABR a, elemento x Output: T RU E se e solo se x è presente in a 1: if isEmpty(a) then 2: return F ALSE 3: else if root(a) = x then 4: return T RU E 5: else if root(a) < x then 6: return ricercaABR(lef t(a), x) 7: else 8: return ricercaABR(right(a), x) 9: end if Se l’ABR a è perfettamente bilanciato, la complessità della ricerca è O(log n), dove n è il numero dei nodi; nel caso in cui l’ABR sia completamente sbilanciato, la complessità della ricerca è lineare, ossia pari a O(n). L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 85 7.9. ESERCIZI 7.9 ESERCIZI 1. Ricostruire graficamente l’albero binario rappresentato dalla seguente struttura: [7, [4, [ ], [8, [3, [ ], [ ] ], [5, [ ], [ ] ] ] ], [9, [ ], [6, [ ], [ ] ] ] ] 2. Una visita in preordine ed una in inordine di un albero binario producono rispettivamente le seguenti sequenze: abdcef bdaecf Ricostruire graficamente l’albero binario. 3. Determinare il numero di nodi di un albero binario. 4. Determinare il numero di foglie di un albero binario. 5. Determinare il numero di nodi interni di un albero binario. 6. Determinare la somma dei valori presenti in un albero binario. 7. Determinare il massimo valore presente in un albero binario. 8. Determinare il massimo valore delle foglie di un albero binario. 9. Decidere se un dato elemento è presente in un albero binario. 10. Determinare quante volte è presente un dato elemento in un albero binario. 11. Determinare il numero di elementi distinti di un albero binario. 12. Decidere se due alberi binari sono uguali. 13. Determinare l’altezza di un albero binario. 14. Determinare la larghezza (massimo numero di nodi che si trovano su uno stesso livello) in un albero binario. 15. Determinare l’insieme dei valori delle foglie di un albero binario. 16. Decidere se un albero binario è perfettamente bilanciato. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 86 CAPITOLO 7. ALBERI 17. Determinare l’altezza di un albero binario. 18. Determinare il valore minimo ed il valore massimo assumibile dall’altezza di un albero binario costituito da n nodi. 19. Definire un coefficiente di bilanciamento di un albero binario che esprima il grado di bilanciamento dell’albero. 20. Un albero binario si dice completamente sbilanciato se ogni suo nodo ha al più un figlio. Decidere se un albero binario è completamente sbilanciato. 21. Decidere se due dati alberi binari sono isomorfi (stessa forma), ossia se hanno la stessa struttura, indipendentemente dai valori dei loro nodi. 22. Descrivere una strategia per elaborare un albero binario senza ricorrere alla ricorsione. 23. Decidere se in un ABR è presente un dato elemento. 24. Studiare la complessità della ricerca in un ABR in funzione del numero n dei nodi, dell’altezza h dell’albero e del grado di bilanciamento. Distinguere i casi ottimo, medio e pessimo. 25. Determinare quante volte in un ABR è presente un dato elemento. 26. Determinare l’elemento più grande presente in un ABR. 27. Determinare in un ABR il numero di elementi compresi fra due dati estremi. 28. Costruire un ABR a partire da una sequenza di elementi, prendendo il primo elemento della sequenza come radice dell’albero ed inserendo nell’albero gli elementi dal secondo in poi senza spostare gli elementi già inseriti. 29. Illustrare, con un esempio, la seguente proprietà: Una visita inordine (in ordine simmetrico) di un ABR produce una sequenza ordinata dei nodi. Utilizzando questa proprietà, costruire un ABR a partire da una generica sequenza di nodi. 30. Ordinare una sequenza basandosi sulla proprietà che afferma che la visita preordine di un ABR genera una sequenza ordinata. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 7.9. ESERCIZI 87 31. Un’espressione aritmetica composta da operatori binari infissi espressa nella usuale notazione algebrica con parentesi, può essere rappresentata sotto forma di albero binario (detto albero sintattico) dove i nodi non terminali contengono gli operatori e le foglie gli operandi. Descrivere mediante un albero sintattico l’espressione (7 + (6 − 2)) ∗ 3. Valutare un’espressione rappresentata mediante un albero sintattico. 32. Determinare la distanza di due nodi di un albero binario definita come la lunghezza del minimo cammino che li congiunge. 33. Decidere se un albero binario è incluso in un altro. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 88 CAPITOLO 7. ALBERI L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 Capitolo 8 Grafi I grafi sono strutture matematiche che rivestono interesse per un’ampia gamma di campi applicativi. I grafi costituiscono una struttura matematica discreta che viene studiata dal punto di vista algebrico, indipendentemente dalle specifiche aree applicative. La teoria dei grafi costituisce un’importante parte della combinatoria; i grafi inoltre sono utilizzati in aree come topologia, teoria degli automi, funzioni speciali, geometria dei poliedri, algebre di Lie. Intuitivamente un grafo è una rete di nodi (rappresentati mediante dei punti) e di collegamenti fra di essi (rappresentati mediante delle linee di congiunzione). I grafi vengono utilizzati per descrivere modelli di sistemi e processi studiati nell’informatica (programmi, circuiti, reti di computer, mappe di siti), nell’ingegneria (sistemi fluviali, reti stradali, trasporti), nella chimica, nella biologia molecolare, nella ricerca operativa, nella organizzazione aziendale, nella linguistica strutturale, nella storia (alberi genealogici, filologia dei testi). I seguenti esempi danno un’idea di alcuni campi applicativi dei grafi: - una rete stradale può essere rappresentata mediante un grafo in cui i nodi sono le città e le linee rappresentano i collegamenti fra le città - le reti logiche sono rappresentabili mediante dei grafi in cui i nodi sono le porte logiche and, or e not e le linee corrispondono ai collegamenti fra le porte - la struttura di un ipertesto è rappresentabile mediante un grafo in cui i nodi sono le pagine e le linee sono i collegamenti fra le pagine - le molecole possono essere rappresentate mediante dei grafi in cui i nodi sono gli atomi che le compongono e le linee esprimono i legami fra gli atomi 89 90 8.1 CAPITOLO 8. GRAFI DEFINIZIONI I termini fondamentali della teoria dei grafi sono riportati nella seguente definizione. DEFINIZIONE 17. Un grafo è costituito da un insieme di elementi detti vertici e da un insieme di lati che collegano coppie di vertici 1 . Una coppia non ordinata di vertici è detta lato, mentre una coppia ordinata è detta arco. Un grafo composto solo da lati è detto grafo non-orientato; un grafo composto solo da archi è detto grafo orientato (o diretto) o digrafo. Due vertici u, v connessi da un lato vengono detti estremi del lato (arco); un lato (arco) risulta identificato dalla coppia formata dai suoi estremi (u, v). Un lato (arco) che ha due estremi coincidenti si dice cappio. Un cammino (catena nel caso di grafo orientato) da un vertice u ad un vertice v è una successione di vertici (v1 , . . . , vn ) tali che v1 = u, vn = v ed inoltre per ogni i = 1, . . . , n − 1, (vi , vi+1 ) è un lato del grafo. Se uno dei vertici è ripetuto il cammino viene detto semplice; se il cammino passa per tutti i vertici viene detto euleriano; se v1 = vn il cammino viene detto circuito o ciclo. Un circuito è detto hamiltoniano se passa una sola volta per tutti i vertici del grafo (escluso il primo). Un grafo non orientato si dice connesso se esiste un cammino congiungente ogni coppia di vertici. t H H t @ @ HH H HH H t t @ @t t HH Ht @ @ t Figura 8.1: Grafo composto da 8 vertici e 12 lati. 1 Una coppia di vertici può essere unita da più lati o archi; in questi casi si parla di lati (archi) multipli o multilati (multiarchi) ed il grafo viene detto multigrafo. In caso contrario si parla di grafo semplice. In questo capitolo si considerano solo grafi semplici. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 91 8.2. TIPOLOGIE DI GRAFI In alcune argomentazioni risulta utile riferirsi alla seguente definizione formale. DEFINIZIONE 18. Un grafo è una coppia G = (V, E) dove V è un insieme di elementi detti vertici E è un insieme di elementi detti lati costituiti da coppie di elementi di V , ossia E ⊆ V × V Il grafo G = (∅, ∅), privo di vertici e di lati, è detto grafo nullo. 8.2 TIPOLOGIE DI GRAFI In un grafo si possono memorizzare informazioni in diversi modi: • orientando il lato, ottenendo un grafo orientato • memorizzando delle informazioni nei nodi; tali informazioni vengono dette label ed il grafo che si ottiene viene detto grafo labellato • memorizzando delle informazioni nei lati; tale informazioni vengono dette pesi e il grafo che si ottiene viene detto grafo pesato La figura 8.2 descrive un grafo orientato, pesato e labellato. h 6HH φ λ -e µ d γ HH Hπ HH 6@ I H jg H f ν H 6 δ@ η κ @ a α @ -b β c Figura 8.2: Grafo orientato, pesato e labellato. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 92 CAPITOLO 8. GRAFI 8.3 OPERAZIONI SUI GRAFI Le operazioni fondamentali che si possono eseguire su un grafo grafo G sono sinteticamente descritte nel seguente elenco: • creare un grafo vuoto • aggiungere un vertice u • aggiungere un lato a congiungente due nodi u e v • eliminare un dato vertice u (e i lati esso collegati) • eliminare l’arco congiungente due dati nodi u e v • ottenere la lista degli archi che escono da un dato vertice u • ottenere la lista degli archi entranti in un dato vertice u 8.4 RAPPRRESENTAZIONI DEI GRAFI Un grafo viene rappresentato mediante delle strutture dati elementari. Vengono abitualmente utilizzate le strutture predisposte dagli usuali linguaggi di programmazione: array, liste, matrici. La particolare scelta dipende dalla tipologia del grafo e dalle operazioni che si intendono realizzare e, quindi, dal particolare problema da risolvere. • rappresentazione mediante insiemi: basandosi direttamente sulla defini-zione 18, un grafo può essere rappresentato mediante l’insieme dei vertici e dei lati. Ad esempio il grafo -e d 6@ I 6 f @ @ a @ -b c viene rappresentato mediante la seguente coppia di insiemi: ({a, b, c, d, e, f }, {(a, b), (a, d)(b, d), (b, e), (b, f ), (c, b), (d, e), (f, e)}) L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 93 8.4. RAPPRRESENTAZIONI DEI GRAFI • rappresentazione mediante matrice di adiacenza: un grafo di n nodi può essere rappresentato mediante una matrice binaria, identificando ciascun nodo mediante un valore naturale progressivo, 1, 2, 3, . . . , n. Ad esempio il grafo 4t @ @ @ 5t 6t @ @t 2 t 3 @ t 1 viene rappresentato mediante la matrice binaria 0 1 0 1 0 0 1 0 1 1 1 1 0 1 0 0 0 0 1 1 0 0 1 0 0 1 0 1 0 1 0 1 0 0 1 0 La rappresentazione di un grafo mediante una matrice di adiacenza è utilizzabile nel caso di grafi orientati e non orientati; in quest’ultimo caso la matrice risulta simmetrica. Una matrice di adiacenza è utilizzabile anche nel caso di grafi pesati nel quale caso al posto dell’1 viene messo il valore del peso dell’arco corrispondente. Spesso, nella matrice di adiacenza vengono indicati solo i valori non nulli e vengano lasciati non definiti i valori corrispondenti allo 0. Nel caso di un grafo labellato, alla matrice binaria bisogna aggiungere un array dove sono registrati le informazioni dei vertici. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 94 CAPITOLO 8. GRAFI • rappresentazione mediante lista di adiacenza: la rappresentazione mediante una lista di adiacenza si basa sull’idea di rappresentare ciascun nodo con ad esso associata una lista i cui elementi rappresentano gli archi uscenti dal nodo stesso; l’elemento corrispondente ad un generico nodo u ha la struttura [u, lista degli archi uscenti da u] Ad esempio il grafo λ -e µ d γ 6@ I f 6 δ@ η @ a α @ -b β c viene rappresentato mediante la lista composta [ [a, [ [b, α], [d, γ] ] ], [b, [ [d, δ], [e, ], [f, η] ] ], [c, [ [b, β] ] ], [d, [ [e, λ] ] ], [f, [ [e, µ] ] ] ] ] 8.5 8.5.1 PROBLEMI SUI GRAFI Problema del cammino minimo Dato un grafo orientato e pesato determinare un cammino di peso minimo da un nodo di partenza ad un nodo di arrivo, ossia un cammino per il quale è minima la somma dei pesi degli archi che lo compongono. Si tratta di un tipico problema dei trasporti. Si pensi ad esempio al problemi di minimizzare i consumi per il trasporto da una città ad un’altra. L’algoritmo risolutivo più noto è attribuito a E. W. Dijkstra ed ha una complessità O(n2 ) dove n è il numero dei nodi. 8.5.2 Problema del collegamento minimo Dato un grafo non orientato pesato che rappresenta una rete di collegamenti fra i vertici, si vuole eliminare i collegamenti superflui e mantenere solo quelli indispensabili in modo da minimizzare il costo totale. Ciò corrisponde a voler trovare un albero di supporto minimo di un grafo non orientato connesso. Una della applicazioni più significative è quella del progetto di una rete di comunicazioni in cui i nodi rappresentano delle città. Per questo problema sono noti algoritmi di complessità polinomiale. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 8.5. PROBLEMI SUI GRAFI 8.5.3 95 Problema del commesso viaggiatore Il problema del commesso viaggiatore fu considerato per la prima volta agli inizi degli anni ’30 del secolo scorso, anche se, sotto altra veste, comparve all’interno della teoria dei grafi già nel diciannovesimo secolo. Il problema può essere enunciato come segue: Dato un grafo orientato e pesato, determinare un circuito hamiltoniano (ossia passante una sola volta per tutti i nodi del grafo) di costo minimo. Nella sua formulazione più caratteristica il problema si enuncia come segue: Un commesso viaggiatore deve visitare un dato numero di città. Ogni città è collegata a tutte le altre da una strada di cui si conosce la lunghezza. Determinare il percorso più breve che passa per ogni città una sola volta e ritorna alla città di partenza. 8.5.4 Problema dell’isomorfismo fra grafi Dati due grafi G1 e G2 si dicono isomorfi (stessa forma) se sono sostanzialmente lo stesso grafo, cioè se è possibile trovare una corrispondenza biunivoca f fra i nodi di G1 e G2 in modo tale che esiste un arco da u a v in G1 allora esiste un arco f (u) in G2 e viceversa. A tutt’oggi, non si conoscono algoritmi non esponenziali rispetto al numero dei nodi; nel caso peggiore si è infatti sempre costretti a provare n! permutazioni possibili dei nodi di uno dei due grafi. 8.5.5 Problema della planarità di un grafo Un grafo non orientato dicesi planare se è possibile disegnarlo su un piano senza sovrapporre i lati. Il problema della planarità si enuncia come segue: Dato un grafo non orientato e connesso, stabilire se è planare o meno, ed eventualmente farlo vedere. Il problema ha interesse pratico nella progettazione di reti stradali, circuiti stampati, ecc. in cui si vuole evitare di sovrapporre dei collegamento. Già Eulero aveva trovato una semplice condizione necessaria: m ≤ 3n−6 dove n indica il numero dei nodi ed m il numero dei lati. Nel 1974 è stato scoperto un algoritmo lineare rispetto ad n che risolve questo problema. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014 96 CAPITOLO 8. GRAFI 8.6 ESERCIZI 1. Definire cos’è un grafo pesato, non orientato e labellato. Fare un esempio di un tale grafo, contenente 5 vertici e 7 lati. Descrivere una struttura dati adeguata a rappresentare il grafo. 2. Descrivere una situazione in cui risulta idoneo ricorrere ad un grafo pesato, non orientato e non labellato. Fare un esempio di un tale grafo, composto da 5 vertici e 8 lati. Descrivere una struttura dati adeguata a rappresentare il grafo. 3. Descrivere un grafo orientato, non pesato e non labellato, avente 6 vertici e 11 lati. Descrivere una struttura dati adeguata a rappresentare il grafo. 4. Descrivere un grafo orientato, pesato e non labellato, avente 6 vertici e 7 archi. Descrivere una struttura dati adeguata a rappresentare il grafo. 5. Descrivere un grafo non orientato, pesato e labellato, avente 6 vertici e 11 lati. Descrivere una struttura dati adeguata a rappresentare il grafo. 6. Descrivere un grafo orientato e pesato, composto da 6 vertici e 8 lati. Descrivere una struttura dati adeguata a rappresentare il grafo. 7. Disegnare il grafo rappresentato dalla seguente matrice di adiacenza: 1 0 0 1 1 0 0 0 1 1 0 0 0 0 1 0 Modificare la matrice in modo da aggiungere un lato dal vertice 1 al vertice 4, scambiare il verso al lato congiungente i vertici 2 e 3, aggiungere un vertice (5) e collegarlo al vertice 3. L. Calvi - Quaderni di Informatica - Strutture ed Algoritmi - Feltre - 2014