Docsity
Docsity

Prepara i tuoi esami
Prepara i tuoi esami

Studia grazie alle numerose risorse presenti su Docsity


Ottieni i punti per scaricare
Ottieni i punti per scaricare

Guadagna punti aiutando altri studenti oppure acquistali con un piano Premium


Guide e consigli
Guide e consigli


Algoritmi e Strutture Datii: RAM, Algoritmi di Ricerca, Alberi di Ricerca e Numeri Primi, Sintesi del corso di Algoritmi E Strutture Di Dati

La funzione di codifica C(x,y) e la sua applicazione alla RAM, algoritmi di ricerca e alberi di ricerca. Viene inoltre discusso il concetto di numeri primi e come calcolarli. Il testo include specifiche tecniche e teoremi utili per l'understanding di questi concetti.

Tipologia: Sintesi del corso

2019/2020

Caricato il 01/11/2021

gaia-sasso-1
gaia-sasso-1 🇮🇹

1 documento

1 / 19

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
CAPITOLO 1. ALGORITMI E MODELLI DI CALCOLO
- Comunicazione , linguaggio e linguaggi
Linguaggi: insieme di parole “stringhe” sequenze di simboli di un dato alfabeto insieme finito di
simboli
- Naturali : uomo
- Artificiali : pc / macchine (es linguaggi di programmazione)
Aspetti:
- Lessicale: come sono scritti , corretta formazione delle parole cioè corretta ortografia
- Sintattico : correttezza forma, corretta formazione delle frasi a partire dai termini
- Semantica: correttezza contenuto (errori: priva di senso, ambigua)
Per rispettare la correttezza è necessario fornire in modo non ambiguo delle specifiche lessicali, sintattiche
e semantiche che possono essere espresse in modo informale (come descriviamo una lingua straniera) o
con un apparato formale.
La definizione lessicale e sintattica dei linguaggi si può effettuare mediante un apparato formale
determinato da Chomsky e detto delle Grammatiche Generative.
Stringa: sequenza di simboli su un alfabeto, Vuota: indicata con lambda (λ).
W*: L'insieme delle stringhe finite dell'αbeto, dove l'αbeto è un insieme finito di W.
Il linguaggio su un αbeto, dove l'αbeto è un insieme finito W è un sottoinsieme L dell'insieme delle stringhe
di W*.
Blank: spazio vuoto
Descrizione del linguaggio formale:
- Metodo di riconoscimento: procedimento con in input una stringa e in output la riposta “si/no” in
caso appartenga o meno al linguaggio.
Comodo ma non consigliato per lo studio di un interno linguaggio in quanto è per tentativi
- Metodo di generazione: permette di generare una ad una tutte le stringhe del linguaggio.
Utile per avere chiare le indicazioni sulla struttura del linguaggio.
Questo metodo è stato introdotto da Chomsky per dare una veste rigorosa ai linguaggi naturali e
alle loro grammatiche definendo le grammatiche generative, buono per la formalizzazione dei
linguaggi di programmazione.
- Apparato algebrico: permette di dare espressioni esplicite per gli elementi del linguaggio
Oppure si può caratterizzare il linguaggio come soluzione di sistemi di equazioni algebriche
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13

Anteprima parziale del testo

Scarica Algoritmi e Strutture Datii: RAM, Algoritmi di Ricerca, Alberi di Ricerca e Numeri Primi e più Sintesi del corso in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

CAPITOLO 1. ALGORITMI E MODELLI DI CALCOLO

- Comunicazione , linguaggio e linguaggi Linguaggi: insieme di parole “ stringhe”  sequenze di simboli di un dato alfabeto  insieme finito di simboli

  • Naturali : uomo
  • Artificiali : pc / macchine (es linguaggi di programmazione) Aspetti:
  • Lessicale: come sono scritti , corretta formazione delle parole cioè corretta ortografia
  • Sintattico : correttezza forma, corretta formazione delle frasi a partire dai termini
  • Semantica: correttezza contenuto (errori: priva di senso, ambigua) Per rispettare la correttezza è necessario fornire in modo non ambiguo delle specifiche lessicali, sintattiche e semantiche che possono essere espresse in modo informale (come descriviamo una lingua straniera) o con un apparato formale. La definizione lessicale e sintattica dei linguaggi si può effettuare mediante un apparato formale determinato da Chomsky e detto delle Grammatiche Generative. Stringa: sequenza di simboli su un alfabeto, Vuota: indicata con lambda (λ). W: L'insieme delle stringhe finite dell'αbeto, dove l'αbeto è un insieme finito di W. Il linguaggio su un αbeto, dove l'αbeto è un insieme finito W è un sottoinsieme L dell'insieme delle stringhe di W. Blank: spazio vuoto Descrizione del linguaggio formale:
  • Metodo di riconoscimento: procedimento con in input una stringa e in output la riposta “si/no” in caso appartenga o meno al linguaggio. Comodo ma non consigliato per lo studio di un interno linguaggio in quanto è per tentativi
  • Metodo di generazione: permette di generare una ad una tutte le stringhe del linguaggio. Utile per avere chiare le indicazioni sulla struttura del linguaggio. Questo metodo è stato introdotto da Chomsky per dare una veste rigorosa ai linguaggi naturali e alle loro grammatiche definendo le grammatiche generative, buono per la formalizzazione dei linguaggi di programmazione.
  • Apparato algebrico: permette di dare espressioni esplicite per gli elementi del linguaggio Oppure si può caratterizzare il linguaggio come soluzione di sistemi di equazioni algebriche

