possibile soluzione - Dipartimento di Ingegneria informatica

Esame di Algoritmi e Strutture Dati
Corso di Laurea in Ingegneria Informatica
Canali A-L, M-Z
17 dicembre 2003
Cognome .......................................................................................................................
Nome ............................................................................................................................
Matricola .......................................................................................................................
Docente.........................................................................................................................
Domanda 1, punti 6 Con riferimento al seguente algoritmo
public static double algo(double x) { // assumere x >= 0
final double EPS = 1E-10;
double start = 0;
double end = x + 1; // ok qualunque valore > x
double mid, mid2;
while(end - start > EPS) {
mid = (start + end) / 2;
mid2 = mid * mid;
if(mid2 > x) end = mid;
else start = mid;
}
return (start + end) / 2;
}
determinare:
1. quale funzione matematica viene calcolata dall'algoritmo
2. il costo computazionale dell'algoritmo, in funzione della dimensione dell'input,
ovvero del numero di bit sufficienti per la sua rappresentazione (nell'analisi,
assumere EPS parametro costante)
POSSIBILE SOLUZIONE
1. L’algoritmo calcola la radice quadrata di x, con una precisione di EPS/2. L’idea è quella di
mantenere un intervallo [start, end] in cui sicuramente si trova la radice quadrata di x, e
dimezzare progressivamente l’ampiezza di tale intervallo (ispirandosi all’algoritmo di ricerca
binaria), finché non si raggiunge un’ampiezza di EPS. Quando questo avviene, la radice
quadrata di x differisce dal punto medio dell’intervallo meno di EPS/2.
2. Il costo dell’algoritmo è O(h), dove h è il numero di iterazioni del ciclo while eseguite. Al
termine di ciascuna iterazione si dimezza l’ampiezza dell’intervallo di ricerca.
L’ampiezza iniziale è di x+1; dopo la prima iterazione l’ampiezza diventa (x+1)/2, …, dopo
l’ultima iterazione (x+1)/2h<EPS.
Risolvendo la disequazione troviamo che h ≥ log2((x+1)/EPS)=O(log2 x).
Ora dobbiamo esprimere la complessità computazionale dell’algoritmo in funzione della
dimensione dell’input, tenendo conto della rappresentazione di x.
Se x è rappresentato in virgola fissa con n bit, x=O(2n) e quindi h=O(log 2n)=O(n): la
complessità è lineare.
Approfondimento
Se invece x è rappresentato in virgola mobile, è della forma x = m * 2 e. Dal momento che 0 <=m < 1 (si ricordi che
abbiamo assunto x>=0), x=O(1*2e). Sia n il numero di bit usato per rappresentare e: poiché e=O(2n), risulta
x=O(2^(2n)). Pertanto h=O(log 2^(2n))=O(2n) e la complessità è esponenziale.
Lasciamo come esercizio al lettore lo studio di quello che succede se l’esponente è rappresentato in eccesso a 2k-1.
1/5
Domanda 2, punti 6 Descrivere i due algoritmi di ordinamento HeapSort ed
InsertionSort. Se l'input è un array ottenuto scambiando gli elementi di posto i e j di un
array ordinato, quale algoritmo è più conveniente? Giustificare la risposta.
POSSIBILE SOLUZIONE (per la descrizione degli algoritmi si rimanda al libro di testo)
Sia a l’array ordinato: l’array da ordinare è del tipo:
a[0] a[1] … a[i-1] a[j] a[i+1] … a[j-1] a[i] a[j+1]… a[n-1].
InsertionSort scandisce l’array, e mantiene ordinata la sottosequenza degli elementi che
precedono l’elemento attualmente scandito.
Ecco quello che accade quando InsertionSort scandisce l’array:
 elementi a[0], …, a[i-1]: sono già ordinati, InsertionSort non fa nulla;
 elemento a[j]: maggiore di a[i-1], non fare nulla;
 elemento a[i+1]: minore di a[j] ma maggiore di a[i-1], arretrarlo di una posizione
