Nome e Cognome ______________________________________ Matricola __________________ Corso di Algoritmi e Strutture Dati—Informatica per il Management Prova Scritta, 9/2/2017 • • • • • • • Chi deve recuperare il progetto del modulo 1 ha 1 ora e 30 minuti per svolgere gli esercizi 1, 2, 3 Chi deve recuperare il progetto dei moduli 2/3 ha 1 ora e 30 minuti per svolgere gli esercizi 4, 5, 6 Chi deve sostenere la prova completa ha 2 ore e 30 minuti per svolgere tutti gli esercizi proposti. Durante la prova è consentito consultare libri e appunti. Non è consentito l'uso di dispositivi elettronici (ad esempio, cellulari, tablet, calcolatrici elettroniche...), né interagire in alcun modo con gli altri studenti pena l'esclusione dalla prova, che potrà avvenire anche dopo il termine della stessa. Le risposte devono essere scritte a penna su questi fogli, in modo leggibile e adeguatamente motivate. Le parti scritte a matita verranno ignorate. Eventuali altri fogli possono essere utilizzati per la brutta copia ma non devono essere consegnati e non verranno valutati. I voti saranno pubblicati su AlmaEsami e ne verrà data comunicazione all'indirizzo mail di Ateneo (@studio.unibo.it). Lo studente ha 7 giorni di tempo per rifiutare il voto; tutti i voti non esplicitamente rifiutati entro tale data sono considerati accettati, e verranno in seguito verbalizzati. BARRARE LA CASELLA CORRISPONDENTE ALLA PARTE SVOLTA □ Svolgo solo la parte relativa al modulo 1 (esercizi 1, 2, 3); □ Svolgo solo la parte relativa ai moduli 2/3 (esercizi 4, 5, 6); □ Svolgo tutto il compito NON SCRIVERE NELLA TABELLA SOTTOSTANTE Es. 1 /4 Es. 2 /2 /4 Es. 3 /6 Es. 4 /6 Es. 5 /4 Es. 6 /2 /2 /2 Nome e Cognome ______________________________________ Matricola __________________ Esercizio 1. Una colonia di batteri viene posta all'interno di un contenitore da laboratorio. Sia B(n) il numero di batteri presenti nel contenitore dopo n giorni, n = 1, 2, … . Supponiamo che B(n) soddisfi la regola seguente: { 1 se 1≤n≤4 B(n)= B(n−3)+ B(n−4) se 5≤n≤10 B(n−3)+ B(n−4 )− B(n−10) se n>10 1. Scrivere un algoritmo efficiente che, dato in input un intero n ≥ 1, restituisca il valore B(n). [punti 4] 2. Determinare il costo asintotico dell'algoritmo di cui al punto precedente, motivando la risposta [punti 2] Soluzione. Questo esercizio può essere risolto in modo simile al calcolo dei numeri di Fibonacci. Un possibile algoritmo che richiede tempo O(n) e spazio O(n) è il seguente integer BATTERI(integer n) integer B[1..n]; for integer i ← 1 to n do if (i ≤ 4) then B[i] ← 1; else if (i ≤ 10) then B[i] ← B[i - 3] + B[i – 4]; else B[i] ← B[i – 3] + B[i – 4] – B[i – 10]; endif endif endfor return B[n]; E' anche possibile una soluzione più complessa, che richiede tempo O(n) ma spazio O(1), usando un buffer circolare per tenere traccia degli ultimi 10 valori di B, che sono quelli realmente necessari per calcolare il valore successivo. La soluzione ricorsiva basata sull'implementazione dell'equazione di ricorrenza era estremamente inefficiente: integer BATTERI(integer n) if (n ≤ 4) then return 1; else if (n ≤ 10) then return BATTERI(n - 3) + BATTERI(n - 4); else return BATTERI(i – 3) + BATTERI(i – 4) – BATTERI(i – 10); endif endif Nome e Cognome ______________________________________ Matricola __________________ Il costo asintotico T(n) soddisfa l'equazione di ricorrenza seguente: { 1 se 1≤n≤4 T (n)= T (n−3) + T (n−4) + 1 se 5≤n≤10 T (n−3) + T (n−4) + T (n−10) + 1 se n>10 Si presti attenzione che nel caso n > 10 l'espressione si differenzia da quella di B(n) perché ha tutte somme! T(n) indica il numero di chiamate ricorsive della funzione BATTERI, quindi se n > 10 il numero di invocazioni ricorsive è pari alla somma del numero di invocazioni necessarie per calcolare BATTERI(n – 3), più il numero di invocazioni necessarie per calcolare BATTERI(n – 4), più il numero di invocazioni necessarie per calcolare BATTERI(n – 10). La soluzione dell'equazione di ricorrenza può essere calcolata in modo simile a quanto fatto per i numeri di Fibonacci (dato che i dettagli non sono banali, mi aspettavo che tutti avrebbero scritto l'algoritmo lineare, che è molto simile a quello visto a lezione per i numeri di Fibonacci). Osservando che T(n) è una funzione crescente, possiamo scrivere: T (n) = T (n−3)+T (n−4)+T (n−10)+1 ≥ T (n−10)+T (n−10)+T (n−10)+1 ≥ 3 T (n−10) ≥ 9T (n−20) ≥ … ≥ 3 k T (n−k×10) da cui si può concludere che T(n) = (3 almeno esponenziale in n con base 31/10. n/10 ), cioè il costo asintotico dell'algoritmo ricorsivo è Nome e Cognome ______________________________________ Matricola __________________ Esercizio 2. Per ciascuna delle seguenti relazioni, indicare nella colonna di destra se è vera (V) o falsa (F). [punti 4] Vero (V) o Falso (F) V n (n+1) = O(n2 ) 2 n3 +1000 n2=O(n2 ) F n log n = O(n2 ) V n2 log n = O(n2 ) F n log n = O(n) F log n = O(n) V n = O(n log n) V n 2 = O (n log n) F n+ log(n) = O(log n) F n(n+1)(n+2) = O(n2 ) F Nome e Cognome ______________________________________ Matricola __________________ Esercizio 3. Un albero binario non vuoto si definisce degenere se ogni nodo diverso da una foglia ha un solo figlio (per convenzione, si assume che l'albero vuoto sia degenere). Ad esempio, gli alberi seguenti sono degeneri: Scrivere un algoritmo DEGENERE(t) che, dato in input un riferimento alla radice t di un albero binario non vuoto, rappresentato mediante puntatori ai figli, restituisce true se e solo se l'albero è degenere. [punti 6] Soluzione. bool DEGENERE(tree T) it (T = null) then return true; else if ( T.left ≠ null and T.right ≠ null) then return false; else // se arriviamo in questo punto siamo certi che (i) T non è vuoto, e // (ii) Almeno uno tra T.left e T.right è vuoto (anche entrambi) return DEGENERE(T.left) and DEGENERE(T.right); endif endif Nome e Cognome ______________________________________ Matricola __________________ Esercizio 4. Consideriamo una sequenza S[1..n] di n ≥ 1 caratteri, composta solamente di lettere minuscole dell'alfabeto. Scrivere un algoritmo efficiente per determinare la lunghezza della sottosequenza più lunga, composta da caratteri che occupano posizioni adiacenti in S, che risulti ordinata in senso non decrescente (in base al consueto ordinamento alfabetico 'a' < 'b' < … < 'z'). In altre parole, vogliamo determinare la lunghezza della più lunga sottostringa S[i..j] tale che S[i] ≤ S[i + 1] ≤ … ≤ S[j]. Ad esempio, se S = “algoritmi”, l'algoritmo restituisce 3 in quanto la sottosequenza più lunga composta da caratteri adiacenti non decrescenti è “gor”; se S = “dijkstra”, l'algoritmo restituisce 6 in quanto la sottosequenza più lunga è “dijkst”. Infine, se S = “abcaabdfgazaz", l'algoritmo restituisce 6. (Suggerimento: sia L[i] la lunghezza massima delle sottostringhe non decrescenti di S aventi S[i] come ultimo carattere...) [punti 6] Soluzione. Si applica quasi lo stesso algoritmo che è stato visto a lezione per il calcolo del sottovettore di somma massima. Sia L[i] la lunghezza massima delle sottostringhe di S i cui caratteri siano in ordine non decrescente, con il vincolo che l'ultimo carattere sia S[i]. Possiamo calcolare L[i], i = 1, … n usando la programmazione dinamica. Alla fine, il risultato sarà il massimo tra i valori L[i]. L'algoritmo seguente risolve il problema in tempo Θ(n). integer SUBSTRCRESCENTE( char S[1..n] ) integer L[1..n]; L[1] ← 1; integer Lmax ← 1; for integer i ← 2 to n do if (S[i - 1] ≤ S[i]) then L[i] ← L[i - 1] + 1; else L[i] ← 1; endif if ( L[i] > Lmax ) then Lmax ← L[i]; endif endfor return Lmax; Per questo esercizio sono state presentate le “soluzioni” più fantasiose, basate su esercizi di programmazione dinamica che nulla avevano a che fare con quanto richiesto dal problema. Nome e Cognome ______________________________________ Matricola __________________ Esercizio 5. Una rete stradale viene rappresentata mediante un grafo orientato pesato G = (V, E, w), dove gli n nodi rappresentano altrettanti incroci, e sono indicati dagli interi 1, 2, … n, e gli archi rappresentano segmenti stradali che connettono coppie di nodi. Trattandosi di un grafo orientato, gli archi possono essere attraversati solo lungo la direzione corretta. Una auto elettrica si trova inizialmente nel nodo 1 con la batteria totalmente carica. Supponiamo che la batteria possa immagazzinare P Joule di energia. La funzione peso w associa a ciascun arco orientato (u, v) un valore reale positivo w(u, v) che rappresenta l'energia (in Joule) che l'auto consuma per attraversare il tratto di strada rappresentato dall'arco (u, v). In corrispondenza di alcuni incroci sono disponibili dei punti di ricarica, in cui è possibile caricare completamente la batteria. In particolare, è dato un array R[1..n] di valori booleani, tali che R[i] = true se e solo se nel nodo i è presente una stazione di ricarica. Definire un algoritmo efficiente che, dato in input il grafo G = (V, E, w), la capacità P della batteria e l'array R[1..n], restituisca true se e solo se esiste un cammino che consente all'auto elettrica di andare dal 1 al nodo n senza esaurire la batteria lungo una strada; se necessario, l'auto può fare rifornimento presso uno o più nodi lungo il tragitto presso i quali sono presenti stazioni di ricarica. (Suggerimento: sia d[v] l'energia minima che si consuma per raggiungere il nodo v partendo dal nodo 1, dopo aver eventualmente fatto il pieno in v se è presente una stazione di ricarica) [punti 4] Determinare il costo asintotico dell'algoritmo proposto, motivando la risposta. [punti 2] Soluzione. Si può applicare l'algoritmo di Dijkstra o Bellman-Ford per il calcolo dei cammini minimi usando il nodo 1 come sorgente. L'algoritmo deve essere modificato: per ogni nodo v si denota con d[v] il consumo minimo di energia richiesto per andare dalla sorgente al nodo v. Se v è un nodo di ricarica (R[v] = true) e il nodo v è raggiungibile senza restare a piedi lungo la strada, occorre settare d[v] = 0. Questo si giustifica considerando che in v si può caricare completamente la batteria, quindi ripartendo dal nodo v è come se si fosse consumato zero. Supponendo di usare l'algoritmo di B-F, si ottiene lo pseudocodice seguente. Possiamo eliminare le parti dell'algoritmoche non servono, come ad esempio l'array dei predecessori; le modifiche sono evidenziate. boolean RAGGIUNGIBILE(G=(V, E, w), double P, boolean R[1..n]) integer v, u, i; double d[1..n]; for v ← 1 to n do d[v] ← +∞; endfor d[1] ← 0; for i ← 1 to n - 1 do for each (u,v) in E do if ( d[u] + w(u,v) ≤ P and d[u] + w(u,v) < d[v] ) then if ( R[v] ) then d[v] ← 0; else d[v] ← d[u] + w(u,v); endif endif endfor endfor return (d[n] < +∞); Il costo asintotico dell'algoritmo è quello dell'algoritmo di B-F, cioè O(nm) Nome e Cognome ______________________________________ Matricola __________________ Qualcuno ha proposto soluzioni in cui il valore di P veniva decrementato per ogni arco attraversato. Questo non è corretto, in quanto l'algoritmo di Dijkstra o B-F esplora percorsi alternativi in cui i consumi possono risultare diversi. Quindi non si può usare una unica variabile P per tenere traccia del consumo, ma occorre fare riferimento alla quantità d[u] + w(u, v) Nome e Cognome ______________________________________ Matricola __________________ Esercizio 6. Si consideri il seguente grafo non orientato. A B C G D E F 1. Supponiamo che gli archi evidenziati rappresentino l'albero prodotto da una visita in ampiezza (visita BFS). Rappresentare il grafo mediante liste di adiacenza in modo che l'algoritmo di visita produca l'albero mostrato. Si assuma che l'algoritmo di visita prenda in considerazione gli archi nell'ordine in cui sono indicati nelle liste di adiacenza. Indicare da quale nodo è iniziata la visita. [punti 2] 2. Supponiamo che gli archi evidenziati rappresentino un Minimum Spanning Tree (MST) del grafo. Nell'ipotesi in cui i pesi dei dieci archi siano tutti e soli gli interi dell'insieme {1, 2, … 10}, assegnare un peso a ciascun arco in modo da ottenere il MST mostrato. E' richiesto l'uso di tutti i pesi {1, 2, … 10}; archi diversi devono avere pesi diversi. [punti 2] Soluzione. La visita inizia dal nodo B. Per ottenere un MST come quello indicato è sufficiente assegnare agli archi evidenziati i pesi minori (da 1 a 6).