- Algoritmi e calcolabilità Algoritmo : descrizione precisa delle azioni per risolvere un problema di qualsiasi genere PROBLEMA ( da risolvere )  PROCEDIMENTO (per risolverlo )  AGENTE DI CALCOLO (che esegue le istruzioni ) Espresso in forma rigorosa in modo da essere comprensibile ed eseguibile senza margine d errore dal calcolatore. Passi: unità non ambigua, comprensibile e eseguibile  dettagli come linguaggio e ambiente Ambiente di calcolo: ambiente in cui si opera

  • Oggetti : su cui si opera
  • Strumenti: con cui operare Requisiti di un algoritmo:
  • Lunghezza finita
  • Struttura in (un numero finito di) passi discreti
  • Non ambiguità per l’agente di calcolo : deve essere in grado di eseguire ogni passo
  • Non casualità : risultato certo e ripetibile
  • Determinismo: effettua una sola scelta per volta - Macchine di Turing Si ottiene dotando un controllo a stati finiti di un nastro illimitato su cui possono essere scritti e letti caratteri di un αbeto. Le operazioni possibili sul nastro consistono nella scrittura di un nuovo simbolo e nello spostamento del punto di lettura di una posizione a destra o a sinistra. Configurazione: la tripla (q, α, i) dove q è lo stato attuale , α (alfa) è la stringa ottenuta dal nastro togliendo gli infiniti blank iniziali e finali e i la distanza del punto di lettura dall’inizio di α. Mossa: passaggio da una configurazione all’altra, che avviene come segue:
  • sia αbeto, dove l'αbeto è un insieme finito di =a1a2...an, il simbolo è ai se 1≤i≤n altrimenti blank
  • se δ(a,q)=(y,b,m) allora la nuova configurazione è (y,β,j) con β che è in pratica quanto hai scritto finora e j è la posizione del nuovo punto di lettura
  • se la configurazione iniziale è (s,αbeto, dove l'αbeto è un insieme finito di ,1) e quella terminale è (q,β,i), β è il risultato dell'applicazione della MdT alla stringa α. Definita come una quintupla M =(K, V, s, δ, F) dove:
  • K è un insieme finito di stati
  • V è l’alfabeto in ingresso che contiene il simbolo di blank (“b”)
  • s ∈ K è detto stato iniziale
  • δ è una funzione di transizione che associa ad una coppia (x,q) con x ∈ V, q ∈ K una tripla (y,a,m) con y ∈ K, a ∈ V, m ∈ {destra, sinista}. Non è richiesto che δ sia definita per tutte le coppie (x,q), comunemente le funzione è data elencando tutte le quintuple (x,q,y,a,m) per cui δ (x,q) è definita.
  • F ⊆ K è un insieme di stati finali

Teoremi:

  • data una MdT esiste una RAM R che la simula
  • data una RAM esiste una MdT M che la simula Teorema 1: Data una MdT m esiste una RAM R che la simula.  dato lo stesso input entrambe non terminano o restituiscono lo stesso output La dimostrazione si basa sulla costruzione di una RAM la cui memoria illimitata simula il nastro della MdT e il cui programma produce in memoria le stesse azioni che le quintuple producono nel nastro della MdT. Teorema 2: Data una RAM R esiste una MdT M che la simula.--> La dimostrazione si basa sulla costruzione di una MdT il cui nastro simula la memoria illimitata della RAM e le cui quintuple producono nel nastro le stesse azioni che in memoria produce il programma. Un qualsiasi calcolatore idealizzato ammettendo una quantità di memoria illimitata costituisce un modello di calcolo equivalente ad una RAM.

ALBERO DI DECISIONE

Ogni nodo rappresenta una decisione con tutte le sue possibili scelte Considera solo i confronti necessari per risolvere un problema. Binario: ogni nodo ha due foglie (limitazione inferiore alla profondità: per esempio non posso esistere alberi binari a 3 livelli con più di otto foglie).

- Analisi e sintesi di algoritmi Complessità computazionale concreta: studio efficienza degli algoritmi e il costo della risoluzione dei problemi. Dimensione del problema: quantità associata ad una particolare istanza, legata la numero dei dati in input. Misure di costo: quantità che possono essere scelte e tengono conto della quantità di una particolare risorsa utilizzata per risolvere il problema. Esistono anche le misure si spazio e tempo. Complessità di un algoritmo (costo) : la funzione che associa alla dimensione del problema il costo della sua risoluzione in base alla misura scelta. Utilizzando numeri interi, non decrescenti e non negativi. Non sempre lo stresso algoritmo con dati diversi ma stessa dimensione presenta la stessa complessità:

  • nel caso peggiore: si prende in considerazione la possibilità più sfavorevole
  • media: la media dei casi che si possono presentare Complessità di un problema : funzione che associa alla dimensione del problema il minimo costo della risoluzione in base alla misura scelta Dipende dal dato in ingresso e non sola dalla sua dimensione:
  • nel caso peggiore
  • media Stima della complessità di un problema: determinazione di funzioni che la limitano dal basso e dall’alto. Per determinare la complessità di un problema P con un algoritmo A bisogna individuare la limitazione superiore f(n) e inferiore g(n). Se per il problema P si trova un algoritmo A di complessità f(n) che lo risolve si dice che f(n) è la limitazione superiore alla complessità di P. Se si dimostra che non esiste alcun algoritmo di complessità minore o uguale a g(n) che risolva P, allora di dice che g(n) è una limitazione inferiore alla complessità di P. Se le due limitazioni sono uguali f(n)=g(n) allora abbiamo che la complessità di P è f(n). Quando la limitazione superiore è dello stesso ordine della limitazione inferiore si è determinato l’ordine di grandezza della complessità del problema. Ordine di grandezza di una funzione: In informatica si usa la seguente notazione asintotica per indicare il possibile ordine di grandezza di una funzione. La variabile indipendente n è in genere un intero positivo e indica la “dimensione” dei dati di un problema (per es. il numero di bit necessario a descrivere i dati). La funzione di cui si studia l’ordine di grandezza è in genere proporzionale al tempo di calcolo (complessità in tempo), oppure alla memoria impiegata oltre a quella necessaria ai dati d’ingresso (complessità in spazio).

L'ordine di grandezza è la crescita della funzione f(n) per n → ∞.

Possiamo avere che:

  1. g(n) è di ordine o di f(n) scritto come g(n)∈ O(f(n))
  2. g(n) è di ordine omega di f(n) scritto g(n)∈Ω (f(n)) Con la prima notazione si indica un insieme di funzioni g(n) per cui esistono due costanti c e n0 tali che g(n)<cf(n) pero ogni n>n0.

- Algoritmi di ricerca Ricerca di uno specifico elemento detto chiave in un insieme di dati omogenei detto tabella. Casi:

  • Ricerca in insiemi di chiavi prestabiliti : tabella organizzata secondo delle chiavi sulle quali si effettuano poi delle ricerche
  • Si permette l’uso di algoritmi di ricerca e inserzione: tabella vuota che viene strutturata alternando ricerche e inserzioni
  • Si prevede di effettuare sia operazioni di ricerca / inserzione / cancellazione Algoritmo 1. Ricerca lineare Ricerca in una lista, memorizzata in un array. Gli elementi sono scanditi in sequenza fino a che si trova quello cercato oppure la fine della lista. La complessità nel caso peggiore è n (se x non o c’è o è in ultimo devo scandire n posizioni). Se la chiave esiste e ha la probabilità di essere in una qualsiasi posizione il numero medio di ricerche è (n+1) / 2.
  • Se la tabella è di grandi dimensioni risulta inefficiente
  • L’inserzione si effettua alla fine con costo O(1)
  • La cancellazione richiede invece il compattamento degli elementi dell’array e richiede O(n) operazioni
  • Se la chiave è in posizione k si scandiscono k celle per trovarla e n-k per ricompattare l’array. Algoritmo 2. Ricerca binaria Array ordinato, controllo la posizione centrale se è x, altrimenti se x è più piccolo si effettua la ricerca sulla prima metà dell'array, se è più grande sulla seconda metà.
  • La complessità per la ricerca di n elementi è R(n)≤R(n/2)+0(1), quindi R(n)∈0(logn).
  • L’inserzione richiede lo spostamento degli elementi successivi e come la cancellazione richiede O(n) operazioni.
  • Albero binario di ricerca: ogni nodo ha al più due figli e contiene una chiave con eventuali informazioni associate. La chiave presente in ogni nodo segue nell'ordinamento totale tutte le chiavi presenti nel sottoalbero sinistro e precede tutte le chiavi del sottoalbero destro. Esistono particolari funzioni per l'implementazione in alberi binari (vedi libro) Algoritmo 3. Ricerca in alberi binari Si controlla la radice e in caso di insuccesso si restringe al sottoalbero destro o sinistro. Se l'albero è vuoto restituisco falso, se x è uguale alla radice restituisco vero, se è più piccolo della radice reitero la funzione sul sottoalbero sinistro altrimenti sul destro.
  • La complessità dipende dalla struttura dell’albero: caso peggiore se è sbilanciato
  • La relazione di ricorrenza per il costo della ricerca in un albero che contiene n chiavi è R(n)≤R(n1)+0(1) da cui R(n)∈0(n).
  • La complessità media: se è ben distribuito O(log n ) come la ricerca binaria. Algoritmo 4. Inserzione in alberi binari
  • Se è vuoto: si inserisce una foglia
  • Altrimenti dopo in confronto con la chiave della radice: 1.se x è minore del valore della radice si costruisce un nodo avente come radice la radice , il sottoalbero destro e si reitera la funzione su x e sul sottoalbero sinistro
  1. altrimenti si costruisce un nodo avente come radice la radice, il sottoalbero sinistro e per il destro si reitera la funzione su x e sul sottoalbero destro.
  • Complessità: dipende dalla struttura dell’albero (sbilanciato: complessità lineare quindi n , se è ben bilanciato O(log n) come per la ricerca ) Algoritmo 5. Cancellazione da alberi binari Stesso costo computazionale della ricerca / inserzione Una volta trovato q:
  1. q è una foglia : cancellazione diretta
  2. q ha un figlio: q viene sostituito dal figlio
  3. q ha 2 figli: q viene sostituito dal massimo del sottoalbero sinistro (elemento di livello più alto privo di figli destri del sottoalbero sinistro) o dal minimo del sottoalbero destro. La complessità dei casi 1, 2 è costante; nel caso 3 se è sbilanciato è lineare. Grado di bilanciamento di un albero: bilanciato  ogni nodo che non è una foglia dell’ultimo livello ha due figli Un albero si dice bilanciato rispetto a una costante Δ se dette Cd e Cs le massime lunghezze dei cammini tra un nodo e le sue foglie del sottoalbero destro e sinistro si ha per ogni nodo che |Cd-Cs|≤ Δ Perfettamente bilanciato: Se Δ=0 ogni nodo del penultimo livello ha due figli. Il numero n di nodi è n = 2 α -1 ovvero k = log 2 (n+1) dove k è il massimo livello. Alberi AVL: Se Δ=1. Tra gli alberi AVL i più bilanciai sono i perfettamente bilanciati e i meno bilanciati sono gli alberi di Fibonacci. Quasi perfettamente bilanciati: tutti i nodi hanno 2 figli tranne eventualmente alcuni del penultimo livello e tutti quelli dell’ultimo che sono privi di figli. Alberi di Fibonacci si creano applicando ricorsivamente:
  • F 0 è l'albero vuoto
  • F 1 è l'albero con un solo elemento
  • Fi per i>1 è formato da un nodo radice che ha come figli Fi-1 e Fi- con i>2. Si verifica per induzione che la profondità di Fi è i, il grado di sbilanciamento è sempre 1 e il numero dei nodi di Fi è Fi+2 - 1 dove Fi è l'i-esimo numero di Fibonacci definito dalla relazione
  • F 0 = 0
  • F 1 = 1
  • Fi = Fi-1 + Fi-2, i> La profondità k di un albero AVL con n nodi ha ordine di grandezza 0(logn). Un altro caso di alberi AVL sono quelli quasi perfettamente bilanciati, i nodi hanno due figli tranne alcuni del penultimo livello e quelli dell'ultimo che non ne hanno. L'inserzione e la cancellazione possono influire sul grado di bilanciamento dell'albero, per questo si utilizzano delle procedure che restituiscono un albero AVL con un tempo di O(log n) e con costo O(log n) anche nel caso pessimo. Ribilanciamento pagina 36 Algoritmo 6. Ricerca del massimo Primo nodo senza figli destri che si incontra partendo dalla radice e scendendo a destra.
  • Se l'albero è vuoto restituisce la radice altrimenti reitero la funzione sul destro dell'albero.
  • Complessità nel caso peggiore: lineare se l’albero non è bilanciato; O(log n) nel caso medio e per gli alberi AVL. Ricerca del minimo : analogo basta cambiare la ricerca a destra con la ricerca a sinistra
  • per la chiave c numerica e n=2k^ si può scegliere: hash= il numero rappresentato da k bit centrai di c^2
  • per c non numerico,si può ridurre al caso precedente spezzando la chiave in tanti segmenti di lunghezza pari alla parola di macchina usata per rappresentare i numeri interi e poi combinandoli tra loro con opportune operazioni: + o or esclusivo.
  • Le funzioni hash vengono utilizzate per verificare la correttezza della trasmissione. Python ha la funzione hash(o) che calcolo un valore per un oggetto, però è dipendente dalla sessione. Algoritmo 12. Gestione di tabelle hash Una volta calcolata la funzione di hash della chiave da trattare questa ci da un indirizzo nella tabella; se questa posizione è libera la scansione è terminata. Se la cella è occupata da una chiave si genera un indirizzo alternativo e il procedimento continua fino a quando non si trova la chiave cercata o una cella libera. Esempio1: La scelta del nuovo indirizzo è quella della scansione lineare cioè il nuovo indirizzo è uguale al precdente aumentato di una unità. Purtroppo sono probabili i cosiddetti agglomerati primari di chiavi, cioè zone piene che favoriscono ulteriori collisioni. Esempio 2: Si può usare allora la scansione quadratica ((h+j2)%n) con n numero primo, però se abbiamo due indirizzi h1 e h2 e un certo i e si ha (h 1 + i^2 = h 2 + i^2 ) mod n da quel punto in poi le successioni collideranno, o si può usare quella quadratica a coefficienti pesati ((h+wj*2)%n) con w=hash(x)//n.
  • La complessità dipende dalla grandezza della tabella, se ha m chiavi il tempo medio di ricerca è α=m/(n+1). S(α) è il numero medio di accessi in tabella per una ricerca con successo I(α) è il numero medio di accessi per una ricerca senza successo.
  • Le tabelle hash sono più efficienti degli alberi binari per il tempo di ricerca ma devono fissare in anticipo la grandezza della tabella, tra l'altro la cancellazione di una chiave richiederebbe di ordinare gran parte della tabella. Ricerca Inserimento Cancellazione Ricerca max Ricerca min Array O(n) O(1) O(n) O(n) O(n) Liste O(n) O(1) O(1) O(n) O(n) Array ordinati O(log n) O(n) O(n) O(1) O(1) Alberi binari (media) (^) O(log n) O(log n) O(log n) O(log n) O(log n) Alberi binari (peggiore) O(n) O(n) O(n) O(n) O(n) Alberi AVL O(log n) O(log n) O(log n) O(log n) O(log n) Heap O(n) O(log n) O(log n) O(l1) O(n) Tabelle hash Indip. da n^ Indip. da n^ - - -

8. Algoritmi di ordinamento

Ordinamento di un array secondo una data relazione di ordine totale.

  • Numeri: crescente / decrescente
  • Stringhe: alfabetico / lessicografico Un array di n elementi ha n! possibili permutazioni Teorema: detta Ord(n) la complessità del problema dell’ordinamento di n elementi, con un algoritmo basato sui confronti, si ha: Ord(n) ∈ Ω(n log n)  Si dimostra con il conto delle foglie

Dimostrazione: un algoritmo di ordinamento riceve in ingresso un array di n elementi e restituisce lo stesso ordinato nel modo prescritto, per fare ciò deve effettuare una sequenza di scelte che gli permettono di trovare la combinazione corretta tra le n! totali. L’albero di decisone basato su un algoritmo di confronto ha almeno n! foglie. Esempio lunghezza cammino più lungo dell’albero di decisione: confronto due non inferiore a log2n! e a tre log 3 n! Applicando la formula di Stirling (approssima fattoriali grandi) abbiamo n!=√2nπ *(n/e)n^ + termini di orine inferiore da cui log n! ∈ O(n log n) e log n!∈ Ω(n log n) Limitazione inferiore : Un generico array non può essere ordinato con meno di Ω(n log n) confronti, ciascuno con un numero finito di alternative (2,3). Poiché la complessità in tempo di un algoritmo, non può essere inferiore al numero di confronti da effettuare si conclude che Ω(n log n) è una limitazione inferiore. Per fornire limitazione superiore complessità è necessario studiare specifici algoritmi. Procedura scambia: cambia due elementi di un array utilizzando l’assegnamento simultaneo di due valori. Algoritmo 1. Bolle Si effettuano scambi tra due elementi contigui quando non sono nel corretto ordine. Fai il ciclo per ogni numero, scorri a partire dal secondo elemento, se è più piccolo del terzo gli scambio, altrimenti no e vado avanti così finché tutto l'array non è ordinato.

  • Costo: O(n^2 ) Algoritmo 2. Selezione Scorro l'array dal primo elemento al penultimo (non ha senso confrontare l'ultimo con se stesso), Assegno a minimo il valore del 1 elemento, controllo il resto dell'array fino alla fine, se trovo qualcosa di più piccolo lo metto nella posizione indicata dal cursore.
  • Costo: O(n^2 ), il numero degli scambi è al più n-
  • Non tiene conto di array semi-ordinati. Algoritmo 3. Mergesort Se l'array ha un solo elemento finisco perché è ordinato. Altrimenti divido l'array a metà con la funzione mergesort ricorsiva che li ordina separatamente , successivamente chiama merge per la fusione. Si usa un array temporaneo di appoggio, delle dimensioni di v. La merge ha due indici i e j, uno controlla la prima parte dell'array, l'altro la seconda, confronto l'elemento che sta scorrendo i con quello di j, salvo quello più piccolo in temp (array temporaneo) e vado avanti con gli indici e con l'indice k che mi scorre temp. Vado avanti finchè ho scandito tutta la prima parte dell'array e tutta la seconda, muovo gli indici i,j e k.
  • La complessità è nlogn ed è quindi la migliore anche se occupa molta memoria. Algoritmo 4. Quicksort Se c'è un solo elemento finisco. In caso contrario scelgo un perno e scandisco l’array a partire dalle due estremità scambiando le coppie che non soddisfano u < x < v , quando ho finito ho due sotto array in ordine rispetto al perno Ho tre valori: i, j,x che è l'elemento in mezzo. Vado avanti finchè i due cursori non si sono incontrati: - finchè x è maggiore di i vado avanti con i; - finchè j è minore di x vado indietro con j; se i è più piccolo di j faccio lo scambio, i va avanti, j indietro.

