Triangolo di Tartaglia

Il Triangolo di Tartaglia

Triangolo di Tartaglia
I primi otto livelli del triangolo di Tartaglia

Il triangolo di Tartaglia è una "tabella" di numeri che si costruisce partendo da 1 (per il triangolo classico) e aggiungendo livelli sotto, ciascuno di un elemento in più di quello sopra.

Gli elementi del livello successivo sono la somma degli elementi immediatamente sopra.

Costruzione del terzo livello del triangolo

Il coefficiente binomiale

La corrispondenza tra le righe del triangolo e il numero di riga è utile in molti casi:

0 ⇒ 1
1 ⇒ 1, 1
2 ⇒ 1, 2, 1
3 ⇒ 1, 3, 3, 1
4 ⇒ 1, 4, 6, 4, 1
5 ⇒ 1, 5, 10, 10, 5, 1

Per esempio, l'n-esima riga del triangolo dice qual è il coefficiente nello sviluppo di un polinomio.

Nota: si assume che la prima riga, contenente solo 1, sia la "riga zero".

Un paio di esempi possono essere più chiari.

Voglio risolvere (a+b)2. Corrisponde a (a+b)(a+b), quindi a*a + a*b + b*a + b*b, e quindi a2+2ab+b2.

Voglio risolvere (a+b)3. Corrisponde a (a+b)(a+b)(a+b). Ora sappiamo che (a+b)(a+b) = a2+2ab+b2, quindi possiamo scrivere (a2+2ab+b2)(a+b). Otteniamo a2*a+a2*b+2ab*a+2ab*b+b2*a+b2*b, che raggruppando i termini uguali corrisponde a a3+3a2b+3ab2+b3.

Se volessi risolvere (a+b)4 sarei costretto a numerosi (e noiosi) passaggi, per ottenere alla fine a4+4a3b+6a2b2+4ab3+b4

Con un po' di spirito di osservazione, notiamo che in tutti i polinomi abbiamo un grado che sale e uno che scende (nel nostro esempio, a sale e b scende): per esempio in un polinomio di grado 4, a scende da 4 a zero: 1a4b0+4a3b1+6a2b2+4a1b3+1a0b4 mentre il grado di b sale 1a4b0+4a3b1+6a2b2+4a1b3+1a0b4

Per quanto riguarda invece i coefficienti, notiamo che sono dati esattamente dalla riga del triangolo di Tartaglia corrispondente al grado del polinomio: 1a4b0+4a3b1+6a2b2+4a1b3+1a0b4

Se volessi risolvere quindi (a+b)5 mi basterebbe:

inserire a con esponente da 5 a 0 a:

a5 + a4 + a3 + a2 + a1 + a0

inserire b con esponente da 0 a 5:

a5 b0 + a4 b1 + a3 b2 + a2 b3 + a1 b4 + a0 b5

inserire il coefficiente binomiale della sesta riga, corrispondente al polinomio di grado 5:

1 a5 b0 + 5 a4 b1 + 10 a3 b2 + 10 a2 b3 + 5 a1 b4 + 1 a0 b5

Poi - se voglio - riscrivo lo sviluppo togliendo gli esponenti a zero e a uno, per essere un po' più elegante:
a5+5a4b+10a3b2+10a2b3+5ab4+b5

Notazioni matematiche

Il coefficiente binomiale si indica con

(rk)

in cui n è il numero della riga e k è il numero dell'elemento.
Convenzionalmente k ≤ n; se invece k > n si assume che il valore sia 1 (valore costante).

Il calcolo del coefficiente binomiale è dato da

n!k!n-k!

Coefficiente e statistica

Il lancio di una moneta (non truccata) può dare come esito uno tra due risultati equivalenti.

Se lancio più volte la moneta ottengo un albero di possibilità (non probabilità!), che corrisponde, per quattro lanci, al seguente (T = Testa, C = Croce):

0 (non ho ancora lanciato)
1 T C
2 T C T C
3 T C T C T C T C
4 T C T C T C T C T C T C T C T C

Al termine dei quattro lanci ho sicuramente seguito uno e uno solo dei sedici percorsi possibili.

Noto che i percorsi sono sedici perché non ho ancora lanciato mai la moneta: infatti se al primo lancio ottenessi croce, le probabilità che possa seguire un percorso "a sinistra" diventano zero.

Le probabilità che ottenga sempre testa sono 1/16, così come le probabilità che ottenga sempre croce (questa è facile), ma quante probabilità ci sono che ottenga esattamente due volte testa? Sono le stesse che ottenga esattamente croce (anche questa è facile), ma contiamole:

0 (non ho ancora lanciato)
1 T C
2 T C T C
3 T C T C T C T C
4 T C T C T C T C T C T C T C T C

e contiamo con altri colori le volte che ottengo tre volte testa o tre volte croce:

0 (non ho ancora lanciato)
1 T C
2 T C T C
3 T C T C T C T C
4 T C T C T C T C T C T C T C T C

Ci sono:

  • 1/16 probabilità di ottenere quattro teste
  • 4/16 = 1/4 di ottenere tre teste e una croce
  • 6/16 = 3/8 di ottenere due teste e due croci
  • 4/16 = 1/4 di ottenere tre croci e una testa
  • 1/16 probabilità di ottenere quattro croci

Controprova: sommo tutte le probabilità e ottengo 16/16.

Anche in questo caso, il triangolo di Tartaglia riporta esattamente le probabilità di testa o croce leggendo l'n-esima riga come l'n-esimo lancio. Notiamo che la prima riga corrisponde a non aver ancora lanciato, e per convenzione è 1.

Caratteristiche del TdT

Somma delle righe

Le possibilità di lanci di moneta sono esponenti di due: al primo lancio ho 21 possibilità (T, C), al secondo lancio ne ho 22 (TT, TC, CT, CC), al terzo ne ho 23 (TTT, TTC, TCT, TCC, CTT, CCT, CTC, CCC), al quarto ne ho 24 e così via.

Per questo la somma di ciascuna riga del TdT è un esponente di due:

riga 0: 1 = 20

riga 1: 1+1 = 21

riga 2: 1+2+1 = 22

riga 3: 1+3+3+1 = 23

e così via

Simmetria

Il TdT è simmetrico lungo l'asse centrale. Questo può essere visto come una conseguenza del fatto che testa e croce, dal punto di vista della probabilità, sono equivalenti, così come la scelta di a e b nel binomio.

Questo significa che l'elemento k della riga n e l'elemento n-k+2 della riga n sono sempre uguali.

L'elemento 2 della riga 5 e l'elemento 5-2+2 della riga 6 sono uguali

Diagonale zero

La prima diagonale, che chiameremo "diagonale zero" è una serie di 1.

Prima diagonale

La prima diagonale è un progressivo, che rappresenta il numero di riga partendo da zero.

Seconda diagonale

La seconda diagonale si ottiene aggiungendo la differenza tra l'elemento e quello precedente più uno.

Il primo elemento è 1. Il secondo elemento è dato dalla differenza tra il precedente (0) e il corrente (1) più uno = 3.

Il terzo è 3+(3-1)+1 = 6, il quarto 6+(6-3)+1 e così via.

In modo ricorsivo, si ha che

f(n) = {1 se n = 1; f(n-1)+[f(n-1)-f(n-2)]+1 altrimenti}

La definizione ricorsiva è poco agevole: per fortuna, con un po' di intuizioni "geometriche" è possibile trovare una formula più facile.

Questa immagine rappresenta una serie in cui nella prima riga c'è un elemento, nella seconda due e così via. Ebbene, la somma di tutti i quadrati fino a una certa riga corrisponde esattamente al numero resitituito dalla terza diagonale di Tartaglia.

Per come sono costruiti questi numeri, vengono detti numeri "triangolari".

Facciamo un esempio per n=4:

In questo esempio si dimostra che il quarto numero triangolare è 10

Se aggiungiamo ai "quadratini" che rappresentano il numero, altrettanti "quadratini", otteniamo un rettangolo di dimensione n*(n+1)

Con qualche facile trasformazione, otteniamo che la formula per ottenere il numero corrispondente all'elemento n-esimo della diagonale è

n*n+12

Terza diagonale

La differenza tra l'elemento n-esimo e n+1-esimo della terza diagonale corrisponde al numero precedente della seconda: 4-1 = 3, 10-4 = 6, 20-10 = 10 eccetera.

La terza è l'ultima diagonale trattata, perché dalla quarta in poi il principio è sempre uguale. A dirla tutta, anche per le diagonali precedenti valeva lo stesso, ma era più semplice trattare il problema specifico che quello generico.

La formula "secca" per ottenere la serie della quarta diagonale è

n*n+1*n+26

Il fatto che n sia moltiplicato per sé stesso tre volte fa intuire una corrispondenza geometrica tra la serie e una figura tridimensionale.

Infatti, in un tetraedro composto da "palline", il numero di palline totali a un certo livello corrisponde al numero della terza diagonale:

Per n = 3, il numero corrispondente nella terza diagonale è 10, come il numero di sfere della figura

Generalizzazione

La formula generalizzata per il calcolo dei numeri della k-esima diagonale è quindi:

n+k-1!n-1!*k!

Per farla ancora più semplice, in excel è

Ovviamente la formula funziona con qualunque riga, anche con quelle già discusse.