scambiandolo con a[j];
 elementi a[i+2], …, a[j-1]: come sopra, arretrarli di una posizione (in questo modo
a[j] è mantenuto l’ultimo elemento della sottosequenza ordinata)
 elemento a[i]: va arretrato fino alla posizione i (tramite j-i scambi)
 elementi rimanenti a[j+1], …, a[n-1]: già ordinati, non fare nulla.
Contiamo gli scambi effettuati: sono 2(j-i)-1 < 2n. La complessità è lineare.
HeapSort, invece, per prima cosa trasforma l’array da ordinare in un heap, distruggendo
l’ordinamento preesistente: la sua complessità è quella del caso peggiore, O(n log n).
In questo caso è più conveniente InsertionSort.
2/5
Domanda 3, punti 6 Progettare una struttura di dati che supporti le seguenti operazioni,
tutte eseguite con costo computazionale O(1):
1. insert(x). Inserisce un nuovo elemento x.
2. removeLast(). Elimina, restituendolo, l'ultimo elemento inserito (se la struttura
dati è non vuota).
3. removeFirst(). Elimina, restituendolo, il primo elemento inserito (se la struttura
dati è non vuota).
Costruire una classe Java che realizzi la struttura specificata.
POSSIBILE SOLUZIONE
Si può usare una lista doppiamente concatenata, inserendo gli elementi in coda. Per rimuovere
l’ultimo elemento è sufficiente rimuovere l’elemento in coda; per rimuovere il primo elemento
inserito (cioè il più vecchio) rimuovo l’elemento in testa.
public class ElemStruttura {
public Object info;
public ElemStruttura next;
public ElemStruttura prev;
}
public class Struttura {
protected ElemStruttura first, last;
public Struttura() {
last=first=null;
}
public void insert(Object el) {
if(last==null) { //la struttura è vuota
last=first=new ElemStruttura();
last.info=el;
last.next=last.prev=null;
} else { //la struttura non è vuota
last.next=new ElemStruttura();
last.next.prev=last;
last=last.next;
last.next=null;
last.info=el;
}
}
public Object removeLast() {
if(last==null) {
return null; //errore di struttura vuota
} else {
Object aux=last.info;
last=last.prev;
if(last==null) //la struttura si è svuotata
first=null;
else
last.next=null;
return aux;
}
}
public Object removeFirst() {
if(first==null) {
return null; //errore di struttura vuota
} else {
Object aux=first.info;
first=first.next;
if(first==null) //la struttura si è svuotata
last=null;
else
first.prev=null;
return aux;
}
}
}
3/5
Domanda 4, punti 6 Scrivere un algoritmo (Java) che, dato un albero di ricerca T e una
chiave k, spezzi T in due alberi di ricerca, il primo con tutte le chiavi di T minori o eguali a
k, il secondo con tutte le chiavi di T maggiori di k (assumere che in T non vi siano chiavi
duplicate).
Valutare il costo computazionale dell'algoritmo (si noti che non è necessaria la visita
dell'intero albero).
POSSIBILE SOLUZIONE (illustrazione dell'algoritmo)
L’idea è quella di scendere nel BST T: ogni volta che mi trovo su un nodo, a seconda del
valore della chiave corrente, so con certezza che uno dei due sottoalberi del nodo corrente
contiene esclusivamente valori minori (oppure maggiori) di k. Per questo sottoalbero non
ho problemi: posso staccarlo da T e attaccarlo direttamente al BST dei nodi minori (risp.
maggiori) di k. Invece, devo continuare a esplorare l’altro sottoalbero, scendendo verso il
figlio relativo. Poiché sto scendendo dritto verso una foglia, compiendo un numero
costante di operazioni per nodo, la complessità computaz. è Ө(h), dove h è l’altezza di T.
public class DueBSTNode { //Tipo di ritorno del risultato di spezzaAlbero.
public BSTNode primo, secondo;
...
}
...
public static DueBSTNode spezzaAlbero(BSTNode root, int k) {
//Creo una radice fittizia di un nuovo BST, che uso come “record generatore”:
//nel suo sottoalbero SINISTRO costruisco il BST con le chiavi MAGGIORI di k,
//nel suo sottoalbero DESTRO costruisco il BST con le chiavi MINORI di k.
//Questo (arbitrario) ordinamento sarà comodo nell’algoritmo.
BSTNode radiceFittizia=new BSTNode();
//Creo un riferimento al PADRE del prossimo nodo da inserire in
//ciascuno dei due BST che sto costruendo (dei minori e dei maggiori di k).
BSTNode piccoliCorr=radiceFittizia, grandiCorr=radiceFittizia;
//Nel prossimo ciclo, root è un riferimento alla radice
//della parte del BST T che mi rimane da "spezzare" (all’inizio, tutto T).
while(root!=null)
if(root.key<=k) {
//In questo caso, root e tutto il suo sottoalbero SINISTRO contengono
//valori MINORI di k; perciò vanno agganciati a piccoliCorr:
piccoliCorr.rightChild=root;
//Invece non so dire nulla delle chiavi del sottoalbero DESTRO di root,
//quindi continuo la sua scansione:
root=root.rightChild;
//Aggiorno piccoliCorr: devo staccare il sottoalbero DESTRO di root(*) su
//cui non so dire nulla, liberando così il figlio DESTRO del nodo appena
//attaccato a piccoliCorr. Nota che sempre il figlio DESTRO è disponibile
//ad accettare nuovi sottoalberi:
piccoliCorr=piccoliCorr.rightChild; //scendo...
piccoliCorr.rightChild=null; //...e stacco il sottoalbero destro di root.
} else {
//Caso simmetrico al precedente.
grandiCorr.leftChild=root;
root=root.leftChild;
grandiCorr=grandiCorr.leftChild;
grandiCorr.leftChild=null;
}
return new DueBSTNode(radiceFittizia.rightChild, radiceFittizia.leftChild);
}
//(*) il vecchio root, naturalmente!
4/5
Domanda 5, punti 6
Scrivere un algoritmo (Java) che, dati un grafo non orientato G = (V, E) e un intero
positivo k, decida se G è un grafo composto da esattamente k componenti connesse.
Definire la rappresentazione del grafo e valutare il costo computazionale dell'algoritmo.
POSSIBILE SOLUZIONE
In questa proposta di soluzione, l’algoritmo è espresso in pseudocodice.
Ogni volta che eseguiamo una DFS su G a partire da un vertice v1, visitiamo tutti e soli i
nodi raggiungibili da v1. In altre parole, poiché il grafo G è non orientato, visitiamo tutta e
sola la componente connessa a cui appartiene v1.
Ci chiediamo se esiste un’altra componente connessa in G. Questo accade se e soltanto
se esiste un vertice v2 che non è stato raggiunto dalla DFS a partire da v1; ossia, se esiste
un vertice v2 che non è stato marcato dalla DFS.
Eseguendo un’altra DFS a partire da v2 troviamo una nuova componente connessa, quella
a cui appartiene v2; e così via, finché tutti i vertici sono stati marcati (cioè: tutte le
componenti connesse sono state scoperte).
Contando quante volte devo eseguire la DFS a partire da un vertice non marcato,
contiamo le componenti connesse di G.
La complessità computazionale è quella di una DFS, ossia O(|V|+|E|).
boolean componentiConnesse (Grapg G, int k) {
int conteggioComponenti=0
for (<tutti i vertici v di G>)
marcato(v) = false;
while(<esiste un vertice v di G: marcato(v)==false>) {
conteggioComponenti++;
DFS(G, v);
}
return (conteggioComponenti==k);
}
void DFS(Graph G, Vertex v) {
//Questa DFS visita la componente connessa a cui appartiene v.
marcato(v) = true;
for (<tutti i vertici w adiacenti a v>)
if (!marcato(w))
DFS(G, w);
}
5/5