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.