Questa formula è utile in matematica combinatoria e in statistica perché è quella che si usa nelle combinazioni con ripetizioni (quelle che rispondono alla domanda "Ho 12 penne e 5 cassetti. In quanti modi posso distribuire le penne nei cassetti?")

Fibonacci

Le "semi-diagonali" del triangolo, se sommate, danno i numeri di fibonacci.

Un numero di Fibonacci è dato dalla somma dei due numeri precedenti, tranne i primi due che sono sempre 1 e 1: la serie è quindi 1, 1, 2, 3, 5, 8, 13, 21, 34...

Undici

11 è un numero che contiene le cifre della riga 1
112 contiene le cifre della riga 2
113 contiene le cifre della riga 3
114 contiene le cifre della riga 4
115 NON contiene le cifre della riga 5, perché una delle cifre della riga 5 è un 10, e nel sistema decimale (l'unico con cui "funziona" questo gioco) ha due cifre.

Commento al compito

  1. extends e implements sono diversi: una classe può estendere un'altra classe, un'interfaccia può estendere un'altra interfaccia, ma una classe non può estendere un'interfaccia
  2. Iterable e Iterator NON sono la stessa cosa 🙂
    Iterable è un'interfaccia.
    Iterable richiede di implementare un metodo che restituisce un Iterator e si chiama iterator<Tipo>(). Il metodo può anche solo restituire un oggetto che implementa un Iterator.
    L'oggetto Iterator può essere anche una classe innestata creata ad hoc, che implementa boolean HasNext() e Tipo Next().
  3. se devo dichiarare una proprietà valida per tutta l'interfaccia devo anche inizializzarla dentro l'interfaccia.
  4. se implemento un oggetto generics, i metodi che nell'interfaccia prendono T devono prendere un parametro Object, non T, a meno che T non estenda qualche altro tipo.

Alcune parole chiave della programmazione a oggetti (ordine sparso)

Wrapper

Un wrapper è un oggetto che rappresenta un tipo primitivo.

Per esempio, Float è il wrapper di float

Un wrapper contiene, a differenza di un tipo primitivo, dei metodi, per esempio per convertire una stringa in numero (parse, parseTo).

Autoboxing

L'Autoboxing è la capacità del compilatore di convertire automaticamente oggetti wrapper nei corrispondenti tipi nativi e viceversa.

Per esempio, il codice

Float f = 3.2;

è perfettamente valido perché 3.2 è convertito in un oggetto Float il cui valore è 3.2.

Casting

Il casting è l'operazione di modifica di un tipo di un oggetto con un altro tipo compatibile.

Per esempio, il seguente codice fa un ciclo in un elenco di figure, e se trova un cerchio ne calcola il raggio.

for(Shape s: shapes) {
    if(s instanceof Circle) {
        return ((Circle)s).getRadius();
    }
}

Il ciclo deve essere effettuato su oggetti di tipo Shape, ma la proprietà radius è presente solo nei cerchi.
Il cast si ottiene anteponendo il nuovo tipo tra parentesi.

int i = 5;
float f = (float)i;

Metodo

Un metodo è una serie di istruzioni disponibili in un oggetto.
Descrivendolo in modo intuitivo, il metodo è ciò che l'oggetto fa.

  • Può essere private, protected o public.
  • Può essere static, se non richiede che l'oggetto sia istanziato.
  • Può essere final/sealed; in questo caso non è ridefinibile (vedi overload).
  • La sintassi richiede che sia sempre specificato cosa restituisce: un oggetto, un tipo primitivo oppure void (niente).
  • Può prendere parametri.

L'insieme degli elementi dell'elenco si chiama firma dell'oggetto.

Override/sovrascrittura

L'override è la riscrittura di un metodo ereditato.

Per esempio

public class Shape {
    @Override
    public String toString() {return "Questa è una figura geometrica";}
}
public class Rectangle extends Shape {
    @Override
    public String toString() {return "Questo è un rettangolo";}
}

richiamando toString() in un oggetto di tipo Rectangle si ottiene "Questo è un rettangolo".

Overload/sovraccarico (polimorfismo)

L'overload consiste nella possibilità di utilizzare lo stesso metodo con parametri diversi.

class Rectangle {
    public Rectangle(float top, float left, float width, float height) {
        ...
    }
    public Rectangle(float top, float left, float side) {
        ... // disegna un quadrato
    }
}

Varargs

Si usa per specificare un numero non conosciuto a priori di parametri.

public String concat(String... list) {
    String appo = "";
    for(String s: list) appo += s;
    return appo;
}

questa procedura prende un numero indeterminato di stringhe e le concatena

Instanza

Una classe è la definizione di una serie di caratteristiche di un oggetto. Un'istanza è l'insieme dei valori di queste caratteristiche per uno degli oggetti.

Un oggetto si istanzia con la parola chiave new e di seguito uno dei costruttori della classe.

Costruttore

Il costruttore di un oggetto è un metodo particolare che ha lo stesso nome della classe e non ha valori di ritorno (non è specificato nemmeno void).

Il costruttore è un metodo eseguito automaticamente quando si istanzia una classe.

I costruttori possono avere firme diverse, cioè prendere parametri diversi, come i metodi normali (a differenza dei metodi normali, che ritornano un tipo, i costruttori si differenziano solo per i parametri passati).

this

Rappresenta l'istanza corrente.

Di solito è utilizzato per fare riferimento all'istanza stessa, per esempio quando va passata a un metodo come parametro, oppure nel caso in cui ci sia ambiguità nei nomi di variabile.

class Example {
    int valore;
    public Example(int valore) {
        this.valore = valore; // il primo è la proprietà della classe, il secondo è il nome della variabile
    }
}

super

Rappresenta la classe superiore di una classe che eredita.

static

Definisce che una classe è statica.

Una classe statica non può essere istanziata, e quindi i metodi e le proprietà che ne fanno parte possono essere richiamate senza fare uso di new.

class Example {
    public static String myUpper(String lower) {
        return String.toUpper(lower);
    }
    public Example() {
        String s = "ciao";
        System.out.println(Example.myUpper(s));
    }
}

Array

Gli array sono strutture dati che contengono molti valori dello stesso tipo. Il numero di elementi deve essere dichiarato all'inizio e non può essere cambiato.

Gli array sono indicizzati con numeri che vanno da 0 (zero) a n-1, dove n è il numero di elementi. Quindi un array di 10 elementi avrà come indici 0,1,2,3,4,5,6,7,8,9.

L'indice di un array deve essere compreso tra parentesi quadre:

int[] elenco = new int[10];
elenco[3] = 18 // il quarto elemento

enum

La parola chiave enum permette di creare delle variabili che contengono uno di un elenco di valori fissi.

public class Example {
    public enum weekDays = {LUN, MAR, MER, GIO, VEN, SAB, DOM};
    public weekDays today;
    public Example(weekDays d) {
        this.today = d;
    }
    public Example() {
        this.today = weekDays.DOM; // se non specifico diversamente, è domenica
    }
}

Classe astratta

Una classe astratta è un tipo di classe che non può essere istanziata. L'unico modo di utilizzare una classe astratta è specializzarla in una classe non astratta.

In una classe astratta alcuni metodi possono riportare la sola firma; poi nella classe specializzata il metodo viene definito. In questo modo la classe astratta garantisce la presenza di un metodo con quella determinata firma, ma lascia il programmatore libero di definire per ogni sottoclasse il comportamento desiderato.

abstract class Animale {
    int numeroZampe;
    public void corri();
}

class Cane extends Animale {
    public Cane() {
        numeroZampe = 4; // numeroZampe è ereditato dalla superclasse
    }
    public void corri() {
        // implementazione della corsa a quattro zampe
    }
}

class Umano extends Animale {
    public Umano() {numeroZampe = 2;}
    public void corri() {
        // implementazione della corsa a due zampe
    }
    public void scrivi(String s) {
        // la classe specializzata può aggiungere metodi propri
    }
}

Interfaccia

Un'interfaccia definisce proprietà e metodi di una classe, senza però implementarne i metodi.

Nei linguaggi senza ereditarietà multipla, le interfacce sono utili per implementare in oggetti diversi - magari che ereditano già da una classe - alcune caratteristiche.

class Book extends Sellable implements Readable

in questo esempio definiamo una classe Book come un oggetto che eredita da una superclasse Sellable (vendibile) che implementa l'interfaccia Readable (leggibile).

Classe ospite

Una classe ospite è una classe definita dentro un'altra classe (solitamente le classi sono definite in file che portano lo stesso nome della classe più l'estensione .java).

Una classe ospite - se non static - può accedere alle proprietà e ai metodi della classe ospitante, anche se private.

Classe anonima

Una classe anonima è un'istanza di una classe non definita (es. un'interfaccia o una classe astratta) che, per un uso locale, definisce "al volo" tutti i metodi necessari.

public interface ICiao {
    public String saluta(String nome);
}

public class Example {
    public Example() {
        String nome = "Antonio";
        ICiao saluto = new ICiao(nome) {
            @Override
            public String saluta(String nome) {
                System.out.println("Saluta "+nome);
            }
        }
    }
}

ovviamente la classe istanziata non ha un tipo definito (è anonima) e non avendo una definizione se non quella data "al volo" non può essere usata fuori dal metodo che la definisce.

Una classe anonima può accedere a tutti i membri della classe che la contiene, anche a quelli privati.

Le classi anonime sono molto utilizzate in contesti in cui è frequente la gestione degli eventi (handlers).

Generics

