Capítulo 2 Análisis de algoritmos PROBLEMAS RESUELTOS

Anuncio
Capítulo 2
Análisis de algoritmos
EJEMPLO 2.1
procedimiento burbujaMejorado(E/S arr: A; E entero: n)
var
entero: Sw, j, k
inicio
Sw  0
j  1
mientras ((j < n) y (Sw=0)) hacer
Sw  0
k  1
mientras (k < (n - j)) hacer
si (A[k+1] < A[k]) entonces
Aux  A[k]
A[k]  A[k+1]
A[k+1]  Aux
Sw  1
fin-si
fin_mientras
j  j + 1
fin-mientras
fin_procedimiento
EJEMPLO 2.3
O(loga(n)) = O(logb(n)/ logb(a)) = O( (1 / logb(a)) * logb(n)) = O(c * logb(n)) = O(logb(n))
PROBLEMAS RESUELTOS
2.1
Para n >= 1 se tiene
2
2
a. n + 2n +1
es O(n )
2
3
b. n (n+1) es O(n )
2
2
c. n + (1/n) es O(n )
2
2
2 2
2
2
n + 2n +1 <= n + 2n +n =(1+2+1)n = O(n )
2
3 2
3
3
n (n+1)= n + n <= (1+ 1) n =O(n )
2
2
2
2
n + (1/n) <= n + 1<=(1+1) n =O(n )
Para r >= 10
d. r + r log r es O(r log r)
r + r log r <= r log r+ r log r=(1+1)r log r = O(r log r)
Para x >= 1
2
2
e. x +ln x es O(x )
2
2
2
2
2
x +ln x<= x + x = (1+1)x = O(x )
2.2.
a.
b.
c.
n(n+1)/2.
(1/n) - n(n+1).
-2n
e
- n(n+1).
2
Es O(n )
2
Es O(n )
2
Es O(n )
e.
f.
g.
4 2
n (n -1).
n
-8
n ln n + e -6e .
2
-n
8
n ln n + e -6n .
6
Es O(n )
n
Es O(e )
8
Es O(n )
1
2
Algoritmos y estructuras de datos. Una perspectiva en C. Libro de Problemas
d.
2
n + 5n ln n.
2
Es O(n )
h.
2
n
8
n ln n + e -6e .
n
Es O(e )
2.3.
2
a) (30000/10000) *5 segundos = 45 segundos.
5
b) (30000/10000) *5 segundos =1215 segundos = 20.25 minutos.
c) ( (30000) log (30000)) /(10000 log (10000)) *5 = 16,8 segundos.
30000 10000
20000
d) 2
/2
*5 = 2
*5 segundos.
30000 10000
20000
e) 5
/5
*5 = 5
*5 Segundos.
2.4
1 hora = 1*60 minutos = 60*60 segundos = 60*60*1000 milisegundos = 3600000 milisegundos.
2
3
Para que el algoritmo A sea mejor que el algoritmo B, debe ocurrir que n *3600000 < n . Por lo tanto 3600000 < n.
De esta forma el umbral n0 a partir del cual es mejor el algoritmo A que el B es para un tamaño de 3600000. ¡El
ejemplar debe ser de tamaño mayor que 3 millones y medio!.
Análisis de algoritmos
3
2.5
T (n)   k 1, 4,71 
n
a.
La sentencia de dentro del bucle es de orden 1. Por lo tanto:
n
 O ( n)
3
T (n)  k 11  nO(n)
n
b.
c.
Las sentencias de dentro del bucle son de orden 1. Por lo tanto:
Para estudiar la complejidad se modifica el algoritmo añadiendo una variable auxiliar t de la siguiente forma
i = 1; x = 0; t =0;
while (i <= n)
{
x += 3;
i *= 3;
t++;
};
De esta forma se tiene que i = 3t . Por lo tanto cuando termine el bucle mientras se tiene que 3t > n y 3 t-1 <= n.
Es decir que t -1 <= log3(n) y t > log3(n). Así la complejidad del algoritmo es O(log3(n))
d. Utilizando el resultado obtenido en el ejercicio anterior y teniendo en cuenta el bucle externo tendremos
T (n)  i 1 log( n)  n log( n)O(n log( n))
n
n
e.
La sentencia mas interior de los bucles es de orden 1. Por lo tanto:
f.
La sentencia mas interior de los bucles es de orden 1. Así:
n
n
n
n
n
n
n
T (n)  1   n  n * n  n 2 O(n 2)
i 1 j 1
i 1
n
T (n)  1  n   n2  n3 O(n3)
i 1 j 1 k 1
g.
i 1 j 1
i 1
La sentencia mas interior de los bucles es de orden 1. De esta forma:
n
n
j
n
n
n * (n  1)
n * (n  1)
 n*
