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


Teoria della computazione e computabilità, Schemi e mappe concettuali di Algoritmi E Strutture Di Dati

I concetti fondamentali della teoria della computazione e della computabilità, tra cui il problema dell'arresto, il problema della totalità, i problemi di decisione e semidecidibilità, gli insiemi ricorsivi e ricorsivamente enumerabili, il teorema del punto fisso di Kleene e la riduzione. Vengono inoltre presentate le tesi di Church e l'enumerazione algoritmica degli insiemi. utile per gli studenti di informatica e matematica interessati alla teoria della computazione e alla computabilità.

Tipologia: Schemi e mappe concettuali

2020/2021

In vendita dal 22/02/2023

chiara-angileri
chiara-angileri 🇮🇹

15 documenti

1 / 26

Toggle sidebar

Questa pagina non è visibile nell’anteprima

Non perderti parti importanti!

bg1
COMPUTABILITA
Problema risolvibile: esiste una MT che lo risolve~decidibile Problema irrisolvibile~indecidibile
Halting problem: indecidibile (ma semidecidibile) Nessuna TM può decidere se, data una generica TM M e un
generico ingresso x, M si arresta (in uno stato finale) con lingresso x.
Lemma: (𝑥)={1 𝑠𝑒 𝑓𝑥(𝑥) ≠⊥
0 𝑎𝑙𝑡𝑟𝑖𝑚𝑒𝑛𝑡𝑖 non è computabile.
Se un problema è non risolvibile {𝑢𝑛 𝑠𝑢𝑜 𝑐𝑎𝑠𝑜 𝑝𝑎𝑟𝑡𝑖𝑐𝑜𝑙𝑎𝑟𝑒 𝑝𝑜𝑡𝑟𝑒𝑏𝑏𝑒 𝑒𝑠𝑠𝑒𝑟𝑒 𝑟𝑖𝑠𝑜𝑙𝑣𝑖𝑏𝑖𝑙𝑒
𝑢𝑛𝑎 𝑠𝑢𝑎 𝑔𝑒𝑛𝑒𝑟𝑒𝑙𝑖𝑧𝑧𝑎𝑧𝑖𝑜𝑛𝑒 è 𝑛𝑜𝑛 𝑟𝑖𝑠𝑜𝑙𝑣𝑖𝑏𝑖𝑙𝑒
Se un problema è risolvibile {𝑢𝑛 𝑠𝑢𝑜 𝑐𝑎𝑠𝑜 𝑝𝑎𝑟𝑡𝑖𝑐𝑜𝑙𝑎𝑟𝑒 è 𝑟𝑖𝑠𝑜𝑙𝑣𝑖𝑏𝑖𝑙𝑒
𝑢𝑛𝑎 𝑠𝑢𝑎 𝑔𝑒𝑛𝑒𝑟𝑒𝑙𝑖𝑧𝑧𝑎𝑧𝑖𝑜𝑛𝑒 𝑝𝑜𝑡𝑟𝑒𝑏𝑏𝑒 𝑒𝑠𝑠𝑒𝑟𝑒 𝑛𝑜𝑛 𝑟𝑖𝑠𝑜𝑙𝑣𝑖𝑏𝑖𝑙𝑒
Problema della totalità: la funzione k(y)=1 se 𝑓𝑦 è totale non è computabile. Dato un (generico) programma, vogliamo
sapere se terminerà lesecuzione per qualsiasi dato in ingresso o se potrà, per qualche dato, andare in loop infinito.
Problema di decisione: domanda che ha due possibili risposte o no, la domanda riguarda un qualche ingresso. È
quindi un problema di calcolo di una funzione totale che ha come immagini 0 e 1.
Problema semidecidibile: problema per il quale cè un algoritmo che dice sì se la risposta è sì, può andare in loop se la
risposta è no.
Funzione caratteristica: 𝑐𝑠(𝑥)=𝑠𝑒 𝑥 𝑆 𝑎𝑙𝑙𝑜𝑟𝑎 1 𝑎𝑙𝑡𝑟𝑖𝑚𝑒𝑛𝑡𝑖 0
Insieme ricorsivo (decidibile): la sua funzione caratteristica è computabile.
Insieme ricorsivamente enumerabile (semidecidibile): se S è linsieme vuoto o S è limmagine di una funzione gs
totale e computabile.
Teorema ½ + ½ = 1: Se S è ricorsivo allora è anche ricorsivamente enumerabile. S è ricorsivo sse sia S sia il suo
complementare sono ricorsivamente enumerabili.
La classe di insiemi decidibili è chiusa rispetto al complemento.
Se S è linsieme di indici di funzioni totali e computabili allora S non è RE: non esiste un formalismo RE che possa
definire tutte le funzioni computabili e totali e solo quelle.
Teorema: S è RE S = Dh (h: computabile e parziale) | S è RE S = Ig (g: computabile e parziale).
Teorema del punto fisso di Kleene: sia t una funzione totale e computabile. Allora si può sempre trovare un intero p
tale che 𝑓𝑝= 𝑓𝑡(𝑝), la funzione 𝑓𝑝 è detta punto fisso di t.
Teorema di Rice: sia F un insieme di funzioni computabili. Linsieme S degli indici delle TM che calcolano le funzioni
di F: 𝑆 = {𝑥 | 𝑓𝑥 𝐹} è decidibile sse 𝐹 = o F è linsieme di tutte le funzioni computabili, in tutti i casi non banali S
non è decidibile.
Riduzione: a. Vogliamo verificare se xS, siamo in grado di verificare se y 𝑆′, se cè una funzione computabile e
totale t tale che 𝑥 𝑆 𝑡(𝑥) 𝑆′ allora possiamo rispondere alla domanda xS in modo algoritmico.
b. Vogliamo sapere se possiamo risolvere 𝑥 𝑆, sappiamo che non si può risolvere 𝑦 𝑆′ (𝑆′ non è
decidibile), se troviamo una funzione t computabile e totale tale che 𝑦 𝑆 𝑡(𝑦) 𝑆 allora possiamo concludere
che 𝑥 𝑆 non è decidibile.
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a

Anteprima parziale del testo

Scarica Teoria della computazione e computabilità e più Schemi e mappe concettuali in PDF di Algoritmi E Strutture Di Dati solo su Docsity!

COMPUTABILITA’

Problema risolvibile: esiste una MT che lo risolve~decidibile Problema irrisolvibile~indecidibile

Halting problem : indecidibile (ma semidecidibile) → Nessuna TM può decidere se, data una generica TM M e un

generico ingresso x, M si arresta (in uno stato finale) con l’ingresso x.

Lemma : ℎ

𝑥

non è computabile.

Se un problema è non risolvibile →{

𝑢𝑛𝑎 𝑠𝑢𝑎 𝑔𝑒𝑛𝑒𝑟𝑒𝑙𝑖𝑧𝑧𝑎𝑧𝑖𝑜𝑛𝑒 è 𝑛𝑜𝑛 𝑟𝑖𝑠𝑜𝑙𝑣𝑖𝑏𝑖𝑙𝑒

Se un problema è risolvibile →{

𝑢𝑛 𝑠𝑢𝑜 𝑐𝑎𝑠𝑜 𝑝𝑎𝑟𝑡𝑖𝑐𝑜𝑙𝑎𝑟𝑒 è 𝑟𝑖𝑠𝑜𝑙𝑣𝑖𝑏𝑖𝑙𝑒

Problema della totalità : la funzione k(y)=1 se 𝑓

𝑦

è totale non è computabile. Dato un (generico) programma, vogliamo

sapere se terminerà l’esecuzione per qualsiasi dato in ingresso o se potrà, per qualche dato, andare in loop infinito.

Problema di decisione : domanda che ha due possibili risposte sì o no, la domanda riguarda un qualche ingresso. È

quindi un problema di calcolo di una funzione totale che ha come immagini 0 e 1.

Problema semidecidibile : problema per il quale c’è un algoritmo che dice sì se la risposta è sì, può andare in loop se la

risposta è no.

Funzione caratteristica : 𝑐 𝑠

Insieme ricorsivo (decidibile): la sua funzione caratteristica è computabile.

Insieme ricorsivamente enumerabile (semidecidibile) : se S è l’insieme vuoto o S è l’immagine di una funzione g s

totale e computabile.

Teorema ½ + ½ = 1 : Se S è ricorsivo allora è anche ricorsivamente enumerabile. S è ricorsivo sse sia S sia il suo

complementare sono ricorsivamente enumerabili.

La classe di insiemi decidibili è chiusa rispetto al complemento.

Se S è l’insieme di indici di funzioni totali e computabili allora S non è RE: non esiste un formalismo RE che possa

definire tutte le funzioni computabili e totali e solo quelle.

Teorema : S è RE ↔ S = D h

(h: computabile e parziale) | S è RE ↔ S = I g

(g: computabile e parziale).

Teorema del punto fisso di Kleene : sia t una funzione totale e computabile. Allora si può sempre trovare un intero p

tale che 𝑓 𝑝

𝑡(𝑝)

, la funzione 𝑓

𝑝

è detta punto fisso di t.

Teorema di Rice : sia F un insieme di funzioni computabili. L’insieme S degli indici delle TM che calcolano le funzioni

di F: 𝑆 = {𝑥 | 𝑓

𝑥

∈ 𝐹} è decidibile sse 𝐹 = ∅ o F è l’insieme di tutte le funzioni computabili, in tutti i casi non banali S

non è decidibile.

Riduzione : a. Vogliamo verificare se x∈S, siamo in grado di verificare se y∈ 𝑆′, se c’è una funzione computabile e

totale t tale che 𝑥 ∈ 𝑆 ↔ 𝑡(𝑥) ∈ 𝑆′ allora possiamo rispondere alla domanda ‘x∈S’ in modo algoritmico.

b. Vogliamo sapere se possiamo risolvere 𝑥 ∈ 𝑆, sappiamo che non si può risolvere 𝑦 ∈ 𝑆′ (𝑆′ non è

decidibile), se troviamo una funzione t computabile e totale tale che 𝑦 ∈ 𝑆

∈ 𝑆 allora possiamo concludere

che 𝑥 ∈ 𝑆 non è decidibile.

TEORIA DELLA COMPUTAZIONE

Tesi di Church

Non c'è nessun formalismo per modellare il calcolo meccanico che sia più potente della TM (o formalismi equivalenti).

→Tutti i problemi risolvibili sono tutti e soli quelli risolvibili da una MT.

Enumerazione algoritmica

Un insieme S può essere enumerato algoritmicamente ( E ) se possiamo trovare una biiezione tra e S e 𝑁:

• E: 𝑆 ↔ 𝑁

  • E può essere calcolato con un algoritmo (cioè una TM).

→Le TM possono essere enumerate algoritmicamente: è possibile associare a ciascuna macchina di Turing un indice

sull'insieme dei naturali (detto numero di Goedel) che la descrive in maniera univoca E :{TM}↔ 𝑁.

Possiamo sempre scrivere un programma in C (ovvero una TM) che, dato n, produce l'n-esima TM, e viceversa. E (M)

è detto numero di Gödel di M e E è una Gödelizzazione. Il numero di Gödel in particolare identifica l'indice della

macchina di Turing.

Convenzioni

Se n non appartiene al dominio di f, scriverò 𝑓(𝑛) =⊥

Se P è un programma (una MT) e n un suo possibile input, scriverò P(n)↓ per dire che P termina la computazione su n,

P(n)↑ altrimenti.

Poiché parliamo di numeri, le seguenti nozioni saranno considerate equivalenti:

  • "Problema"

• "Calcolo di una funzione 𝑓: 𝑁 → 𝑁 "

Macchine di Turing Universali

Una MT universale è una TM programmabile, che computa la funzione 𝑔

𝑦

(𝑥), in cui 𝑓

𝑦

(𝑥) è la funzione

calcolata dall'y-esima TM sull'ingresso x. → Le UTM non possono computare tutte le funzioni.

Risolvibilità e soluzioni

Sapere che un problema è risolvibile non vuol dire che sappiamo come risolverlo (e quale sia la soluzione).

→Un problema è risolvibile se esiste una TM che lo risolve.

Caso banale: il problema consiste nel rispondere a una domanda booleana [si/no]. Un problema è una funzione, e

risolverne uno implica calcolare una funzione. Nel caso banale: se un problema è booleano (anche detto problema di

decisione) è decidibile, ovvero esiste una macchina di Turing che può risolverlo.

Giacché le macchine di Turing possono essere enumerate e che tutti i problemi computabili sono tutti e soli quelli

risolvibili dalle MT, possiamo concludere che l'insieme dei problemi computabili ha la stessa cardinalità dei numeri

naturali.

  • Se un problema non è risolvibile, allora un suo caso speciale potrebbe esserlo.
  • Una generalizzazione di un problema non risolvibile è necessariamente non risolvibile.
  • Se un problema è risolvibile, una sua generalizzazione potrebbe non essere risolvibile.

Tecniche di dimostrazione di indecidibilità

Problemi noti decidibili

Se siamo in grado di trovare un algoritmo che termina sempre il problema è decidibile. (f calcolabile e totale)

  • Se un problema è formalizzabile come il calcolo di una funzione f a dominio finito, allora f sarebbe sicuramente

calcolabile (essendo descrivibile mediante una tabella finita).

  • Sapere se due programmi computano la stessa funzione sapendo che terminano per ogni input e che il dominio

di input è finito.

• Una formula chiusa è sempre vera o falsa, quindi è decidibile.

• Data f(x) una f.b.f. in logica del I ordine, con 𝑥 ∈ 𝑁, è decidibile dire se questa formula è soddisfacibile? No.

Se 𝑥 ∈ { 0 , … , 2

61

− 1 }?

Problemi noti semidecidibili

Se troviamo un algoritmo che potrebbe non terminare ma se lo fa restituisce ‘Sì’ come risposta il problema è

semidecidibile. (f calcolabile e non totale)

  • Sapere se due programmi che terminano per ogni input computano funzioni differenti.
  • Sapere se due funzioni definite sullo stesso dominio sono differenti.

Problemi noti indecidibili

  • Halting Problem: nessuna TM può decidere se, per una generica TM M e un generico ingresso x, M si arresta

con l'ingresso x. Con arresto si intende arresto in uno stato finale. Formalmente, nessuna TM può calcolare la

funzione totale 𝑔: 𝑁𝑥𝑁 → { 0 , 1 } definita come 𝑔

𝑦

HALT={𝑖|𝑓

𝑖

Lemma

La funzione ℎ

𝑥

≠⊥ [𝑔

= 1 ]

0 𝑎𝑙𝑡𝑟𝑖𝑚𝑒𝑛𝑡𝑖 [𝑔

= 0 ]

non è computabile (calcolabile).

  • Dimostrare che una funzione è totale: è impossibile costruire una macchina di Turing in grado di dire se una

funzione y è definita per ogni x su N.

In formule: 𝑘

𝑦

(𝑥) ≠⊥ ∀𝑥 ∈ 𝑁 [𝑓

𝑦

𝑡𝑜𝑡𝑎𝑙𝑒]

0 𝑎𝑙𝑡𝑟𝑖𝑚𝑒𝑛𝑡𝑖 [𝑓

𝑦

𝑛𝑜𝑛 𝑡𝑜𝑡𝑎𝑙𝑒]

non è computabile (calcolabile).

In realtà questo problema è una versione ancora più generale dell’halting, in quanto ci chiediamo se dato un

programma esso terminerà sempre a prescindere dagli input (nel problema dell'halt invece si chiede se un dato

programma con un dato input termina o meno).

  • Sapere se due programmi computano la stessa funzione sapendo che terminano per ogni input.

Teorema di Rice

  • Non è decidibile stabilire se un generico algoritmo calcola una funzione fissata (per Rice).
  • Non è decidibile il problema di stabilire se due programmi calcolano la stessa funzione, infatti è un problema

più generale del problema di stabilire se un programma calcola una funzione data.

  • La funzione computata da un programma gode di una certa proprietà (sempre pari, ha un codominio limitato,

ecc.)?

  • I={funzioni computabili con immagine finita} non ricorsivo per Rice.

• Z

0

={x | z 0

𝑓 𝑥

}(insieme delle funzioni computabili che hanno z 0

nella loro immagine) non ricorsivo per Rice.

Riduzione : ci si riconduce a problemi noti non decidibili (halt della MT, funzione totale, ecc.)

Un problema P' è ridotto a un problema P se un algoritmo per risolvere P viene usato per risolvere P', cioè:

  • P è risolvibile
  • c'è un algoritmo che, per ogni data istanza di P': determina una corrispondente istanza di P e costruisce

algoritmicamente la soluzione dell'istanza di P' dalla soluzione dell'istanza di P.

➔ Il problema si riduce ad un problema conosciuto universalmente come decidibile, semidecidibile o non

decidibile. Esempio: sapere se una macchina di Turing termina non un problema decidibile.

Formalmente, se 𝐴, 𝐵 ⊆ 𝑁, una funzione 𝑟: 𝑁 → 𝑁 è una riduzione di A a B (si scrive 𝐴 →

𝑟

𝐵) se:

  • Se 𝑥 ∈ 𝐴, allora 𝑟(𝑥) ∈ 𝐵;
  • Se 𝑥 ∉ 𝐴, allora 𝑥 ∉ 𝐵;
  • r è totale e computabile.

Se 𝐴 →

𝑟

𝐵 allora: 1.Se A è non ricorsivo → anche B è non ricorsivo; 2. Se B è ricorsivo → anche A è ricorsivo.

Dall’indecidibilità del problema dell’arresto della MT deduciamo l’indecidibilità del problema della terminazione del

calcolo in generale:

  • Siano dati una MT M , e un numero 𝑥 ∈ 𝑁
  • Costruisco un programma P che simula M e memorizzo il numero x su un file f
  • Il programma P termina la computazione su f se e solo se 𝑔
  • Se sapessi decidere se P termina, sarei in grado di risolvere il problema dell’arresto della MT.

È decidibile dire se, durante l’esecuzione di un generico programma P si accede ad una variabile non inizializzata?

  • Assumiamo per assurdo che sia decidibile
  • Riduciamo questo problema a quello dell’halting problem in questo modo:
  • Dato un generico programma Q(n), costruisco un programma P fatto in questo modo: {int x, y; Q(n); x=y;},

avendo cura di usare variabili non presenti in Q

  • L’accesso y=x alla variabile non inizializzata x da parte di P è fatto se e solo se Q termina
  • Se fossi in grado di decidere il problema dell’accesso a una variabile non inizializzata, potrei decidere il

problema della terminazione del calcolo.

Dimostrazione mediante reductio ad absurdum : si suppone per assurdo che un problema sia decidibile: se si giunge

a delle contraddizioni allora la tesi sbagliata ed il problema non decidibile.

COMPLESSITA’

Modelli di calcolo e loro complessità

Data la computazione 𝑐 0

𝑟

di una macchina di Turing M a k nastri deterministica:

Si definisce complessità temporale: 𝑇 𝑚

(𝑥) = 𝑟 se M termina in 𝑐

𝑟

Si definisce complessità spaziale: 𝑆 𝑚

(𝑥)=la somma delle quantità massime di nastro occupate, per ogni nastro.

𝑆

𝑚

(𝑥)

𝑘

𝑚

  • FSA: hanno sempre 𝑆

𝐹𝑆𝐴

𝐹𝑆𝐴

  • PDA: hanno sempre 𝑆

𝐴𝑃𝐷

𝐴𝑃𝐹

Notazioni

  • Siano 𝑓, 𝑔: 𝑅 → 𝑅, si dice che f è un O-grande di g se f è definitivamente limitata superiormente da un multiplo di

g, cioè ∃𝑛

0

0

∶ 𝑓(𝑛) ≤ 𝑐 ∙ 𝑔(𝑛), ovvero: lim

𝑛→∞

𝑓(𝑛)

𝑔(𝑛)

𝑛𝑜𝑛 è 𝑖𝑛𝑓𝑖𝑛𝑖𝑡𝑜

La notazione O-grande indica il limite asintotico superiore della complessità, ovvero un valore sicuramente

più grande del reale valore della complessità della funzione all'infinito.

  • Siano 𝑓, 𝑔: 𝑅 → 𝑅, si dice che f è un Ω-grande di g se f è definitivamente limitata inferiormente da un multiplo di

g, cioè ∃𝑛

0

0

∶ 𝑓(𝑛) ≥ 𝑐 ∙ 𝑔(𝑛), ovvero: lim

𝑛→∞

𝑓(𝑛)

𝑔(𝑛)

è > 0

La notazione Omega-grande indica il limite asintotico inferiore della complessità, ovvero un valore

sicuramente più piccolo del reale valore della complessità della funzione all'infinito.

  • Siano 𝑓, 𝑔: 𝑅 → 𝑅, si dice che f è un Θ-grande di g se f è definitivamente limitata superiormente e inferiormente

da multipli di g, cioè ∃𝑛

0

0

∶ 𝑑 ∙ 𝑔(𝑛) ≤ 𝑓(𝑛) ≤ 𝑐 ∙ 𝑔(𝑛), ovvero: 0 < lim

𝑛→∞

𝑓(𝑛)

𝑔(𝑛)

La notazione Teta-grande indica il limite asintotico esatto della complessità.

Nozioni di base per l’analisi di complessità

  • Le istruzioni di base (somma, sottrazione, moltiplicazione, assegnazione) sono O(1).
  • Blocchi di codice hanno complessità che pari alla somma della complessità dei singoli blocchi\istruzioni.
  • Tutti i cicli hanno una complessità pari al prodotto della complessità del corpo del ciclo per il numero di

volte che esso viene eseguito (per indici incrementali da 0 a q la complessità è O(q), se l'indice viene

moltiplicato o diviso per un numero k ad ogni ciclo allora la complessità è log

𝑘

Teoremi di accelerazione lineare

  • Se L è accettato da una TM M a k nastri in 𝑆

𝑀

(𝑛), per ogni 𝑐 ∈ 𝑅

posso costruire una TM M' a k nastri che

accetta L con 𝑆

𝑀

𝑀

  • Se L è accettato da una TM M a k nastri in 𝑆

𝑀

(𝑛), posso costruire una TM M' a 1 nastro (non nastro singolo)

che accetta L con 𝑆

𝑀

𝑀

(𝑛) (concateno i contenuti dei k nastri su uno solo).

  • Se L è accettato da una TM M a k nastri in 𝑆

𝑀

(𝑛), per ogni 𝑐 ∈ 𝑅

posso costruire una TM M' a 1 nastro che

accetta L con 𝑆

𝑀

𝑀

  • Se L è accettato da una TM M a k nastri in 𝑇

𝑀

(𝑛), per ogni 𝑐 ∈ 𝑅

posso costruire una TM M' a k+1 nastri che

accetta L con 𝑇

𝑀

′ (𝑛) = max (𝑛 + 1 , 𝑐𝑇

𝑀

Macchina RAM

La macchina RAM è dotata di un nastro di lettura In, uno di scrittura Out, ed è dotata di una memoria con accesso a

indirizzamento diretto: l'accesso non necessita di scorrimento delle celle.

Criterio di costo logaritmico

Il criterio logaritmico viene introdotto per sopperire alle non idealità della macchina RAM: fare qualunque operazione

su un intero costa quanto il suo numero di cifre in base b: log 𝑏

𝑖 = Θ(log

), il costo delle operazioni aritmetico-logiche

elementari dipende dall'operazione (definiamo 𝑑 = log 2

  • Addizioni, sottrazioni, op. al bit: Θ(𝑑)
  • Moltiplicazioni: Θ

2

  • Divisioni: Θ(𝑑

2

Per i cicli bisogna considerare che se gli indici vengono incrementati di valori costanti allora la complessità del ciclo è

2

𝑛) (ovvero n il numero di giri e log(n) per tener conto dell'addizione), mentre se l'indice viene moltiplicato o

diviso per k allora la complessità diventa 𝑂(log 𝑘

𝑛 𝑙𝑜𝑔(𝑛)). Visto che per cambiare la base di un logaritmo è sufficiente

dividere il suo valore per una costante che dipende dalle due basi la complessità diventa 𝑂((𝑙𝑜𝑔(𝑛))

2

Tabella costi logaritmici

Rapporti tra criterio di costo

Ci sono casi in cui la macchina RAM è peggiore della macchina di Turing: un esempio è 𝐿 = {𝑤𝑐𝑤

𝑟

} in cui la macchina

RAM ha costo minimo Θ(𝑛𝑙𝑜𝑔(𝑛)) per colpa del contatore.

Teorema di correlazione polinomiale

Sotto ragionevoli ipotesi di criteri di costo, se un problema è risolvibile mediante il modello 𝑀 1

con complessità (spaziale

o temporale) 𝐶 1

(𝑛), allora è risolvibile da un qualsiasi altro modello (Turing completo) 𝑀

2

con complessità 𝐶

2

1

), dove 𝜋(. ) è un opportuno polinomio.

Correlazione tra TM a k nastri e RAM

  • La TM impiega al più Θ(𝑇

𝑅𝐴𝑀

(𝑛)) per simulare una mossa della RAM.

  • Se la RAM ha complessità 𝑇

𝑅𝐴𝑀

(𝑛) essa effettua al più 𝑇

𝑅𝐴𝑀

(𝑛) mosse.

  • La simulazione completa della RAM da parte della TM costa al più Θ((𝑇

𝑅𝐴𝑀

2

Note:

  • log

𝑛

𝑘= 1

𝑛

( 𝑛+ 1

)

2

𝑛 2

𝑘= 1

𝑛(𝑛+ 1 )( 2 𝑛+ 1 )

6

𝑛 3

𝑘= 1

𝑛

2

(𝑛+ 1 )

2

4

𝑛 𝑘

𝑘= 0

𝑥

𝑛+ 1

− 1

𝑥− 1

𝑘

𝑘= 0

1

1 −𝑥

𝑛 𝑘

𝑘= 0

𝑎−𝑎𝑟

𝑛+ 1

1 −𝑟

𝑛 𝑘

𝑘= 0

𝑛+ 1

Ridurre la complessità:

  • Per ridurre la complessità spaziale si possono usare i nastri di memoria come contatori codificati in binario.
  • Una soluzione efficiente per diminuire la complessità di algoritmi quadratici o superiori è ordinare dapprima il

vettore.

Ordinamento:

  • Il limite inferiore per l’ordinamento (𝜃(𝑛𝑙𝑜𝑔

) vale solo per gli algoritmi basati su scambi.

RAM:

  • In una singola cella della macchina RAM può essere memorizzato, a costo costante, un valore arbitrariamente grande

come 2

2

𝑛

, invece la sua memorizzazione a criterio di costo logaritmico costa il logaritmo del suo valore ( 2

𝑛

), mentre

a costo costante costa 1.

  • Quando la RAM deve memorizzare una sequenza di n elementi in celle consecutive impiega 𝜃(log

) per

memorizzare l’i-esimo elemento, e quindi 𝜃(𝑛𝑙𝑜𝑔

) per memorizzare l’intera sequenza.

Linguaggi:

  • I linguaggi regolari possono essere riconosciuti da una macchina RAM nello stesso tempo adottando il criterio di

costo logaritmico e di costo costante, infatti essendo essi riconoscibili mediante memoria finita, una macchina RAM

utilizzata per il loro riconoscimento non ha bisogno di una quantità di memoria dipendente dai dati, quindi qualsiasi

operazione, anche a criterio di costo logaritmico, richiede un tempo limitato a priori. Invece alcuni linguaggi non

regolari richiedono un tempo di riconoscimento che, valutato con costo logaritmico è necessariamente superiore alla

complessità valutata a criterio di costo costante.

  • Se è possibile riconoscere un linguaggio ricorsivo con complessità temporale 𝜃(𝑓

), allora è possibile riconoscere

anche il suo complemento con la stessa complessità. Infatti, una MT che riconosce un linguaggio ricorsivo L può

essere sempre trasformata in una MT che riconosce 𝐿

𝑐

semplicemente scambiando accettazione con non

accettazione all'ultima mossa che comporta l'arresto della MT. Ciò evidentemente non altera la complessità.

  • È falso il fatto che se è possibile riconoscere un linguaggio ricorsivamente enumerabile con complessità temporale

𝜃(𝑓(𝑛)), allora è possibile riconoscere anche il suo complemento con la stessa complessità. Infatti, il complemento

di un linguaggio ricorsivamente enumerabile potrebbe non essere neanche ricorsivamente enumerabile e quindi non

esistere una MT che lo accetti.

  • È sempre possibile riconoscere, mediante una MT a K nastri, 𝐿 1

2

con complessità 𝜃(𝑓

1

2

). Infatti, basta

costruire una MT che simuli prima M1 e poi M2 e accetti solo se accettano entrambe: la complessità è la somma

delle due complessità.

  • È sempre possibile riconoscere, mediante una MT a K nastri, 𝐿 1

2

con complessità 𝜃(𝑀𝑎𝑥{𝑓

1

2

}). Infatti, basta

costruire una MT che simuli M1 e M2 in parallelo.

  • È possibile riconoscere, mediante una MT a K nastri, 𝐿 1

2

con complessità 𝜃 < 𝜃(𝑓

1

2

). Infatti, l'intersezione

tra due linguaggi può essere linguaggio vuoto, riconoscibile in tempo costante.

  • I ragionamenti applicati rimangono validi indipendentemente dal modello di calcolo adottato (ad eccezione della

simulazione parallela da parte della macchina nostro singolo).

  • Il riconoscimento di un linguaggio non contestuale deterministico può essere effettuato da una MT a K nastri con

complessità temporale 𝜃(𝑛). Infatti, la MT a K nastri può simulare l'automa a pila deterministico senza alterare la

complessità ed è noto che ogni automa a pila deterministico ha complessità lineare.

  • Il riconoscimento di un linguaggio non contestuale deterministico non può essere effettuato da una MT a nastro

singolo con complessità temporale 𝜃(𝑛). Infatti, è stato dimostrato che il linguaggio 𝑤𝑐𝑤

𝑅

richiede una complessità

almeno 𝜃(𝑛

2

) per una MT a nastro singolo.

  • Il riconoscimento di un linguaggio non contestuale deterministico non può essere effettuato da una RAM con

complessità temporale 𝜃(𝑛). Infatti, è stato dimostrato che il linguaggio 𝑤𝑐𝑤

𝑅

richiede una complessità almeno

  • Il riconoscimento di un linguaggio regolare può essere effettuato da una MT a nastro singolo con complessità

temporale 𝜃(𝑛). Infatti, la MT a nastro singolo può emulare un automa a stati finiti senza alterarne la complessità.

  • Il riconoscimento di un linguaggio regolare può essere effettuato da una RAM con complessità temporale 𝜃(𝑛).

Infatti, la RAM può emulare un automa a stati finiti con una quantità di memoria finita e quindi in modo tale che

ogni singola mossa costi un valore costante anche a criterio di costo logaritmico.

  • Un linguaggio non contestuale può essere riconosciuto da una MT a K nastri con complessità spaziale

𝜃 < 𝜃(𝑙𝑜𝑔𝑛). Infatti, un linguaggio regolare può essere riconosciuto usando una quantità di memoria costante.

Altro:

  • È sempre possibile peggiorare a piacere le prestazioni di qualsiasi macchina: fissata quindi una macchina di una

categoria che risolve un certo problema, posso sempre costruirne una di un’altra categoria con prestazioni peggiori.

  • Esistono problemi per i quali la complessità temporale della soluzione ottenuta mediante un'opportuna MT è

comunque inferiore a quella ottenuta mediante una qualsiasi macchina RAM, infatti il problema della riscrittura in

ordine inverso di una sequenza di caratteri può essere risolto da una MT in tempo lineare. Siccome però esso richiede

la memorizzazione di tutti i dati prima di poterli scrivere, la memorizzazione dell’i-esimo dato richiederà ad una

RAM un tempo 𝜃(log

). Ciò comporta una complessità totale almeno 𝜃(𝑛𝑙𝑜𝑔

  • Esistono problemi per i quali la complessità temporale della soluzione ottenuta mediante un'opportuna macchina

RAM è comunque inferiore a quella ottenuta mediante una qualsiasi MT, infatti sappiamo che l'accesso, ad esempio

all’elemento mediano di una sequenza di n elementi richiede un tempo 𝜃(𝑙𝑜𝑔𝑛) mediante una RAM ma

necessariamente almeno 𝜃(𝑛) mediante una MT.

  • Data una qualsiasi funzione calcolabile f esiste sempre un algoritmo A che la calcola, codificato nel linguaggio della

RAM, la cui complessità, valutata a criterio di costo logaritmico sia <= di un opportuno polinomio applicato alla

complessità valutata a criterio di costo costante. Infatti, basta che A sia un algoritmo che simuli il comportamento

di una MT che calcola f.

  • Per qualche funzione calcolabile f esiste un algoritmo A che la calcola, codificato nel linguaggio della RAM, tale

per cui non esiste un algoritmo A1 ad esso equivalente la cui complessità, valutata a criterio di costo logaritmico sia

<= di un opportuno polinomio applicato alla complessità di A, valutata a criterio di costo costante. Infatti, è ben

noto che la funzione 2

2

𝑛

è calcolabile in tempo lineare a criterio di costo costante ma ha complessità spaziale, e

quindi temporale, almeno esponenziale a criterio logaritmico.

  • Alcune funzioni (ad esempio la gran parte di quelle calcolate da automi a stati finiti) hanno complessità lineare sia

a criterio di costo costante che a criterio di costo logaritmico. Non è quindi possibile trovare un algoritmo che a

criterio di costo costante abbia complessità minore che a criterio di costo logaritmico.

Nota: la coda funziona in modo "circolare", ovvero:

  • se Q.tail = Q.length e un nuovo elemento è inserito, il

prossimo valore di Q.tail sarà 1

  • se Q.head = Q.tail la coda è vuota
  • se Q.head = Q.tail + 1 la coda è piena

Liste doppiamente concatenate

Una lista doppiamente concatenata è fatta di oggetti con 3 attributi:

  • key, che rappresenta il contenuto dell'oggetto,
  • next, che è il puntatore all'oggetto seguente,
  • prev, che è il puntatore all'oggetto precedente.

Implementazione della lista doppiamente concatenata

Sia x un oggetto nella lista:

  • Se x.next = NIL, x non ha successore (è l'ultimo elemento della lista).
  • Se x.prev = NIL, x non ha predecessore (è il primo elemento, ovvero la "head").
  • Ogni lista L ha un attributo L.head, che è il puntatore al primo elemento della lista.

Operazioni su liste doppiamente concatenate

Dizionari

I dizionari sono insiemi dinamici che supportano solo le operazioni di INSERT, DELETE, SEARCH. Agli oggetti di un

dizionario si accede tramite le loro chiavi. Assumiamo che le chiavi siano numeri naturali, e che la cardinalità m

dell'insieme delle possibili chiavi U è ragionevolmente piccola, così da realizzare un dizionario tramite un array di

puntatori di m elementi: in questo modo si ha indirizzamento diretto , e l'array prende il nome di tabella a indirizzamento

diretto. Ogni elemento T[k] dell'array contiene il riferimento all'oggetto di chiave k , se un tale oggetto è stato inserito

in tabella, NIL altrimenti.

Operazioni sulle tabelle a indirizzamento diretto:

Tabella hash

Una tabella di hash è un caso speciale di dizionario, la quale usa una memoria proporzionale al numero di chiavi

effettivamente memorizzate al suo interno. Il vantaggio rispetto alle altre strutture dati è che in generale tutte le

operazioni sono O(1).

Idea fondamentale: un oggetto di chiave k è memorizzato in tabella in una cella di indice h(k) , dove h è una funzione

hash :

  • se m è la dimensione della tabella, h è una funzione ℎ: 𝑈 → { 0 , … , 𝑚 − 1 }
  • la tabella T ha m celle, T[0],...,T[m-1]
  • h(k) è il valore hash della chiave k

Problema delle collisioni: ho |U| possibili chiavi, ed una funzione che le deve mappare su un numero m < | U |, quindi

necessariamente avrò molte chiavi diverse tali che ℎ

1

2

). Per risolvere questi problemi si adottano diverse

strategie:

  • Open hashing (indirizzamento chiuso) : in questo caso ogni elemento della hashtable è una lista concatenata:

gli elementi vengono semplicemente aggiunti alla lista ogni volta che ne viene richiesto l'inserimento risolvendo

difatti il problema della collisione.

Definiamo fattore di carico 𝛼 =

𝑛

𝑚

dove n è il numero di elementi memorizzati ed m il numero di slot disponibili nella

hashtable. Supponendo che la probabilità di collidere sia 1/m allora la lunghezza media di una lista è 𝐿 = 𝑛

1

𝑚

Quindi il tempo medio per cercare una chiave k nella hashtable con indirizzamento chiuso è 𝑇(𝑛) = 𝜃( 1 + 𝛼).

Se n = O(m) , allora 𝛼 = 𝑂( 1 ), quindi in media ci mettiamo tempo costante.

  • Closed hashing (indirizzamento aperto) : in questo caso non si usa memoria aggiuntiva e la tabella contiene

tutte le chiavi. L'idea è quella di calcolare l'indice dello slot in cui va memorizzato l'oggetto; se lo slot è già

occupato, si cerca nella tabella uno slot libero. La ricerca non viene fatta in ordine: la sequenza di ricerca (detta

sequenza di ispezione ) è un valore calcolato dalla funzione hash.

 Ispezione lineare: ℎ

𝑚𝑜𝑑 𝑚 → si individua il primo slot libero dopo quello

individuato dalla funzione di hash. Soffre di problemi di addensamento in quanto vi sono molte celle

consecutive occupate che peggiorano le prestazioni in fase di ricerca.

 Ispezione quadratica: ℎ(𝑘, 𝑖) = (ℎ

1

2

2

) 𝑚𝑜𝑑 𝑚 → in questo caso si procede per tentativi

controllando gli slot, i è il numero di tentativi fatti e c 1

e c 2

scelte in modo che la sequenza di tentativi

copra (prima o poi) tutta la tabella (altrimenti si rischia il loop). Anche in questo caso chiavi con la stessa

posizione danno luogo alla stessa sequenza di ispezione.

 Hashing doppio: ℎ(𝑘, 𝑖) = (ℎ

1

2

2

altra funzione di hash e i è il numero di

tentativo di inserimento. In questo caso è bene che ℎ

2

(𝑘) sia sempre primo rispetto a m.

Operazioni in caso di indirizzamento aperto:

È possibile utilizzare un vettore per contenere le chiavi dell’albero (efficiente se l’albero è completo).

  • La radice dell’albero è stoccata in posizione 1 nel vettore.
  • Dato un nodo contenuto in posizione i, il suo figlio sinistro è in posizione 2i, e il suo figlio destro in posizione

2i+1.

  • Il padre del nodo, se esiste, si trova in posizione floor(i/2)

Note:

➢ Isomorfismo di alberi: dati due alberi T e T’ determinare se hanno la stessa struttura a meno del valore delle chiavi

e dei dati: faccio la visita degli alberi in parallelo, prima di continuare la visita controllo che ci sia il figlio per

entrambi gli alberi → O(n)

➢ In un albero binario pieno , se ho n nodi, il numero delle foglie è

𝑛+ 1

2

, mentre il numero di nodi interni è

𝑛− 1

2

➢ Algoritmo per contare i nodi di un albero: contatutti(A,r) → O(n)

Alberi binari di ricerca (BST)

Un albero binario di ricerca o BST è un albero binario in cui per ogni nodo x vale la proprietà che tutti i nodi del

sottoalbero sinistro siano minori o uguali di x e tutti quelli del sottoalbero destro siano maggiori.

~Un vettore più essere trasformato in un albero binario di ricerca tramite l'algoritmo di Heapsort che viene eseguito in

Algoritmi di visita e operazioni

È possibile attraversare un albero secondo diversi modi:

  • Attraversamento in ordine o simmetrico: si visita prima il sottoalbero sinistro, poi la radice e poi quello destro.

(Stampa le chiavi in ordine).

  • Attraversamento preordine: si visita prima la radice poi il sottoalbero sinistro e poi quello destro.
  • Attraversamento postordine: si visita prima il sottoalbero sinistro, poi quello destro e poi la radice.
  • Ricerca: confronta la chiave della radice con quella cercata se

sono uguali, l'elemento è quello cercato, se la chiave della

radice è più grande cerca nel sottoalbero sinistro, se la chiave

della radice è più piccola cerca nel sottoalbero destro. Il tempo

di esecuzione è O(h), con h l'altezza dell'albero.

  • L'elemento minimo (rispettivamente massimo ) in un BST è quello più a sinistra (risp. destra). Entrambi gli

algoritmi hanno tempo di esecuzione O(h).

  • Il successore (risp. predecessore ) di un oggetto

x in un BST è l'elemento y del BST tale che y.key è

la più piccola (risp. più grande) tra le chiavi che sono

più grandi (risp. piccole) di x.key. Quindi, se il

sottoalbero destro di un oggetto x dell'albero non è

vuoto, il successore di x è l'elemento più piccolo

(cioè il minimo ) del sottoalbero destro di x. Se il

sottoalbero destro di un oggetto x dell'albero è vuoto,

il successore di x è il progenitore più prossimo a x

per cui x appare nel suo sottoalbero sinistro.

Il tempo di esecuzione è O(h).

  • Per l'inserimento l'approccio è il seguente: scendere nell'albero fino a che non si

raggiunge il posto in cui il nuovo elemento deve essere inserito, ed aggiunge questo

come foglia. Il tempo di esecuzione è O(h).

  • Per la cancellazione abbiamo 3 possibili casi che dipendono dal nodo z

da cancellare:

 se z non ha sottoalberi: si mette a NIL il puntatore del padre di z

che puntava a z

 se z ha 1 sottoalbero: bisogna spostare l'intero sottoalbero di z in

su di un livello

 se z ha 2 sottoalberi: bisogna trovare il successore del nodo z da

cancellare, copiarne la chiave in z , quindi cancellare il successore.

Il tempo di esecuzione per tree-delete è O(h). [y: nodo da eliminare,

x: quello con cui lo sostituiamo]

Altezza di un albero (n è il numero di nodi):

  • h = Θ(log(n)) se l'albero è completo: un albero è completo se per ogni nodo x, o x ha due figli, o x è una foglia.
  • h = Θ(log(n)) se l'albero è bilanciato: un albero è bilanciato se e solo se, per ogni nodo x dell'albero, le altezze

dei 2 sottoalberi di x differiscono al massimo di 1.

  • h = Θ(n) nel caso pessimo, ovvero nodi tutti "in linea".
  • l'altezza attesa è h = O(log(n)) se le chiavi sono inserite in modo casuale con distribuzione uniforme.
  • Ogni albero binario di altezza h ha un numero di foglie di al più 2

h

Note:

➢ Costruire un BST bilanciato con chiavi in un array A di lunghezza n.

  1. Ordino A → 𝑂(𝑛𝑙𝑜𝑔(𝑛))
  2. Inizializzo un BST vuoto.
  3. Inserisco gli elementi in questo ordine: prima l’elemento centrale (che diventerà la radice), a sinistra l’elemento

centrale del sottoarray di sinistra, a destra l’elemento centrale del sottoarray di destra, e itero questo

procedimento → 𝑂(𝑛𝑙𝑜𝑔

La routine per l'inserimento dei nodi è la seguente:

Fondamentalmente la routine si comporta come un inserimento

normale in un BST solo che il nodo appena inserito è sempre rosso

(per evitare di cambiare l'altezza nera) e i suoi figli puntano a

T.NIL (e non a NULL). Alla fine della procedura, tuttavia, è

possibile che siano state violate delle proprietà degli alberi. Per

aggiustare tutte le possibili violazioni si richiama la routine RB-

INSERT-FIXUP.

I casi di violazione a cui ci si riferisce nel codice sono i seguenti:

  • Caso 1 : lo ‘zio’ y rosso, chiamo RB-INSERT-FIXUP(x.p): x.p

potrebbe avere un padre rosso, se x.p è la radice posso colorarla

di nero.

  • Caso 2 : lo ‘zio’ y è nero e z=x.right: effettuiamo

LeftRotate(T,x), ora la riparazione va eseguita su z, con

z.left=x rosso (Caso 3).

  • Caso 3 : lo ‘zio’ y è nero e z=x.left: scambiamo i colori di x e x.p

ed eseguiamo RightRotate(T,p.x).

  • Caso 3: x è nero, suo fratello destro w è nero con figlio sinistro

rosso e figlio destro nero. Ricade nel caso 4.

  • Caso 4: x è nero, suo fratello destro w è nero con figlio destro

rosso.

La routine per la cancellazione è la seguente:

Come per la INSERT esiste una routine in grado di aggiustare l'albero

qualora qualche proprietà sia stata violata:

I casi riportati si riferiscono ai seguenti:

  • Caso 0: x è un nodo rosso o la radice: viene colorato di nero.
  • Caso 1: x è un nodo nero, il suo fratello destro w è rosso e il padre è nero (può ricadere nel caso 2, 3 o 4).
  • Caso 2: x è nero, suo fratello destro w è nero ed entrambi i loro figli sono neri. Se arriviamo al caso 2 a partire

dall'1 allora il genitore di x è rosso.

  • Caso 3: x è nero, suo fratello destro w è nero con figlio sinistro rosso e figlio destro nero. Ricade nel caso 4.
  • Caso 4: x è nero, suo fratello destro w è nero con figlio destro rosso.

Note:

➢ Operazioni di ‘fixaggio’ su alberi rosso neri senza puntatori al padre:

  • Inserimento: problema che in genere si causa (con il puntatore al padre)→inserisco un nodo rosso e poi faccio

in modo di sistemare la colorazione

→ prima di inserire, mi accerto con modifiche locali, che sto scendendo lungo una direzione nera, non devo

cambiare le black height.

  • Caso 1: padre nero con entrambi i figli rossi → coloro il padre di rosso ed i figli di nero (posso sempre farlo

assumendo che il padre sia nero, perché io parto dalla radice che è nera e per induzione non posso generare

violazioni nuove). Quindi se il fratello del nodo verso cui voglio dirigermi è rosso, lo coloro di nero ed i figli di

rosso.

  • Caso 2: padre nero con un figlio nero e l’altro rosso (quello lungo cui voglio scendere)→faccio una rotazione

del nodo rosso verso sopra, successivamente lo coloro di nero ed il figlio di rosso: adesso il nodo della direzione

in cui volevo andare è diventato nero e continuo la ricerca su di lui. Quindi se il fratello del nodo verso cui

voglio dirigermi è nero, ruoto il nodo su cui voglio dirigermi e scambio il colore con il fratello.

➔ Nell’inserimento, prima di richiamare la insert al nodo successivo, se questo è rosso, applicare una delle due

soluzioni risolutive.

Heap

Un heap è una struttura dati ad albero, la chiave del nodo padre è sempre maggiore (max-heap) di quella dei figli, e non

vi è nessuna relazione tra le chiavi di due fratelli. Un heap binario è un albero binario quasi completo, ovvero che ha

tutti i livelli occupati tranne al più l'ultimo che può essere anche incompleto fino ad un certo punto (partendo da sinistra).

È conveniente materializzare lo heap come una struttura dati implicita:

  • È un albero quasi completo→le foglie mancanti sono quelle che occupano la parte finale dell’array in cui è

stoccato.

  • A.heapsize: indica il numero di elementi dello heap.
  • A.lenght: indica la lunghezza dell’array di supporto.
  • In un max heap l’elemento con chiave più grande è la radice.

Code con priorità

Una coda con priorità è una struttura dati a coda in cui è possibile dare una priorità numerica agli elementi all’interno.

Elementi con priorità maggiore verranno estratti sempre prima di elementi con priorità minore indipendentemente

dall’ordine di inserimento. L’implementazione più comune di una coda con priorità è un max-heap: la priorità di un

elemento è data dalla sua chiave.

Max-Heapify

Max-Heapify riceve un array e una posizione in esso: assume che i due

sottoalberi con radice stoccata in Left(i)=2i e Right(i)=2i+1 siano dei max

heap, e modifica A in modo che l’albero radicato in i sia un max-heap.

La procedura causa la discesa del nuovo valore verso le foglie sino al punto

in cui è maggiore dei figli.

La complessità temporale di maxheapify è proporzionale all'altezza

dell'albero T(n) = O(log(n)).

Build Max-Heap

A questo punto dato un array da ordinare lo trasformiamo in un

max-heap mediante la seguente (si parte dalla metà dell'array perché

la parte destra contiene tutti i nodi foglia che sono già max-heap per

definizione). La complessità temporale è 𝑇(𝑛) = 𝑂(𝑛𝑙𝑜𝑔(𝑛)), da

una più profonda analisi otteniamo che è in realtà 𝑂(𝑛).