CTX - 03 - Complessità Computazionale


1 Informazioni Lezione

Data: [2018-03-14 mer]


In questa lezione introduciamo i numeri di Fibonacci e ne dimostriamo qualche proprietà. Continuiamo poi introducendo la notazione O-grande, che viene utilizzata per esprimere il costo asintotico degli algoritmi, e discutendo la rappresentazione dei numeri tramite la scrittura posizione, e alcune proprietà di questa rappresentazione, come ad esempio la lunghezza. Chiudiamo la lezione discutendo la complessità di un algoritmo, introducendo due classi di algoritmi: quelli esponenziali, e quelli polinomiali.

2 Numeri di Fibonacci

Definiamo l'\(n\) -esimo numero di fibonacci \(F_n\) nel seguente modo:

\[F_1 = 1,\quad F_2 = 2, \quad F_n = F_{n-1} + F_{n-2} \quad \forall n > 2\]

Relazionata ai numeri di fibonacci troviamo poi la seguente equazione:

\[\begin{split} x^2 - x - 1 = 0 &\iff x^2 = x + 1 \\ &\iff \frac{1}{x+1} = \frac{1}{x^2} \\ &\iff \frac{x}{x+1} = \frac{1}{x} \end{split}\]

Le radici di questo polinomio sono \(\alpha = \frac{1 + \sqrt{5}}{2}\) e \(\alpha ' = \frac{1 - \sqrt{5}}{2}\). La radice positiva, \(\alpha\) viene anche chiamata rapporto auro.

Sarà importante nel corso di questa lezione tenere a mente il fatto che \(\alpha\) e \(\alpha '\) sono gli unici numeri reali tali che \(\alpha^2 = \alpha + 1\). Questa relazione verrà indirettamente utilizzata per analizzare la complessità dell'algoritmo di euclide per il calcolo del massimo comun divisore tra due interi.


2.1 Proprietà dei Numeri di Fibonacci

A seguire una serie di proprietà che legano i numeri di Fibonacci \(F_n\) a queste radici.


2.1.1 \(F_n = \frac{\alpha^n - \alpha'^n}{\sqrt{5}} \quad \forall n\)

Procediamo per induzione su \(n \in \mathbb{N}\).

  • Passo base: Per \(n = 1\) e \(n = 2\) si può verificare a mano che la relazione è vera.

  • Passo Induttivo: Sia quindi \(n > 2\). Assumiamo come ipotesi induttiva generalizzata che la relazione vale per ogni \(h \leq n\) e dimostriamo che vale anche per \(n+1\).

    \[\begin{split} F_{n} = F_{n-1} + F_{n-2} &= \frac{\alpha^{n-1} - \alpha'^{n-1}}{\sqrt{5}} + \frac{\alpha^{n-2} -\alpha'^{n-2}}{\sqrt{5}} \\ &= \frac{1}{\sqrt{5}}\alpha^{n-2}(\alpha + 1) - \frac{1}{\sqrt{5}}\alpha'^{n-2}(\alpha' + 1) \\ &= \frac{1}{\sqrt{5}}\alpha^{n-2}(\alpha^2) - \frac{1}{\sqrt{5}}\alpha'^{n-2}(\alpha'^2) \\ &= \frac{\alpha^n - \alpha'^n}{\sqrt{5}} \end{split}\]



2.1.2 \(F_n > \alpha^{n-2} \quad \forall n \geq 3\)

Procediamo per induzione su \(n \in \mathbb{N}\).

  • Passo base: Per \(n = 3\) abbiamo \(F_3 = 2\) e \(\alpha^{3-2} = \alpha \approx 1.61803\). Per \(n = 4\) abbiamo \(F_4 = 3\) e \(\alpha^{4-2} = \alpha^2 = \alpha + 1 \approx 2.61803\).

  • Passo Induttivo: Sia quindi \(n > 4\). Assumiamo la relazione per ogni \(h < n\), e dimostriamola per \(n\).

    \[\begin{split} F_n = F_{n-1} + F_{n-2} &> \alpha^{n-3} + \alpha^{n-4} \\ &= \alpha^{n-4}(\alpha + 1) \\ &= \alpha^{n-4}(\alpha^2) \\ &= \alpha^{n-2} \end{split}\]



2.1.3 \(MCD(F_{n+1}, F_{n+2}) = 1 \quad \forall n\)

Applicando l'algoritmo delle divisioni successive ai \(F_{n+1}\) e \(F_{n+2}\) otteniamo:

\[\begin{align*} &F_{n+2} = 1 \cdot F_{n+1} + F_{n}, \quad \quad 0 \leq F_{n} < F_{n+1} \\ &F_{n+1} = 1 \cdot F_{n} + F_{n-1}, \quad \quad 0 \leq F_{n-1} < F_{n} \\ &F_{n} = 1 \cdot F_{n-1} + F_{n-2}, \quad \quad 0 \leq F_{n-2} < F_{n-1} \\ &. \\ &. \\ &. \\ &F_{4} = 1 \cdot F_{3} + F_{2}, \quad \quad 0 \leq F_{2} < F_{3} \\ &F_{3} = 2 \cdot F_{2} + 0, \end{align*}\]

Troviamo quindi \(MCD(F_{n+1}, F_{n+2}) = F_{2} = 1\). Osserviamo inoltre chel'algoritmo, su questa instanza, necessità di \(n-1\) divisioni successive.



2.1.4 \(\alpha^n = F_n \alpha + F_{n-1} \quad \forall n \geq 2\)

Procediamo per induzione su \(n \in \mathbb{N}\).

  • Passo base: Per \(n = 2\) troviamo \(alpha^2 = \alpha + 1\), che è vera per come \(\alpha\) è stato definito.

  • Passo induttivo: Sia quindi \(n > 2\). Assumiamo come ipotesi induttiva che la relazione vale per ogni \(h < n\), e dimostriamola per \(n\).

    \[\begin{split} \alpha^n &= \alpha^{n-1}\alpha \\ &= (F_{n-1} \alpha + F_{n-2}) \alpha \\ &= F_{n-1}\alpha^2 + F_{n-2}\alpha \\ &= F_{n-1}(\alpha + 1) + F_{n-2}\alpha \\ &= (F_{n-1} + F_{n-2})\alpha + F_{n-1} \\ &= F_{n}\alpha + F_{n-1} \end{split}\]

In particolare, applicando l'ultimo risultato al caso \(n = 5\) ottengo

\[\alpha^5 = F_5 \cdot \alpha + F_4 = 5 \frac{1+\sqrt{5}}{2} + 3 > 10\]

ovvero,

\[\log_{10}{\alpha} > \frac{1}{5}\]


3 Notazione O-Grande

Supponiamo di avere due funzioni aritmetiche, ovvero funzioni della forma

\[\begin{split} f: \mathbb{N} \to \mathbb{R} \\ g: \mathbb{N} \to \mathbb{R} \end{split}\]

Procediamo col dare una definizione estremamente utile per stimare il numero di operazioni elementari effettuate da un algoritmo.

Definizione: \(f = O(g(n))\) se \(\exists c \in \mathbb{R}: \exists N \in \mathbb{N}\) tale che

\[f(n) \leq c \cdot g(n) \quad \forall n \geq N\]

notiamo che tale condizione equivale alla seguente

\[\forall n \in \mathbb{N}: \exists D \in \mathbb{R}: f(n) \leq D \cdot g(n)\]

solo che questa volta la costante \(D\) cambia in funzione di \(n\), \(D = D(n)\)


3.1 Esempio

Mostriamo qualche semplice stima

  • Se \(f\) è un polinomio di grado \(n\), allora \(f = O(n^n)\).

  • Si ha che \(\ln{n} = O(n^\epsilon)\), \(\forall \epsilon > 0\). Il che ci dice che la crescita del logaritmo è molto lenta rispetto alla crescita di un polinomio.

4 Scrittura Posizionale

Il modo in cui scriviamo i numeri è fondamentale. Non a caso l'avvento della scrittura posizionale ha permesso lo sviluppo di una serie di algoritmi in grado di eseguire i calcoli in modo semplice e veloce. L'avvento della notazione posizionale, che a noi sembra intuitiva e naturale, ha cambiato drasticamente il modo in cui l'umanità si relazione ed utilizza i numeri nella vita di tutti i giorni.

Possiamo rappresentare un numero naturale \(n \in \mathbb{N}\) in una determinata base \(b \in \mathbb{N} \setminus \{0, 1\}\) utilizzando la seguente convenzione

\[n = (d_{k-1}, d_{k-2}, ..., d_0)_b := \sum_{i = 0}^{k-1} d_{i}b^{i} \in \mathbb{N}\]

dove i simboli \(d_i \in \{0, 1, ..., b-1\}\) rappresentano le cifre del numero e sono interi compresi tra \(0\) e \(b-1\).

Questo modo di rappresentare un numero \(n\) prende il nome di scrittura posizionale, in quanto il valore di una cifra dipende anche dalla posizione che quella cifra ricopre nella rappresentazione simbolica del numero. Ad esempio, nel numero \(2002\) scritto in base \(10\), la cifra \(2\) ricopre due valori distinti: una volta sta per il numero due, un'altra per il numero duemila.

Un risultato notevole è che tale forma di scrittura esiste sempre ed è univoca. Tale risultato è una conseguenza diretta dell'algoritmo della divisione in \(\mathbb{N}\) (e in \(\mathbb{Z}\)), in quanto

  • \(d_0\) è il resto della divisione di \(n\) per \(b\), con \(0 \leq d_o < b\).

  • \(d_1\) è il resto della divisione di \(\frac{n - d_o}{b}\) per \(b\), con \(0 \leq d_1 < b\).

  • etc.

L'esistenza e l'unicità delle varie cifre sono quindi garantite dall'esistenza e unicità del resto e del quoziente quando divido due numeri interi o naturali.


4.1 Lunghezza e grandezza di un numero

Sia \(n\) un numero, e andiamo a rappresentare il numero \(n\) in base \(b\), \(n = (d_{k-1}, d_{k-2}, ..., d_0)_b\), allora

\[b^{k-1} \leq n < b^k \iff k-1 \leq \log_b{n} < k\]

e quindi

\[k = \lceil \log_b{n} \rceil\]

con \(\lceil a \rceil := \min \{ h : h \geq a\}\). Da questa relazione possiamo notare che il numero di cifre (simboli) necessari a rappresentare il numero \(n\) sono \(\lceil \log_b{n} \rceil\). Possiamo quindi pensare alla lunghezza di un numero \(n\), come al numero di cifre necessarie per scrivere \(n\) in una base \(b\), mentre alla grandezza di un numero come alla magnitudine del numero, inteso come valore indipendente da come il numero viene scritto.

Notiamo inoltre che

\[\log_b{n} = \frac{\ln{n}}{\ln{b}}\]

utilizzando la notazione \(O\) -grande troviamo

\[\log_b{n} = O(\ln{n}) \text{, e } \ln{n} = O(\log_b{n})\]

In altre parole, queste due funzioni sono asintoticamente equivalenti. Quindi, la lunghezza di un numero è logaritmica rispetto alla sua grandezza.


4.2 Come Pensiamo ai Numeri

Sia \(n \in \mathbb{N}\), definiamo

\[w_b(n) := \text{numero cifre necessarie per \textit{rappresentare} $n$ in base $b$}\]

dalle discussioni precedenti sappiamo che

\[w_b(n) = O(\log{n}) = O(n^{\epsilon}) \quad \forall \epsilon > 0\]

Ma che vuol dire tutto questo effettivamente? Una interpretazione di tale risultato è la seguente: la lunghezza di un numero \(n\), anche se scritto in binario (il che massimizza la lunghezza in quanto la scrittura binaria utilizza solamente \(2\) cifre), cresce molto più lentamente rispetto al valore del numero \(n\), al crescere di \(n\). Questo, tra le altre cose, è fondamentale per noi esseri umani.

Quando pensiamo ad un numero naturale \(n \in \mathbb{N}\), lo possiamo fare essenzialmente in due modi:

  1. Lo possiamo pensare rispetto al modo in cui lo rappresentiamo. Quindi se dico venticinque voi vi immaginate la rappresentazione simbolica \(25\).

  2. Lo possiamo anche pensare rispetto alla sua grandezza. Se dico cinque voi potete pensare, ad esempio, ai membri della vostra famiglia, oppure ai cinque film che più preferite, oppure a cinque sedie, messe una dopo l'altra. Insomma, pensare un numero rispetto alla sua grandezza significa (dal mio punto di vista) immaginare effettivamente un qualsiasi insieme formato da quel particolare numero di elementi.

Ora, se dico trecentomila, voi lo pensate rispetto alla sua grandezza, o rispetto alla sua rappresentazione? Molto probabilmente la risposta è la seconda, e questo è del tutto naturale: per noi esseri limitati è difficile immaginare un insieme così grande. Invece, è molto più semplice immaginare delle piccole cifre, ordinate tra loro per rappresentare quel numero bestiale, che in realtà non è nemmeno poi così mostruoso. Questa osservazione, che se uno ci pensa bene è davvero banale, è alla base della Teoria della Complessità, che studia quanto sono difficili da attuare gli algoritmi.

5 Complessità di un Algoritmo

La teoria della complessità vuole essenzialmente rispondere alla seguente domanda: Quanto ci costano, approssimatamente, gli algoritmi? Prima di provare a rispondere a questa domanda dobbiamo però definire che cosa intendiamo per "costo di un algoritmo".

Un algoritmo è una sequenza di "passi elementari" che, eseguita in modo totalmente automatico, ci permette, partendo da un particolare input, di ottenere un particolare output che risolve il nostro specifico problema. Per noi quindi, il costo di un algoritmo è il numero, o la O-stima del numero, di questi "passi elementari" che l'algoritmo esegue su input per calcolare la soluzione del problema. Lavorando poi strettamente in base \(2\), come del resto fanno i calcolatori moderni, chiameremo operazioni bit questi passi elementari. Le operazioni bit possono quindi essere intese come quel numero limitato di azioni indivisibili che la macchina (più precisamente il modello di calcolo) compie quando sta eseguendo un algoritmo.

Più formalmente, la complessità di un algoritmo è il numero (o O-stima) del numero di operazioni bit che l'algoritmo esegue su input limitati da un certo \(n \in \mathbb{N}\).

Il tempo occorrente per effettuare un algoritmo su input limitati da un certo \(n \in \mathbb{N}\) è invece la durata temporale tra l'istante in l'algoritmo viene azionato su un calcolatore fisico e l'istante in cui l'algoritmo termina. A differenza della complessità, il tempo di esecuzione dipende da quante operazioni bit il calcolatore che utilizzo è in grado di effettuare in un secondo.

Generalmente, se \(A\) rappresenta un algoritmo, indicheremo con \(c(A)\) la complessità di \(A\) e con \(T(A)\) il tempo di esecuzione dell'algoritmo \(A\) su un calcolatore fisico. Troviamo quindi la seguente relazione:

\[T(A) = c \cdot c(A) = O(c(A))\]

Dove la costante \(c \in \mathbb{R}\) dipende dalle prestazioni del calcolatore fisico e può essere limitato dalle leggi dell'Universo.


5.1 Complessità Polinomiale ed Esponenziale

Ovviamente non ha senso stabilire il costo di un algoritmo se poi questo costo non viene confrontato rispetto ad altri algoritmi. Introduciamo quindi due definizioni chiavi, che ci permettono di distinguiere due classi di algoritmi: quelli che utilizzano una quantità di risorse limitate da un polinomio, che chiameremo algoritmi polinomiali, e quelli che invece utilizzano una quantità di risorse limitata da un numero che cresce esponenzialmente, che prenderanno il nome di algoritmi esponenziali.

Sia \(A\) un algoritmo che coinvolge i numeri \(n_1, n_2, ..., n_h\). Abbiamo le seguenti due definizioni.

Definizione 1: La complessità di \(A\) si dice polinomiale se

\[c(A) = O((\log{n_1})^{k_1}(\log{n_2})^{k_2}...(\log{n_h})^{k_h})\]

con \(k_1, k_2, ..., k_h \in \mathbb{N}\).

Definizione 2: La complessità di \(A\) si dice esponenziale se

\[c(A) = O(n_1^{k_1}n_2^{k_2}...n_h^{k_h})\]

con \(k_1, k_2, ..., k_h \in \mathbb{N}\).

Volendo algoritmi effettivamente implementabili siamo quindi interessati alla classe degli algoritmi polinomiali. Bisogna però notare che se un algoritmo è polinomiale, non è sempre detto che sia effettivamente veloce, una volta implementato su un calcolatore. Viceversa, certi algoritmi, teoricamente esponenziali, quando poi vengono implementati e utilizzati nella pratica su calcolatori moderni con istanze relativamente limitate, possono comunque portare a risultati utili.


5.2 Complessità Operazioni Elementari

Andiamo quindi a riportare la complessità computazionale delle quattro operazioni elementari: Siano \(a, b: a \geq b\) due interi maggiorati da \(n\).


5.3 Complessità Operazioni Non Elementari

Per finire la lezione andiamo a discutere la complessità di alcune operazioni non elementari.


5.3.1 Massimo Comun Divisore

Per calcolare la complessità del massimo comun divisore \(MCD(a, b)\) è importante il seguente risultato.

Teorema(Di Lamé): Sia \(div(a, b)\) il numero di divisioni effettuate per calcolare il \(MCD(a, b)\) utilizzando l'algoritmo euclideo, con \(a, b\) interi tali che \(b \leq a\). Allora, se \(w_{10}(b)\) indica il numero di cifre decimali per rappresentare il numero \(b\), abbiamo che

\[div(a, b) \leq 5 \cdot w_{10}(b) + 1\]

Dimostrazione: Applicando l'algoritmo euclideo ad \(a := r_0\) e \(b := r_1\) otteniamo:

\[\begin{align*} &r_0 = q_1 \cdot r_1 + r_2, \quad \quad &0 \leq r_2 < r_1 \\ &r_1 = q_2 \cdot r_2 + r_3, \quad \quad &0 \leq r_3 < r_2 \\ &. &\\ &. &\\ &. &\\ &r_{n-2} = q_{n-1} \cdot r_{n-1} + r_n, \quad \quad &0 \leq r_n < r_{n-1} \\ &r_{n-1} = q_{n} \cdot r_n + 0& \end{align*}\]

L'algoritmo termina dopo \(n\) passi e otteniamo \(r_n = MCD(a, b)\). Osserviamo poi che

\[\begin{align*} &r_{n} \geq 1 = f_2 \\ &r_{n-1} = q_nr_n \geq 2r_n = 2f_2 = f_3 \\ &r_{n-2} = q_{n-1}r_{n-1} + r_n \geq r_{n-1} + r_{n} \geq f_3 + f_2 = f_4 \\ &r_{n-3} = q_{n-2}r_{n-2} + r_{n-1} \geq r_{n-2} + r_{n-1} \geq f_4 + f_3 = f_5 \\ &. \\ &. \\ &. \\ &r_{2} = q_{3}r_{3} + r_{4} \geq r_{3} + r_{4} \geq f_{n-1} + f_{n-2} = f_{n} \\ &r_{1} = q_{2}r_{2} + r_{3} \geq r_{2} + r_{3} \geq f_{n} + f_{n-1} = f_{n+1} \end{align*}\]

troviamo quindi che \(b = r_{1} \geq f_{n+1}\).

Sapendo inoltre che \(f_n > \alpha^{n-2}\), troviamo che \(b \geq f_{n+1} > \alpha^{n-1}\), e, prendendo il logaritmo in entrambi i lati, otteniamo:

\[\log_{10} {b} > (n-1)\log_{10}{\alpha} \iff n < \frac{\log_{10}{b}}{\log_{10}{\alpha}} + 1\]

Infine, utilizzando il fatto che \(\log_{10}{\alpha} > \frac{1}{5}\), otteniamo

\[div(a, b) = n < 5\log_{10}{b} + 1 = 5w_{10}(b) + 1\]

ovvero il numero di divisioni effettuate dall'algoritmo euclideo per il calcolo di \(MCD(a, b)\) è limitato superiormente da \(w_{10}(b)\).

\[\tag*{$\blacksquare$}\]

Siamo ora in grado di calcolare la complessità del calcolo di \(MCD(a, b)\), con \(n \geq a \geq b\). Infatti, dato che il costo per ogni singola divisione è \(O(\log^2 n)\), e dato che effettiamo al massimo \(O(\log b) = O(\log n)\), otteniamo una complessità totale di

\[O(\log n) \cdot (\log^2 n) = O(\log^3 n)\]



5.3.2 Fattoriale

Sia \(n \in \mathbb{N}\), e consideriamo la funzione fattoriale definita nel seguente modo:

\[n! := \begin{cases} (n-1)! \cdot n & n > 1 \\ 1 & n = 1 \end{cases}\]

Andiamo adesso a stimare il numero di operazioni elementari necessarie per calcolare \(n!\).

Utilizzando la definizione appena data possiamo notare che, per calcolare \(n!\), mi devo prima calcolare \((j+1)!\) per \(j\) che va da \(1\) a \(n-1\). Voglio quindi stimare il numero di operazioni necessari per calcolare il seguente prodotto:

\[(j+1)! = j! \cdot (j+1) \,\,\,\,, \,\,\,\, j \in \{ 1,..., n-1\}\]

Possiamo stimare la lunghezza di \(j+1\) con quella di \(n\). Infatti \(j+1 \leq n\) e quindi \(\log (j+1) \leq \log n\). Per quanto riguarda la lunghezza di \(j!\) invece, notiamo che se effettuiamo il prodotto di due numeri di lunghezza \(n_1\) e \(n_2\), otteniamo un numero di lunghezza \(n_1 + n_2\). Sapendo poi che

\[j! = 1 \cdot 2 \cdot ... \cdot (j-1) \cdot j \leq \underbrace{n \cdot n \cdot ... \cdot n \cdot n}_\text{$j$ volte} = n^j\]

possiamo limitare la lunghezza di \(j!\) con la lunghezza di \(n^j\), che è \(O(j \cdot \log{n})\).

Mettendo tutto insieme possiamo stimare il calcolo di \((j+1)!\) tramite

\[(j \cdot \log{n}) \cdot \log{n} = j \cdot \log^2{n}\]

Il costo totale risulta quindi essere: \(c(n!) = n \cdot O(n\log^2{n}) = O(n^2 \log^2{n})\), che notiamo è esponenziale rispetto alla dimensione dell'input.



5.3.3 Prodotto tra Interi

Ricordiamo che l'algoritmo standard per moltiplicare due interi \(a, b\) con \(a \leq b\) ha una complessità dell'ordine di \(O(\log{a} \cdot \log{b})\). Se poi \(n\) è un intero tale che \(n \geq a, b\), otteniamo una complessità di \(O(\log^2{n})\).

Siamo quindi portati a pensare che non esiste un algoritmo in grado di moltiplicare due interi che sia asintoticamente migliore rispetto a quello che impariamo alle scuole elementari. In realtà, anche se sembra molto strana come cosa, questo non è vero: esiste infatti un algoritmo, l'algoritmo di Karatsuba, che riesce a moltiplicare due interi \(a, b\) in tempo \(O(\log^{\log_2 3} n)\). Notando che che \(\log_2 3 < 2\) possiamo affermare che l'algoritmo di Karatsuba è asintoticamente più veloce rispetto l'algoritmo standard per la moltiplicazione di due interi.

Successivamente alla scoperta da parte di Karatsuba del suo particolare algoritmo per moltiplicare due interi, sono stati scoperti tutta una serie di miglioramenti, arrivando ad ottenere una complessita di \(O(\log^{1 + \epsilon} n)\), \(\forall \epsilon > 0\).

Questo risultato ci dice che moltiplicare due costa poco più che scrivere i numeri in memoria.



5.3.4 Prodotto tra Polinomi

Siano \(f(x)\) e \(g(x)\) due polinomi a coefficienti interi di grado \(n_1\) e \(n_2\) rispettivamente, con \(n_1 \geq n_2\). Sia \(n\) un intero che maggiora i coefficienti di entrambi i polinomi. Il prodotto di \(f(x)\) per \(g(x)\) viene definito nel seguente modo

\[[f \cdot g](x) := \sum_{l = 0}^{n_1 + n_2} {\sum_{i + j = l} {a_ih_j}x^l}\]

Iniziamo notando che la complessità di ogni prodotto della forma \(a_ih_j\) può essere stimata con \(O(\log^2 {n})\). Per il calcolo del coefficiente relativo a \(x^l\) dobbiamo, come prima cosa, calcolarci tutti i termini della forma \(a_ih_j\), e questi possono essere al più \(n_2 + 1\), ottenendo una complessità di \(O(n_2\log^2 n)\). Una volta calcolati tutti i termini poi, li dobbiamo sommare tra loro, eseguendo al più \(n_2\) somme. Il costo di ciascuna somma varia a seconda della lunghezza degli addendi. Iniziando però con addendi di lunghezza \(O(\log n)\), ed effettuando al più \(n_2\) somme, possiamo stimare la lunghezza massima raggiungibile con \(O(\log n + \log {n_2})\). Il costo per sommare i vari prodotti è quindi \(O(n_2(\log n + \log n_2))\). Il costo totale per calcolare il coefficiente del termine \(x^l\) è quindi stimato da \(O(n_2(\log^2 n) + n_2(\log n + \log n_2))\) = \(O(n_2(\log^2 n + \log n_2))\). Infine, dovendo ripetere tale procedimento per ogni \(l = 0, ..., n_1 + n_2\), otteniamo una complessità totale

\[O(\underbrace{(n_1 + n_2)}_{l = 0,...,n_1 + n_2} \cdot \underbrace{n_2(\log^2n + \log n_2)}_{\text{calcolo coefficiente $x^l$}})\]



5.3.5 Idendità di Bézout

Una volta che ci siamo calcolati il \(MCD(a, b)\), per trovare l'identità di Bézut non dobbiamo far altro che eseguire \(O(\log b)\) volte una moltiplicazione e una divisione, ottenendo una complessità totale di

\[\underbrace{ O(\log^3 n)}_{\text{calcolo MCD(a, b)}} + \underbrace{O(\log^3 n)}_{\text{calcolo identità Bézut}} = O(\log^3 n)\]

Notiamo poi che esistono infinte identità di Bézut. Infatti, fissati \(a, b \in \mathbb{Z}\) abbiamo che partendo da una identità ne possiamo sempre costruire un'altra nel seguente modo

\[\begin{split} MCD(a, b) &= \alpha a + \beta b \\ &= \alpha a + \beta b + 0 \\ &= \alpha a + \beta b + c(ab - ba) \\ &= (\alpha - cb) a + (\beta + ca) \\ \end{split}\]