presente nella lista dei visitati reitero la funzione, se trovo qualcosa restituisco true e termino altrimenti se non trovo niente restituisco false e termino. Esiste anche la funzione iterativa di questa funzione basata sull'uso della pila (vedi libro).

  1. Breadth first : dove vengono visitati tutti i successori di un nodo prima di scendere in profondità. Inserisco in una lista q un determinato nodo e vado avanti finché ci sono elementi nella lista. Elimino il nodo da q, se non è nella lista dei visitati ce lo inserisco, controllo con la funzione goal che sia quello che sto cercando, nel caso restituisco true e concludo. Per ogni nodo metto i suoi figli in q.
  2. Euristica : dove la ricerca viene fatta seguendo il cammino più promettente (valutazione di una stima) E' molto simile alle funzioni precedenti ma usa delle funzioni che mette a disposizione la libreria heap, sulla coda con priorità. Creo la coda a priorità e ci inserisco il nodo (heappush), vado avanti fino alla lunghezza della coda, elimino il nodo dalla cosa (heappop), se non il nodo non è nei visitati ce lo appendo, se è restituito da goal termino altrimenti appendo alla coda anche i figli del nodo

- Non determinismo, problemi P e NP Choice: (espediente didattico)  esegue in un passo solo tutte le alternative, se la soluzione esiste almeno uno degli agenti la trova subito e stampa EUREKA Calcolo non deterministico: viola l’ultima delle condizioni della definizione di algoritmo cioè il determinismo (si effettua una sola scelta per volta) Non deve essere confuso con:

  • Calcolo parallelo: vi è un n prefissato di agenti di calcolo fisici che interagiscono fra loro (processori disponibili sulla macchina)
  • Calcolo probabilistico: scelte guidate dal caso Soluzione: usare il non determinismo come paradigma teorico di programmazione oppure simularlo a livello di linguaggio , realizzando programmi semanticamente equivalenti agli algoritmi non det a spese di un tempo di esecuzione che cresce esponenzialmente alla dimensione del problema. Problemi che si presterebbero a una risoluzione non deterministica: 1) Problema della soddisfattibilità (SAT) Data un'espressione booleana E costruita sulle variabili a 1 ...an con operatori and, or, not cercare un insieme di valori a 1 ...an tali che E(a 1 ...an)=True. Possiamo definire anche una funzione che di fronte a una scelta si moltiplica ed esplora contemporaneamente tutti i nodi usando come espediente l'istruzione choice, istruzione che in realtà non esiste ma è utile per esaminare anche questa possibilità. Se il nodo è quello che stavamo cercando la funzione stampa “eureka” e termina, altrimenti contemporaneamente per ogni figlio di quel nodo verifica che non sia in visitati, e nel caso ce lo appende e reitera la funzione. Questa funzione è un esempio di calcolo non deterministico e viola la condizione di determinismo degli algoritmi; bisogna tener presente che è ben diverso dal calcolo in parallelo e quello probabilistico.