I tipi generici sono tipi definiti successivamente dalla classe che li utilizza.

Un esempio tipico di generics sono quelli usati nelle liste

// ArrayList<T>
ArrayList<String> ListaDiStringhe = new ArrayList<>();
ArrayList<MyObject> ListaDiMyObject = new ArrayList<>();

Un parametro generic può essere limitato anche a tipi che soddisfino alcune condizioni, come ereditare da un'altra classe o da un'interfaccia

// MyArrayList<T extends MyInterface>
ArrayList<MyTypeExtendingMyInterface1> uno;
ArrayList<MyTypeExtendingMyInterface2> due;

enhanced for

La sintassi "migliorata" del ciclo for consiste in for(tipo istanza: collezione)

Questo:

for(int i = 0; i < collezione.count(); i++)
    myObject o = collezione.get(i);

diventa:

for(myObject o: collezione)

differenze tra collezioni (collection) e insiemi (set) e mappe (map)

collezione set map
Valori duplicati no sì (valori, ma non chiavi)
Elementi ordinati non tutti sì, per chiave

map

map è una classe astratta che utilizza chiave e valore.

Esistono varie specializzazioni di map, tra cui:

  • HashMap (consente valori null)
  • HashTable (non consente valori null)
  • TreeMap (consente ordinamento per chiave)

Gli elementi sono gestiti tramite i metodi get() e put()

lambda expression

Le espressioni lambda permettono di scrivere codice più sintetico. Si può sostituire il codice in questa maniera, nel caso in cui sia definita un'interfaccia myInterface con un metodo myMethod().

  1. creare una nuova classe basata sull'interfaccia myInterface, implementare il metodo myMethod; creare un'istanza dell'oggetto e poi richiamare myMethod
public interface myInterface {
    public int myMethod(int a, int b);
}
public myClass implements myInterface {
    @Override
    public int myMethod(int a, int b) {return a+b;}
}
public static void run() {
    ArrayList<myInterface> myArrayList = new ArrayList<>();
    myClass c = new myClass();
    myArrayList.add(c);
}
  1. utilizzare direttamente l'interfaccia tramite una classe astratta: si richiama new myInterface() e si implementa direttamente dentro il new il metodo myMethod
public interface myInterface {
    public int myMethod(int a, int b);
}
public static void run() {
    ArrayList<myInterface> myArrayList = new ArrayList<>();
    myArrayList.add(new myInterface() {
        public int myMethod(int a, int b) {return a+b};
    }.myMethod(a, b));
}
  1. utilizzare una lambda expression al posto dell'implementazione
public interface myInterface {
    public int myMethod(int a, int b);
}

public static void run() {
    ArrayList<myInterface> myArrayList = new ArrayList<>();
    myArrayList.add((int a, int b) -> { return a+b; });
}

stream

Uno stream è un oggetto che rappresenta una sequenza di elementi, su cui è possibile fare delle operazioni di ricerca, filtro e ordinamento.

Sono utilizzati molto in accoppiata con le lambda expression.

myList.stream()
    .filter((a) -> a.value < 100)
    .sort()
    .forEach((a) -> System.out.println(a.value));

Classe immutabile

Una classe immutabile è una classe i cui elementi non cambiano dopo l'inizializzazione.

Tutti i campi sono dichiarati final, così come la classe, e non ci sono modificatori (es. setter).

Esami di PO

Definizioni

Setter

Spiegare cosa si intende per metodo setter. Fornire almeno una motivazione per la quale l'uso di setter è preferibile alla definizione di campi pubblici. Fare un esempio concreto (con codice) di un setter che svolge una funzione non ottenibile con un campo pubblico.

Svolgimento

Un metodo setter serve a impostare il valore di una proprietà privata o protetta. Tramite un metodo setter è possibile eseguire altro codice, oltre all'assegnazione del valore, tra cui eventuali controlli di validità del valore stesso.

Esempio:

private int lucky;
public void setLucky(int value) {
    if(value == 13 || value == 17) lucky = 0; else lucky = value;
}

Sovrascrittura

Spiegare approfonditamente in concetto di sovrascrittura di un metodo e quali sono le condizioni nelle quali puo essere utile. Fare un esempio pratico mostrando le necessarie porzioni di codice.

Svolgimento

La sovrascrittura si usa quando una classe eredita da una superclasse, e desidera modificare il funzionamento di un metodo ereditato.

Consiste nel creare un metodo, nella subclasse, che ha lo stesso nome e gli stessi parametri dell'omonimo nella superclasse e permette di ridefinirne il comportamento.

Un esempio comune di sovrascrittura riguarda il metodo toString() presente nella classe object:

class Example {
    int value;
    String key;

    public String toString() {
        return key + " => " + value;
    }
}

Espressioni lambda

Considerare l'interfaccia ActionListener con l'unico metodo void actionPerformed(ActionEvent e). Considerare il JButton b e mostrare (con codice) come utilizzare il metodo void addActionListener(ActionListener l) per aggiungere un ActionListener che stampi a console la stringa "Java", codi cato rispettivamente come una classe anonima e come un'espressione Lambda.

Svolgimento
JButton jbutton = new JButton(); 
jbutton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Java");
    }
});
 
jbutton.addActionListener((e) -> {
    System.out.println("Java");
});

Design e programmazione

I seguenti esercizi servono a veri care la capacita dello studente di progettare e implementare soluzioni utilizzando il paradigma a oggetti e il linguaggio Java. Cio che sara valutato e la correttezza e completezza della soluzione sul piano progettuale e concettuale. Non sara dato peso ad errori sintattici. E possibile utilizzare libro e appunti.

Realizzare una classe MagicBox<T> che permetta di inserire al suo interno un oggetto di tipo T, di veri care se un determinato oggetto e nella scatola, di conoscere in numero di oggetti nella scatola e di rimuovere da essa uno speci co
oggetto.

