Calcolo della radice quadrata di un numero

Le procedure che abbiamo introdotto sino ad ora sono essenzialmente delle funzioni matematiche che specificano un valore che viene calcolato a partire da uno o più parametri. A differenze delle funzioni matematiche, le procedure definite al calcolatore devono essere anche efficienti, ovvero devono terminare la loro esecuzione in tempo breve. Vediamo ora un semplice esempio di cosa vuol dire avere una procedura efficiente.

Prendiamo per esempio la definizione matematica seguente:

$$\sqrt{x} = y \quad \mbox{ se e solo se }\quad y \geq 0 \mbox{ e } y^2 = x$$

Queste definizione è corretta da un punto di vista matematico e potremmo usarla per controllare se un dato numero sia la radice quadrata di un altro. Tuttavia, questa definizione non descrive una procedura per calcolare la radice quadrata di un numero.

Per poter calcolare la radice quadrata di un numero abbiamo bisogno di un algoritmo che viene implementato con una o più procedure.

Enumerazione esaustiva (forza bruta)

Se ci limitiamo a considerare i numeri interi, potremmo utilizzare la definizione precedente per trovare la radice quadrata di un numero intero positivo attraverso un'enumerazione esaustiva: per trovare la radice quadrata di $x$, possiamo provare tutti i numeri da $x$ a 1, e verificare ogni volta se il numero "tentativo" sia il quadrato dell'altro.

ESERCIZIO 2.1: Scrivere una procedura (funzione) che prende in input un numero intero maggiore o uguale a 1, e prova tutti i numeri da $x$ a 1. Se trova la radice quadrata esatta la restituisce, altrimenti restituisce $-1$.


In [1]:
def Enumerate(y, x):
    # print(y)
    if y == 0:
        return -1
    if x == y*y:
        return y
    return Enumerate(y-1, x)

In [2]:
print(Enumerate(16, 16))


4

In [3]:
print(Enumerate(15, 15))


-1

DOMANDA: Come fare per trovare la radice quadrata di un numero reale positivo?

DOMANDA: Cosa vuol dire che due numeri reali sono uguali?

ESEMPIO: Come viene valutata l'espressione logica: $$\frac{1}{10}+\frac{1}{10}+\frac{1}{10} = \frac{3}{10}$$


In [4]:
1/10+1/10+1/10 == 3/10


Out[4]:
False

NOTA: Bisogna fare molta attenzione quando si usano i numeri reali al calcolatore!

Rivediamo quindi leggermente la definizione di radice quadrata, introducendo il concetto di tolleranza numerica. In pratica, cerchiamo un numero reale positivo tale che

$$\sqrt{x} = y \quad \mbox{ tale che } \quad |y^2 - x| < \epsilon, \quad y \geq 0 $$

dove $\epsilon$ è una costante molto piccola, per esempio $10^{-7}$.

Ricerca per bisezione

Potremmo cercare di migliorare la procedura di enumerazione esaustiva considerando che i numeri che stiamo controllando sono ordinati. Potremmo evitare di controllare tutti i numeri, uno alla volta, e potremmo cercare di fare dei "salti".

ESERCIZIO 2.2: Scrivere una procedura che, per trovare la radice quadrata di un numero, ogni volta divide l'intervallo di ricerca in due parti uguali, e continua ad esplorare solo la parte in cui effettivamente si può trovare la radice cercata.


In [5]:
def Abs(x):
    if x < 0:
        return -x
    return x

def Istess(a,b):
    return Abs(a-b) < 0.0001

def SqrtReals(x, a, b):
    print(x, a, b)
    y = (a+b)/2
    if Istess(x, y*y):
        return y
    else:
        if y*y > x:
            return SqrtReals(x, a, y)
        else:
            return SqrtReals(x, y, b)

In [6]:
print(SqrtReals(36, 0, 36))


36 0 36
36 0 18.0
36 0 9.0
36 4.5 9.0
36 4.5 6.75
36 5.625 6.75
36 5.625 6.1875
36 5.90625 6.1875
36 5.90625 6.046875
36 5.9765625 6.046875
36 5.9765625 6.01171875
36 5.994140625 6.01171875
36 5.994140625 6.0029296875
36 5.99853515625 6.0029296875
36 5.99853515625 6.000732421875
36 5.9996337890625 6.000732421875
36 5.9996337890625 6.00018310546875
36 5.999908447265625 6.00018310546875
36 5.999908447265625 6.0000457763671875
36 5.999977111816406 6.0000457763671875
36 5.999977111816406 6.000011444091797
5.999994277954102

Il metodo di Newton