Per molti problemi basta trovare se esiste almeno una soluzione (studiare i problemi nella loro forma decisionale). 2) Problema decisionale della soddisfattibilità (DSAT) Data un'espressione E costruita sulle variabili logiche a 1 ...an con gli operatori and, or, not stabilire se esiste un insieme di valori a 1 ...an tale che E(a 1 ...an)=True. Altri problemi sono: 3) Problema decisionale del ciclo hamiltoniano DHAM : dato un grafo G di n vertici stabilire se esiste in G un ciclo che attraversa tutti i vertici esattamente 1 volta 4) Delle scatole DBIN: dato un insieme A di n interi positivi e due interi positivi h e k stabilire se esiste una partizione di A in k sottoinsiemi disgiunti tale che la somma degli elementi di ogni insieme sia uguale o minore a h 5) Dello zaino DKNA : dato un insieme A di n interi positivi e c e z interi pos stabilire se esiste un sottoinsieme di A i cui elementi abbiamo somma compresa tra c e z 6) Delle equazioni diofantine quadratiche DEDQ : dati tre interi positivi a,b,c di n cifre stabilire se ax^2 +by+c=0 ha radici. 7) Del numero cromatico DCOL: dato un grafo G con n vertici e un intero positivo k stabilire se G può essere colorato con k colori in modo che i nodi adiacenti non siano dello stesso colore 8) Della primalità DPRI: stabilire se un numero positivo intero di n cifre è primo o può essere scomposto in numeri primi Fasi articolazione algoritmo non deterministico per risolvere un problema decisionale:

  • Scrivere un programma deterministico che, data una soluzione candidata, verifica banalmente se questa è accettabile o meno
  • In un singolo passo, provare in modo non det , tutte le possibili soluzioni candidate. Definizioni: P : insieme di tutti i problemi decisionali risolubili in tempo polinomiale da un algoritmo deterministico NP : insieme di tutti i problemi decisionali risolubili in tempo polinomiale da un algoritmo non deterministico Quindi P ⊆ NP Inoltre DSAT ∈ NP e DPRI ∈ NP Riduzione polinomiale  dati due problemi decisionali pe q si diche che p si riduce polinomialmente a q e si indica p ∠ q se ogni soluzione di p può ottenersi deterministicamente in tempo polinomiale da una soluzione di q, ovvero esiste un algoritmo deterministico polinomiale che trasforma istanze di p in istanze equivalenti di q. Teorema di Cook: per qualunque p ∈ NP vale p ∠ DSAT. La dimostrazione mostra come per ogni algoritmo polinomiale non det e per ogni insieme di dati si possa costruire una espressione logica che vale TRUE sse la computazione termina con successo. Una importane conseguenza del teorema è che si si trovasse un algoritmo det polinomiale per DSAT allora varrebbe certamente: P = NP Tutti gli sforzi per trovare un tale algoritmo sono finora risultati vani e comunemente si congettura P ⊂ NP