Prevedere esplicitamente anche un metodo ArrayList<T> toArrayList() che restituisce un ArrayList contenente tutti gli elementi nella MagicBox (non importa l'ordine).
Considerare inoltre un'interfaccia funzionale Filter dotata di un unico metodo boolean accept(Object x), e aggiungere a MagicBox un metodo MagicBox<T> filterWith(Filter f) che restituisce una nuova MagicBox contenente solo
gli elementi per i quali f restituisce valore true tramite accept.

Mostrare un esempio di utilizzo che usi un'espressione Lambda.

public class MagicBox {

    private ArrayList elements;
    
    public static void main(String[] args) {
        MagicBox m = new MagicBox();
        m.addElement(4);
        m.addElement("ciao");
        m.addElement(new MagicBox());
        System.out.println(m.countOfElements());
        m.removeElement(4);
        System.out.println(m.countOfElements());
        System.out.println(m.isInMagicBox("ciao"));
        MagicBox m1 = m.FilterWith(new Filter() {
            @Override
            public boolean accept(Object x) {
                if(!(x instanceof String)) return false;
                if("ci".equals(x.toString().substring(0, 2))) return true;
                return false;
            }
        });
        System.out.println(m1.countOfElements());
    }
    
    public MagicBox() {
        elements = new ArrayList<>();
    }
    
    public boolean isInMagicBox(T element) {
        return elements.stream().anyMatch(x -> x.equals(element));
    }
    
    public int countOfElements() {
        return elements.size();
    }
    
    public void addElement(T element) {
        elements.add(element);
    }
    
    public void removeElement(T element) {
        if(isInMagicBox(element)) elements.remove(element);
    }

    public ArrayList<T> ToArrayList() {
        return (ArrayList<T>)elements.clone();
    }
    
    public MagicBox FilterWith(Filter f) {
        MagicBox filtered = new MagicBox();
        elements.stream().filter(x -> f.accept(x)).forEach(x -> filtered.addElement(x));
        return filtered;
    }
}

Compiti sui tempi di esecuzione

27/05/2015

Si confrontino le seguenti funzioni utilizzando le relazioni O, Ω e Θ (giustificando le risposte):

f(n) g(n)
10n3 + log n + 2n n2 + log n5
n! 2n
2n 2n+100
Svolgimento
10n3 + log n + 2n n2 + log n5

In questo caso è possibile considerare solo il grado più alto di entrambe le espressioni. Siccome n2 + log n5 si può scrivere come n2 + 5log n, il confronto è tra

f(n) = n3 e g(n) = n2

quindi f(n) = Ω(g(n))

n! 2n

In questo caso è opportuno iniziare a sviluppare le serie per capire quale cresce più velocemente.

n! = n * (n-1)! = n * (n-1) * (n-2)! eccetera

2n = 2 * 2n-1 = 2 * 2 * 2n-2 eccetera

per n = 0, n! = 1 (per definizione) e 20 = 1

per n = 1, n! = 1 (per definizione) e 21 = 2

per n = 2, n! = 2 (per definizione) e 22 = 4

per n = 3, n! = 6 (per definizione) e 23 = 8

per n = 4, n! = 24 (per definizione) e 24 = 16

da questo punto in poi, ogni n contribuisce a moltiplicare n in n! e 2 in 2n

Quindi possiamo dire che per n0 = 5 vale che f(n) = Ω(g(n))

2n 2n+100

Questo caso è semplicemente f(n) = Θ(g(n)) con c1 = c2 = 2100

28/01/2015

Si risolvano le seguenti ricorrenze:

  • T(n) = 3T(n/2) + n2
  • T(n) = 4T(n/2) + n2
  • T(n) = T(n/2) + 2n
Svolgimento

Si può utilizzare per tutti e tre i problemi il teorema master per la ricorsione:

nlog23 ≈ n1,1 < n2

in questo problema f(n) =n2, g(n) = n1,1+ε, con 0 < ε < ∼0,9, quindi f(n) = Ω(g(n))

Rientriamo nel terzo caso, quindi dobbiamo accertarci che esista un c < 1 t.c. af(n/b) < cf(n), quindi 3(n/2)2 = 3/4 n2 < cn2. Scegliendo un 3/4 < c < 1 si ha che cn2 è sempre maggiore.

Perciò il costo è T(n) = Ω(f(n)) = Ω(n2)

Nel secondo problema nlog24 = n2
Rinetriamo nel terzo caso, quindi T(n) = Θ(n2 ln n), poiché l'algoritmo deve eseguire n2 operazioni per l'altezza dell'albero.

Nel terzo problema nlog21 = n0 = 1 < 2n
Quindi f(n) = 2n = Ω(1)

Siamo ancora nel terzo caso, si può dire che T(n) = Θ(2n) a patto che 2n/2 < c2n con 1/2 < c < 1.

23/01/2014

Si definiscano formalmente le relazioni O, Ω, Θ e, utilizzando le definizioni date e
nient’altro, si dimostri la verità o la falsità di ciascuna delle seguenti affermazioni:

  1. n logn = Θ(n2)
  2. n + √3n = Ω(logn)
  3. 4n log n = Ο(4n + log n2)
  4. 2n+k = Ο(2n ), dove k è una costante intera positiva
  5. 2n+n = Ο(2n)
Svolgimento

Si dice f(n) = O(g(n)) se ∃ c > 0 tale che 0 ≤ cf(n) ≤ g(n)∀ n ≫ n0

Si dice f(n) = Ω(g(n)) se ∃ c > 0 tale che 0 ≤ g(n) ≤ cf(n) ∀ n ≫ n0

Si dice f(n) = Θ(g(n)) se ∃ c1, c2 > 0 tale che 0 ≤ c1f(n) ≤ g(n) ≤ c2f(n) ∀ n ≫ n0

  • n logn = Θ(n2)

f(n) = o(g(n)) perché 0 < n logn < cn2 ⇒ 0 < logn < cn
che è sempre vera per n ≫ n0

Quindi il primo problema è falso.

  • n + √3n = Ω(logn)

Se il secondo problema è vero, esiste c > 0 tale che 0 ≤ log n ≤ c(n+√3n)

Si può scrivere come elog n < ec(n+√3n)
cioè n < ecn(1+√3)
Evidentemente c può "assorbire" (1 + √3), basta trovare un c1 = c + √3c; con c > 0 la crescita di ecn è esponenziale, e quindi tende a essere più grande di n per valori grandi di n.

In alternativa, utilizzando il teorema per cui f(n) = Ω(g(n)) ⇒

limnfngn=

abbiamo che

limnn+√3nlogn=Hlimn1+√31n=limnn+√3n=

quindi il secondo problema è vero.

  • 4n log n = Ο(4n + log n2)

Il terzo problema è un confronto tra 4n logn e 4n + 2log n, che si può riscrivere, dividendo per 4n, come

log n = O(1 + (log n)/2n)

Per la definizione di O, il problema è vero se esiste c tale che c(log n) ≤ 1 + (log n)/2n

Semplificando ancora, dividendo per log n, si ottiene c ≤ 1/log n + 1/2n

Per n molto grande, 1/log n tende a zero, ma 1/2 n tende a infinito, mentre c è una costante. Di conseguenza il terzo problema è vero.

  • 2n+k = Ο(2n), dove k è una costante intera positiva

Questo è vero se esiste c > 0 tale che c(2n+k) ≤ 2n. Scegliendo c < 1 si ha che quando h*c < 2n il problema è vero, e siccome h e c sono costanti, mentre 2n tende a infinito, il problema è vero.

  • 2n+n = Ο(2n)

Riscrivendo il problema come 2n2n = Ο(2n), si ha che perché il problema sia vero deve essere che 0 ≤ 2n2n ≤ 2n, ma semplificando si ha 0 ≤ 2n 1, che è palesemente falso.

Algoritmi di ordinamento (con implementazione in C)

Premessa

Tutti gli algoritmi usano il seguente codice:

#include <stdio.h>
#define NUM_OF_ELEMENTS 10000
main() {
	int i;
	int numeri[NUM_OF_ELEMENTS];
	srand(time(NULL));
	for(i = 0; i < NUM_OF_ELEMENTS; i++) {
		numeri[i] = rand();
	}
	for(i = 0; i < NUM_OF_ELEMENTS; i++) {
		printf("%d ", numeri[i]);
		if(i % 5 == 4) printf("\n");
		}
	printf("\n\n");
	sort(&numeri, NUM_OF_ELEMENTS);
	for(i = 0; i < NUM_OF_ELEMENTS; i++) {
		printf("%d ", numeri[i]);
		if(i % 5 == 4) printf("\n");
	}
	getchar();
}

In alcuni casi la chiamata alla funzione sort può avere una firma leggermente diversa (es. possono avere come parametro l'indice di partenza, tipicamente zero).

Algoritmi con costo di esecuzione O(n2)

Tipicamente gli algoritmi di ordinamento con costo di esecuzione O(n2) sono caratterizzati da una reiterazione di tutti gli elementi per ogni "sistemazione".

Selection sort

L'algoritmo inizia con un elemento, che per definizione è orditato.

Dal secondo elemento in poi, abbiamo un insieme già ordinato - convenzionalmente a sinistra - e un insieme da ordinare - a destra.

Per ciascun elemento non ancora ordinato, si sceglie il minore tra gli elementi non ancora ordinati e si scambia con quello più a sinistra.

1 3 4 7 8 10 29 44 14 73 19 36
1 3 4 7 8 10 29 44 14 73 19 36
1 3 4 7 8 10 14 44 29 73 19 36
1 3 4 7 8 10 14 44 29 73 19 36

Il tempo di esecuzione si può calcolare dividendo l'array in due.

Per ciascun elemento già ordinato si sottrae uno al totale di elementi da ordinare, quindi con n elementi la prima iterazione sarà tutti gli elementi (n), la seconda su tutti tranne il primo (n-1) e così via, fino a fare n iterazioni su

j=1nn-j

Il totale delle iterazioni è il ben noto

nn-12

Apparentemente è un tempo migliore di n2, ma a ben guardare il limite per n sufficientemente grande di

limnnn-12n2=limnn22-n2n2

Dividendo il numeratore e il denominatore per n2,

limn12-12n

Si ottiene che per n sufficientemente grande, l'algoritmo tende a un costo di Θ(n2) con costante = 1/2.

Il tempo di esecuzione non varia per il caso medio, ottimo e pessimo.

L'implementazione può essere fatta in loco.

void sort(int *numeri, int conteggio) {
	int i;
	int j;
	int m;
	int temp;
	for(i = 0; i < conteggio-1; i++) {
		m = i;
		for(j = i+2; j < conteggio; j++) {
			if(numeri[j] < numeri[m]) m = j;
		}
		temp = numeri[m];
		numeri[m] = numeri[i+1];
		numeri[i] = temp;
	}
}

Insertion Sort

L'algoritmo inizia con un elemento, che per definizione è orditato.

Dal secondo elemento in poi, abbiamo un insieme già ordinato - convenzionalmente a sinistra - e un insieme da ordinare - a destra.

Per ciascun elemento non ancora ordinato, si sceglie quello più a sinistra e si scorre da sinistra a destra l'insieme ordinato fino a trovare il primo elemento maggiore dell'elemento considerato.

A questo punto si scorrono a destra di una posizione tutti gli elementi maggiori e si inserisce l'elemento considerato nella posizione lasciata libera.

1 18 27 44 89 101 29 45 14 73 19 36
1 18 27
44

89

101
29 45 14 73 19 36

il 29 è memorizzato in una variabile

1 18 27 * 44 89 101 45 14 73 19 36

* questa posizione è ancora occupata dal 44, ma ai fini dell'algoritmo è libera

1 18 27 29 44 89 101 45 14 73 19 36

Il tempo di esecuzione si può discutere in maniera estremamente simile a Selection Sort.

L'implementazione può essere fatta in loco.

void sort(int *numeri, int conteggio) {
	int i;
	int j;
	int k;
	int num;
	for(i = 0; i < conteggio-1; i++) {
		num = numeri[i+1];
		for(j = 0; j <= i; j++) { if(numeri[j] > num) {
				for(k = i+1; k > j; k--) {
					numeri[k] = numeri[k-1];
				}
				numeri[j] = num;
				break;
			}
		}
	}
}

Bubble Sort

L'algoritmo inizia con un elemento, che per definizione è orditato.

Dal secondo elemento in poi, abbiamo un insieme già ordinato - convenzionalmente a sinistra - e un insieme da ordinare - a destra.

Per ciascun elemento non ancora ordinato, si sceglie quello più a sinistra e si scambia con tutti quelli già ordinati che sono maggiori dell'elemento considerato.
È maggiore il 32 o il 18? Si fa lo scambio!

32 18 45 14 73 19 36
18 32 14 45 19 73 36

È maggiore il 32 o il 45? Siccome è il 45, non c'è scambio e l'indice si incrementa.

18 14 32 45 19 73 36
18 14 32 19 45 73 36
18 14 32 19 45 36 73

Anche in questo caso il tempo di esecuzione è Θ(n2), poiché per n volte posizioniamo l'n-k-esimo elemento a destra.

L'implementazione può essere fatta in loco.

void sort(int *numeri, int conteggio) {
	short scambio;
	int i;
	int j;
	int temp;
	scambio = 0;
	for(i = 0; i < conteggio - 1; i++) {
		for(j = 1; j < conteggio - i + 1; j++) { if(numeri[j-1] > numeri[j]) {
				temp = numeri[j-1];
				numeri[j-1] = numeri[j];
				numeri[j] = temp;
				scambio = 1;
			}
		}
		if(scambio == 0) break;
	}
}

Algoritmi con costo di esecuzione O(n logn)

Gli algoritmi con questo tempo di esecuzione sono i divide-et-impera, algoritmi in cui il vettore viene diviso (tipicamente in due) tante volte fino ad arrivare a blocchi con uno o due soli elementi.

Merge Sort

Il vettore viene spezzato a metà ricorsivamente, fino ad arrivare a piccoli blocchi di uno o due elementi. Successivamente tutti gli elementi, ordinati al loro interno, sono fusi (merge) in un unico blocco grande (circa) il doppio.

V1 1 4 14 19 29 36 44 73
V2 3 7 11 21 29 39 52
Vettore risultante

La freccia indica la posizione del puntatore, il grigio indica che il numero è il più basso tra i due

V1 ⇒1 4 14 19 29 36 44 73
V2 ⇒3 7 11 21 29 39 52
Vettore risultante 1

 

V1 1 ⇒4 14 19 29 36 44 73
V2 ⇒3 7 11 21 29 39 52
Vettore risultante 1 3

 

V1 1 ⇒4 14 19 29 36 44 73
V2 3 ⇒7 11 21 29 39 52
Vettore risultante 1 3 4

...eccetera...

V1 1 4 14 19 29 36 44 ⇒73
V2 3 7 11 21 29 39 52 ⇒ *

* il puntatore ha superato i limiti, quindi è sufficiente copiare il contenuto dell'altro array

Vettore risultante 1 3 4 7 11 14 19 21 29 29 36 39 44 52

Il tempo di esecuzione del merge è C(n) = n-1 + 2C(n/2). Per il teorema master, O(nlog22) = O(n-1) ⇒ C(n) = Θ(n logn)

L'implementazione può essere fatta con un vettore di appoggio.

void merge(int * numeri, int inizio, int pivot, int conteggio) {
	int * merged;
	int i;
	int ja, jb;
	ja = inizio;
	jb = pivot;
	merged = malloc(sizeof(int) * (conteggio - inizio));

	for(i = inizio; i < conteggio; i++) {
		if((numeri[ja] < numeri[jb] || jb >= conteggio) && ja < pivot)
			merged[i-inizio] = numeri[ja++];
		else
			merged[i-inizio] = numeri[jb++];
	}
	for(i = inizio; i < conteggio; i++) numeri[i] = merged[i-inizio];
	free(merged);
}
void sort(int *numeri, int inizio, int conteggio, int rec) {
	int pivot;
	int i;
	if(conteggio - inizio > 1) {
		pivot = (conteggio - inizio) / 2 + inizio;
		sort(numeri, inizio, pivot, rec+1);
		sort(numeri, pivot, conteggio, rec+1);
		merge(numeri, inizio, pivot, conteggio);
	}
}

Quick Sort

Quick Sort è un algoritmo in cui il grosso del costo consiste nella fase del divide, perché richiede di scegliere un elemento pivot e di spostare tutti gli elementi minori o uguali del pivot a sinistra e tutti quelli maggiori a destra, e di applicare ricorsivamente alle due metà l'algoritmo fino a ottenere blocchi di uno o due elementi.

Limiti e caratteristiche del Quick Sort

Quick Sort richiede la scelta di un elemento, contenuto nella parte di vettore che stiamo ordinando, che sia "circa" mediano, in modo da ottenere due parti "circa" uguali. Scegliendo un elemento completamente a caso, la probabilità seguirà la classica curva gaussiana, e la probabilità di scegliere il minimo o il massimo è estremamente bassa.

Però il Quick Sort processa per la maggior parte dei casi vettori di pochi elementi, in cui la probabilità di scegliere il minimo o il massimo è non trascurabile.

Scegliendo tre elementi a caso - e non uno - e utilizzando come pivot la mediana, si riduce in modo esponenziale la probabilità di scegliere un estremo (p.e. se c'è 1/100 probabilità di ottenere un minimo o un massimo, scegliendo tra tre numeri la probabilità diventa 1/100 * 1/100 * 1/100 = 1/1.000.000).

Quick Sort richiede un "aggiustamento" nel caso in cui ci possano essere molti valori duplicati. In questo caso ci si potrebbe trovare con semi-array contenenti sempre lo stesso numero, e in questo caso il divide porterebbe a un semi-array uguale a quello originale e un altro semi-array vuoto, all'infinito.

Per questo è necessario prevedere un controllo che tutti gli elementi dell'array siano non tutti uguali (basta un elemento diverso).

Esempio d'uso

Nota: gli elementi inseriti nella funzione med (=mediana) sono scelti casualmente.

52 4 19 36 73 29 21 44 1 39 3 7 11 29 14

Pivot: med(39, 29, 36) = 36

Ricorsione: sinistra

4 19 36 29 21 1 3 7 11 29 14

Pivot: med(19, 7, 11) = 11

Ricorsione: sinistra-sinistra

4 1 3 7 11

Pivot: med(1, 3, 7) = 3

Ricorsione: sinistra-sinistra-sinistra

1 3

A questo punto, se gli elementi non sono ordinati si scambiano.
In questo caso, 1, 3

Ricorsione: sinistra-sinistra-destra

4 7 11

A questo punto, la mediana è 7, che divide l'array in 4, 7 e 11.

4 e 7 non vengono scambiati perché sono già in ordine e 11 resta da solo. Fondendo i due array si ottiene 4, 7, 11

Ricorsione: sinistra-destra

19 36 29 21 29 14

Pivot: med(19, 21, 14) = 21

Ricorsione: sinistra-destra-sinistra

19 21 14

A questo punto, la mediana è 19, si ottengono 19, 14 e 21.
19 e 14 vengono scambiati e si ottiene 14, 19, 21

Ricorsione: sinistra-destra-destra

36 29 29

A questo punto, la mediana è 29. Si ottengono 29, 29 e 36.
29 e 29 sono uguali, quindi non avviene alcuno scambio e si restituisce la coppia di numeri.
Il risultato è 29, 29, 36

Ricorsione: destra

52 73 44 39

Pivot: med(52, 73, 44) = 52

Ricorsione: destra-sinistra

52 44 39

La mediana è 44, si ottengono 44, 39 e 52.
44 e 29 si scambiano, e unendo l'elemento singolo 52 si ottiene 39, 44, 52

Ricorsione: destra-destra
Si ottiene l'elemento unico 73.

Unendo nell'ordine di chiamata, dal ramo più a sinistra a quello più a destra, si ottiene

1 3 4 7 11 14 19 21 29 29 36 39 44 52 73

Il tempo di esecuzione del Quick Sort è O(n logn).

// NOTA: Questa implementazione funziona solo con valori distinti
// NOTA: Questa implementazione non utilizza il sistema della mediata descritto nel testo

void sort(int * a, int start, int end) {
	int s, e; // inizializzati a start e end, scorrono l'array dall'inizio o dalla fine
	int pivot; // l'elemento pivot. Nella prima implementazione, è un elemento scelto completamente a caso
	int i, tmp, r;
	
	// gestisco il caso di un array di 1 e 2 elementi
	if(start >= end) return;
	if(start + 1 == end) {
		if(a[start] > a[end]) {
			tmp = a[start];
			a[start] = a[end];
			a[end] = tmp;
		}
		return;
	}
	
	srand(time(NULL));
	
	s = start;
	e = end;
	srand(time(NULL));
	r = (rand() % (end-start+1)) + start;
	pivot = a[r]; // non è un'implementazione equiprobabile, ma per i nostri scopi è più che sufficiente
	while(s < e) {
		if(a[s] <= pivot) s++; else { if(a[e] > pivot) e--;
			if(a[s] > pivot && a[e] <= pivot) { tmp = a[s]; a[s] = a[e]; a[e] = tmp; } } } if(a[s] > a[e]) {
		tmp = a[s];
		a[s] = a[e];
		a[e] = tmp;
	}
	if(a[s] <= pivot) e++; sort(a, start, e-1); if(e >= s) sort(a, e, end);
}

Algoritmi con costo di esecuzione O(n) e simili

Integer Sort

Integer Sort ha tempi migliori degli algoritmi precedenti perché suppone che gli elementi da ordinare siano tutti numeri interi positivi (in realtà è facile includere nell'algoritmo anche quelli negativi).

L'idea è di creare un array che utilizzi gli interi da ordinare come degli indici e l'elemento del secondo array incrementa semplicemente l'indice ogni volta che si incontra quel numero.

Per esempio:

5, 9, 13, 13, 6, 9, 5, 14, 2 è l'array da ordinare.

max(array) = 14, quindi si crea un array di 14 elementi (per semplicità sarà indicizzato da 1 a n, contro la prassi degli array) e si inizializzano i contatori a zero.

[0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0] [0]

Per ogni numero esaminato si incrementa il rispettivo contatore:

5

[0] [0] [0] [0] [1] [0] [0] [0] [0] [0] [0] [0] [0] [0]

9

[0] [0] [0] [0] [1] [0] [0] [0] [1] [0] [0] [0] [0] [0]

13

[0] [0] [0] [0] [1] [0] [0] [0] [1] [0] [0] [0] [1] [0]

13

[0] [0] [0] [0] [1] [0] [0] [0] [1] [0] [0] [0] [2] [0]

...eccetera. L'ordine finale è il seguente:

[0] [1] [0] [0] [2] [1] [0] [0] [2] [0] [0] [0] [2] [1]

Scorrendo l'array si ottiene

2, 5, 5, 6, 9, 9, 13, 13, 14

Il tempo di esecuzione dipende dagli n elementi e dal k massimo, ed è O(n+k).

L'implementazione è la seguente:

sort(int * a, int count) {
	int i, k, max;
	int * appo;
	
	max = 0;
	k = 0;
	
	for(i = 0; i < count; i++) if(max < a[i]) max = a[i];
	
	appo = malloc(sizeof(int) * max + 1);
	for(i = 0; i <= max; i++) appo[i] = 0;
	for(i = 0; i < count; i++) appo[a[i]]++;
	for(i = 0; i <= max; i++) while(appo[i] > 0) {a[k++] = i; appo[i]--;}
}

Compiti ASD

Tempi di esecuzione (teoria)

Alcune soluzioni si trovano qui.

27/05/2015

Si confrontino le seguenti funzioni utilizzando le relazioni O, Ω e Θ (giustificando le risposte):

f(n) g(n)
10n3 + log n + 2n n2 + log n5
n! 2n
2n 2n+100

28/01/2015

Si risolvano le seguenti ricorrenze:

  • T(n) = 3T(n/2) + n2
  • T(n) = 4T(n/2) + n2
  • T(n) = T(n/2) + 2n

23/01/2014

Si definiscano formalmente le relazioni O, Ω, Θ e, utilizzando le definizioni date e
nient’altro, si dimostri la verità o la falsità di ciascuna delle seguenti affermazioni:

  1. n logn = Θ(n2)
  2. n + √3n = Ω(logn)
  3. 4n log n = Ο(4n + log n2)
  4. 2n+k = Ο(2n ), dove k è una costante intera positiva
  5. 2n+n = Ο(2n)

08/09/2014

Per un certo problema sono stati trovati due algoritmi risolutivi (A1 e A2) con i seguenti tempi di esecuzione:

A1 : T(n) = 3·T(n/2) + n2
A2 : T(n) = 4·T(n/2) + n2

Si dica, giustificando tecnicamente la risposta, quale dei due algoritmi è preferibile per input di dimensione sufficientemente grande.

12/06/2014

Si confrontino le seguenti funzioni utilizzando le relazioni O, Ω e Θ:

f(n) g(n)
100 n + log n n + (log n)2
log n log n3
2n 3n

01/09/2015

Utilizzando la definizione di O, e nient’altro, si stabilisca se le seguente affermazioni sono vere o false:
a) f(n) = O(n)
b) f(n) = O(n2)
dove f(n) = (2n − 1) / 2.

Costi di esecuzione (strutture dati)

27/05/2015

Completare la seguente tabella indicando la complessità delle operazioni che si riferiscono a un dizionario di n elementi. Per l’operazione Successore si assuma di essere sull’elemento x a cui si applica l’operazione.

Ricerca Massimo Successore Minimo Costruzione
Albero binario di ricerca
Min-Heap

Soluzione: https://diario.softml.it/costi-di-esecuzione-strutture-dati-27-05-2015/

Funzioni efficienti (grafi)

27/05/2017

Il seguente algoritmo accetta in ingresso la matrice di adiacenza di un grafo non orientato e restituisce un valore Booleano (TRUE / FALSE)

MyAlgorithm( A )
1. n = rows(A) /* determina il numero di vertici del grafo */
2. let B an n x n matrix /* alloca memoria per una matrice B n x n */
3. for i = 1 to n
4.   for j = 1 to n
5.     b[i,j] = 0
6.     for k = 1 to n
7.       b[i,j] = b[i,j] + a[i,k]*a[k,j]
8. for i = 1 to n
10.  for j = i + 1 to n
11.     if a[i,j]*b[i,j] <> 0 then
12.return FALSE
13.return TRUE

Si calcoli la complessità computazionale di MyAlgorithm e si determini la sua funzione (in quali casi restituisce TRUE? in quali casi FALSE?). (Nota: Nel determinare la   complessità si ignori per comodità la complessità delle istruzioni 1 e 2).
Si simuli inoltre il suo comportamento sui seguenti due grafi, verificando che restituisca il risultato atteso:

La soluzione è riportata qui: https://diario.softml.it/funzioni-efficienti-grafi-27-05-2017/

28/01/2015

Si scriva l’algoritmo di Floyd-Warshall, si dimostri la sua correttezza, si fornisca la sua complessità computazionale e si simuli accuratamente la sua esecuzione sul seguente grafo:

28/01/2015

Un piroscafo collega regolarmente n città portuali. Ad ogni porto u il proprietario del piroscafo ricava P(u) euro, ma per andare dal porto u al porto v spende in totale C(u,v) euro. Se P(u)/C(u,v) > 1, il proprietario del piroscafo ha quindi realizzato un guadagno. Si vuole stabilire se esiste un itinerario ciclico di k città (x0, x1, ..., xk = x0) per il quale

i=1kPxii=1kCxi-1xi

dove μ ≥ 1 è una quantità stabilita a priori che rappresenta il guadagno minimo che il proprietario del piroscafo intende realizzare.

Si scriva un algoritmo che accetti in ingresso i ricavi P per ogni porto, i costi C per ogni coppia di porti e la costante μ, e stabilisca se un tale itinerario esiste o meno. Si discuta la correttezza dell'algoritmo proposto e la sua complessità computazionale.

08/09/2014-1

Sia G = (V, E) un grafo orientato e pesato, sia s∈V un vertice “sorgente” e si supponga che G sia stato inizializzato con INIT-SINGLE-SOURCE(G, s). Si stabilisca se la seguente affermazione è vera o falsa: «Se G non contiene un ciclo di peso negativo raggiungibile dalla sorgente s, allora nessuna sequenza di passi di rilassamento potrà assegnare al campo d[s] un valore diverso da 0.»

Nel primo caso si fornisca una dimostrazione, nel secondo un controesempio.

08/09/2014-2

Si vuole costruire una rete stradale che colleghi cinque città (A-E), minimizzando i costi complessivi di realizzazione. I costi per la costruzione di una strada tra due città sono sintetizzati nella seguente tabella (dove +∞ significa che la strada è irrealizzabile):

A B C D E
A 0 3 5 11 9
B 3 0 3 9 8
C 5 3 0 +∞ 10
D 11 9 +∞ 0 7
E 9 8 10 7 0

Si formuli il problema dato in termini di un problema di ottimizzazione su grafi, e si descriva un algoritmo per la sua soluzione discutendone correttezza e complessità. Infine, si simuli accuratamente l’algoritmo presentato per determinare una soluzione del problema.

29/05/2014

Si scriva un algoritmo di complessità O(n2 log n) per determinare le distanze tra tutte le coppie di vertici in un grafo sparso G avente pesi sugli archi positivi, dove n è il numero di vertici in G.

12/09/2016

Si stabilisca quale problema risolve il seguente algoritmo, che accetta in ingresso un grafo non orientato G = (V, E):

MyAlgorithm(G)
1. A = ∅
2. ordina i vertici di G per grado crescente
3. for each u ∈ V[G] do /* vertici estratti secondo l’ordine stabilito nel passo 2 */
4.    if MyFunction(G,A,u) then
5.       A = A ∪ {u}
6. return A
MyFunction(G,A,u)
1. for each v ∈ A do
2.    if (u,v) ∈ E[G] then
3.       return FALSE
4. return TRUE

Si dimostri la correttezza dell’algoritmo, si determini la sua complessità e si simuli infine la sua esecuzione sul seguente grafo:

01/09/2015

Il seguente algoritmo accetta in ingresso la matrice di adiacenza A di un grafo orientato ed un intero k (con k ≥ 2), e restituisce un valore Booleano (TRUE / FALSE):

MyAlgorithm(A,k)
1. n = rows(A) /* determina il numero di vertici del grafo */
2. let B be the n x n identity matrix
3. for i = 1 to k - 1
4.   B = MyFunction(A,B)
5.   for i = 1 to n
6.     for j = i + 1 to n
7.       if B[i,j]*A[j,i] ≠ 0 then
8.       return FALSE
9. return TRUE

L’algoritmo utilizza la seguente funzione che accetta in ingresso due matrici quadrate (di dimensione identica) e restituisce un’altra matrice quadrata della stessa dimensione:

MyFunction(X,Y)
1. n = rows(X)
2. for i = 1 to n
3.   for j = 1 to n
4.     Z[i,j] = 0
5.     for k = 1 to n
6.       Z[i,j] = Z[i,j] + X[i,k]*Y[k,j]
7. return Z

Si calcoli la complessità computazionale complessiva di MyAlgorithm e si determini qual è il problema che risolve (ovvero: in quali casi restituisce TRUE? in quali casi FALSE?). (Nota: Nel determinare la complessità si ignori per comodità la complessità delle istruzioni 1 e 2 di MyAlgorithm e dell’istruzione 1 di MyFunction).

Si simuli inoltre il suo comportamento sui seguenti due grafi, verificando che restituisca il risultato atteso:

Grafi (cammini minimi)

27/05/2015-1 e 12/06/2014 e 12/09/2016

Il problema di determinare i cammini minimi tra tutte le coppie di vertici di un grafo pesato G=(V, E, w) con pesi positivi si può risolvere iterando |V| volte l'algoritmo di Dikstra oppure utlizzando l'algoritmo di Floyd-Warshall. Si riempia la tabella sottostante, specificando le complessità degli algoritmi indicati in funzione della
tipologia di grafo utilizzato:

Grafo sparso Grafo denso
Iterated_Dijkstra (array)
Iterated_Dijkstra (heap)
Floyd-Warshall (27/05/15)
Bellman-Ford (12/06/14)

Supponendo che il grafo sia aciclico, quale algoritmo conviene usare? Perché?

27/05/2015-2

Sia G = (V, E) un grafo non orientato, connesso e pesato e sia (S, V \ S) un taglio di G. Si supponga che l'arco leggero che attraversa il taglio sia unico e lo si denoti con (u,v). In altri termini, per tutti gli altri archi (x,y) che attraversano il taglio, si avrà w(u,v) < w(x,y). Si dimostri che tutti gli alberi di copertura minima di G dovranno necessariamente contenere l'arco (u,v).

28/01/2015

Si determinino due diversi alberi di copertura minimi nel seguente grafo:

12/06/2014-1

Si scriva l’algoritmo di Kruskal, si dimostri la sua correttezza, si fornisca la sua complessità computazionale e si simuli accuratamente la sua esecuzione sul seguente grafo:

12-06-2014-2

Si supponga di voler cambiare una banconota da P euro in monete da a1, a2, …, ak euro. E’ possibile? In caso affermativo, qual è il minimo numero di monete da utilizzare per effettuare il cambio? Per esempio, se si volesse cambiare una banconota da 5 euro in monete da 1 e da 2 euro, sarebbero necessarie almeno 3 monete.

Se si disponesse soltanto di monete da 2 euro, invece, il cambio non sarebbe possibile.

Si formuli questo problema come un problema di cammini minimi su grafi. (Suggerimento: si costruisca un grafo con P + 1 vertici numerati da 0 a P e si inseriscano archi e pesi in modo tale che i cammini dal vertice 0 al vertice P corrispondano a possibili sequenze di monete da utilizzare per il cambio.)

A titolo esemplificativo, si consideri il caso di una banconota da P = 10 euro e monete da a1 = 1 e a2 = 2 euro.

Si costruisca il grafo corrispondente e si utilizzi l’algoritmo di Dijkstra per risolvere il problema.

29/05/2014-1

Si scriva l’algoritmo di Bellman-Ford, si dimostri la sua correttezza, si fornisca la sua complessità computazionale e si simuli accuratamente la sua esecuzione sul seguente grafo (utilizzando il vertice A come sorgente):

29/05/2014-2

Sia G = (V, E) un grafo pesato non orientato e connesso e sia w : E → R la funzione peso.
Sia Tmin un albero di copertura minimo di G e sia T un qualsiasi altro albero di copertura (non necessariamente minimo). Inoltre sia (u,v) ∈ E un arco di peso massimo in Tmin e (x,y) ∈ E un arco di peso massimo in T. Si dimostri che:

w(u,v) ≤ w(x,y).

In altri termini, fra tutti gli alberi di copertura, l’albero di copertura minimo ha il più piccolo arco di peso massimo. (Suggerimemto: si ragioni per assurdo e si usi la classica tecnica del “taglia-e-incolla”.)

12/09/2016

Si determini un albero di copertura minimo nel seguente grafo:

01/09/2015

Sia G = (V, E) un grafo non orientato, connesso e pesato. Dato un taglio (S, V \ S) di G, sia (u,v) un arco che lo attraversa tale che per tutti gli altri archi (x,y) che attraversano il taglio risulta w(u,v) ≤ w(x,y) (arco leggero). Si stabilisca, giustificando formalmente la risposta, se la seguente affermazione è vera o falsa: «Se T è un albero
di copertura di G che non contiene (u,v), allora T non è un albero di copertura minimo.»

Si può affermare la stessa cosa se si assume che tutti i pesi di G siano distinti? Perché?

Nota: nel fornire le giustificazioni non si faccia ricorso al teorema fondamentale degli MST.

Funzioni efficienti (alberi)

27/05/2015

Dato un albero binario, i cui nodi sono colorati di bianco, rosso o verde:

a. scrivere una funzione efficiente che stabilisca se esiste un cammino di tre nodi nell’albero i cui colori formano la bandiera italiana. (Il cammino può partire da un nodo qualsiasi, non necessariamente dalla radice.)

b. Fornire la chiamata della funzione dal programma principale.

c. Analizzare la complessità di tale funzione.

Il tipo Node utilizzato per rappresentare l’albero binario è il seguente:


typedef struct node{
char * colore;
struct node * left;
struct node * right;
} * Node;

28/01/2015

Un nodo di un albero binario u è detto intermedio se la somma delle chiavi contenute nei nodi del sottoalbero di cui u è radice è uguale alla somma delle chiavi contenute nei nodi sul percorso che collega u alla radice dell’albero (u escluso).

a) Scrivere un algoritmo efficiente che restituisca il numero di nodi intermedi.

b) Analizzare la complessità della soluzione trovata. Il tipo Node utilizzato per rappresentare l’albero binario è il seguente:

typedef struct node{
int key;
struct node * left;
struct node * right;
} * Node;

23/01/2014

Dato un albero binario i cui nodi sono colorati di bianco o di nero, scrivere una funzione C efficiente che calcoli il numero di nodi aventi lo stesso numero di discendenti bianchi e neri. (Un nodo è discendente di se stesso.)

Inoltre analizzare la complessità di tale algoritmo.

Il tipo Node utilizzato per rappresentare l’albero binario è il seguente:

typedef struct node{
char * colore;
struct node * left;
struct node * right;
} * Node;

Si può ordinare un dato insieme di n numeri costruendo un albero binario di ricerca che contiene questi numeri (usando ripetutamente Tree-Insert per inserire i numeri uno alla volta) e stampando poi i numeri utilizzando un certo tipo di visita. Scrivere l’algoritmo che realizza questo ordinamento e specificare il tipo di visita effettuata e il relativo algoritmo.

Quali sono i tempi di esecuzione nel caso peggiore e nel caso migliore per questo algoritmo di ordinamento?

08/09/2014

Dato un albero binario, scrivere un procedura efficiente che cancelli il figlio sinistro di ogni nodo se è una foglia e contiene la stessa chiave del nodo padre.

Calcolare la complessità al caso pessimo della funzione indicando la corrispondente relazione di ricorrenza.