O(n3)
2
2
i 1
n
T (n)  1  j  
i 1 j 1 k 1
h.
i 1 j 1
La sentencia mas interior de los bucles es de orden 1.
n
i
1
i * (i  1) n i * i n i n * (n  2) * (n  1) n * (n  1)

 

O(n3)
2
6
4
i 1
i 1 2
i 1 2
n
T (n)   j  
i 1 j 1
i. Para estudiar la complejidad se modifica el algoritmo añadiendo una variable auxiliar t de la siguiente forma :
x = 0;
for (i = 1;i <= n; i++)
{ j=1;
k=0;
while(j <= i)
{
j *= 2;
k++;
x += 2;
4
Algoritmos y estructuras de datos. Una perspectiva en C. Libro de Problemas
}
}
De esta forma se tiene que j = 2k . Por lo tanto cuando termine el bucle mientras, tendremos que 2k > j y 2k-1 <= j.
Es decir que k -1 <= log2(j) y k > log2(j). Así la complejidad del bucle mientras log2(j)). Por lo tanto:
n
T (n)   log2 ( j )O(n log(n)
j 1
2.6
a.
Solo hay una llamada recursiva, y por tanto la recurrencia es :
T(n)= 1 + T(n-1) si n >1 y 1 en otro caso.
De esta forma, aplicando expansión de recurrencias se tiene:
T(n) = 1 + T(n-1) =1 + 1+ T(n-2) =……..=1+1+ 1+ ..+ 1(n veces) = n O(n).
b.
Solo hay una llamada recursiva, y por tanto la recurrencia es:
T(n)= 1 + T(n/2) si n >1 y 1 en otro caso.
Por expansión de recurrencias se tiene:
T(n) = 1 + T(n/2) = 1 + 1+ T(n/4) =------= 1 + 1 + …..+1 (k veces=log2(n))= log2(n) O(log2(n))
c.
Solo hay una llamada recursiva, y dentro de cada recursividad de tamaño n hay una iteración que tarda tiempo n
por tanto la recurrencia es: T(n)= n + T(n/2) si n >1 y 1 en otro caso.
T(n) = n + T(n/2) = n + n/2+ T(n/4) =……= n + n/2 + n/4+…..+4+2 +1=
n(  k  0
k  log( n )
d.
1
1
2n )  n(2  1 )  2n  1O(n)
)  n(
k
1
n
2
2
1
Solo hay una llamada recursiva, y una iteración que tarda tiempo n y la recurrencia es:
T(n)= n + T(n-1) si n >1 y 1 en otro caso.
Así T(n) = n + T(n-1) = n + n-1+ T(n-2) =……. = n + n-1 + n-2+ …..+2+1 =
e.
n(n  1)
O(n 2)
2
Hay dos llamadas recursivas, y una sentencia que tarda tiempo 1 por lo tanto la recurrencia es:
T(n)= 1 +2 T(n-1) si n >1 y 1 en otro caso.
De esta forma, aplicando expansión de recurrencias se tiene:
3
n-1 n-1 *
n
n
T(n) = 1 + 2T(n-1) = 1 + 2+ 4T(n-2) =……..= 1 + 2 + 4+ 2 +….+.2
=2
2 –1 = 2 -1  O(2 )
f.
Hay dos llamada recursivas, y una iteración que tarda tiempo n por lo tanto la recurrencia es:
T(n)= n + 2T(n/2) si n >1 y 1 en otro caso.
De esta forma, aplicando expansión de recurrencias se tiene:
T(n) = n + 2T(n/2) = n +2 n/2+ 4T(n/4) =------= n + n + n+
+…..n ( k =log2(n) veces)=
n log2(n)  O(n log2(n))
g.
Hay una llamada recursiva, y una doble iteración que tarda tiempo n2 por lo que la recurrencia es:
Análisis de algoritmos
5
T(n)= n2 + 2T(n/2) si n >1 y 1 en otro caso.
Aplicando expansión de recurrencias se tiene:
2
n ( k  0
k  log( n )
1
1
1
)  n 2 ( 2n )  n 2 (2  )  2 n 2  nO(n 2)
k
1
n
2
2
1
T(n) = n2 + 2T(n/2) = n2 +2 (n/2)2+ 4T(n/4) = n
k log( n )
n2

k 0
2

n2 n2
n
 2  2 3 T ( 3 )  .... 
2 2
2
1
1
1
2 2n
n (
)  (n 2 )
k
1
2

2
2.7
En las tres funciones hay dos llamadas recursivas, y una sentencia que tarda tiempo 1 (véase el tema de árboles) por lo
tanto la recurrencia es:
T(n)= 1 +2 T(n-1) si n >1 y 1 en otro caso. De esta forma, aplicando expansión de recurrencias se tiene:
3
n-1 n-1 *
n
n
T(n) = 1 + 2T(n-1) = 1 + 2+ 4T(n-2) =------= 1 + 2 + 4+ 2
+…..2
=2
2 –1 = 2 -1  O(2 )
Se supone que n es la profundidad del árbol.
2.8.
int Factorial(int n)
{
if (n <= 1)
return (1);
else
return (n * Factorial(n-1));
}
int
{
Combinatorio(int n, int m)
return (Factorial(n) / (Factorial(m) * Factorial(n-m)));
}
La complejidad de la función Factorial se calcula mediante su recurrencia que es la siguiente:
T(n)= 1 + T(n-1) si n >1 y 1 en otro caso.
De esta forma, aplicando expansión de recurrencias se obtiene:
T(n) = 1 + T(n-1) =1 + 1+ T(n-2) =------=1+1+ 1+ ..+ 1(n veces) = n O(n)
Por lo tanto para estudiar la complejidad de la función Combinatorio basta con observar que hay tres llamadas a la
función Factorial con tamaños n, m, y n - m. Por lo tanto la complejidad de la función Combinatorio es
n + n – m + m = 2n  O(n).
2.9.
float Menor(float a[], int n)
{
6
Algoritmos y estructuras de datos. Una perspectiva en C. Libro de Problemas
int i;
float m;
m = a[0];
for (i = 1; i < n; i++)
if (m > a[i])
m = a[i];
return (m);
}
La sentencia mas interior del bucle es de orden 1. Por lo tanto: T (n)  n 1  nO(n)
k 1
2.10.
float Evaluar(float a[], int n, float x)
/*los coeficientes del polinomio vienen dados en el array a, de tal manera que el
i
coeficiente de x se encuentra en a[i]. El grado del polinomio es n */
{
int i;
float Suma, Pro;
Suma = 0;
Pro = 1;
for (i = 0; i <= n; i++)
{
/*en cada iteración Pro = xi */
Suma = Suma + Pro * A[i];
Pro *= x;
}
return (Suma);
}
La sentencia mas interior de los bucles es de orden 1. Por lo tanto:
T (n)  i 1 1  nO(n)
n
2.11.
void SumaMatrices(float a[][max], float b[][max], float c[][max], int n)
/* se supone que max es una constante previamente declarada*/
{
int i, j;
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
c[i][j] = a[i][j] + b[i][j];
}
La sentencia mas interior de los bucles es de orden 1. Por lo tanto:
n
n
n
T (n)  1   n  n * n  n2 O(n2)
i 1 j 1
i 1
2.12.
void Producto(float a[][max], float b[][max], float c[][max], int n)
/* se supone que max es una constante previamente declarada*/
{
int i, j, k;
float aux;
Análisis de algoritmos
7
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
{
aux=0;
for (k = 0; k < n; k++)
aux += a[i][k] * b[k][j];
c[i][j] = aux;
}
}
La sentencia mas interior de los bucles es de orden 1. Por tanto:
n
n
n
n
n
T (n)  1   j 1 n   n * n  n3 O(n3)
i 1 j 1 k 1
n
i 1
i 1
2.13
n 1
El número total de iteraciones es:
 k  O( n
2
)
k 1
2.14.
El algoritmo tiene dos bucles anidados de inicialización, y tres bucles anidados de cálculos. Por lo tanto la complejidad
del algoritmo es:
n
T (n) 
n
n
n
n
 1  n
1
i 1 j 1
2
 n3  O(n3)
k 1 i 1 j 1
2.15.
Se estudia primeramente la complejidad de la función Mínimo. Esta función sólo tiene un bucle. Por tanto la función de
tiempo será
n
T ( n) 
1  n  1  O(n )
i 2
La función Dijkstra, tiene: una inicialización mediante un bucle desde, y un bucle que hace una llamada a la función
mínimo y anidado con él otro bucle que actualiza el vector D, así como operaciones sobre conjuntos. Se considera que
estas operaciones son constantes.
En este caso, la complejidad viene dada por:
n 1
n
T (n) 
 
1
i 1
n
(n 
i 1

1) 
k 2
n 1
 2n  1  (2n  1)(n  1)  O(n )
2
i 1
2.16
El algoritmo tiene dos bucles anidados de inicialización, y tres bucles anidados de cálculos. Por lo tanto la complejidad
del algoritmo es:
n
T (n) 
n
n
n
n
 1  n
1
i 1 j 1
k 1 i 1
j
2
 n3  O(n3)
8
Algoritmos y estructuras de datos. Una perspectiva en C. Libro de Problemas
2.17.
La función Prim, tiene: una inicialización mediante dos bucles anidados, y un bucle que hace una llamada a la función
mínimo y anidado con él otro bucle que actualiza el vector D, así como operaciones sobre conjuntos. Se considera que
estas operaciones son constantes. En este caso, la complejidad viene dada por:
n
T (n) 
n 1
n 1
n
 n   (n  1)  n   2n 1  n
2
i 1
i 1
k 2
2
 (2n  1)(n  1)  O(n2)
i 1
2.18.
int Euclides(int m, int n)
/*m,n son ambas positivas*/
{
int r;
while ( m > 0)
{
r = n % m;
n = m;
m = r;
}
return (n);
}
Se comprueba que la complejidad del algoritmo es O(log(m))= O(log(n)) .
Para ello se demuestra que n% m es siempre menor que n/2 (cociente de la división entera). En efecto
si m > n/2 entonces n % m = n - m < n – n / 2 = n / 2.
si m <= n / 2 es evidente que n % m < n / 2.
Sea k el número de iteraciones. Sean ni y mi los valores de n y m al final de cada iteración.
Así mk=0 ya que al final del bucle m vale cero y ni = mi-1,m i = ni-1 % mi-1
De esta forma mi = ni-1 % mi-1 < ni-1 / 2 = mi-2 / 2.
Sea que k impar es decir hay un número natural t tal que k = 2t+1. Se tiene:
2
t
mk-1 < mk-3 / 2 < mk-5 / 2 <… < m0 / 2 . De esta forma k-1 <= 2 log2(m0). Es decir k = O(log2(m))  O(log(m))
De forma análoga se procede cuando k es par.
2.19
float K_esimo (float A[], int Izq, int Der, int k);
{
int Centro, aux, x, i, j;
Centro = ( Izq + Der ) / 2;
x = A[Centro];
i = Izq;
j = Der;
do
{
while ( A[i] < x)
i ++;
/* aumentar i porque A[i] es pequeño */
Análisis de algoritmos
while ( A[j] > x)
j --;
if ( i <= j)
{
Aux = A[i];
A[i] = A[j];
A[j] = Aux;
i++;
j--;
};
9
/*disminuir j porque A[j] es grande */
/* no se han cruzado, hay que cambiar de orden */
}
while (i <= j);
/*se ha terminado la partición, los pequeños a la izquierda y los grandes a la derecha */
if ( j < k)
Izq=i;
/* hay que buscar por la derecha */
if (k>i)
Der=j;
/* hay que buscar por l izquierda */
if (Izq < Der)
return (K_esimo(A, Izq, j, k));
else
return (A[k]);
/* no se ha encontrado la posición */
/*se ha encontrado el elemento */
}
Cada llamada recursiva a la función K_esimo requiere una pasada a través del array, y esto tarda 0(n). Eso
significa que el número total de operaciones para cada nivel de recursión son operaciones 0(n). De esta forma la
recurrencia será en el mejor de los casos y caso esperado: T(n)= n + T(n/2) si n >1 y 1 en otro caso.
Aplicando expansión de recurrencias se tiene:
T(n) = n + T(n/2) = n + n/2+ T(n/4) =------= n + n/2 + n/4+
+…..4+2 +1=
1
1
k  log( n ) 1
2n )  n(2  1 )  2n  1O(n)
n(k 0
)  n(
k
1
n
2
2
En el peor de los casos la recurrencia será: T(n)= n + T(n-1) si n >1 y 1 en otro caso.
Aplicando expansión de recurrencias se tiene:
T(n) = n + T(n-1) = n + n-1+ T(n-2) =------= n + n-1 + n-2+
+…..2+1 =
n(n  1)
O(n 2)
2
2
Por lo tanto el tiempo medio y mejor de ejecución es 0(n) y en el peor de los casos es 0(n )
El tiempo esperado será O(n) ya que lo normal es dividir A en dos partes aproximadamente iguales.
2.20.
void hanoi(char A, char B, char C, int n)
{
if (n == 1)
printf("Mover disco %d desde varilla %c a varilla %c \n ", n, A, C);
else
{
hanoi(A, C, B, n - 1);
10
Algoritmos y estructuras de datos. Una perspectiva en C. Libro de Problemas
printf("Mover disco %d desde varilla %c a varilla %c \n ", n, A, C);
hanoi(B, A, C, n - 1);
}
}
La recurrencia para encontrar el tiempo que tarda en ejecutarse viene dada por:
T(1)=1
T(n) = 2T(n-1)+1 si n>1.
Para realizar la inducción debe procederse primeramente con una inducción constructiva que construye la posible
solución y una inducción demostrativa que demuestra la solución porpuesta.
Inducción constructiva
Para calcular la complejidad por el método de la inducción suponemos que la solución a buscar es de la forma a*2 n , ya
que la solución debe ser de la forma 2 n por una constante por tener dos llamadas recurisvas. Así se tiene por tanto que
T(1)= a*21 = 1, lo que significa que a = ½. Por otro lado como T(n) = 2T( n - 1) + 1 si n >1, se obtiene que ½ * 2 n
= ½ * 2 n-1 + 1. De esta forma 2 n = 2 n-1 + 2 que es falso en general ( para n=3 por ejemplo 8 es distinto de 4+2). De
lo anterior se deduce que la hipótesis de partida es falsa, pero es debido a que aparece un sumando extraño. Para
intentar paliar el problema, se fortalece la solución a buscar, suponiendo que la solución es de la forma a*2 n +b. Se
tiene ahora que T(1)= a*21 +b = 1. Por otro lado como T(n) = 2T(n - 1) + 1 si n>1, se obtiene a * 2 n + b= a* (2 *
2 n - 1 +b) + 1, por lo que b= 2b + 1. Es decir que b =-1. Sustituyendo el valor de b en a* 21 + b = 1 se tiene que a =
1. Por lo que la solución a buscar tiene la forma 2 n -1
Inducción demostrativa
Se supone ahora que T(n)= 2 n - 1 por hipótesis de inducción. Hay que demostrar que es cierta la igualdad para todo
número natural n positivo. Se comprueba que es cierto para el caso n = 1. Para n = 1 T(1) = 1 por la igualdad. La
fómula expresa por su parte T(n)= 2 n -1, que para el caso n=1 es T(1) = 2 - 1 = 1. Por lo que la fórmula es correcta.
Se supone que es cierto para el caso n - 1 y demostrémoslo para el caso n. Para el caso n - 1 se tiene por hipótesis de
inducción que T(n - 1) = 2 n-1 - 1. Hay que demostrar que T(n)= 2 n - 1. Pero T(n) = 2T(n-1)+1 por la recurrencia, por
lo que T(n)= 2 * (2 n-1 - 1 ) + 1 = 2 n - 2 + 1 = 2 n - 1 = T(n). Como se quería demostrar.
2.21
En la solución obtenida en el apartado d del ejercicio 2.6 se ha obtenido que el tiempo de la función en cuadrático, por
lo que resulta coherente suponer que la solución es un polinomio de grado dos. t(n)= an2+bn+c que es la hipótesis de
inducción constructiva.
Inducción constructiva
Para obtener los posibles valores de a, b y c se procede de la siguiente forma. Como T(1)=1 = a12 + b1 + c, se obtiene
1=a+b+c.
Por otro lado como T(n)= n + T(n - 1) sustituyendo la hipótesis de inducción constructiva.
T(n) = an2 + bn + c = n + a(n - 1)2 + b(n - 1) + c. Es decir que an2 + bn + c = an2 +( 1 – 2a + b)n + a – b + c. Al
igualar los dos polinomios se llega a que : b= 1 - 2a + b, y c = a + b + c.
Con lo que al despejar se obtiene que los valores de a y b son: a =1/2 y b=-1/2.
Al sustituir los valores de a y b en la expresión 1=a + b + c se obtiene el valor de c = 1.
Inducción demostrativa
Análisis de algoritmos
11
Para demostrar que efectivamente T(n)= ½ n2 –½ n +1, se procede de la siguiente forma:
Es cierto para el caso n=1.
T(1)=1 por la recurrencia que coincide con T(1)= ½ 12–½ *1+1=1.
Se supone que la hipótesis de inducción cierta para el caso n - 1 y se demuestra para el caso n.
T(n - 1) = ½ (n - 1)2 –½ (n - 1) + 1 por hipótesis.
Como T(n ) = n + T(n - 1)
Se tiene que
T(n) = n + ½ (n - 1)2 – ½ (n - 1) + 1 = ½ n2 – ½ n + 1. Como se quería demostrar.
2.22
La recurrencia T(n) = T(n - 1) + T(n - 2), se traduce en la siguiente ecuación en diferencias finitas:
T(n) - T(n - 1) - T(n - 2) = 0.
Se sabe que una ecuación en diferencias finitas de la forma aT(n) + bT(n - 1) + cT(n 2) = 0 tiene asociado un polinomio característico que es ax2 + bx + c = 0. Si este polinomio característico tiene dos
raíces reales y distintas R1 y R2 entonces la solución general de la ecuación en diferencias finitas es: T(n) = C1 R1n +
C2R2n siendo C1 y C2 constantes a determinar por las condiciones iniciales de la ecuación en diferencias finitas. En el
caso particular de la ecuación asociada a los número de Fibonacci, el polinomio característico es x2 - x - c = 0, con lo
que se tiene que:
x
2
 b  b 2  4ac 1  1  4 *1* (1) 1  5


2a
2 *1
2
De esta forma se tiene que las dos raíces son:
R1 
1 5
2
y R2 
1- 5
2
Con lo que la solución general es de la forma:
n
1 5 
1- 5 
  C2 

T (n)  C1 

 2 
2




n
Como las condiciones inicailes son T(0)=0 y T(1)=1al sustituir en la ecuación anterior se obtiene que
0
1 5 
1- 5 
  C2 

0  C1 

 2 
2




1
0
1
1 5 
1- 5 
1
1
  C2 
 con lo que C1 
y C2  
1  C1 



5
5
 2 
 2 
Los números de fibonacci son de la forma:
n
n
1 1 5 
1 1- 5 

 

 que tiene crecimiento exponencial.
f ( n) 
5  2 
5  2 
Al número

1 5
se le conoce como razón aúrea o número aúreo, por su belleza. Este número era ya conocido
2
por los antiguos griegos (El conciente entre la altura de las Columnas del Partenón de Atenas y la distancia entre ellas es
la razón aúrea). La razón existente entre el largo y ancho de un folio Din A4 es también el número aúreo. El pintor
Salvador Dalí lo usó en su conocido cuadro de las tazas colgantes. El método de ordenación de la mezcla polifásica de
orden dos (usa tres ficheros) es el más eficiente y está también asociado a la razón aúrea. Ver capítulo 6.
Descargar