Caso: Stringhe binarie A = {0,1}  la distanza è il numero di 1 nel risultato della operazione di exclusive or (XOR) tra le due stringhe.

- Distanza di Levensthein: date due stringhe su un alfabeto A la distanza (chiamata anche edit-distance) è il numero minimo di sostituzioni, cancellazioni o inserzioni di simboli sufficienti a trasformare una stringa nell’altra Esempio: “treno” e “cena”  la distanza è 3 (trena, trena, cena) Possiamo usare queste relazioni: - d(λ, α)=|α| - d(β, λ)=|β| - d(αx , βx) = d(α, β) - d(αx , βy) = 1+min(d(α , βy), d(αx, β), d(α, β)) dove g λ, è la stringa vuota, alfa e beta stringhe (anche vuote) e x y singoli caratteri. La complessità dell’algoritmo è esponenziale nella lunghezza delle stringhe a causa delle chiamate ripetute della funzione con gli stessi argomenti, è indispensabile usare la tecnica della programmazione dinamica utilizzando un array bidimensionale per memorizzare i valori calcolati. - Teoria dell’Informazione e codici Risoluzione ambiguità: - usando 1 come separatore - assegnando la stessa lunghezza Codice univocamente decifrabile : permette di separare i simboli senza ambiguità Istantaneo : un codice univocamente decifrabile che può essere decodificato senza attendere i simboli della parola successiva (nessuna parola del codice non deve essere il prefisso di un’altra) Le stringhe di A* possono essere strutturate come un albero binario, i figli della stringa α sono α0 α1. Le parole del codice sono le foglie. Completo : non posso essere aggiunte altre foglie e tutti i rami vengono bloccati Per rendere la codifica univocamente decifrabile si usa parole della stessa lunghezza, se è diversa le parole di A* più brevi vengono date ai simboli B più frequenti. Algortimo 1. Decodifica di un codice istanatneo Il codice viene memorizzato sotto forma di albero binario. Mano a mano che ricevo i bit da decodificare scendo lungo l’albero scegliendo destra se ricevo 0 e sinistra altrimenti, quando si trova una foglia si scrive il simbolo di B corrispondente a quella parola. Se per i simboli B è definita una distribuzione di proprietà allora si può definire la lunghezza media del codice: Dato un alfabeto A{0,1} e delle stringhe A* e dovendo codificare in binario una sequenza di informazioni rappresentate come simboli di un alfabeto qualsiasi B={s 1 ,s 2 ...sk) il processo avviene in due fasi:

  1. Si sceglie un insieme di k parole di A* detto codice
  2. Si assegna in modo univoco una parola del codice a ogni simbolo di B