La rappresentazione dell’albero binario utilizza esclusivamente i campi left, right e key e il prototipo della procedura è:

void cancella(Node u)

12/06/2014-1

Dato un albero binario di ricerca T, scrivere la funzione Tree-maximum(Node x) che restituisce un nodo con chiave massima di un sottoalbero di T radicato nel nodo x (x è un nodo dell’albero T e x ≠ NULL). Quale è la complessità di questa funzione? Quale è il caso migliore? E il caso peggiore?

12/06/2014-2

Progettare un algoritmo efficiente per stabilire se un albero binario è quasi completo, cioè tutti i livelli dell’albero sono completamente riempiti, tranne eventualmente l’ultimo che ha le foglie addossate a sinistra.

Calcolare la complessità al caso pessimo dell’algoritmo indicando, e risolvendo, la corrispondente relazione di ricorrenza.

La rappresentazione dell’albero binario utilizza esclusivamente i campi left, right e key.

12/09/2016

Scrivere una funzione efficiente check che dato un albero binario di ricerca verifica se è soddisfatta la seguente condizione: per ogni intero k, se le chiavi k e k+2 sono nell'albero allora anche la chiave k+1 è nell'albero.

Analizzare la complessità della funzione. È preferita una soluzione con spazio aggiuntivo O(1).