Il metodo più usato per calcolare la radice quadrata è il metodo di approssimazioni successive introdotto da Newton. Il metodo consiste nel trovare la soluzione attraverso aggiustamenti successivi di una soluzione tentativo: se abbiamo un valore $y$ che dovrebbe essere il valore tentativo della radice quadrata di un altro numero $x$ possiamo ottenere una approssimazione migliore facendo la media tra $y$ e $x/y$. Il metodo di Newtnon consiste quindi di partire da un $y_0$, e ad ogni iterazione di calcolare:

$$y_{i+1} = \frac{y_i + \frac{x}{y_i}}{2}$$

ESEMPIO: Ricerca della radice quadrata di 2.

Valore Tentativo $y$ Quoziente $x/y$ Media tra $y$ e $x/y$
1 2/1 (1+2)/2=1.5
1.5 2/1.5=1.3333 (1.3333+1.5)/2=1.4167
1.4167 2/1.4167=1.4118 (1.4118 + 1.4167)/2=1.4142
1.4142 ... ...

ESERCIZIO 2.3: Scrivere una o più procedure per trovare la radice quadrata di un numero, utilizzando il metodo di Newton scritto sopra.


In [7]:
def Newton(x, y):
    # print(x, x/y, y)
    if Istess(x, y*y):
        return y
    return Newton(x, (y+x/y)/2)

In [8]:
print(Newton(2, 1))


1.4142156862745097

DOMANDA: è possibile trovare un criterio di arresto alternativo, che sia funzione dell'errore commesso nel calcolo della radice quadrata?

Studiamo ora come possiamo stimare l'errore del metodo di Newton, senza conoscere il valore esatto di $\sqrt{x}$. Chiamiamo $E_i$ l'errore di approssimazione che stiamo commettendo:

$$E_i = y_i - \sqrt{x}$$

da cui

$$y_i = \sqrt{x} + E_i$$

L'errore che commettiamo alla prossima iterazione, sarà pari a

$$E_{i+1} = y_{i+1} - \sqrt{x} = \frac{y_i + \frac{x}{y_i}}{2} - \sqrt{x}$$

Facendo un po' di conti a partire dall'espressione precedente, si arriva a far vedere che

$$E_{i+1} = \frac{E_i^2}{2 y_i} > 0$$

In pratica, dopo il primo tentativo $y_0$, tutti gli errori successivi saranno sempre positivi. Inoltre l'errore ad ogni iterazione diventa sempre più piccolo, in quanto

$$E_i = y_i - \sqrt{x} < y_i \quad \mbox{ e quindi } 0< \frac{E_i}{y_i} <1$$

e inoltre

$$E_{i+1} = \frac{E_i^2}{2 y_i} = \frac{E_i}{y_i} \times \frac{E_i}{2} < \frac{E_i}{2}$$

Riassumendo, abbiamo mostrato che l'errore diventa sempre più piccolo in quanto

$$0 < E_{i+1} < \frac{E_i}{2} < E_i.$$

Vediamo ora come possiamo usare le espressioni precedenti per ottenere un criterio di arresto usando una stima sull'errore. Dalle relazioni precedenti abbiamo che

$$0 < y_{i+1} - \sqrt{x} < y_{i} - \sqrt{x}$$

da cui

$$\sqrt{x} < y_{i+1} < y_{i}$$

Se riprendiamo la definzione di errore ad una data iterazione, abbiamo che

$$E_i = y_i - \sqrt{x}= y_i - y_{i+1} + y_{i+1} -\sqrt{x} = (y_i - y_{i+1}) + E_{i+1} < (y_i - y_{i+1}) + \frac{E_{i}}{2}$$

ovvero

$$E_{i+1} < \frac{E_i}{2} < y_i - y_{i+1}.$$

In pratica, se si vuole calcolare la radice quadrata di un numero reale positivo con un margine di errore minore di $\epsilon < 0$, basterà verificare la condizione:

$$y_i - y_{i+1} < \epsilon.$$

Confronto tra le tre funzioni trovate

Possiamo provare a fare un piccolo confronto tra le tre funzioni trovate in termini di efficienza, andando a contare, per esempio, quante volte è stata chiamata la funzione ricorsiva. Per poterlo fare, basta aggiungere un parametro formale il quale viene incrementato di uno ogni volta che effettuiamo una chiamata ricorsiva. Tale valore puo' essere stampato a video prima di restituire il valore finale.

ESERCIZIO 2.4: Modificare le procedure precedenti per contare il numero di chiamate ricorsive con le tre procedure precedenti: (i) enumerazione esaustiva, (ii) metodo di bisezione, e (iii) metodo di Newton.


In [ ]:
# DA COMPLETARE