dove |ci| è la lunghezza dell’i-esima parola di codice e pi è la probabilità del simbolo associato. È conveniente assegnare le parole di codice più corte ai simboli più probabili, ma la soluzione ottimale al problema della codifica è tutt’altro che evidente. Il problema può essere trattato con la teoria dell’informazione. La grandezza: È detta Entropia associata alla distribuzione {p1,p2,..,pk}. L’entropia di una distribuzioni di proprietà ha una interpretazione legata alla quantità di informazione fornita dal risultato di un esperimento casuale. Definizione: sia E un evento che occorre con probabilità p(E). Se ci viene che l’evento E si è avverato allora abbiamo ricevuto: I(E) = - log p(E) Unità di informazione. L’unità di misura dipende dalla base del logaritmo, se la base è due, l’unità di misura è detta bit Si noti che se p(E) = ½ allora l(E) = 1 bit Bit: quantità di informazione ricevuta quando è specificata una particolare alternativa tra due ugualmente probabili. L’entropia di una distribuzione di probabilità è quindi la quantità media di informazione che viene fornita dal verificarsi degli eventi che hanno quella distribuzione di probabilità. L’entropia è massima per eventi equiprobabili e tale massimo vale per log 2 k bit. L’entropia è il limite inferiore alla lunghezza media del codice; la lunghezza media del codice espressa in bit non può essere inferiore alla quantità di informazione legata all’evento di cui vogliamo codificare i risultati. Algoritmo 1. Codifica ottimale di Huffman Data una distribuzione di probabilità {p1,p2,..,pk} e supponendo p1 < p2 < pk. Le due parole più lunghe del codice che verrà generato saranno associate a p1 e p2. Usiamo un bit per distinguere tra i due eventi e consideriamo l’evento [accade p1 o p2 ]. Tale evento ha probabilità p1 + p2 e l’insieme {p1 + p2 , p3, .., pk} è ancora una distribuzione di k - valori di probabilità. Si considerano k eventi come k alberi formati da una sola radice. Ogni volta che due eventi meno probabili vengono raggruppati, l’evento risultante è un albero formato da un nodo e dai due sottoalberi relativi agli eventi che vengono raggruppati. Iterando il procedimento si ottiene un unico albero binario a cui è associato un codice istantaneo completo. Si può dimostrare che tale codice ha una lunghezza media r tale che: e che inoltre non è possibile trovare un codice con lunghezza media inferiore. Un’efficiente implementazione di questo algoritmo si può realizzare utilizzando un heap di alberi binari come coda a priorità.

  1. Calcolo dei numeri primi Numeri primi: numeri naturali che non possono essere scomposti in prodotto di fattori minori. (teorema fondamentale dell’aritmetica) A = {2,3,7,11,13,17..} I numeri primi non formano un insieme finito quindi la successione di A non termina mai.