Funzioni efficienti (divide-et-impera e altro)

28/01/2015

Scrivere un algoritmo efficiente, di tipo divide et impera, che conta il numero di occorrenze della sequenza ‘a’ ‘r’ memorizzata in posizioni adiacenti in un array di caratteri.

Analizzare la complessità indicando e risolvendo la corrispondente relazione di ricorrenza.

Esempio:

Per l’array ‹a, b, c, r› la risposta sarà 0.
Per l’array ‹b, a, r, c, a, r› la risposta sarà 2.

08/09/2014

Progettare un algoritmo efficiente che, dato un array A di n numeri interi e un intero x, determini se esistono due elementi in A (in posizioni diverse) la cui somma è esattamente x.

Calcolare la complessità al caso pessimo dell’algoritmo.

29/05/2014-1

Si determini la complessità asintotica dell’algoritmo AlgA definito come segue:

AlgA(n)
1. s ← 0
2. for i ← 1 to n
3. do s ← s + AlgB(n)
4. return s
AlgB(m)
1. if m = 1
2. then return 0
3. else return B(m/2) + m

29/05/2014-2

Progettare un algoritmo efficiente di tipo divide et impera che dato un vettore di interi restituisce true se tutti i valori sono distinti, false altrimenti. Analizzare la complessità dell’algoritmo proposto.

