Ragazzi, iniziamo. Ah, scusate, oggi
siamo stan cerco di tenermi un po'
energico.
Ok.
Ok. Oggi iniziamo
un topic completamente diverso.
Ah! Ah! Oggi iniziamo
complessità strutturale.
Io lo trovo affascinante, ma la mia
valutazione non conta perché è la mia
area di ricerca e
molti dei miei colleghi lo trovano
veramente orrendo. Posso capirlo,
a me diverte. Ok, quindi complessità
strutturale, cos'è la complessità
strutturale dei problemi? Ok, prima
cosa, come sono andate
le lezioni finora? Si capiva? Ok,
sicuramente non avrete tutto chiaro o
tutto disponibile al momento a richiamo,
però vabbò è abbastanza naturale. Cose
nuove, un po' troppi teoretici un
attresso all'altro, insomma, that's al
ok, come siete messi con la nozione di
riduzione oggi? Non ne vediamo, però
dalla prossima volta.
Riduzioni. Riduzioni, ci siete? Ok,
perché riduzioni ci servono, per seò
complessità strutturale senza riduzioni
è praticamente impossibile da fare. La
nozione è identica. Aggiungeremo un
pezzettino in più alla nozione di
riduzione e il resto si si usa nello
stessissimo modo.
Ok? Quindi se finora
facciamo il nostro solito disegnino,
questo qua, qui c'abbiamo R,
qua c'abbiamo R, poi abbiamo visto
questa cosa un po' più sofisticata della
classe C.
Qui non la non la depict, non la
dipingiamo, non la disegniamo. Ok,
allora finora ci siamo. Quello che di
qui ci siamo occupati finora
sostanzialmente è dati dei linguaggi o
dei problemi di decisione, stabilire se
quel linguaggio stava qua, se stava qua,
se stava qua fuori. L'ultima volta
abbiamo visto se sta in co re, quindi se
sono linguaggi complementari di R o se
stanno fuori re e co re, quindi sono
ancora più difficili. Ok, lì abbiamo una
pletora di problemi difficili, c'è una
scala di problemi sempre più difficili.
Non non li guarderemo, a noi non
interessa, ok? Eh, e sono argomenti
sofisticati che riguardano la logica
matematica. Se vi interessa, quello è il
tipo di area che dovreste andare a a
spulciare, che si chiama teoria della
ricorsione. Ok? E lì si studiano questi
problemi molto sofisticati,
però da un punto di vista un po' più
pratico, sebbene affascinante, la teoria
è sempre affascinante, però da un punto
di vista pratico, sapere se un problema
è indecidibile o doppiamente
indecidibile, insomma di un po'
non così utile, però se quello è ci
stanno, ci sta tutta una
categorizzazione di problemi
infinitamente complicati che noi non
guarderemo, sono tutti fuori il re, for
re, eccetera. Ok? Cioè si costruisce
proprio a strati. Il problema che
abbiamo visto l'altra volta, Halt per
ogni sta in una classe a parte sua, il
suo complemento sta in un'altra classe a
parte. Allora poi uno si chiede ma c'è
qualcosa che è al di fuori di quello?
Sì, c sta e sono problemi ancora più
difficili, però non non li andiamo a
vedere. Quello di cui ci andremo a
occupare da ora per il resto della
del corso è sostanzialmente
questo insieme qua. Ok? Noi ci andiamo
ora a occupare dei problemi ricorsivi,
dei problemi decidibili, ok? Perché
abbiamo visto a sufficienza problemi
indecidibili, abbiamo visto che ce ne
stanno tanti, che sono belli tosti. Ok?
C'è tutta un'area della ricerca
dell'informatica teorica che invece si
focalizza
eh all'interno della classe R, ok? Cioè
stabilire
la complessità dei problemi dentro R.
Allora, la questione è meramente di tipo
storico perché nel momento in cui tutta
questa teoria nacque, negli anni 30
conturing, la teoria delle funzioni
ricorsive in realtà predata, quello,
però insomma le persone erano in quel
momento abbastanza
interessate dal capire se un problema
fosse decidibile o meno. La questione è
che a quel momento era in quel momento
storico era tutto abbastanza astratto.
Non ci stavano macchine reali di
calcolo, a parte
che ne so, la differenzial engine o
l'analytical engine di Babac, ma la
differenzial engine, per esempio, non
era mai stata costruita. Un prototipo
funzionante della differenzial engine è
stata fatta di recente e si è visto che
quella macchina avrebbe funzionato se
qualcuno l'avesse costruito. Però
macchine di calcolo sofisticate ci
stavano negli anni 30 che ci stava?
gestavano le macchine per il censimento
automatico.
Quelle erano macchine, però non è che
sapevano far di conto, leggevano le
schede perforate per fare le statistiche
in maniera automatica, facevano dei
conteggi, ma non erano programmabili. Le
macchine programmabili erano macchine
che stavano nella testa dei matematici,
ok? Quindi non c'era nulla a
disposizione e quello di cui ci
chiedevano è ma questa macchina semmai
fosse in grado di costruirla che genere
di problemi sarebbe in grado di
risolvere? E di fatto i primi i primi
risultati di indecidibilità, no? Il
problema dell'arresto che data 1935-36,
se ricordo bene la parte di Turing
riguardava una macchina astratta, cioè
non è che questa macchina esisteva, però
si sapeva già che sem mai fossimo stati
in grado di costruirla ci sarebbero
stati problemi che quella macchina non
sarebbe stata in grado di risolvere. Ok?
Quindi la gente più che altro si buttò
su questa cosa, cosa è decidibile, cosa
non cosa non è decidibile,
tipo il teorema di Ris degli anni 50,
quindi erano quel genere di cose che
venivano
venivano studiate. però con l'avanzare
della tecnologia, quindi anni 40,
seconda guerra mondiale, la
disponibilità di calcolatori elettronici
per il calcolo dei tracciati balistici
eccetera, inizia iniziammo come genere
umano ad avere i primi computer, i primi
computer programmabili, i primi computer
elettronici a valvole eccetera, tipo
ENIAC, EDAC, queste cose qua, tale per
cui iniziamo ad avere i primi programmi,
ok?
E mentre il
la gente si scontrava con cosa si può
calcolare o cosa non si può calcolare,
nel momento in cui avevamo delle
macchine fisiche, iniziò a diventare
evidente che certi problemi li
risolvevamo in poco tempo e altri
problemi li risolvevamo in molto più
tempo. Alcuni problemi richiedevano una
grande quantità di memoria, altri
problemi ne richiedevano molto meno. Ok?
Quindi lo studio della complessità
strutturale dei problemi che adesso
definiremo in maniera un po' più preciso
nasce da una questione meramente
tecnologica, cioè in quel momento
avevamo a disposizione delle macchine
reali, la gente si è iniziata a chiedere
"Ma questo problema in quanto tempo lo
vedrò risolto?" Questa domanda nasce dal
fatto che la macchina esisteva e quindi
la gente si trovava lì ad aspettare la
risposta. quando avevamo quando non
c'erano le macchine, allora non era una
questione che veniva presa in
considerazione. Le persone si
chiedevano: "Ok, è in grado di farlo?
Non è in grado di farlo". Dopodiché,
quando iniziarono veramente a
programmare i primi computer, mi si
resero conto che c'erano problemi molto
più difficili di altri, cioè dei
problemi che erano che richiedevano con
gli algoritmi di cui disponevano molto
più tempo di tanti altri algoritmi o di
altri problemi. Ok? A quel punto la
questione che nacque è: "Ma l'algoritmo
che abbiamo per risolvere questo
problema è lento? perché siamo noi che
non siamo furbi a sufficienza da tirarne
fuori uno veloce o la o il problema è
talmente complicato che noi non
riusciremo mai ad avere un algoritmo
veloce. Ok? E quindi questa cosa venne
formalizzata
dopo un bel po' di tempo. I seminal
paper in cui definirono la complessità
computazionale delle macchine di touring
sono del 1965.
Sono tre lavori che hanno dato avvio
allo studio della complessità
strutturale. Gli autori erano Hartmanis,
Stern e Lewis che scrissero tre lavori
apparsi tutti nel 1965
in cui descrivevano questa nozione, se
avevano inventato questa nozione di
complessità delle macchine, ok? Cioè,
prima di allora questa idea non ci
stava, magari le persone se n'erano pure
accorte che serviva questo genere di
nozione, ma fino a quella data non
abbiamo una descrizione formale di
questo concetto. Ok?
Che cosa introdussero? Sostanzialmente
loro introdussero il concetto di
complessità temporale e complessità
spaziale delle macchine di Turing che
adesso definiremo in maniera un po' più
precisa. semplicemente intuitivamente
con questa nozione.
La complessità temporale di una macchina
di Touring è legata al numero di passi
che la macchina fa prima di arrestarsi e
dare la risposta. Ok? Questo era il
senso sostanzialmente
perché notarono, appunto, infatti se
leggete l'introduzione di quel lavoro
dice sì, notiamo che sui computer reali
alcuni algoritmi girano più veloci,
altri algoritmi girano più lenti.
la necessità di sviluppare una teoria
che tenga conto di questo, cioè fra
tutti i problemi decidibili, quali sono
quelli facili, quali sono quelli
difficili e proposero quindi l'idea di
caratterizzare la complessità
temporale delle macchine di touring come
contando il numero di steps. Ok? E
questa cosa era abbastanza facile da
definire perché la macchina in touring
noi abbiamo i passaggi, i singoli
passaggi, uno step, due step, tre step.
Quindi quella cosa è molto facile da
contare rispetto alla complessità
computazionale di algoritmi che rannano
su macchine reali e a quel punto è un
po' difficile contare il numero di
passaggi, ma su macchine di Touring è
semplicissimo. Contiamo il numero di
transizioni prima che la macchina si
arresti in maniera similare, poi lo
introdurremo però sì quando faremo
complessità spaziale, oggi introduciamo
solo complessità temporale, sempre in
quegli anni introdussero, ve lo do
intuitivamente, il concetto di
complessità spaziale, nel senso ma
quante celle la macchina deve leggere,
no, o scrivere prima di dare la
risposta, perché la complessità spaziale
è una cosa sono un po' diverse perché io
le celle le posso sovrascrivere, quindi
magari sovrascrivendolo in maniera
opportuna posso mantenere il numero di
celle usate più piccolo e quello andava
in qualche modo a to mimic a mimare, no,
la quantità di memoria che era
necessaria in un computer per poter
risolvere un certo problema. Ok? Quindi
il tutto nasce da una questione
meramente tecnologica. Nel momento in
cui avendo a disposizione macchine
reali, le persone si iniziarono a
chiedere "Ma com'è possibile che alcuni
algoritmi vanno veloci e altri vanno
lenti?" E allora iniziò tutto questo
studio. Ok?
Questo studio si chiama complessità
strutturale
perché ci vuole occupare della della
complessità della struttura dei
problemi, cioè com'è che la struttura di
un problema influenza
o meno la complessità degli algoritmi
che lo risolvono. Voi avete visto nei
corsi di algoritmi che gli algoritmi
sono caratterizzati da una certa
complessità. Quello che noi andremo a
fare nelle lezioni a venire non è
studiare la complessità degli algoritmi
che comunque ci serve perché la
complessità degli algoritmi viene usata
come base per lo studio della
complessità strutturale. Oggetto della
complessità strutturale è ma qual è la
complessità del problema? Questa è la
differenza. Ok? Non ci interessiamo più
del singolo algoritmo che risolve un
certo problema. Noi ci
interessiamo, vogliamo capire ma la
struttura di questo problema come
impatta la complessità degli algoritmi
che lo risolvono? Questo problema è
tosto di suo tale per cui tutti gli
algoritmi che lo risolvono prenderanno
sempre esponenziale. Questa è la
domanda. Questa è la domanda che ci
porremo da qui al termine del corso. Ok?
studiare sostanzialmente da un problema,
stabilire la famiglia
di problemi dalle quali di complessità
simile dalle quali questo problema viene
preso. Ok?
Quindi questo è un po' un po'
l'introduzione, non è un argomento che
in genere insomma la gente ci fa ricerca
perché è abbastanza abbastanza teorico,
però studiare la complessità strutturale
dei problemi, ad esempio, ci permette di
stabilire dove si annida la complessità
del problema, cioè quale pezzo di questo
problema lo rende difficile. E se io
impongo dei constraint semplificatori su
quella parte là, il problema diventa
polinomiale. Quindi questo poi ci
permette, per esempio, di sviluppare
algoritmi euristici dove si possono fare
delle assunzioni, eccetera. Ok? Quindi
lo studio della complessità strutturale
è uno studio che ci permette di capire
dove si annidi la difficoltà dei
problemi, ok? e questo ci dà
informazioni sullo sviluppo di algoritmi
per la loro risoluzione.
Alrght? Allora, noi sostanzialmente
quindi ci focalizzeremo
su questo oggetto qui l'insieme dei
problemi decidibili. Quindi da ora in
poi tutti gli algoritmi di cui di cui
parleremo sono algoritmi che decidono
linguaggio. Quindi avremo sempre
garanzia di risposta sì, avremo sempre
garanzia di risposta no. Ok? Quindi sono
algoritmi standard, quelli che avete
sempre visto in tutti i corsi, cioè
algoritmi veri e propri, una sequenza
finita di passi che quindi finisce,
termina sempre. Ok? Allora, l'obiettivo
è sostanzialmente
iniziare ad arricchire la struttura di
questo insieme andando a identificare
all'interno di R classi di linguaggi di
difficoltà differenti. troveremo classi
di problemi facili, classi di problemi
difficili o equivalentemente classi di
linguaggi facili e classi di linguaggi
difficili perché non sospendiamo la
l'assunzione che abbiamo fatto, cioè che
studiamo i problemi tramite i linguaggi.
Quello si fa sempre, quindi avremo
classi di linguaggi semplici e classi di
linguaggi
difficili. Ok? Allora, possiamo
introdurre quindi i primi concetti che
riguardano la complessità computazionale
delle macchine di Touring, degli
algoritmi delle macchine di Touring.
Badate che in quello che diremo oggi,
oggi parleremo della complessità
computazionale delle macchine, però noi
sappiamo che queste macchine ormai sono
algoritmi se ci focalizziamo all'interno
delle classe dei linguaggi decidibili
perché sono algoritmi che terminano. Ok?
Quindi, quando dirò la complessità di di
una macchina o la complessità di un
algoritmo, starò sostanzialmente usando
dei sinomi di Ok? Alri. Yes. Questo
grazie al teorema di Bon. Eh, non lo so.
Non non conosco il risultato. Il teorema
di chi? Iini e Bon mi manca. Ok, poi me
lo segna, vado a verificare.
Allora, ehm,
linguaggi, dobbiamo definire la
complessità temporale temporale dei
delle macchine di touring o degli
algoritmi. Ok, prima nozione,
computation time, tempo di esecuzione.
Io lo chiamerei computation time,
sia
m una macchina
di Turing
è W,
una stringa
input
per M.
Il computation time o il tempo di
esecuzione
di m su w è il numero di passi
che
M
esegue
prima
di arrestarsi
su W. Ok? Quindi una definizione molto
semplice.
Come la definiamo invece se la macchina
M è non deterministica?
Dobbiamo leggermente adattare la
definizione. Sì. E possiamo fare il
numero di, cioè noi abbiamo l'albero di
computazione qui, possiamo fare il
numero di noi, di m più o meno la
definiamo così. Prendiamo il computation
time e la lunghezza del branch, così è
finito, no?
Sì, è la lunghezza della più lunga
computazione della macchina su W. Ok?
Quindi, se c'ha rami più lunghi di
altri, noi andiamo a prendere il più
lungo. Quindi
se la macchina
M è non deterministica,
il computation time
è dato
dalla
lunghezza
del branch più lungo,
del branch di computazione più lungo,
più lungo.
Ok, quindi questo per noi è il
computation time di una macchina di
touring. Prendiamo e contiamo i passi.
Molto semplice. Ok.
Come? Scusi, il computational time è
dato è data dalla lunghezza del branch
di computazione più lungo nel momento in
cui siamo su una macchina non
deterministica. Ok?
Alrght.
sia t n
una funzione
che va da interi a interi
tale che
t n
decreasing
è strettamente positiva.
Ok? Quindi queste sono due condizioni
importanti. TDN è una funzione
che va da che mappa interi su interi. È
una funzione non decrescente e
strettamente positiva. Una funzione si
fatta la chiamiamo time function,
funzione temporale, non lo so in
italiano. Time function, in genere viene
chiamata time function. Quindi cos'è una
time function? Una time function è
semplicemente una funzione da interi
versi interi che è strettamente positiva
ed è non decrescente. Ok? La potevamo
chiamare Giovanni, l'hanno chiamata time
function. Ok?
Allora, noi abbiamo il concetto di
running time,
running time di una macchina di Touring.
Ok? Qui quindi il la prima nozione era
il computation time di una macchina di
touring e ora abbiamo una cosa diversa
che è il running time di una macchina di
Turing. Ok?
Oplà. Sia tdn
una time
function.
La macchina
di Turing
M a running time. Questo è il passaggio
che ci serve. Running time
TDN.
Se
per tutte
le stringhe
W
a parte
un numero finito,
questa è una finezza, un numero finito,
il computation time
di m su W non è non eccede
t valutata sulla lunghezza della string
input. Ok?
Ripeto, quindi noi abbiamo la due
nozioni, computation time di una
macchina di tunering, running time di
una macchina di Il computation time di
una macchina di touring è proprio il
numero di passi che la macchina fa su
una certa stringa. Quindi noi parliamo
del computation time di una macchina di
Touring su una certa stringa. Ok? Lì
proprio andiamo a contar. Nel caso in
cui la macchina diuring è non
deterministica, il computation timer
della macchina
di Turing è dato dalla lunghezza del
branch di computazione più lungo. Ok?
Quindi questo è computation time da un
da un lato. Sulla nozione di computation
time costruiamo
la nozione di running time di una
macchina, ok? Che è più vicino alla
nozione di complessità di algoritmo che
conoscete, ok? Perché voi conoscete la
nozione di complessità di algoritmo che
è data da una certa funzione, ok?
Quindi, computation time è quanti passi
la macchina di Touring fa prima di
fermarsi su una certa stringa. Running
time invece è una stima tramite una
funzione del tempo che la macchina di
Touring ci mette a calcolare. Ok? Comeè
definito il running time di una macchina
di touring? Prima ci serve il concetto
di time function. Time function è un
nome che diamo a delle funzioni che
mappano interi su interi e sono non
decrescenti e strettamente positive.
Quindi la time function è una funzione
fatta così. Noi diciamo che una macchina
di touring m
ha running time tdn con tdn una time
function. Se per ogni stringa che noi
diamo in input alla nostra macchina M,
il computation time di M su W è bound da
cosa? Da t valutato sulla lunghezza
della stringa input, ok? E questo deve
valere su tutte le stringhe in input, a
parte un numero finito di S. Ok? Questa
è proprio la definizione più pulita che
si trova sul testo di Conselen, mi pare,
ok? Non la troverete su altri testi,
però Ruffle è il running time è un bound
funzionale su quanto tempo macchina
gira, quello è. Ok,
scusate prima scusi che c'è il sole
dietro non la vedo. Ok. sia funzione
tale che è un percing
è eh strettamente positiva. Sì, è
scritto un po' a caso
perché perché vogliamo che il time
function perché viene definito così,
così viene definito proprio sui sul
testo sul paper di hardis. Una time
function viene caratterizzata in questo
modo perché uno ci aspettiamo che la
macchina impieghi del tempo superiore a
zero a rispondere, ok? e due perché più
grosso è l'input più ci aspettiamo che
la macchina richieda tempo. Ecco perché
le time function sono definite in questo
modo. Ok? Eh, alright.
Questo
chiara questa definizione? Quindi
computation time da un lato che è una
cosa molto grezza, cioè molto raw,
direttamente agganciata al numero di
passi che la macchina fa prima di
arrestarsi su una certa stringa. Poi c'è
il running time che è come una
descrizione più ad alto livello che ci
dice "Guarda, questa macchina si arresta
in n²". Ok? Quindi se gli dai una
stringa di taglia 10 si ferma in alpi
100 passi. Se gli dai una stringa di
taglia 100 si ferma in al più 10.000
passi. Ok? Quindi running time è una
cosa che lega il tempo di esecuzione
della macchina a una funzione, ok? Che è
quello che in genere immagine avrete
visto sul corso di algoritmi, cioè
questo algoritmo è big o di n cubo per
dire. Ok? Quindi il running time è
quello che avete sempre visto, solo che
ora l'abbiamo definito sulla nozione di
macchina di macchina di tuning. Ok?
Facciamo un piccolo recap di altre
nozioni, giusto per essere sicuri che
abbiamo tutti in testa la stessa
definizione,
che sono la notazione asintotica
e anche per i nostri colleghi matematici
che non so se l'hanno vista. Notazione
asintotica.
Asintotica.
Oggi scrivo veramente
Ok.
Alright. Big O. Big O. Ok.
Allora, noi diciamo che una funzione fn
è
big o di gdn per un'altra funzione gdn
se
esistono
due costanti c e ed n0
tali che per ogni n maggiore o uguale di
n0
fn è bound dall'alto da c gn.
Ok? Questa è la definizione di
bigotagion. Facciamo una velocissima
rappresentazione.
Ok.
Alri, questo è,
diciamo, fn.
Questo qui è c gn.
Allora, che cosa ci dice questa
definizione? Che la funzione f(n)
è bounded.
Ci vedete?
Abbasso un pochino. Un secondo
sto giro. Come mai?
Ah!
era sceso. Ok?
Allora, una funzione f(n) è bounded
dalla funzione è bounded da sopra dalla
funzione g di n se esiste un certo punto
n0
dopo il quale la funzione c * g n
è maggiore di fn. Ok? Quindi prima di n0
può succedere quello che vogliamo. Dopo
n0
f n deve essere bounded dall'alto dalla
funzione g n moltiplicata per una certa
costante. Ok?
Questa è la definizione di big o dn
è scriveremo in questo modo è big o dn.
Altra notazione che potremo utilizzare
che fn appartiene a big o di gdn.
Intenderemo la stessa cosa. Quando
scriverò fn appartiene a big o di gn sto
intendendo che fn è big o di gdn, ok?
Che sui libri si trova entrambi o con il
verbo essere o con il simbolo di
appartenza.
FNO di Gn. Sì, perché f è limitato da C
n molpicato. Sì, sì, sì, sì, sì, sì.
Significa intuitivamente
che l'ordine di grandezza della crescita
di fn è bounded da gdn, cioè fn non
cresce più velocemente di gn. Questa è
la è l'intuizione. Questa anche uguale
come può essere anche uguale. Può essere
anche uguale, per esempio,
sicuro vi ricorderete che n qu
di n³, ok? Poi quanto noi vogliamo
vicino il bound alla funzione f(n),
quello dipende da dal tipo di analisi
che stiamo facendo, no? Quindi
l'intuizione Big O semplicemente ci sta
dicendo a finale che il tasso di
crescita di fn
superiore al tasso di crescita di GDN,
ok? Significa che è limitato dall'alto
da gdn. Poi questo limite può essere
l'asco o può essere più stringente. Ok?
Quindi questo era Big O. Sì, certo.
Altra nozione big omega.
Quanto spazio ho? Ok? Fn
è big omega di gn
oppure appartiene a big omega di gdn
se esistono una costante c e un numero
n0
tale che per ogni n mag uguale di n0 f n
è maggiore o uguale di c * gn, quindi
esattamente l'opposto.
Ok, facciamo un disegnino.
Allora,
quindi abbiamo n
fn e questo è
c * gn. Ok? E quindi è esattamente il
contrario. Noi diciamo che f
è big omega
di gn se il tasso di crescita di fn
inferiore al tasso di crescita di gn e
quindi gn fa limita la funzione fn dal
basso. Ok? Quindi è un lower bound,
mentre big o è un upper bound, big omega
è un lower bound. Ok?
Anche in questo caso il bounding può
essere stretto, può essere stringente o
lasco. Ok? Quindi noi abbiamo per
esempio che n qua
di n log n²
e big omega di n, per esempio. Ok? Cioè
queste sono funzioni che si sganciano
sempre di più. Perché siamo interessati
a questo? Perché nel momento in cui
andremo ad analizzare il tempo di
esecuzione
delle nostre macchine di Touring, se ci
mettono 3* n qu passi o ce ne mettono 2*
n qu a noi non importa perché a noi
quello che importa è il tasso di
crescita. ci saranno dei teoremi che
forse vi menziono ma sicuramente non vi
dimostro in cui sostanzialmente si fa
vedere che non ha senso andare a
guardare le costanti perché data una
macchina ce n'è sempre una più veloce
che ci permette di ridurre in maniera
lineare il tempo di esecuzione. Quindi
se una macchina ranna in 6 n² quel
teorema ci garantisce che ce n'è una
stessa macchina che ranna in n². Il
trucco è che io posso prendere tanti
simboli,
condensarli in simboli,
in una varietà maggiore di simboli.
Quindi quando leggo una cosa, in realtà
è come se stessi leggendo, che ne so, 8
bit a un colpo, perché c'ho, che ne so,
64 simboli diversi sul nastro e quindi
ogni volta che ne leggo uno è come se
stessi leggendo otto di fila e quindi
posso accelerare il tempo di esecuzione.
Ok? Si chiama linear speedup, che io
rendo questa cosa qua. Ok.
Biga, big teta ve lo dico. Ok.
Big teta.
Fn
è big teta di gn.
Se
fn
è sia big omega di gn
e
bigo di gdn e big omega di gdn.
Alri. Si legge bene come si legge bene.
Ah, ok. Ve la ridico a voce perché sulla
sul poi sul PDF prende una funzione fn
è big teta di gn
se fn è sia
big o di gn che omega di gn.
Quindi vuol dire che GDN non solo è un
upper bound della funzione f(n), ma è
anche un lower bound, il che significa
che fn ha un tasso di crescita che è
equivalente a gdn. Ok? Quindi
aggiungiamo questa nozione qua. Perché
introduciamo questo? Perché tramite
questa nozione noi introduciamo il
concetto di complessità di problemi. Ok?
Time
complexity
upper bound
di un problema
P. Ok. Cos'è il time complexity
upperbound di un problema qui?
Il time complexity upper bound di un P
è big O di f n dove fn è una time
function.
Se tutti
gli algoritmi o tutte le macchine di
Turing che risolvono P
hanno un tempo di esecuzione, hanno un
running time che è big o di fn. Ok?
Quindi la complessità di un problema
viene caratterizzata tramite la
complessità degli algoritmi che lo
risolvono. Ok? Quindi noi diciamo che la
complessità, il time complexity di
upperbound di un problema è big o di fn
se tutti gli algoritmi che lo risolvono,
ma tutti sì, proprio tutti, tutti gli
algoritmi che lo risolvono hanno un
running time che è big o di fn.
Ok? È chiaro? Quindi un problema a che
ne so
sorting
sorting di arrays. È vero, no? Che il
time complexity upperbound del
dell'ordinamento degli array è big o di
n qu?
No.
Ah, sì, sì. Sì.
Ok. È vero o no? Sì.
Sì, perché tutti
no. Sorry, sorry, ho sbagliato a definir
eh qua infatti mi stavo mi stavo
confondendo. Devo ritornare il conto.
Boh, cancelliamo questa definizione. Go
back. Time complexity upper bound di un
problema. Sorry guys, ecco perché non mi
ritornava la la definizione di questo.
Il time complexity upper bound di un
problema P è big O di FN.
Esatto. Grammarro. Ok. E codi fn se
esiste anche un solo algoritmo la cui
complessità è Bodi Fdn. Ok, così ha
senso.
Ecco perché mo a sorte non mi veniva.
Com'è possibile questa cosa? Come almeno
un algoritmo il cui running time è
quello. Ok? Quindi ora la domanda di
prima è sensata, quindi definizione di
nuovo. Il complexity
time complexity upper bound di un
problema P è big o di fn se esiste
almeno un algoritmo che lo risolve in
time big o d fn. Ok? Quindi ora la
domanda
is meaningful. È vero, no, che il time
complexity upper bound del sorting è big
o di n qu?
Sì, è vero, no, che il time complexity
upper bound del sorting è bigod n cubo.
Sì, è vero, no che il time complexity
upper bound è 2^ n. Sì, no.
Sì, perché ci sta un algoritmo che ranna
in al più 2^ n. Selection sort è bigo di
2^ n. È molto lasco, però è big di 2^ n.
È vero o no che il time complexity upper
bound del sorting è n log n? Sì, perché
c'abbiamo merge sort eccetera. È vero,
no, che il time complexity upper bound
del sorting è big ODL? No, perché non
siamo in grado di ordinare in tempo
lineare e di fatto esiste una tecnica
che dimostra, penso che l'abbiate vista,
che non si può ordinare come? In place.
In place. Sì, sì, sì, sì. Eh, e non
possiamo fare nemmeno assunzioni
statistiche sulla distribuzione
dell'array perché sennò si può eh andare
più veloce. Ok? Quindi abbiamo un array
che su cui non possiamo dire niente. In
quel caso siamo costretti a n log n
perché si può dimostrare che con un
numero asempoticamente inferiore di
confronti l'array potrebbe non essere
ordinato in maniera sensata. Ok? Quindi
quello è il minimo che possiamo fare.
Possiamo definire ora il time complexity
lowerbound.
di un problema qui, eh? Time
complexity
lower bound
di un
problema
P è big o big omega di FM.
Se
tutti gli algoritmi
che lo risolvono
richiedono tempo big omega di fn. Ok?
Ripeto, il time complexity
upper bound di un problema
è
big
omega di fn se tutti gli algoritmi che
lo risolvono
hanno un tempo di esecuzione che è big
omega di fn.
Domanda. È vero, no, che il time
complexity upper bound del sorting è big
omega di n?
Sì, perché tutti gli algoritmi che
ordinano vettori ci mettono almeno
linear time, in particolare ce ne
mettono di più.
È vero, no, che il time complexity lower
bound del sorting è n log n?
Sì, ok. Perché tutti gli algoritmi ci
mettono almeno quello. È vero o no che
il time complexity lowerbound
dell'ordinamento è n qu?
No, perché abbiamo algoritmi che rannano
meglio di quello. Ok? Quindi questa è la
nozione. Quindi tramite la definizione
del time complexity degli algoritmi che
risolvono un certo problema, noi siamo
in grado di parlare della complessità
temporale dei problemi. Ok? Quindi un
problema,
la complessità temporale di un problema
è legata alla complessità temporale
degli algoritmi che lo risolvono. Noi
diremo che un problema è trattabile o
facile se il time complexity upper bound
di questo problema è polinomiale.
Ok? basta che sia polinomiale. In quel
modo noi diremo che il problema è
trattabile o facile.
Se invece il problema non ammette
algoritmi polinomiali, allora diremo che
il problema è difficile. Ok? Quindi è un
po' strano perché la definizione di
problema facile e problema difficile si
basa sugli algoritmi che lo risolvono.
Quindi se abbiamo algoritmi efficienti
noi diciamo che il problema è facile. Se
l'algoritmo efficiente non c'è diciamo
che il problema è difficile. Ok? Adesso
uno potrebbe dire, eh, però un problema
che si si risolve in n^ 1000 non è che è
così così facile da risolvere. Se invece
abbiamo un algoritmo esponenziale che è
2^ n / 1000 per dire, no?
Però di fatto questo sì, questo ci dà un
certo rimando della pratica, però la
questione è che su input che crescono
all'infinito gli algoritmi esponenziali
prima o poi diventano ingestibili. Non
non si può. Non so se mai ci avete
provato a far rannare Napsack su istanze
di 30-40 elementi. Accendete il
computer. Se non avete tecniche tipo
branch and bound, euristiche varie di
ordinamento, uscite, andate a cena,
tornate, il computer sta ancora girando.
Ok? Quindi tutti gli algoritmi
esponenziali su grossi input alla fine
collassano, non non riusciamo a starli
appresso. Però una cosa interessante è
che gran stranamente gran parte dei
problemi semplici ammette
eh algoritmi polinomiali il cui
esponente non è strambo e 3 4 ci sono ci
sono problemi che sono n^ s.
Il test di primalità lo vedremo dopo, è
n^ 12, ok? Però insomma sono numeri
piccolini, come su problemi esponenziali
difficilmente avrei non ci stanno
problemi che 2^ n / 1000 in genere è 2^
2 * n 2^ n / 2. Cioè questi sono le
costanti che in genere troviamo, ok?
E E va bene, ci fermiamo per un po' di
pause.
Facciamo un 10 minuti.
M pausa, pausa, pausa.
Ok, ripartiamo in caso finiamo prima.
Ok,
quindi abbiamo introdotto computation
time di una macchina diuring, running
time di una macchina diuring che per noi
è anche il running time di un algoritmo.
Tramite il running time di algoritmo e
le funzioni e la notazione asintotica
abbiamo definito complex time complexity
upper bound di un algoritmo di un
problema e time complexity lower bound
di un problema.
Quindi essenzialmente il time complexity
upper bound di un problema P è bigo di
fn se esiste un algoritmo che necessita
di quel tempo per risolverlo. Il time
complexity lower bound di un problema è
bigom fn se tutti gli algoritmi
necessitano di almeno fn big di fn per
risolverlo. Ok? I problemi facili sono i
problemi per cui abbiamo algoritmi
polinomiali. Questa è una bellissima
definizione self referenti, ok? E gli i
problemi difficili sono i problemi per i
quali non abbiamo algoritmi che li
risolvono in tempo polinomiale. Ok?
Quindi questa è la cosa.
Quello che facciamo ora
è andare a definire all'interno della
classe R delle classi di complessità,
ok? Perché noi abbiamo detto che la
classe R è una classe di decidibilità o
di calcolabilità. dentro R ci sta tutto
quello che è calcolabile.
Adesso il nostro intento sarà andare a
identificare dentro R ciò che è a bassa
complessità e ciò che è ad alta
complessità, cioè quei linguaggi che per
essere risolti necessitano di poco tempo
e i linguaggi che per essere decisi
necessitano di tanto tempo. Ok? E quindi
noi vogliamo andare a fare un lavoro più
di fino all'interno di R per stabilire
cosa si risolve facilmente e cosa si
risolve difficilmente. Ok? Andiamo
quindi a introdurre la nozione di classe
di complessità.
classe
di complessità
temporale. Poi abbiamo anche le classi
temporale, le classi di complessità
spaziale, ma questa è una cosa che
vedremo più in là, ok?
Sia tdn
una time function,
ok?
denotiamo
d time
di fn
l'insieme
di tutti i linguaggi L
tali per cui esiste una macchina di
touring deterministica
e questo è importante, eh
che decide
TDN. Ok, qua ho scritto unaità. Oplà,
questo è ttdn.
Ok.
Yes. L. Ok. di time di tdn, dove tdn è
una time function, è l'insieme di tutti
i linguaggi tali per cui esiste una
macchina di Turing M deterministica,
quindi guardate qua, de deterministica
D time. Ok? Ecco per cui usiamo questa
notazione che decide L
in tempo
big o di TDN.
Ok? Quindi questa è definizione di
classe di complessità temporale.
D time di TDN è l'insieme di tutti i
linguaggi L tali per cui abbiamo una
macchina o un algoritmo a questo punto
deterministico, questo è importante, che
decide il linguaggio L in tempo big o di
tdn.
Chiaro?
Definiamo a questo punto formalmente la
classe che sicuramente vi avranno
menzionato un numero infinito di volte
che è la classe P.
Polynomial time.
P
è la classe
ottenuta tramite l'unione per C mag> 1
di d time
di n^ c.
Ok?
Quindi questa è la definizione formale
della classe P polynomial time che
contiene tutti i linguaggi
che possono essere decisi
da una macchina di Turing deterministica
in tempo polinomiale per un certo
esponente fissato. Noi non sappiamo
quale sia questo esponente, basta che è
fissato. Deve essere un polinomio. Ok?
Adesso
m
ordinare un array
sta in P.
Sì.
Ordinare un'arresta in P.
Sì.
No,
e questo è un errore comune che lo
correggiamo fin dall'inizio così non ci
caschiamo.
Ordinare un array non è un problema di
decisione.
Dime contiene problemi di decisione.
Questo è un errore proprio, lo leggete
pure sui paper. Vengono pubblicate da
gente che non si occupa di complessità
strutturale e infila in Ped NP. problemi
non di decisione. No, questi queste
classi contengono solo problemi di
decisione.
Sommare due numeri appartiene a P?
No, perché non è un problema di
decisione. Ok? Quindi P contiene solo
problemi di decisione.
P polynomial time è la classe dei
problemi di decisione che possono essere
decisi da macchine di touring
deterministiche in tempo polinomiale.
Ok? Quindi per quello che avevamo detto
prima,
dentro P ci stanno quelli che per noi
sono i linguaggi trattabili, ok? Quindi
quello che sta dentro P noi sono i
problemi facili. Quello che sarà fuori P
ma dentro R sono i problemi per noi
difficili. Ok? Per definizione andiamo a
guardare un po' un paio di esempi di
problemi che si trovano in P.
Richbility.
Ok. raggiungibilità su un grafo.
Quindi che cos'è rbility? Dato un grafo
diretto,
una sorgente e una destinazione,
vogliamo stabilire se esiste un percorso
dal nodo sorgente al nodo destinazione.
Ok? Questo è un problema di di
decisione?
Sì, perché la domanda è dato un grafo,
dato ST, è vero, no, che da S riusciamo
a raggiungere T dentro G? Come lo
possiamo caratterizzare come linguaggio?
Rachability
è l'insieme delle triple grafo ST, tali
per cui
G. Ok, scriviamo meglio.
G è un directo
diretto.
S e T sono
nodi
in G
esiste
un percorso
da S a T
G.
Ok? Cioè, quindi è la definizione del
nostro linguaggio. Ovviamente una
macchina di Turing per essere in grado
di decidere questo linguaggio deve
sostanzialmente essere in grado di
stabilire se c'è un percorso da ST
dentro G, ok? Anzi, essere in grado di
risolvere il problema della
raggiungibilità.
Questo problema è in P o non è in P?
Perché l'ho aggiunto qua?
Eh, sì. Ve lo ricordate un algoritmo che
risolve questo algoritmo in polinomial
time? Già extra. Già extra. Ok. Eh, chi
viene da matematica ha mai sentito
questa cosa qua?
Fatevi guardare negli occhi. No, ok, lo
spieghiamo.
Allora,
la metafora, no, come lo spiegavo in
Inghilterra, noi abbiamo questo grafo,
supponiamo che siamo S e T e vogliamo
stabilire se è possibile andare da S
verso T. Ok? I dettagli di questo
algoritmo in pseudocodice stanno sugli
appunti di Calautti. Voi andate là, ve
li leggete, noi ci limitiamo a un
escorsous veloce. Come funziona? Allora,
sostanzialmente funziona in questo modo.
Ad alto livello. Partiamo da S,
ok?
Lo buttiamo in un sacco
che è il sacco delle cose che stiamo
raggiungendo. Apriamo il sacco, vediamo
che c'è. Tiriamo fuori S. Ok.
S avrà dei nodi adiacenti, cioè da S
possiamo raggiungere due nodi. In questo
esempio specifico possiamo raggiungere
questo qua e questo qua. Chiamiamoli che
ne so A e B.
Quello che si fa è che si parte da si
vede chi sono gli adiacenti, li
prendiamo, li infiliamo in questo sacco,
dopodiché riprendiamo il sacco, lo
apriamo, tiriamo fuori un nodo e
facciamo la stessa cosa. Tipo supponiamo
che dal sacco abbiamo tirato fuori A.
Dove posso andare con A? Posso andare su
B.
Prendo B, lo butto dentro. Poi si deve
stare attenti, questo magari lo vedete
nel dettaglio dell'algoritmo, è che non
devo ciclare più volte sulle stesse
cose, senò iniziamo a luppare. Ok?
Quindi lo becco là, non aggiunto niente
al sacco, tiro fuori dal sacco, ci sta
B. Dove posso andare con B? Posso andare
in T. Prendo T, lo metto nel sacco,
riapro il sacco, tiro fuori quello che
c'è dentro. T e dove volevo arrivare?
Rispondo di sì. Quindi l'idea
sostanzialmente di questo algoritmo è
che io prendo un nodo, mi controllo chi
sono gli adiacenti, me li metto in un
insieme, in un sacco, in una busta, ok?
Dove c'è tutta questa roba e quelli sono
i nodi che ho raggiunto fino a quel
momento. Quando apro il sacco, tiro
fuori i nodi e cerco di capire da lì
dove posso arrivare. A poco a poco,
livello per livello, riuscirò a vedere
se da S riesco ad arrivare a T. Ok?
Di quanti passi necessitiamo per fare
questa cosa?
Raffli, eh?
N + n eh un po' di più
al quadrato se non f Sì, è n qu più o
meno, perché per ogni nodo male che ci
va riusciamo a raggiungere tutti gli
altri. Ovvio che se riuscissimo a
raggiungere tutti gli altri poi
sarebbero direttamente a T. Però un
bound molto lasque. Prendo un nodo,
tutti gli altri li devo mettere in n
busta, poi qui tiro fuori dalla busta,
magari riesco a raggiungere tutti
nuovamente e quindi devo fare n volte
questo processo che mi costa n, quindi
ruffly questo approccio ci costa n². Di
conseguenza il problema, poiché ammetto
un algoritmo polinomiale, è un problema
che sta in pi perché è un problema che
eh è un problema è un algoritmo che
chiede tempo polinomiale ed è un
problema di inisione. Ok?
Un altro
primes,
l'insieme degli interi n rappresentati
in binario
tali che n è un numero
primo.
Ok.
Test standard di primari ci serve in una
marea di applicazioni, soprattutto di
tipo crittografico. Dato un intero
rappresentato il binario, noi dobbiamo
stabilire se quell'intero è un numero
primo o no. Ok?
Chiaro?
Qual è un algoritmo naif per risolvere
questo problema?
Il criello di Il crivello di eratostene
oppure
crivello di eratostene occupa una marea
di spazio, però vabbè. Oppure Sì, una
cosa che possiamo fare è cicliamo. Sì,
più che listarli. Sì, si può fare anche
quello, no? testare. Ah, testare. Ok,
quello che possiamo fare è che lo
iniziamo a dividere per due, lo iniziamo
a dividere per 3. Ok? Quindi, preso n,
lo dividiamo prima per 2, poi per 3, poi
per 4, poi per 5, bla bla bla bla, fino
a n - 1. Ok? In realtà non abbiamo
necessità di arrivare a n - 1, basta
arrivare alla radice n, però non è che
ci risolve tanto la questione. Ok?
Quante divisioni facciamo?
Ruffle,
supponiamo di arrivare a N - 1. Big O di
eh Nig O di N. Sì, big o di N.
Divisioni. Ok. Ogni divisione ci costa
polinomial. Ogni divisione ci costa, no?
Polinomiale, eh, costante è nel mondo
delle super macchine. Una macchina di
touring, dividere ci mette un pochino.
Ok? Quindi il costo reale di dividere
due numeri è polinomial per a meno che
sì, la questione è che se poi dobbiamo
dividere i numeri più grossi ci servono
circuiti più grandi. Nel momento in cui
il circuito è a taglia limitata, noi
dobbiamo iniziare a fare più operazioni,
però si fa in tempo polinomiale, eh
nella taglia degli input. Quindi abbiamo
big OD n divisioni
ognuna
che costa
polinomiale.
Ok? Questa complessità è polinomiale o
no?
Mh.
Attenzione,
attenzione,
è polinomiale in cosa? Cioè il running
time degli algoritmi è valutato in base
alla,
guardate la definizione T di
T diav
eh la taglia dell'input. Ok. Questo
numero di divisioni è la taglia
dell'input o il valore dell'input?
Mh.
E il valore dell'input, ok? E il valore
dell'input è polinomiale nella sua
taglia?
No, è esponenziale.
Attenzione, guys, eh, questo algoritmo è
lentissimo. Questo algoritmo è
esponenziale perché il numero di
divisioni che facciamo è pari al valore
del numero in input,
ma la taglia in cui scriviamo il valore
in input che è rappresentato in binario,
quello è logaritmico, cioè
n il suo valore
è proporzionale al logaritmico al
logaritmo in base 2 della taglia di
rappresentare Eh, quindi se noi vogliamo
dire quante divisioni facciamo rispetto
alla taglia dell'input e quelle sono
esponenziali, questo algoritmo è
lentissimo
e senò avremmo risolto. C'era bisogno
dell'algoritmo di Binner Rabbin se
avevamo un algoritmo lineare che ci
risolveva il test di primari. Ok? Quindi
questo algoritmo è lento, questo
algoritmo prende tempo esponenziale, ok?
tutti. Questa è una gran fregatura, eh,
su questi si deve stare molto attento.
Per esempio, contare da 1 a n è un
algoritmo lineare?
No, no, contare non è lineare.
Contare è esponenziale nella daglia
dell'input, a meno che non
rappresentiamo in un a l'input. Se
l'input è rappresentato in un ario,
allora contare prende tempo lineare.
Ma se l'input è rappresentato in
binario, quello prende tempo
esponenziale. Eh, attenzione,
attenzione. Sì, quindi, scusi, e dei
primes sarebbe o di 2 elevato alla
grandezza del Sì, sarebbe questo. big o
di 2^ n alla taglia di n per un una cosa
di questo tipo N^ K per una certa
costante che quello è quanto ci costa
fare la divisione, ok? quello è la
complessità di quell'algoritmo. Eh,
attenzione a questo perché questa è una
cosa su cui si casca facilmente. La
complessità, il running time degli
algoritmi e delle macchine di touring è
sempre valutato rispetto alla taglia
dell'input, non al suo valore. Adesso,
fin quando abbiamo liste array, allora
la cosa è facile perché quelli occupano
spazi in memoria. Il problema è quando
abbiamo
numeri rappresentati in binario che
quelli sono compatti, cioè in poco
spazio riusciamo a rappresentare numeri
grossi e se dobbiamo iniziare a contare
o fare cose di quel tipo, il tempo che
ci serve è esponenziale, non è lineare.
Ok? Quindi questa è una cosa su cui si
deve stare attenti.
Detto ciò, in realtà il problema primes
è un problema polinomiale.
È un problema polinomiale è stato posto
in P agli inizi dell'anno 2000
e se non vado errato la complessità al
momento è big o di n^ s. Ok? È un
algoritmo che non usa nessuno perché è
talmente lento che preferiamo Miller
Rabin che essendo randomizzato però
possiamo comunque abbassare la il tasso
di errori a piacimento, ok? Cioè, quindi
esiste un algoritmo deterministico che
risponde a primes in tempo polinomiale,
ma è un algoritmo catastroficamente
lento e complicato.
Fu la mia tesina del terzo anno, primes
in P. Ok?
Alright, vediamo adesso un'altra classe.
Inverto la spiegazione perché mi piace
più l'idea.
Ehm, un altro paio di problemi. Ok.
Il problema SAT della soddisfacilità
c'è dispità
di formule
buleane.
Lo definiamo ora perché lo vedremo una
marea di volte durante il resto del
corso. Ok?
Allora, noi ci interesseremo di formule
in forma normale congiuntiva. CNF
adesso vi dico che cos'è. Sono formule
buleane che hanno una forma particolare.
Sono formule
che sono date dalla congiunzione
di un numero finito di clausole C1 CN,
cioè quindi questi C1 C2 bla bla bla C
sono
dei pezzetti di sottoformule
che noi chiamiamo clausole, ok?
Quindi è C1, and C2, and C3 and C4 bla
bla.
Ognuna di queste clausole C con I, ad
esempio, ha questa forma qua
L1 or L2 or L3
or L4 bla bla. Cioè una clausola
è
la disgiunzione
di uno o più letterali.
Ripeto, le formule in forma normale
congiuntiva sono formule buleane che
hanno una struttura particolare, non
sono formule qualsiasi, sono formule che
hanno una certa forma, sono
caratterizzate dalla congiunzione di
sottoformule C1, C2, C3, bla bla che noi
chiamiamo clausole. Queste clausole non
è che sono formule a caso, hanno una
forma particolare pure loro, sono la
disgiunzione
di uno o più letterali, si chiamano L1,
L2, L3, eccetera. Che cos'è un
letterale? Un letterale
LJ cos'è? È una variabile buleana oppure
il suo negato.
Ok? Quindi una formula CNF,
ad esempio, può essere questa qua.
X1 or X2 or not X3 and not X2 or X4
and not X3 or X5 or not X6. Voilà. Ok?
Questa è una formula in CNF. Come vedete
ha una struttura particolare. Abbiamo le
clausole che sono in congiunzione fra di
loro
e ogni clausola è una disgiunzione di
letterali dove un letterale è o una
variabile buleana o il suo negato. Ok?
Quindi hanno questa forma, non altra
forma.
Il problema della soddisfacilità
è sostanzialmente questo qua.
Zatto
è l'insieme delle formule F
tale che fai è in CNF
e fai è soddisfacile.
Quand'è che una formula è soddisfacbile?
Quando esiste
un assegnamento di verità
per le sue variabili che rende la
formula vera. Quindi in questo caso, se
noi diamo x1
vero, x2 falso e x5 vero, soddisfacciamo
la formula f. Ok?
Quindi il problema della
soddisfaccibilità
di una formula buleana di questa forma,
ok? E questo, dato una formula garantita
ad avere questa formula qua, decidere se
esiste un assegnamento di verità per le
variabili buuleane, che significa
decidiamo se dare vero o falso alle
variabili buuleane. Quindi decidere se
esiste un assegnamento di verità che
renda la formula
vera. Ok? Questo è il problema della
soddisfacibilità che chiamiamo SAD.
Ok? Supponiamo che qualcuno ci dia un
assegnamento di verità per le variabili.
Ok?
Quanto tempo ci mettiamo
a stabilire se quell'assegnamento
soddisfa o meno la formula?
Costante. Attenzione, abbiamo la formula
in input che ha la sua lunghezza. Ok?
Provarlo o testarlo? No, no, no, no.
Testarlo. Tempo lineare. Come? Il tempo
lineare.
Tempo lineare o polinomiale. Ok. Quindi
qualcuno ci dà un assegnamento.
Nel momento abbia in cui abbiamo un
assegnamento in mano, in tempo
polinomiale noi siamo in grado di
stabilire se la formula è soddisfatta o
meno. Perché? Semplicemente noi dobbiamo
andare a guardare tutte le clausole e
vedere se almeno uno dei suoi letterali
è soddisfatto dall'assegnamento. Quindi
non è che chissà che cosa dobbiamo fare,
poiché la formula quella forma molto
particolare, noi clausola per clausola
andiamo a vedere se uno dei letterali
viene valutato o vero dall'assegnamento
che qualcuno ci ha dato. Ok? Chiaro?
Quindi,
nel momento in cui noi avessimo un
assegnamento di verità, testare se
soddisfa o meno una formula ci costa
tempo polinomiale. Ok? Ma quanti sono i
i possibili
assegnamenti
buleani per le nostre variabili?
2^ n. Ok? Quindi testare tutti i
possibili assegnamenti buleani per
vedere se ne esiste uno che soddisfa la
formula. Quanto ci costa?
Ci costa qualcosa tipo big o di 2^ n
dove n sono il numero di variabile per
qualcosa che è polinomiale nella taglia
della rappresentazione della formula.
Ok? No, non entriamo nel dettaglio. Ok?
Quindi questo algoritmo che testa,
no,
che testa se una formula è soddisfacbile
o meno, ci impiega exponential time. Ok?
Da ciò noi non siamo in grado di
concludere che
SAT sia in P, ma non lo possiamo
escludere. Semplicemente sappiamo che
questo algoritmo non lo colloca in P,
ok? Perché questo algoritmo ci mette
troppo tempo. Vi ricordo che i problemi
in P sono due problemi per i quali
esiste un algoritmo che li risolve in
polinomial time. Prendiamo questo
problema e lo mettiamo da parte. Ne
guardiamo un altro e poi facciamo delle
considerazioni su entrambi.
Independent set.
Questo è un altro problema che vedremo
spesso. Independent.
Sapete cos'è un independent set? Avete
mai sentito?
Ok. Un independent set è questa cosa
qua. Dato un grafo. Noi siamo
appassionati di grafi e ne abbiamo
sempre tanti. Ok.
dato un grafo,
un independent set di un grafo,
in questo caso non diretto, è un insieme
di nodi
che non sono agganciati in alcun modo,
ok? Quindi
S, vediamo, chiamiamo A B C D E F.
Per esempio, l'insieme
A
B C D, questo è che non si capisce
niente. A E D
è un independent set perché fra nessuna
delle,
cioè se prendiamo a coppie i nodi di S,
nessuna di queste coppie è agganciata in
maniera diretta. Ok? Quindi
un'indipendenza set è un insieme di nodi
che sta all'interno di un grafo che ha
la proprietà che
presi a coppie questi nodi non hanno un
arco che li congiunge, ok?
Quindi A per esempio è sganciato da E, A
è sganciato da D, D è sganciato da E.
Ragazzi, qua mi sto riferendo a un arco
diretto, non se c'è un percorso, eh,
quindi un independent set. è un grafo
tale per cui
eh
tale per cui un insieme di nodi tale per
cui questi sono sganciati a coppie. Ok?
Date un grafo.
Esiste un indipendent set di questo
grafo
in genere?
Mh.
Sì. Cioè, quindi cercare indipendenti
set piccoli è un problema estremamente
facile, quindi è un problema che non ci
interessa la definizione
dell'indipendent set, quindi la diamo
differente.
Independent set è l'insieme delle coppie
grafo numero intero k tale per cui
esiste
un independent set di taglia
K.
in G.
Ok?
Questo è il problema dell'indipendenzet
che fra l'altro ci permette di ottenere
un problema di decisione. Ok? Quindi
questa è una variante decisionale del
problema in cui abbiamo dato un dato un
grafo e un numero K5. È vero, no? che
esiste un indipenden set di taglia 5
dentro questo grafo. Ecco, questo è un
problema più interessante rispetto a
dire dato un grafo, esiste un
independent set, è un problema trivial
perché la risposta è sempre sì.
L'insieme vuoto è un indipendent set del
grafo, sempre. Ok? Quindi quando abbiamo
un vincolo sulla cardinalità
dell'indipendenza set, allora la cosa
diventa un po' più interessante. Ok?
Supponiamo che qualcuno ci dia un
insieme S.
Quanto tempo ci mettiamo a stabilire che
quell'insieme S è o no un'indipendet?
Si può fare una BFS su nodo? Forse eh
cos'è una BFS? Si. Eh, ricerca. Ah, ok.
Bread first. Ah, ok, ok, ok. La cosa è
ancora più semplice, cioè ci danno un
insieme, noi dobbiamo stabilire se
quello è un indipendenzetto o meno. Sì.
Sì. Ah, io cè eh cioè se n è il numero
di nodi del grafo, sarà tipo n rentale.
Sì, sì, è è poco, è polinomiale. Cioè
noi per ogni nodo dobbiamo vedere se in
quell'ess che ci danno per caso dentro s
ci sta roba che è attaccata a quel nodo.
Lo facciamo per tutte le coppie. Le
coppie sono quadratiche, quindi in tempo
quadratico noi questa cosa siamo in
grado di farla. Ok?
Ma quanti sono i possibili sotto i
possibili
indipendentet di taglia K?
N
Sì, sì, sì, sì, sì. Sono N su K. Ok.
Abbiamo necessità di eh cercare robe più
grandi? No, perché il problema ci chiede
semplicemente se esiste un indipenden
set di taglia almeno K. Quindi, nel
momento in cui ne troviamo uno di taglia
K, non c'è bisogno di andare a cercare
indipendetti di taglia K + 1. Ok? Ci
fermiamo là. Quindi noi dovremmo
generare
n
N su k insiemi e poi avere un costo di
tipo quadratico. Ok? Questo qua è
nuovamente esponenziale, quindi questo
algoritmo è esponenziale. Ok?
Però
e qui ci introduciamo il concetto
interessante di oggi.
Questo problemi SAT e independent set
abbiamo visto che hanno algoritmi che
rannano in tempo esponenziale,
però perché stiamo escludendo finora di
utilizzare macchine non deterministiche?
Se avessimo macchine non
deterministiche, noi potremmo andare
molto più velocemente. Ok? Perché qual è
l'intuizione,
per esempio, su independent set? Una
macchina non deterministica, invece di
provare tutti i possibili sottoinsiemi
di taglia K per vedere se c'è un
indipendent set di taglia K, quello che
può fare è gessarlo un indipendent set
di taglia K. C'è
in maniera similare, pure sulla
soddisfacilità, una macchina non
deterministica può gessare
l'assegnamento di verità che soddisfa la
formula se uno c'è. Praticamente il
lavoro che può fare la macchina non
deterministica, supponiamo che questa
sia la configurazione iniziale ID0.
Per esempio, su independent set che deve
fare la macchina? deve fare una cosa
molto semplice. Preso ogni nodo, deve
stabilire lo pigliamo, non lo pigliamo.
Ok? Quindi c'ha un branching factor di 2
in cui da un lato dice il nodo lo
prendo, dall'altro il nodo non lo prendo
per il primo nodo. Poi avremo un'altra
cosa così in cui sul secondo nodo decido
se lo prendo o non lo prendo. Dall'altro
lato se il secondo nodo lo prendo o non
lo prendo e lo possiamo fare per tutti i
nodi. Ok?
per arrivare a delle configurazioni nel
quale la macchina ha deciso per tutti i
nodi
se li prende o meno. Ok? E quindi è una
cosa che la macchina, un attimo, vi
ricordate che avevamo fatto l'avevamo
fatto apposta quell'esercizio, quella
quella macchina che era in grado di
trovare le stringhe che si replicavano,
però non lo selezionava da nastro, lo
sputava sul nastro all'inizio. quel
trucchetto ve l'avevo fatto apposta
perché mi serviva qua. Cioè questa
macchina inuring o questo algoritmo nel
momento in cui trova deve trovare un
indipendent set sul grafo G, quello che
fa è sputa sul nastro questa cosa. Il
primo nodo non lo prendo, il secondo
nodo sì, il terzo sì, il quarto pure, il
quinto no, il sesto non mi piace, il
settimo sì, l'ottavo no. Ok? Cioè,
quindi la macchina all'inizio che fa?
Non deterministicamente
sceglie per ogni nodo se lo prende o non
lo prende. Ok? E questa è la fase non
deterministica di Gess in cui sta
gessando l'indipendent
candidato.
È chiaro?
una domanda che non ho capito bene, nel
senso noi eh, anzi lei ha detto che la
macchina
lei intende che costruisce l'albero di
espansione delle mosse oppure dato
l'albero di espansione
tenere la le diciamo il percorso
ottimale per giungere alla conclusione?
La macchina non fa né l'uno né l'altro.
La macchina per la definizione di
accettazione di computazione non
deterministica è la macchina accetta il
proprio input se esiste un modo per
accettare. Quindi se fra tutti quei
branch del possibile modo per la
macchina di dire questo nodo sì, questo
nodo no, questo nodo no, questo nodo sì,
se fra tutte quelle possibilità ce n'è
una che porterebbe la macchina a
rispondere sì sul proprio input, la
macchina dice di sì. Cioè, non è che
fisicamente espande il proprio albero di
computazione perché la macchina la
macchina non esiste. E la definizione,
ritorniamo a quella a quel problema
filosofico iniziale di quando guardavamo
alle macchine non deterministiche. La
macchina se ha un modo, ovviamente non è
finito, è là c'è tutta la parte di
cerchio che dobbiamo aggiungere. Lì
manca ancora.
La macchina
tenta, la macchina scrive queste cose
sul nastro. Fra tutte le cose che la
macchina può scrivere, se ce n'è una che
è un indipendent set la becca, diciamo.
Però di fatto non è che è veramente in
grado di di indovinarla. La macchina
tra le sue strade c'ha quella che la
porta a dire di sì.
Cioè, allora, ho capito quello che dice
lei, però non capisco, pur essendo
comunque una rappresentazione teorica,
cioè non esiste, eh la persona che
effettivamente va a calcolare un il
costo di una macchina determinista, cioè
non va a creare comunque un qualcosa,
non va a creare l'insieme delle
soluzioni per giungere al costo in sé e
a running time, il running time di una
macchina non deterministica è il running
time del branch più lungo, quindi quanto
ci costa ci costa gessare, cioè buttare
sul nastro la decisione sino per ogni
nodo e quello è OD N, ad esempio.
Dopodiché c'è tutta la fase di check che
sarà quadratico per stabilire se la cosa
che è stata ghessata è in effetti
un'indipendenza o meno. Però, cioè
questa è la cosa, la la persona che va a
verificare la macchina, il presupposto
linguistico di questa frase è che noi
abbiamo la macchina, la macchina non
c'è. Cioè, quindi chiedersi che genere
di passi faccia la macchina durante
questo tipo di computazione è qualcosa
di cuorbiante.
A noi ci torna comodo nel momento in cui
lo pensiamo dire "Ok, questa macchina è
indovina", ma non è così, è per noi una
metafora. Ok, grazie. Ok, leiamo
sul sul Ok, quindi la macchina che può
fare per ogni nodo. Sì, questo mi piace.
Questo no, questo lo piglio, questo lo
escludo, quest'altro lo escludo. E
insomma arriva a questo livello della
computazione nel quale la macchina avrà
scritto sul nastro per ogni nodo se lo
prende o meno. Dopodiché qui c'è tutta
la fase di jack che è deterministica,
quindi sono delle liste, no?
In cui la macchina che fa?
Uno, verifica che due cose deve
verificare.
Uno, verifica che il numero dei nodi
scelti sia al-men K, perché la macchina
la sta buttando a caso, eh, quella parte
iniziale la macchina non sta facendo
niente, scrive cose sul nastro, eh,
quindi come facciamo ad accettare uno?
Il numero totale dei nodi su cui abbiamo
deciso di prenderli è almeno K.
E poi nella fase nella seconda fase di
check dobbiamo verificare che quello che
abbiamo scelto di prendere costituisce
un indipendente. Ok? Io dentro posso
mettere anche nodi effettivamente
collegati, però devo controllare se Sì,
quei branch branch relativi a scelte non
sensate terminano in una configurazione
non accettate, quindi vanno a sbattere
contro un muro. Ok? L'importante è che
noi qua nel check ci saranno alcuni
branch
rifiutano, altri che accettano, altri
che rifiutano. Ok? E se c'è un modo per
la macchina di accettare il proprio
input, la macchina dice di sì. Quanto
tempo ci mette la macchina,
Rffley?
Tempo lineare. Ci mette un tempo
polinomiale, ok? Ci mette tempo lineare
a fare il guess e poi ci mette tempo
quadratico a fare questo check, ok?
Similmente per SAT.
Una macchina non deterministica può fare
una cosa simile a per SAT. Gessa in
tempo lineare un assegnamento di verità
per le variabili, dopodiché verifica che
quello che ha ghessato stia
effettivamente soddisfacendo la formula.
Quanto tempo ci mettiamo? polinomiale.
Allora, questi problemi sono
interessanti perché perché ci
selezionano, ci identificano
una classe di complessità importante che
è la classe dei problemi
risolvibili in tempo polinomiale non
deterministico. Ok? Quindi definiamo,
questi sono gli ultimi minuti, ci siamo
quasi.
Sia tn una time function.
La classe di complessità n time di TD n
è l'insieme di tutti i linguaggi L
tali per cui esiste una macchina M non
deterministica
che decide
L in tempo.
o di TDN.
Chiaro? Quindi N time, a differenza di
Dime,
è l'insieme dei linguaggi che possono
essere decisi da macchine di touring non
deterministiche. Ecco perché usiamo N
all'inizio, da macchine di Touring non
deterministiche in tempo bit o BTDN.
molto simile alla definizione
precedente, possiamo definire la classe
NP, che sicuramente avrete già sentito.
La classe NP è semplicemente l'unione su
tutti gli core 1 di cosa? Di n
c eh di n^ c.
Ok? Questa è la classe np.
NP
non sta per non polinomia, quello è un
errore. NP sta per non deterministic
polinomial perché i problemi che stanno
in NP, come vedremo poi la prossima
volta, non è che lo sappiamo se siano o
meno polinomiali.
Non abbiamo algoritmi polinomiali, ma al
momento non possiamo nemmeno escluderlo,
ok? Quello questo è lo stato delle
conoscenze attuali. Quindi NP sta per
non deterministic polynomial time. Ok? E
il problemi SAT e Independence Set è
tutta una pletora di problemi
interessantissimi.
Napsac, Beaming, Hamiltonian Cycle, bla
bla bla, sono tutti problemi che stanno
dentro la classe NP. Ok?
E con questo chiudiamo per oggi.
Grazie mille.
Ok,
fatto.
[Musica]
Quindi la classe MP esiste
sostanzialmente perché non abbiamo
trovato un modo migliore per
rappresontare la macchina non
deterministica.
Es esattamente.
Ho una vaga idea delle date fissare,
però è tipo magorale,
[Musica]
ma se c'è l'orale sarà
orale
se può darsi che
Ok.
[Musica]
Dove sta lauzia?
[Musica]
Niente, la pennzut
non hanno ancora definito, se non
sbaglio, se ci sarà un orale, un
scritto. Allora, ci sarà sicuramente lo
scritto, però è una cosa che stabilisco
in queste settimane, però occhio e croce
sicuramente c'è uno scritto l'orale, se
c'è sarà opzionale. Sarà opzionale. No,
le dimostrazioni saranno pure ceste. Sì,
sì, sì, sì. Però l'orale se non un filo
no era per capire se era molto teorico,
quindi dimostrare teorico, cioè che sia
scritto che sia ci sarà probabilmente
una macchina di touring da scrivere,
però il resto sarà dimostrare che è bla.
dimostrare che è quindi sì, sia lo
scritto che l'orane saranno molto a
parte un esercizio
anche a lei.
Questo l'avevo stoppato. No, no.