Problema 3: Data S alfanumerica, interpretata come un testo, contare le occorrenze delle varie parole. La soluzione utilizza la funzione token(S,sep), appena definita, e il tipo di dato nativo dictionary. L’istruzione D[s] = D.setdefault(s,0)+1 aumenta di 1 il valore associato alla chiave s, se invece la chiave non è presente la inserisce con associato il valore di 1. Prendo in input la stringa e i separatori, ciclo le parole divise e uso un'istruzione che se mi aggiunge di 1 se la parola l'ho già vista, altrimenti mette 1 a una mai vista. Alla fine ordino la lista delle chiavi. Poi mi creo una lista vuota W, faccio un ciclo sulle chiavi ordinate e appendo il valore alla lista accanto alla parola, restituisco il risultato ordinato. Problema 4 : Generare una lista di file-path completi dei file di tipo .txt presenti in una drectory La soluzione utilizza la funzione glob(ex), della libreria glob, dove ex rappresenta un’espressione regolare che definisce i file da listare. Per eseguire il programma bisogna fornire il file-path completo (che dipende dalla struttura dei dati sul proprio hard disk) che noi abbiamo fatto iniziare con path. Il passo successivo è leggere tutti i file. Importa la libreria glob, passo in input dir e restituisco glob. glob della directory seguita dall’espressione “*.txt”. Problema 5: Data una lista di file testuali elencare i caratteri presenti in essi. La funzione charsOneFile(file, D) aggiorna il dictionary D con il contenuto di un file mentre charsAllFiles(fn) applica charsOneFile(file,d) a tutti i file. Uso due funzioni charsOneFile e charsAllFiles. La prima apre il file, lo legge riga per riga, finché ci sono righe va avanti, mette i caratteri del dizionario, e poi mi legge un'altra riga. La seconda inizializza un dizionario vuoto, per ogni file che gli passo lo stampa e invoca la funzione precedente, alla fine mi ordina la lista delle chiavi. Problema 6: Data una lista di file testuali contare la frequenza delle parole. La funzione wordsOneFile(file,d) aggiorna il dictionary D con il contenuto di un file mentre wordsAllFiles(fn) applica wordsOneFile(file, D) a tutti i file. Ho due funzioni: La prima mi apre il file, legge una linea, finchè ci sono righe me le tokenizza e trasforma le maiuscole in minuscole, per ogni parola conta la frequenza della parola e chiude il file. La seconda funzione prendi in input la lista dei file e i separatori, inizializzo un dizionario vuoto, per ogni file mi applica la funzione precedente, creo un vettore con la lista delle chiavi, crea una lista vuota, per ogni parola in una riga appendo al dizionario la frequenza e la rispettiva parola, ordino poi tutto al contrario. Problema 7: aprire un file, per ogni parola scrivere la frequenza e scrivo quante parole hanno quella frequenza, apre un nuovo file per scriverci tutto. Apro il file, inizializzo un dizionario vuoto per la frequenza, converto in stringa la frequenza e la stampo con la parola, in F metto la frequenza e la conto, chiudo questo, faccio la lista delle chiavi, dichiaro una lista vuota, per ogni frequenza in F ci attacco la frequenza e la frequenza della frequenza. Apro il file della frequenza e stampo la frequenza e il numero di volte che compare, chiudo tutto. Facendo un grafico dei risultati l'uso della scala lineare non è molto utile, il grafico in doppia scala logaritmica, con in ascissa il logaritmo in base 10 del numero di occorrenze e in ordinata il logaritmo in base 10 del numero di parole che hanno quel numero di occorrenze, mostra un andamento strutturato, con una retta. Chiamando x il numero di occorrenze e p il numero di parole per occorrenze risulta che la retta ha formula p(x)=60493x^α) se α>β1.59. Per valori più grandi l'approssimazione fornisce stime più piccole di 1.