12/09/2016

Un collezionista di figurine possiede K figurine, non necessariamente distinte. Le figurine vengono stampate con un numero di serie, impresso dal produttore. I numeri di serie sono tutti gli interi compresi tra 1 e N. La collezione sarebbe completa se ci fosse almeno un esemplare di tutte le N figurine prodotte. Purtroppo la collezione non è completa: sono presenti dei duplicati, mentre alcune figurine mancano del tutto. Il vostro compito è di indicare i numeri di serie delle figurine mancanti.

Scrivere un algoritmo efficiente che dato N e l'array S[1..K], ove S[i] è il numero di serie della i- esima figurina posseduta, stampa l'elenco dei numeri di serie mancanti.

L'array S non è ordinato. Ad esempio, se N=10 e S=[1, 4, 2, 7, 10, 2, 1, 4, 3], l'algoritmo deve stampare a video i numeri di serie mancanti 5, 6, 8, 9 (non necessariamente in questo ordine).

Determinare la complessità dell'algoritmo di cui sopra, in funzione di N e K.

Attenzione: K potrebbe essere minore, uguale o maggiore di N.

01/09/2015

Progettare un algoritmo di ordinamento che si comporti come MergeSort, ma che divida ricorsivamente l’array in tre pari anziché due.
a. Scrivere lo pseucodice della nuova procedura MergeSort3.
b. Scrivere lo pseudocodice della nuova procedura Fusione3.
c. Scrivere e risolvere l’equazione di ricorrenza associata.

Hash

28/01/2015

In una tabella Hash di m = 17 posizioni, inizialmente vuota, devono essere inserite le seguenti chiavi numeriche nell’ordine indicato:
18, 36, 155, 19
La tabella è a indirizzamento aperto e la scansione è eseguita per doppio Hashing:
h(k, i) = (k mod m + i * 2k mod 5) mod m
Indicare per ogni chiave le posizioni scandite nella tabella e la posizione finale dove viene allocata.

27/05/2015

Nell’ipotesi di indirizzamento aperto, scrivere uno pseudocodice per HASH-DELETE e modificare HASHINSERT per gestire il valore DELETED. Analizzare la complessità delle due procedure nel caso pessimo.

12/06/2014

In una tabella Hash di m = 17 posizioni, inizialmente vuota, devono essere inserite le seguenti chiavi
numeriche nell’ordine indicato:

23, 40, 15, 58, 85

Indicare per ogni chiave la posizione finale dove viene allocata in questi due casi:

a. Funzione Hash:

h(k) = k mod m

Risoluzione delle collisioni mediante concatenamento

b. La tabella è a indirizzamento aperto e la scansione è eseguita per doppio Hashing:

h(k, i) = (k mod m + i * 2k mod 5) mod m

Si devono indicare tutte le posizioni scandite nella tabella.

Heap

09/09/2014

Dare la definizione di max-heap e dire se ‹23,17,14,6,13,10,1,5,7,12› è un max-heap giustificando la risposta.

29/05/2014

L’operazione Heap-Delete(A, i), cancella l’elemento nel nodo i dall’heap A. Implementare la procedura Heap-Delete in modo che il suo tempo di esecuzione sia O(lg n) per un max-heap di n elementi.

01/09/2015

Supponete di avere un min-heap di n elementi, e di cercare il valore massimo. In quali posizioni del vettore cercate? Giustificare la risposta.

Scrivere un algoritmo che dato un min-heap non vuoto restituisca il massimo. Calcolare la complessità.

Problemi P/NP/NPC

01/09/2015

Si definiscano le classi P, NP, NPC e si stabilisca, giustificando formalmente la risposta, quale delle seguenti relazioni è ritenuta vera (o verosimile):

12/09/2016

Si definisca formalmente la relazione di riducibilità polinomiale tra problemi decisionali ( ≤P ) e si stabilisca se le seguenti affermazioni sono vere o false:

1) La relazione ≤P è transitiva
2) La relazione ≤P è riflessiva
3) Se ≤P è simmetrica, allora P = NP
4) Se P ≤P Q e Q ∈ P, allora P ∈ P
5) Se P, Q ∈ NPC, allora P ≤P Q se e solo se Q ≤P P

Nel primo caso si fornisca una dimostrazione rigorosa, nel secondo un controesempio.

Nota: in caso di discussioni poco formali l’esercizio non verrà valutato pienamente.

Implementazione in C di una pila

La struttura dati pila

La pila è una struttura dati di tipo LIFO per cui l'ultimo elemento inserito è il primo a uscire.

Le operazioni sulla pila sono solo tre:

  • verifica che la pila [non] sia vuota (empty)
  • inserimento di un nuovo elemento (push)
  • restituzione di un elemento e cancellazione dalla pila (pop)

Esempio:

push('c')

c

push('i')

c i

push('a')

c i a

push('o')

c i a o

pop() // ottengo la o

c i a

pop() // ottengo la a

c i

Tempo di esecuzione

Inserimento: ο(1)

Cancellazione: ο(1)

Ricerca: ο(n)

Implementazione

In questa implementazione, la pila è un insieme di nodi, ciascuno dei quali contiene un valore di tipo char e un puntatore al nodo successivo.

// in stack.h
typedef struct stack * Stack;

// in stack.c
struct stack {
char c;
Stack next;
};

Nel file di header è definito un tipo Stack che rappresenta una struct stack *, quindi Stack è un puntatore.

Nel file .c è definita la struttura stack (s minuscola) che contiene il valore del nodo e il puntatore al nodo successivo. Una scrittura alternativa, che non fa uso della definizione del tipo Stack è la seguente:
// in stack.c
struct stack {
char c;
stack * next;
};

init

è stata aggiunta una funzione init() che alloca della memoria e restituisce il puntatore a un nodo vuoto.


Stack init() {
Stack s;
s = (Stack)malloc(sizeof(Stack));
s->next = NULL;
return s;
}

Il cast a Stack presente nella malloc non è strettamente necessario.

empty


int empty(Stack s) {
return s->next == NULL;
}

Dal momento che l'ultimo nodo è vuoto, faccio una verifica sul campo next.

push


void push(Stack *s, char c) {
Stack newStack;
newStack = init();
newStack->c = c;
newStack->next = *s;
*s = newStack;
}

push prende in ingresso il puntatore a uno Stack (quindi un puntatore al puntatore di stack con la s minuscola).

In questo modo quando si usa s si sta utilizzando un indirizzo di memoria, e non un valore. Nella pratica, si agisce sul dato contenuto nell'indirizzo puntato da s; viceversa sarebbe fatta una copia del contenuto di s.

Con puntatore Senza puntatore
push(Stack *s, char c)
è passato un puntatore a s
push(Stack s, char c)
è passato s: in realtà è una "copia" che ha la funzione come scope
Stack newStack;
newStack = init();
newStack->c = c;

Si inizializza un nuovo elemento Stack
Stack newStack;
newStack = init();
newStack->c = c;

Si inizializza un nuovo elemento Stack
newStack->next = *s;
il next contiene un puntatore a s
newStack->next = s;
il next contiene s (una copia del contenuto "originale")
*s = newStack;
l'indirizzo a cui punta s è sostituito dall'indirizzo a cui punta newStack
s = newStack;
s è sostituito dall'indirizzo a cui punta newStack

Al termine di queste operazioni, il contenuto di s nella funzione che utilizza push è cambiato solo nella versione con puntatore; viceversa l'esecuzione di push non ha effetto sulla funzione chiamante nella versione in cui è passato s come valore.

pop

La funzione pop restituisce un char e rimuove l'elemento dalla pila.
char pop(Stack *s) {
char c;
Stack oldS;
oldS = *s;
c = (*s)->c;
*s = (*s)->next;
free(oldS);
return c;
}

Vanno mantenute sia una copia del carattere sia del puntatore, perché dopo aver "spostato" il puntatore a next (*s = (*s)->next;) è necessario prima deallocare lo spazio ormai inutilizzato sia restituire il carattere, che "sopravvive" solo nella copia fatta precedentemente.

Se non si facesse il free dello spazio, questo resterebbe allocato senza che sia puntato da alcun puntatore. Quindi ci sarebbe uno spreco di memoria, crescente a ogni operazione, che si libererebbe solo al termine dell'esecuzione del programma tramite il garbage collector del sistema operativo.

main

main(int argc, char *argv[]) {
Stack s;
char c;
s = init();
push(&s, 'o');
push(&s, 'a');
push(&s, 'i');
push(&s, 'c');
while(!empty(s)) printf("%c", pop(&s));
pop(&s);
getchar();
}

Nel main è necessario passare come parametri non tanto il valore di s, quanto il suo indirizzo di memoria.
L'output di questo esempio sarà "oaic".
Al termine di questo esempio è stato aggiunto un getchar() per comodità.

Ricerca Operativa - 19

OK, 19 non è un voto di cui esattamente vantarsi, e Obama non è più il Presidente degli USA e non elargisce medaglie, MA...

All'inizio del mio percorso di studi avevo preventivato che ci potevano essere due motivi per cui ero giustificato nel sospendere i miei studi per un annetto, e sono alla fine del più piacevole dei due.

In questo contesto, anche solo aver portato a casa un esame - l'unico che ho tentato - mi dà una soddisfazione immensa.

Quindi, Obama: recupera qualche medaglietta dal fondo del cassetto e...